From 0d565fed52dbae3bec9bfd692f027ed59f7a91ba Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Tue, 2 Jun 2026 16:16:53 -0600 Subject: [PATCH 1/4] chore: add /sdk-proxy slash command for hitting the API via the SDK app proxy Drops in a Claude/Cursor slash command at .claude/commands/sdk-proxy.md that lets the agent run real curl requests against the demo company you already have running in the SDK Dev App, without re-provisioning or reading the env file directly. The command teaches the agent to: - Discover the SDK app's port robustly via lsof + cwd match (works across auto-incremented ports 5200/5201/... and multiple sibling-repo SDK apps running in parallel) - Route curl through the running Vite proxy at http://localhost:/api so the in-memory FLOW_TOKEN is always the source of truth (the on-disk env file lags when "Set Manual Token" or "Refresh Token" is used in the Settings panel) - Discover the live company UUID by hitting /v1/companies, never by reading sdk-app/env/.env.demo (which can be stale) - Look up endpoints via the gusto-payroll MCP (find_endpoint / generate_curl / check_scopes / validate_workflow) instead of guessing paths from memory - Use a temp-file pattern (curl -o ... -w "HTTP %{http_code}" && jq < ...) so status code and JSON body never collide in jq's parse stream - Cache the discovered port + company UUID in $TMPDIR with a 120s TTL and a single verify probe, so repeat invocations skip discovery entirely Includes author notes on two subtle footguns: (1) Cursor's slash-command renderer interpolates dollar-digit tokens as positional args from the user's invocation, so awk positional fields cannot be used here -- the discovery snippet uses lsof -F field records + grep/sed instead; (2) the cache hygiene relies on unset-before-source plus rm+unset on every failure path to prevent stale shell state from leaking into rediscovery. No source code changes; only the new .claude/commands/sdk-proxy.md file. Co-authored-by: Cursor --- .claude/commands/sdk-proxy.md | 262 ++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 .claude/commands/sdk-proxy.md diff --git a/.claude/commands/sdk-proxy.md b/.claude/commands/sdk-proxy.md new file mode 100644 index 000000000..451fedd23 --- /dev/null +++ b/.claude/commands/sdk-proxy.md @@ -0,0 +1,262 @@ +# Hit the Gusto API via the SDK Dev App proxy + +This command lets you test API behavior end-to-end against the demo company you already have running in the SDK Dev App. It teaches the agent the SDK app's auth-and-proxy setup, points it at the `gusto-payroll` MCP for endpoint discovery, and has it run real `curl` requests through the same Vite proxy the browser uses. + +The user's free-form arguments after `/sdk-proxy` describe the request in plain language. Examples: + +- `/sdk-proxy create an employee named Alice Johnson` +- `/sdk-proxy list payrolls for the current company` +- `/sdk-proxy add a home address to the current employee` +- `/sdk-proxy show me what a "submit payroll" request body looks like` (discovery only — no execution) + +## How the proxy works + +The SDK Dev App runs a Vite dev-server proxy that rewrites `/api/*` to `${GWS_FLOWS_HOST}/fe_sdk/${FLOW_TOKEN}/*` using whatever `FLOW_TOKEN` the app currently holds in memory. gws-flows then handles OAuth and forwards to ZenPayroll. + +From [sdk-app/vite.config.ts](sdk-app/vite.config.ts): + +```ts +rewrite: (path: string) => path.replace(/^\/api/, `/fe_sdk/${env.FLOW_TOKEN}`), +``` + +**Always curl through the SDK app's local proxy, not directly to the gws-flows host.** The proxy's in-memory token is the source of truth — it stays in sync when the user clicks "Refresh Token" / "Set Manual Token" / "Create New Demo" in the SDK app Settings panel. The on-disk env file does not always match (e.g., "Set Manual Token" only updates memory). + +## Step 0 — Try the cache first (fast path) + +Port and company UUID are stable for the lifetime of a demo session, so we cache them in `${TMPDIR}` per repo. On a cache hit, you skip the `lsof` walk in Step 1 and the companies-list curl in Step 2 — roughly a 1-second savings per invocation, which adds up across a chain of calls. + +Cache lookup snippet (run first, before Step 1): + +```bash +SDK_APP_REPO="$(git rev-parse --show-toplevel)" +CACHE_DIR="${TMPDIR:-/tmp}" +SDK_PROXY_CACHE="${CACHE_DIR%/}/sdk-proxy-cache-$(echo "$SDK_APP_REPO" | shasum -a 1 | cut -c1-8).env" + +# Clear inherited shell state so a missing key in the cache file (or a stale +# value left over from an earlier invocation in the same shell) can't leak into +# Step 1 if we end up taking the rediscovery path. +unset SDK_APP_PORT COMPANY_ID CACHED_AT +CACHE_HIT="" + +if [ -f "$SDK_PROXY_CACHE" ]; then + # shellcheck disable=SC1090 + source "$SDK_PROXY_CACHE" + AGE_SECS=$(( $(date +%s) - ${CACHED_AT:-0} )) + if [ "$AGE_SECS" -lt 120 ] && [ -n "$SDK_APP_PORT" ] && [ -n "$COMPANY_ID" ]; then + # Verify the cached port + company are still live before trusting them + VERIFY_STATUS=$(curl -sS -m 2 -o /dev/null -w '%{http_code}' \ + "http://localhost:$SDK_APP_PORT/api/v1/companies/$COMPANY_ID/locations" 2>/dev/null || echo 000) + if [ "$VERIFY_STATUS" = "200" ]; then + CACHE_HIT=1 + echo "Cache hit (age ${AGE_SECS}s): SDK_APP_PORT=$SDK_APP_PORT COMPANY_ID=$COMPANY_ID" + else + echo "Cache stale (verify returned $VERIFY_STATUS); rediscovering" + rm -f "$SDK_PROXY_CACHE" + unset SDK_APP_PORT COMPANY_ID CACHED_AT + fi + else + echo "Cache expired (age ${AGE_SECS}s); rediscovering" + rm -f "$SDK_PROXY_CACHE" + unset SDK_APP_PORT COMPANY_ID CACHED_AT + fi +fi +``` + +> **Author note for future edits:** the `unset … ; source … ; ` ordering is load-bearing. If you drop the post-failure `unset`, a stale `COMPANY_ID` lingers in the shell environment and Step 1's discovery will silently reuse it on the next iteration. If you drop the pre-`source` `unset`, a cache file missing a key (e.g., from a partially-written file or an older schema) will inherit whatever value the surrounding shell already had. Both leaks are silent — symptom is "the agent confidently used the wrong company / port" — so keep all three `unset`s. + +Behavior: + +- **`CACHE_HIT=1`** → skip Step 1 and Step 2; jump straight to Step 3 with `$SDK_APP_PORT` and `$COMPANY_ID` already populated. Mention the cache age in your summary to the user so they know it wasn't freshly verified beyond the locations probe. +- **`CACHE_HIT=""` and the cache file existed** → it was stale or invalid; the snippet deleted it. Proceed to Step 1. +- **`CACHE_HIT=""` and no cache file** → first invocation in this repo. Proceed to Step 1. + +**TTL is 120 seconds.** Tokens last much longer, but the user might "Create New Demo" mid-session, which would silently invalidate the cached `COMPANY_ID`. 120s is short enough that drift is caught quickly during slow iteration, long enough that back-to-back calls in a single thought reuse the cache. The verify-curl catches mid-session demo swaps even within the TTL. + +If the user explicitly says "use a fresh discovery" or "ignore the cache," skip this step and `rm -f "$SDK_PROXY_CACHE"` before running Steps 1–2. + +## Step 1 — Discover the SDK app's port + +The SDK app **defaults to 5200 but auto-increments** (5201, 5202, …) when that port is taken. The user often runs more than one SDK app at a time across sibling repos (`~/workspace/embedded-react-sdk`, `~/workspace/embedded-react-sdk-2`, `~/workspace/embedded-react-sdk-3`, …), so we need to resolve the port that belongs to **this** workspace's SDK app, not just any. + +Run this discovery snippet from the repo root: + +```bash +SDK_APP_REPO="$(git rev-parse --show-toplevel)" +SDK_APP_PORT="" + +for pid in $(lsof -nP -iTCP:5200-5299 -sTCP:LISTEN -t 2>/dev/null); do + cwd=$(lsof -p "$pid" -a -d cwd -Fn 2>/dev/null | grep '^n' | sed 's/^n//' | head -1) + if [ "$cwd" = "$SDK_APP_REPO" ]; then + SDK_APP_PORT=$(lsof -nP -iTCP:5200-5299 -sTCP:LISTEN -a -p "$pid" -Fn 2>/dev/null \ + | grep '^n' | sed 's/.*://' | head -1) + break + fi +done + +echo "SDK_APP_PORT=$SDK_APP_PORT" +``` + +This uses `lsof -iTCP:5200-5299` to list every PID owning a listening socket in the SDK app's port range, looks up each process's working directory via `lsof -p -a -d cwd -Fn`, and picks the port owned by the process whose cwd matches the current git repo root. + +> **Author note for future edits:** the snippet deliberately uses **only named bash variables** (`$pid`, `$cwd`, `$port`, `$SDK_APP_PORT`, `$SDK_APP_REPO`). Do **not** introduce dollar-digit tokens (a literal dollar sign immediately followed by a single digit 0–9) anywhere in this file — Cursor's slash-command renderer interpolates them as positional arguments from the user's invocation, so an `awk` field reference like dollar-followed-by-2 silently becomes the second word of the user's message by the time the agent sees the prompt. This rules out `awk` positional fields in any snippet here; use `lsof -F` field records + `grep`/`sed` (as above) instead. + +Robust against: + +- Auto-incremented ports (5201, 5202, …) +- Multiple SDK apps running for different repos at once +- Other Vite apps that happen to land in the same port range + +If `SDK_APP_PORT` is empty after the loop, fall back to a signature-endpoint scan to confirm whether any SDK app is reachable at all (for diagnostics — never silently target a different repo's app): + +```bash +for p in 5200 5201 5202 5203 5204 5205; do + if curl -sS -m 1 -o /dev/null -w '%{http_code}' "http://localhost:$p/sdk-app/api/validate-token" 2>/dev/null | grep -q 200; then + echo "Found an SDK app on port $p (verify it belongs to the intended repo before using)" + fi +done +``` + +`/sdk-app/api/validate-token` is the SDK app's own middleware route — no other Vite app responds `200` to it. + +Use the resolved `SDK_APP_PORT` for every subsequent call. **Do not hardcode `5200`.** + +## Step 2 — Confirm the token is live and discover the company + +Probe the proxy with the companies-list endpoint. It's cheap, returns the live company, and confirms the token is valid in one shot. Use the temp-file pattern (see Step 4 for the full rationale) so the body and status code don't collide in `jq`: + +```bash +SDK_PROXY_OUT=$(mktemp -t sdk-proxy.XXXXXX) +curl -sS -o "$SDK_PROXY_OUT" -w "HTTP %{http_code}\n" "http://localhost:$SDK_APP_PORT/api/v1/companies" +# On HTTP 200: extract company id +COMPANY_ID=$(jq -r '.[0].uuid' < "$SDK_PROXY_OUT") +echo "COMPANY_ID=$COMPANY_ID" +``` + +Possible outcomes: + +| Outcome | What it means | What to do | +| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `HTTP 200` + JSON array in temp file | SDK app running for this repo, token valid, company discoverable | Take `[0].uuid` as the current `company_id`; proceed to Step 3 | +| Step 1 found no port and the fallback scan found nothing either | SDK app not running for any repo | Tell the user to run `npm run sdk-app` in another terminal, then re-invoke `/sdk-proxy ...` | +| Step 1 found no port for this repo but the fallback scan found other SDK apps | SDK app(s) running, but none for this workspace | Tell the user which port(s) you found and (when possible, via `lsof -p -a -d cwd`) which repo each belongs to. Don't auto-pick — ask. | +| `HTTP 404` with an HTML body (`` "Page Not Found") **or** `HTTP 401` | Flow token expired or invalid in the running app | Tell the user to open the SDK app Settings panel and click "Refresh Token" or "Create New Demo." Don't run `npm run sdk-app:setup` unless they explicitly ask — that re-provisions a fresh demo and replaces their working session. | + +**Why a stale flow token surfaces as 404 (not 401):** gws-flows maps an invalid `fe_sdk/` path to its generic Rails 404 page, since the route only matches when the token resolves to a live OAuth grant. Treat 404-with-HTML-body identically to 401. + +**Do not read [sdk-app/env/.env.demo](sdk-app/env/.env.demo) to grab IDs or the token.** That file can lag behind the running app's in-memory state. Always discover IDs from the proxy. + +### Discovering downstream entity ids + +For `employee_id`, `contractor_id`, `payroll_id`, etc., follow the same pattern: hit the proxy's list endpoint and pick the first result (or filter by the user's hint, e.g., "the John Lewis employee"). Note: dropping `-w` here is fine — you only need the body for `jq`, and a non-zero `curl` exit will surface failure: + +```bash +curl -sS "http://localhost:$SDK_APP_PORT/api/v1/companies//employees" | jq '.[0].uuid' +curl -sS "http://localhost:$SDK_APP_PORT/api/v1/companies//payrolls" | jq '.[0].uuid' +``` + +### Write the cache + +After Step 1 + Step 2 succeed (and only when `CACHE_HIT` was empty), persist the discovered values so the next invocation hits the fast path: + +```bash +cat > "$SDK_PROXY_CACHE" < \ + "http://localhost:$SDK_APP_PORT/api/v1/companies//" \ + -H "Content-Type: application/json" \ + -H "X-Gusto-API-Version: 2024-04-01" \ + -d '' +jq . < "$SDK_PROXY_OUT" +``` + +This prints the status code from `curl -w` (cleanly, with no extra trailing newline), then `jq` parses just the body file. No interleaving, no parse errors. + +**Why not the simpler `curl -w '...HTTP_STATUS...' | jq`?** `curl -w` appends its output to stdout after the body, so the status footer ends up in `jq`'s parse stream. `jq` emits the body successfully but then errors on the trailing footer text. The data appears, but every call is noisy. The temp-file pattern is the only clean fix. + +Rules when adapting the MCP's generated curl: + +- **Replace the host.** The MCP emits `https://api.gusto-demo.com/...` — swap that whole prefix for `http://localhost:$SDK_APP_PORT/api` and the proxy handles the rest. +- **Strip any `Authorization: Bearer ...` header.** The proxy injects OAuth from the flow token; an extra `Authorization` header confuses it. +- Keep the API version header if the MCP includes one (e.g. `X-Gusto-API-Version`); the SDK and the proxy both honor it. + +### Skip the temp file for binary success/failure checks + +When you only care about the status code (e.g., a quick liveness probe, or a follow-up `GET` you're not going to display), drop the body to `/dev/null` and read `-w`: + +```bash +curl -sS -o /dev/null -w "HTTP %{http_code}\n" "http://localhost:$SDK_APP_PORT/api/v1/companies" +``` + +This is also the right shape for the Step 2 token probe. + +If the request mutates state, report the response payload back to the user including the new entity's `uuid` and `version` (most Gusto resources are version-locked). + +## Step 5 — Interpret and report + +Summarize what happened in 2–4 lines: + +- Verb + path that ran (so it's reproducible) +- Status code +- Key fields from the response (created uuid, validation errors, etc.) + +If the response was a validation error (`422`), surface the `errors[].error_key` and `errors[].message` instead of dumping the raw JSON — those are the actionable bits. + +## Worked example + +``` +User: /sdk-proxy create an employee named Alice Johnson +Agent: + 1. Runs the discovery snippet → SDK_APP_PORT=5201 (5200 was taken by a sibling repo) + 2. Curls http://localhost:5201/api/v1/companies → 200, picks [0].uuid as company_id + 3. Calls gusto-payroll find_endpoint("create employee") → POST /v1/companies/{company_id}/employees + 4. Calls generate_curl, swaps the host for http://localhost:5201/api, strips Authorization + 5. POSTs { first_name: "Alice", last_name: "Johnson", ... } via the proxy + 6. Parses 201 response, reports new employee UUID + version + 7. Optionally suggests follow-up calls (add home address, create job, etc.) +``` + +## Escape hatches + +- **Large or chained output** — if the response will be huge (list endpoints with no filter, full payroll dumps) or you're chaining 5+ calls, run the work in a subagent via the Task tool so the raw curl output doesn't pollute conversation context. Return only the summary. +- **Discovery-only requests** — if the user is asking what a request _looks like_ (not asking you to send it), stop after `find_endpoint` + `generate_curl` and show the template. Don't fire. +- **Destructive operations** — for `DELETE` requests or operations that finalize payroll, repeat the resolved URL back to the user and ask for confirmation before running. +- **SDK app not running** — never auto-start it; that would block this command for ~30s and steal a terminal. Tell the user to start it themselves. +- **Token refresh** — never run `npm run sdk-app:setup` automatically. It re-provisions a brand-new demo, throwing away the company the user is working with. Always ask first. +- **Multiple SDK apps running for different repos** — never auto-pick a port for a different repo. Surface the candidates with their cwds and ask which to target. +- **Force cache bust** — if the user says "fresh discovery," "ignore the cache," "the demo changed," or you've just observed a 401 / 404 / company-mismatch response, `rm -f "$SDK_PROXY_CACHE"` (path computed in Step 0) before re-running Steps 1–2. From 209d35658bc9861c5bc7e2cca772e83996687e5d Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Tue, 2 Jun 2026 16:36:25 -0600 Subject: [PATCH 2/4] chore: switch /sdk-proxy to the public embedded-payroll MCP and auto-prompt install The previous draft of this command referenced the `gusto-payroll` MCP at the internal staging URL (gusto-partner-api.staging.zp-int.com), which isn't installable by partners or by anyone outside Gusto's network. The Embedded Dev Assistant MCP is the documented, public, partner-facing equivalent -- this commit migrates the command to point at it and adds a project-level config so teammates get auto-prompted to install on first workspace open. Changes: - Add `.cursor/mcp.json` at the repo root with the public `embedded-payroll` entry from docs.gusto.com. Cursor auto-detects project-level MCP configs and prompts the user to enable them, so teammates opening the workspace see a one-click install instead of hitting "MCP not loaded" errors when they run /sdk-proxy. - Rename all MCP references in .claude/commands/sdk-proxy.md from `gusto-payroll` -> `embedded-payroll` to match the docs canonical server name. - Replace Step 3's hardcoded tool-name table (find_endpoint / generate_curl / check_scopes / validate_workflow / generate_typescript) with a capability-keyed one. The public MCP's docs describe its surface as "API reference access, documentation search, code/snippet generation" without naming individual tools, and tool names are subject to change as the MCP evolves. Describing what we need by capability lets the agent introspect the live tool surface and pick the right one, rather than failing on a renamed tool. - Add a Prerequisites section at the top of the command linking the official Dev Assistant MCP docs, with the canonical install snippet and a verification step ("ask the agent what MCP tools it has"). - Update the worked example and the discovery-only escape hatch to match the new capability-based phrasing. Net effect: a partner who clones this repo, opens it in Cursor, and clicks "Enable" on the prompt can run /sdk-proxy with no further setup. Co-authored-by: Cursor --- .claude/commands/sdk-proxy.md | 48 ++++++++++++++++++++++++----------- .cursor/mcp.json | 7 +++++ 2 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 .cursor/mcp.json diff --git a/.claude/commands/sdk-proxy.md b/.claude/commands/sdk-proxy.md index 451fedd23..c7ce3744a 100644 --- a/.claude/commands/sdk-proxy.md +++ b/.claude/commands/sdk-proxy.md @@ -1,6 +1,24 @@ # Hit the Gusto API via the SDK Dev App proxy -This command lets you test API behavior end-to-end against the demo company you already have running in the SDK Dev App. It teaches the agent the SDK app's auth-and-proxy setup, points it at the `gusto-payroll` MCP for endpoint discovery, and has it run real `curl` requests through the same Vite proxy the browser uses. +This command lets you test API behavior end-to-end against the demo company you already have running in the SDK Dev App. It teaches the agent the SDK app's auth-and-proxy setup, points it at the `embedded-payroll` MCP for endpoint discovery, and has it run real `curl` requests through the same Vite proxy the browser uses. + +## Prerequisites + +This command depends on the **Gusto Embedded Dev Assistant MCP** (server name `embedded-payroll`). The repo's [.cursor/mcp.json](.cursor/mcp.json) lists it at the project level, so on first open Cursor should prompt you to enable it. If it doesn't, or if you're on a different IDE, follow the official setup guide: [docs.gusto.com — Dev Assistant MCP](https://docs.gusto.com/embedded-payroll/docs/dev-assistant-mcp). For Cursor specifically, add to `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "embedded-payroll": { + "url": "https://embedded-payroll.readme.io/mcp" + } + } +} +``` + +Then restart Cursor. Verify by asking the agent "what MCP tools do you have access to?" — you should see tools from the `embedded-payroll` server. + +If the MCP isn't installed when this command runs, the agent should stop after Step 2 and surface the docs link rather than guessing API paths from memory. The user's free-form arguments after `/sdk-proxy` describe the request in plain language. Examples: @@ -172,21 +190,21 @@ EOF If the user's call mutates state and you observe a 401 / 404 / company-mismatch response, blow away the cache before retrying: `rm -f "$SDK_PROXY_CACHE"`. -## Step 3 — Discover the endpoint via the `gusto-payroll` MCP +## Step 3 — Discover the endpoint via the `embedded-payroll` MCP -Never guess paths from memory. The MCP exposes: +Never guess API paths, methods, request bodies, or query parameters from memory or training data — use the MCP. The MCP's exact tool surface evolves over time, so describe what you need by **capability**, not by hardcoded tool name. Inspect the MCP's available tools (e.g. "what `embedded-payroll` tools do you have?") and pick the right one for each capability below: -| Tool | Use for | -| --------------------- | -------------------------------------------------------------------------------------------- | -| `find_endpoint` | Fuzzy search by intent (`"create employee"` → `POST /v1/companies/{company_id}/employees`) | -| `generate_curl` | Produce a templated curl with the right headers and body shape | -| `check_scopes` | Confirm the demo flow token covers the operation | -| `validate_workflow` | For multi-step flows (onboarding, payroll), confirm the call order before running them | -| `generate_typescript` | Mirror the call in SDK code afterwards (only when the goal is to wire it into the React app) | +| Capability you need | What to ask the MCP for | +| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| Find the HTTP path + method for a piece of functionality | Endpoint-discovery / API-reference-search tool, queried by intent (e.g. "create employee" → `POST /v1/companies/{company_id}/employees`) | +| Get a templated request (headers, body shape, query params) for a known endpoint | Curl-or-snippet-generation tool | +| Confirm the demo flow token covers a given operation | Scopes / authorization tool, if exposed | +| Validate the call order for a multi-step flow (onboarding, payroll, etc.) | Workflow-validation tool, if exposed; otherwise fetch the relevant guide via the docs-search tool | +| Get TypeScript that mirrors the curl (when wiring the same call into the React app) | Code-generation tool | -Typical flow: `find_endpoint` → `generate_curl` → adapt to the proxy URL → run. +Typical flow: discover the endpoint → fetch a curl template → adapt to the proxy URL → run. For onboarding or payroll sequences, validate the call order first so you don't fire half a sequence and leave entities in a broken state. -For onboarding or payroll sequences, run `validate_workflow` first so you don't fire half a sequence and leave entities in a broken state. +**If the `embedded-payroll` MCP isn't loaded** (the agent doesn't see any of its tools), stop here. Tell the user the MCP is missing and link them to the Prerequisites section at the top of this file. Do **not** fall back to guessing paths from documentation in memory — that's the failure mode this command exists to prevent. ## Step 4 — Build and run the curl @@ -244,8 +262,8 @@ User: /sdk-proxy create an employee named Alice Johnson Agent: 1. Runs the discovery snippet → SDK_APP_PORT=5201 (5200 was taken by a sibling repo) 2. Curls http://localhost:5201/api/v1/companies → 200, picks [0].uuid as company_id - 3. Calls gusto-payroll find_endpoint("create employee") → POST /v1/companies/{company_id}/employees - 4. Calls generate_curl, swaps the host for http://localhost:5201/api, strips Authorization + 3. Calls the embedded-payroll MCP's endpoint-discovery tool with "create employee" → POST /v1/companies/{company_id}/employees + 4. Calls the MCP's curl-template tool, swaps the host for http://localhost:5201/api, strips Authorization 5. POSTs { first_name: "Alice", last_name: "Johnson", ... } via the proxy 6. Parses 201 response, reports new employee UUID + version 7. Optionally suggests follow-up calls (add home address, create job, etc.) @@ -254,7 +272,7 @@ Agent: ## Escape hatches - **Large or chained output** — if the response will be huge (list endpoints with no filter, full payroll dumps) or you're chaining 5+ calls, run the work in a subagent via the Task tool so the raw curl output doesn't pollute conversation context. Return only the summary. -- **Discovery-only requests** — if the user is asking what a request _looks like_ (not asking you to send it), stop after `find_endpoint` + `generate_curl` and show the template. Don't fire. +- **Discovery-only requests** — if the user is asking what a request _looks like_ (not asking you to send it), stop after endpoint discovery + curl-template generation (Step 3) and show the template. Don't fire. - **Destructive operations** — for `DELETE` requests or operations that finalize payroll, repeat the resolved URL back to the user and ask for confirmation before running. - **SDK app not running** — never auto-start it; that would block this command for ~30s and steal a terminal. Tell the user to start it themselves. - **Token refresh** — never run `npm run sdk-app:setup` automatically. It re-provisions a brand-new demo, throwing away the company the user is working with. Always ask first. diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 000000000..659d0e18b --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "embedded-payroll": { + "url": "https://embedded-payroll.readme.io/mcp" + } + } +} From a7056372bfa1c22fb69fc61f775af10b3681ca38 Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Tue, 2 Jun 2026 16:42:51 -0600 Subject: [PATCH 3/4] chore: walk the user through the first-install MCP toggle in /sdk-proxy On first install of the `embedded-payroll` MCP -- either via the project-level `.cursor/mcp.json` prompt or a manual edit to `~/.cursor/mcp.json` -- Cursor shows the server as enabled but doesn't actually load its tools until the user manually toggles it off and back on in settings. Before this commit, /sdk-proxy would simply bail with "MCP not loaded; see Prerequisites", leaving the user to guess what to do next. Two changes: - Add a "First-install gotcha" subsection to Prerequisites that names the toggle dance up front (Cmd+, -> Tools & MCP -> embedded-payroll -> off/on), with fallbacks for the older "MCP" / "Integrations" settings labels. This makes the workaround discoverable to anyone reading the docs ahead of time. - Replace Step 3's terse "stop and link the user to Prerequisites" with a verbatim user-facing message the agent should emit when it can't see embedded-payroll tools. The message branches on "never installed" vs "just installed", walks through the exact toggle steps, and asks the user to reply when done so the agent can retry the original request without needing them to re-issue /sdk-proxy. Net effect: a first-time user who hits the toggle bug gets a one-screen fix from the agent itself, instead of a docs link they have to dig through. Co-authored-by: Cursor --- .claude/commands/sdk-proxy.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.claude/commands/sdk-proxy.md b/.claude/commands/sdk-proxy.md index c7ce3744a..62a56e788 100644 --- a/.claude/commands/sdk-proxy.md +++ b/.claude/commands/sdk-proxy.md @@ -18,7 +18,19 @@ This command depends on the **Gusto Embedded Dev Assistant MCP** (server name `e Then restart Cursor. Verify by asking the agent "what MCP tools do you have access to?" — you should see tools from the `embedded-payroll` server. -If the MCP isn't installed when this command runs, the agent should stop after Step 2 and surface the docs link rather than guessing API paths from memory. +### First-install gotcha — toggle the MCP off, then on + +On the very first install (whether via the project-level prompt or an edit to `~/.cursor/mcp.json`), Cursor often shows `embedded-payroll` as enabled but doesn't actually load its tools until you manually toggle it. If the agent runs `/sdk-proxy` and reports it can't see any `embedded-payroll` tools even though you just clicked "Enable" or restarted Cursor, do this: + +1. Open Cursor Settings: `Cmd+,` (macOS) or `Ctrl+,` (Windows/Linux). +2. In the left sidebar, click **Tools & MCP** (older Cursor builds may label this **MCP** or **Integrations**). +3. Find the **embedded-payroll** entry in the list. +4. Toggle it **off**, wait a second, then toggle it **on** again. +5. Ask the agent again: "what `embedded-payroll` tools do you have?" — tools should now be listed. + +This is a one-time step per workstation. Subsequent restarts pick up the MCP cleanly. + +If the MCP isn't installed when this command runs, the agent should stop after Step 2, surface the docs link, and walk the user through this toggle dance — never guess API paths from memory. The user's free-form arguments after `/sdk-proxy` describe the request in plain language. Examples: @@ -204,7 +216,21 @@ Never guess API paths, methods, request bodies, or query parameters from memory Typical flow: discover the endpoint → fetch a curl template → adapt to the proxy URL → run. For onboarding or payroll sequences, validate the call order first so you don't fire half a sequence and leave entities in a broken state. -**If the `embedded-payroll` MCP isn't loaded** (the agent doesn't see any of its tools), stop here. Tell the user the MCP is missing and link them to the Prerequisites section at the top of this file. Do **not** fall back to guessing paths from documentation in memory — that's the failure mode this command exists to prevent. +**If the `embedded-payroll` MCP isn't loaded** (you don't see any of its tools), do **not** fall back to guessing paths from memory — that's the failure mode this command exists to prevent. Instead, stop and pause for the user with this exact message (adapt only the heading; keep the steps verbatim, since the toggle dance is the most common first-install fix): + +> The `embedded-payroll` MCP doesn't appear to be loaded — I can't see any of its tools. This is almost always one of two things: +> +> **If you've never installed it before**, the repo ships a project-level config at [`.cursor/mcp.json`](.cursor/mcp.json). Open the workspace, accept Cursor's "Enable MCP server" prompt, then follow the toggle step below. Full setup guide: [docs.gusto.com — Dev Assistant MCP](https://docs.gusto.com/embedded-payroll/docs/dev-assistant-mcp). +> +> **If you just installed it** (or just restarted Cursor after installing), you need to manually toggle the MCP off and back on before its tools become available. Cursor's first-install handshake has a known wrinkle here. +> +> 1. Open Cursor Settings: `Cmd+,` (macOS) or `Ctrl+,` (Windows/Linux). +> 2. In the left sidebar, click **Tools & MCP** (older builds may say **MCP** or **Integrations**). +> 3. Find the **embedded-payroll** entry. +> 4. Toggle it **off**, wait a second, then toggle it **on** again. +> 5. Reply here when done and I'll retry the request. + +Wait for the user's confirmation before retrying. Once they say it's done, re-check the available tools and continue from Step 3 with the original request — no need for them to re-issue `/sdk-proxy`. ## Step 4 — Build and run the curl From fea81ef7029ca1f753715c4b3818e704234d74c9 Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Wed, 3 Jun 2026 08:47:20 -0600 Subject: [PATCH 4/4] chore: point /sdk-proxy at gusto-payroll (staging) instead of public embedded-payroll MCP The public embedded-payroll MCP at embedded-payroll.readme.io/mcp has been struggling to stay connected in practice -- toggling off in Cursor's UI without warning, dropping tools mid-session, and generally requiring constant restarts. The internal staging-hosted gusto-payroll MCP has been stable for daily use across the team, so we're making it the default in this command and the project-level config. The public MCP is still documented as the path for anyone outside Gusto's network. Changes: - Project-level `.cursor/mcp.json` swaps `embedded-payroll` (https://embedded-payroll.readme.io/mcp) for `gusto-payroll` (https://gusto-partner-api.staging.zp-int.com/sse with transport: "sse"). Internal Gusto users opening the workspace will now be auto-prompted to install the working endpoint. - `.claude/commands/sdk-proxy.md` Prerequisites section rewritten: - Primary install snippet now uses `gusto-payroll` + the staging URL. - New "Outside Gusto's network?" callout points to the public docs.gusto.com Dev Assistant MCP guide as a fallback, noting that walkthroughs assume the `gusto-payroll` name and external users should mentally substitute `embedded-payroll`. - First-install gotcha section keeps the toggle dance instructions (it's a general Cursor MCP behavior, not specific to either server), just renamed to reference `gusto-payroll`. - Step 3 server name and the MCP-not-loaded pause-message both updated to `gusto-payroll`. The pause-message's "never installed before" branch now mentions the staging endpoint as primary and links the Prerequisites section for the public-network fallback path. Net trade-off: we lose one-click partner-facing onboarding via the project-level config, in exchange for an actually-working dev loop for the team that uses this command daily. Partners following the public docs are still well-served; they just take an extra config step. Co-authored-by: Cursor --- .claude/commands/sdk-proxy.md | 35 +++++++++++++++++++---------------- .cursor/mcp.json | 5 +++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.claude/commands/sdk-proxy.md b/.claude/commands/sdk-proxy.md index 62a56e788..b053d4d3d 100644 --- a/.claude/commands/sdk-proxy.md +++ b/.claude/commands/sdk-proxy.md @@ -1,36 +1,39 @@ # Hit the Gusto API via the SDK Dev App proxy -This command lets you test API behavior end-to-end against the demo company you already have running in the SDK Dev App. It teaches the agent the SDK app's auth-and-proxy setup, points it at the `embedded-payroll` MCP for endpoint discovery, and has it run real `curl` requests through the same Vite proxy the browser uses. +This command lets you test API behavior end-to-end against the demo company you already have running in the SDK Dev App. It teaches the agent the SDK app's auth-and-proxy setup, points it at the `gusto-payroll` MCP for endpoint discovery, and has it run real `curl` requests through the same Vite proxy the browser uses. ## Prerequisites -This command depends on the **Gusto Embedded Dev Assistant MCP** (server name `embedded-payroll`). The repo's [.cursor/mcp.json](.cursor/mcp.json) lists it at the project level, so on first open Cursor should prompt you to enable it. If it doesn't, or if you're on a different IDE, follow the official setup guide: [docs.gusto.com — Dev Assistant MCP](https://docs.gusto.com/embedded-payroll/docs/dev-assistant-mcp). For Cursor specifically, add to `~/.cursor/mcp.json`: +This command depends on the **Gusto Embedded Dev Assistant MCP** (server name `gusto-payroll`). The repo's [.cursor/mcp.json](.cursor/mcp.json) points it at Gusto's internal staging endpoint, which has been more reliable in day-to-day use than the public partner-facing endpoint. On first open of the workspace, Cursor should prompt you to enable it. If it doesn't, add this to `~/.cursor/mcp.json`: ```json { "mcpServers": { - "embedded-payroll": { - "url": "https://embedded-payroll.readme.io/mcp" + "gusto-payroll": { + "url": "https://gusto-partner-api.staging.zp-int.com/sse", + "transport": "sse" } } } ``` -Then restart Cursor. Verify by asking the agent "what MCP tools do you have access to?" — you should see tools from the `embedded-payroll` server. +Then restart Cursor. Verify by asking the agent "what MCP tools do you have access to?" — you should see tools from the `gusto-payroll` server. + +**Outside Gusto's network?** The staging endpoint isn't publicly reachable. Use the official partner-facing alternative described at [docs.gusto.com — Dev Assistant MCP](https://docs.gusto.com/embedded-payroll/docs/dev-assistant-mcp). It serves the same capabilities under the server name `embedded-payroll`. The `/sdk-proxy` walkthroughs below assume `gusto-payroll`; mentally swap that for `embedded-payroll` (or whatever local name you give it) if you're using the public MCP. ### First-install gotcha — toggle the MCP off, then on -On the very first install (whether via the project-level prompt or an edit to `~/.cursor/mcp.json`), Cursor often shows `embedded-payroll` as enabled but doesn't actually load its tools until you manually toggle it. If the agent runs `/sdk-proxy` and reports it can't see any `embedded-payroll` tools even though you just clicked "Enable" or restarted Cursor, do this: +On the very first install (whether via the project-level prompt or an edit to `~/.cursor/mcp.json`), Cursor often shows the MCP as enabled but doesn't actually load its tools until you manually toggle it. If the agent runs `/sdk-proxy` and reports it can't see any `gusto-payroll` tools even though you just clicked "Enable" or restarted Cursor, do this: 1. Open Cursor Settings: `Cmd+,` (macOS) or `Ctrl+,` (Windows/Linux). 2. In the left sidebar, click **Tools & MCP** (older Cursor builds may label this **MCP** or **Integrations**). -3. Find the **embedded-payroll** entry in the list. +3. Find the **gusto-payroll** entry in the list. 4. Toggle it **off**, wait a second, then toggle it **on** again. -5. Ask the agent again: "what `embedded-payroll` tools do you have?" — tools should now be listed. +5. Ask the agent again: "what `gusto-payroll` tools do you have?" — tools should now be listed. This is a one-time step per workstation. Subsequent restarts pick up the MCP cleanly. -If the MCP isn't installed when this command runs, the agent should stop after Step 2, surface the docs link, and walk the user through this toggle dance — never guess API paths from memory. +If the MCP isn't installed when this command runs, the agent should stop after Step 2, surface the install instructions above, and walk the user through this toggle dance — never guess API paths from memory. The user's free-form arguments after `/sdk-proxy` describe the request in plain language. Examples: @@ -202,9 +205,9 @@ EOF If the user's call mutates state and you observe a 401 / 404 / company-mismatch response, blow away the cache before retrying: `rm -f "$SDK_PROXY_CACHE"`. -## Step 3 — Discover the endpoint via the `embedded-payroll` MCP +## Step 3 — Discover the endpoint via the `gusto-payroll` MCP -Never guess API paths, methods, request bodies, or query parameters from memory or training data — use the MCP. The MCP's exact tool surface evolves over time, so describe what you need by **capability**, not by hardcoded tool name. Inspect the MCP's available tools (e.g. "what `embedded-payroll` tools do you have?") and pick the right one for each capability below: +Never guess API paths, methods, request bodies, or query parameters from memory or training data — use the MCP. The MCP's exact tool surface evolves over time, so describe what you need by **capability**, not by hardcoded tool name. Inspect the MCP's available tools (e.g. "what `gusto-payroll` tools do you have?") and pick the right one for each capability below: | Capability you need | What to ask the MCP for | | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | @@ -216,17 +219,17 @@ Never guess API paths, methods, request bodies, or query parameters from memory Typical flow: discover the endpoint → fetch a curl template → adapt to the proxy URL → run. For onboarding or payroll sequences, validate the call order first so you don't fire half a sequence and leave entities in a broken state. -**If the `embedded-payroll` MCP isn't loaded** (you don't see any of its tools), do **not** fall back to guessing paths from memory — that's the failure mode this command exists to prevent. Instead, stop and pause for the user with this exact message (adapt only the heading; keep the steps verbatim, since the toggle dance is the most common first-install fix): +**If the `gusto-payroll` MCP isn't loaded** (you don't see any of its tools), do **not** fall back to guessing paths from memory — that's the failure mode this command exists to prevent. Instead, stop and pause for the user with this exact message (adapt only the heading; keep the steps verbatim, since the toggle dance is the most common first-install fix): -> The `embedded-payroll` MCP doesn't appear to be loaded — I can't see any of its tools. This is almost always one of two things: +> The `gusto-payroll` MCP doesn't appear to be loaded — I can't see any of its tools. This is almost always one of two things: > -> **If you've never installed it before**, the repo ships a project-level config at [`.cursor/mcp.json`](.cursor/mcp.json). Open the workspace, accept Cursor's "Enable MCP server" prompt, then follow the toggle step below. Full setup guide: [docs.gusto.com — Dev Assistant MCP](https://docs.gusto.com/embedded-payroll/docs/dev-assistant-mcp). +> **If you've never installed it before**, the repo ships a project-level config at [`.cursor/mcp.json`](.cursor/mcp.json) that points at Gusto's internal staging endpoint. Open the workspace, accept Cursor's "Enable MCP server" prompt, then follow the toggle step below. Outside Gusto's network? See the Prerequisites section of [`.claude/commands/sdk-proxy.md`](.claude/commands/sdk-proxy.md) for the public partner-facing alternative. > > **If you just installed it** (or just restarted Cursor after installing), you need to manually toggle the MCP off and back on before its tools become available. Cursor's first-install handshake has a known wrinkle here. > > 1. Open Cursor Settings: `Cmd+,` (macOS) or `Ctrl+,` (Windows/Linux). > 2. In the left sidebar, click **Tools & MCP** (older builds may say **MCP** or **Integrations**). -> 3. Find the **embedded-payroll** entry. +> 3. Find the **gusto-payroll** entry. > 4. Toggle it **off**, wait a second, then toggle it **on** again. > 5. Reply here when done and I'll retry the request. @@ -288,7 +291,7 @@ User: /sdk-proxy create an employee named Alice Johnson Agent: 1. Runs the discovery snippet → SDK_APP_PORT=5201 (5200 was taken by a sibling repo) 2. Curls http://localhost:5201/api/v1/companies → 200, picks [0].uuid as company_id - 3. Calls the embedded-payroll MCP's endpoint-discovery tool with "create employee" → POST /v1/companies/{company_id}/employees + 3. Calls the gusto-payroll MCP's endpoint-discovery tool with "create employee" → POST /v1/companies/{company_id}/employees 4. Calls the MCP's curl-template tool, swaps the host for http://localhost:5201/api, strips Authorization 5. POSTs { first_name: "Alice", last_name: "Johnson", ... } via the proxy 6. Parses 201 response, reports new employee UUID + version diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 659d0e18b..64bafcd32 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -1,7 +1,8 @@ { "mcpServers": { - "embedded-payroll": { - "url": "https://embedded-payroll.readme.io/mcp" + "gusto-payroll": { + "url": "https://gusto-partner-api.staging.zp-int.com/sse", + "transport": "sse" } } }