Skip to content

Troubleshooting

The resource you requested doesn’t exactly match what’s registered in Keycard. Keycard does exact string matching, so for the MCP server that means the full URL including /mcp, and for Supabase it means the project URL with no trailing slash. The port has to match your local server too.

Section titled “Tool calls fail with “User consent is required.””

Your grant is older than one of the application’s dependencies. This happens if a resource was added to the app after you signed in: the grant you approved doesn’t cover the new resource, so exchanges for it are refused. The fix is to re-run the sign-in from Chapter 3 (in Claude Code, /mcp → re-authenticate; for mcp-remote, clear its cache under ~/.mcp-auth and reconnect). The refreshed grant covers the app’s current dependency list. If you did Chapter 1 in order, you’ll never see this one, because all four dependencies existed before your first sign-in.

Escalation fails after login, or the issue lands in the wrong Linear workspace

Section titled “Escalation fails after login, or the issue lands in the wrong Linear workspace”

Linear ties each authorization to the workspace active in your browser at consent time; there’s no workspace parameter you can set from the app or .env. Keycard records that choice as a grant and the agent caches the resulting token, so it silently reuses the wrong workspace on every later call, and escalate_ticket fails with “team not found” because your LINEAR_TEAM_ID belongs to a different workspace. To fix: revoke the Linear grant in console.keycard.ai (Grants), switch to the correct workspace in Linear, then re-run the Chapter 3 sign-in to re-consent against the right one.

Working as designed; that’s Chapter 2 doing its job. If it’s your authenticated agent getting the 401, its cached token may have expired: trigger the sign-in again and it’ll mint a fresh one.

Re-check the MCP endpoint URL and that the server is actually running. Then check that something else isn’t squatting on the port: lsof -nP -iTCP:8000 -sTCP:LISTEN. A leftover process from another project can answer on [::]:8000 while your server holds the IPv4 side (or vice versa), which produces deeply confusing half-working behavior. Kill the stray, restart npm run dev.

The console shows it exactly once, at creation. Nothing is broken: open your application’s Application Credentials tab, delete the credential, and add a new Client ID & Secret. Update both values in .env and restart the server.

Exchanges keep failing even after you fixed the cause

Section titled “Exchanges keep failing even after you fixed the cause”

If token exchange failed once (a typo’d KEYCARD_URL, a Wi-Fi blip while reaching Keycard) and now every exchange fails the same way even though the cause is gone, restart the server (Ctrl+C, npm run dev). The exchange client caches its first discovery attempt for the life of the process, including a failed one. Restarting clears it. As a rule of thumb, restart after any .env change anyway; the file is only read at startup.

get_support_tickets returns empty or errors after the Chapter 4 switch

Section titled “get_support_tickets returns empty or errors after the Chapter 4 switch”

Three usual suspects, in order: SUPABASE_URL doesn’t exactly match the vault resource identifier in Keycard (exchange fails), the vault resource has no credential stored (the instructor’s step, or yours if self-paced), or your own Supabase project was never seeded (run data/seed.sql from the mcp-server/ reference folder in the SQL Editor).

Audit log shows a scope that doesn’t match your code

Section titled “Audit log shows a scope that doesn’t match your code”

If a token exchange in the audit log requests a different scope than your src/linear.ts source says (e.g. issues:create for the label lookup when the code reads read), your server is running stale compiled output, not your source.

npm run dev runs your TypeScript directly and reloads on every save, so it’s always fresh. Running node dist/server.js (or npm start) serves an old build that doesn’t reflect your edits. Stop any such process and restart with:

Command line
npm run dev

If restarting does’t help, a previous server may still be holding the port. Look for EADDRINUSE in the output, kill the old process, and run npm run dev again.

delete_issue still succeeds after Chapter 7

Section titled “delete_issue still succeeds after Chapter 7”

Two usual causes, in order. First: the new policy set was published but never activated. A candidate version decides nothing; check PoliciesPolicy Sets and confirm your set wears the Active badge, at the version that contains your cap. Second, and sneakier: the cap was written as a permit instead of a forbid. Cedar allows a request when any permit matches, and default-app-delegation permits every on-behalf exchange, so a permit-shaped “cap” sitting next to it never denies anything. Permits can’t subtract; the chapter’s policy is a forbid for exactly this reason.

Your new policy set is missing the platform defaults. Activating a set replaces the previous set wholesale, so a set containing only your cap leaves Keycard with no permits at all, and Cedar denies anything no permit allows. Edit your set (create a new version), add default-user-grants, default-app-direct-access, and default-app-delegation alongside limit-linear-scopes, publish, and activate.

The cap is over-matching. Check three things in the policy: the allowlist spells read and issues:create exactly (containsAll compares exact strings); the containsAll direction is [allowlist].containsAll(context.scopes) (flipping the operands inverts the meaning); and the resource.identifier pin is present and exactly https://api.linear.app. Without the pin, the cap reaches past Linear to your other resources’ delegated exchanges and denies scopes they legitimately request.

Section titled “Linear shows “Denied by policy” on the consent screen after Chapter 7”

The scopes your server advertises in scopesSupported (Chapter 2) ride the consent chain to your app’s dependencies, Linear included — and your Chapter 7 cap allows only read and issues:create. If you advertise anything else (the old mcp:tools, say), that scope reaches Linear at login and the cap denies it, so Linear can’t be consented and sign-in fails. Advertise exactly the Linear scopes your tools use: scopesSupported: ["read", "issues:create"].

escalate_ticket fails with “PII masking failed: Not Found” after Chapter 5

Section titled “escalate_ticket fails with “PII masking failed: Not Found” after Chapter 5”

The masking call is hitting https://api.anthropic.com/messages (no /v1), which 404s. The @ai-sdk/anthropic provider falls back to an ambient ANTHROPIC_BASE_URL environment variable when one is set, and the shell a coding agent launches the server from often sets one without the /v1 path (Claude Code does). The fix is in the chapter’s code: pass the explicit baseURL to createAnthropic, alongside the exchanged key. If you wrote the chapter by hand and skipped it, that’s the line you’re missing.