Appendix E: Troubleshooting Guide
Find your symptom in the table, then jump to its section.
Fast Triage Table
| Symptom | Check first | Usual causes | Escalate to |
|---|---|---|---|
| Server will not start | Runtime version, dependency install, configured command | Broken local environment, bad entrypoint, stdout pollution | Build or runtime owner |
| Client cannot connect | Command path or URL, Origin/Host, proxy | Wrong config, rejected origin, proxy stripping headers | Client integration or platform owner |
| Tools are missing or look wrong | server/discover response, _meta.protocolVersion, tools/list | Version mismatch, undeclared client capability, malformed schema | Server owner |
| Auth fails | WWW-Authenticate, resource metadata, token audience | Expired token, bad discovery, claim mapping bug | Auth owner |
| Requests hang or time out | Readiness, dependency health, retry loop, proxy timeouts | Slow dependency, buffering proxy, model loop | Service owner |
| One tenant sees another tenant’s data | Identity binding, cache keying, task routing | Scope bug, shared cache issue, weak downstream checks | Security and platform owner |
| A change broke working clients | Catalog diff since the last release | Renamed tool, changed parameter meaning | Server owner |
Start With the Smallest Reproduction
Test the domain logic directly, then the MCP surface in the Inspector, then one real client, and only then cross-client variance. Narrow until one layer is obviously responsible.
Server Will Not Start
Check the runtime version, dependencies in the environment the client launches, an absolute command path, required environment variables, and stdout: on stdio one print() breaks the connection.
What you’ll see (the SDK client, when the server printed a banner):
Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='starting notes server', input_type=str]
Confirm it: send one request and print every stdout line that isn’t JSON-RPC. Any output means stdout is polluted.
echo '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' \
| uv run server.py 2>/dev/null | grep -v '^{"jsonrpc"'
Client Cannot Connect
Check that the server stays healthy outside the client, that the URL is canonical, and that the proxy passes the MCP headers through unchanged.
What you’ll see: Invalid Origin header with HTTP 403 (a browser-mediated client whose origin isn’t allowed), Invalid Host header with 421 (a proxy forwarding the public Host to a loopback-bound server; set allowed_hosts, Chapter 7), or -32020 mcp-method header does not match the request body's method (a proxy rewrote one and not the other).
Confirm it:
npx @modelcontextprotocol/inspector --server-url https://notes.example.com/mcp
Tools Are Missing or Look Wrong
In 2026-07-28 there’s no handshake: each request states its version and capabilities in _meta (Appendix A). Check what server/discover advertises, whether the client surfaces the primitive at all, and whether descriptions are specific enough to be chosen. An interface too weak to use well is still a defect.
What you’ll see: -32022 Unsupported protocol version with data.supported (the client speaks a revision the server doesn’t), -32021 with data.requiredCapabilities (a tool needs, say, elicitation the client didn’t declare), or Unknown tool: … as an isError result (the client cached an old catalog).
Confirm it:
curl -s https://notes.example.com/mcp -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
Auth Fails
A 401 means authentication is missing or invalid; a 403 means the principal is known but not entitled. Don’t treat all auth failures as “bad token”: many are really identity-mapping bugs.
What you’ll see: a 401 whose WWW-Authenticate has no resource_metadata (discovery can’t start), or a resource in the metadata that differs from the URL the client used (a 401 loop). The correct bytes are in Appendix B.
Confirm it:
curl -si -X POST https://mcp.example.com/mcp | grep -i www-authenticate
curl -s https://mcp.example.com/.well-known/oauth-protected-resource/mcp
Requests Hang or Time Out
Check dependencies, tool count, response size, retries, and whether a proxy buffers text/event-stream or times out first. Slowness is often an interface problem disguised as an infrastructure problem.
What you’ll see: the same args_hash repeating for one principal in the tool-call log (Chapter 23), or streamed progress arriving all at once at the end.
Confirm it: curl -N the endpoint with a slow tool and watch whether events arrive incrementally.
One Tenant Sees Another Tenant’s Data
High severity until proven otherwise. Check tenant resolution, cache keys, background tasks and downstream checks; if you can’t answer quickly, contain first (Chapter 21).
What you’ll see: a tool-call event whose tenant differs from the token’s tenant claim.
Confirm it: query the event log for tenant != token_tenant over the incident window.
A Change Broke Working Clients
Look for renamed tools, changed parameter meaning, tightened approvals, a moved URL, and schema fields prompts relied on. “Small cleanup” changes cause most of these.
What you’ll see: Unknown tool: search after a rename, or a sudden drop in one tool’s selection share by catalog_version.
Confirm it: diff the tools/list output of the last good release against the current one.
Evidence to Capture Before Escalation
- exact client, client version and environment
- timestamp and trace ID
- tool or resource involved
- auth state or error code
- whether the problem is reproducible
- the last known-good comparison point