Failure Modes & Debugging
Symptom to cause across discovery, schemas, transport, and auth — the MCP bugs that show up again and again, and the fix for each
Failure Modes & Debugging
TL;DR
Most confusing MCP bugs trace back to one of four layers: discovery/capability negotiation, schema/arguments, transport/auth, or a genuinely untrusted server doing something adversarial. Identify which layer a symptom points to first — the fix for each is completely different, and guessing wrong wastes real time.
| Property | Value |
|---|---|
| Level | Intermediate |
| Reading time | ~20 minutes |
| Prerequisites | Production & Operations |
| You will understand | How to trace an MCP symptom back to its real cause, across every layer of the protocol |
Triage First
Before debugging a specific symptom, ask which layer it points to. The four layers below fail independently, and diagnosing the wrong one wastes time — checking an OAuth config when the real problem is a malformed tools/list response, for instance, tells you nothing.
Does the server's capability even show up (tools, resources, prompts)?
│
├── No → discovery / capability negotiation problem
│
└── Yes, but calling it fails
│
├── Rejected before it runs (schema error) → schema/arguments problem
│
├── Never reaches the server at all → transport/auth problem
│
└── Runs, but the result looks wrong or manipulative → untrusted-server problemSymptom to Cause
| Symptom | Usual cause |
|---|---|
| Server's tools never appear in the host | Malformed tools/list response, or the client's own _meta.clientCapabilities never declared support for that primitive |
| Tools appear, but calling one fails immediately | A server/discover version mismatch the client didn't handle gracefully |
| Tool call rejected with a schema validation error | The model's arguments don't match the declared inputSchema — usually an ambiguous or too-loose schema, not a "dumb model" problem |
| OAuth flow loops forever, never completes | Missing or wrong resource parameter (RFC 8707) — the issued token isn't scoped to the right server |
| A token that works for one MCP server is rejected by another | Expected behavior — audience binding is working correctly, not a bug |
| Notifications seem to silently stop arriving | Best-effort delivery, especially across a reconnect — not a client bug, a documented protocol property |
| A single tool call blows the host's context budget | Output size uncapped — see Production & Operations |
| A stdio server "just stops working" after a while | Expected — stdio doesn't auto-reconnect on a dropped connection, unlike remote servers |
| Works locally over stdio, breaks when redeployed remotely | stdio-era assumptions (single client, implicit trust of the local environment) carried into a multi-client, authenticated Streamable HTTP deployment |
Discovery and Capability Negotiation
Mechanism: A client only attempts operations for capabilities it declared support for in its own _meta.clientCapabilities, and only for capabilities the server actually advertised in its server/discover response. If either side is silent about a capability, the other treats it as unsupported — there's no implicit "try it and see."
// Client that never declared elicitation support
"clientCapabilities": { "tools": {} }
// A server offering an elicitation-dependent tool will not be usable —
// the client has no declared way to handle the elicitation/create round tripFix: When a server's tools/resources/prompts don't show up, check both sides of the negotiation — what the server's server/discover response actually advertises, and what the client declared it can handle. A mismatch on either side produces the same silent "nothing appeared" symptom.
Schema and Argument Mismatches
Mechanism: tools/call arguments are checked against the tool's declared inputSchema. A rejection here is a contract problem, not an intelligence problem — if the schema is ambiguous (a loosely-typed field, an unclear enum, a missing required list), the model has genuinely insufficient information to construct valid arguments reliably.
// Ambiguous: is "date" a specific format? Required or optional?
{ "date": { "type": "string" } }
// Fixed: format and requirement are explicit
{ "date": { "type": "string", "format": "date", "description": "YYYY-MM-DD" } },
"required": ["date"]Fix: Tighten the schema before assuming the calling model needs to be "smarter." A precise inputSchema with explicit formats, enums, and a complete required list resolves the large majority of argument-validation failures.
Authorization Loops and Token Confusion
Mechanism: Per Authorization & Security, an access token is requested for one specific server via the resource parameter (RFC 8707), and the server validates the token was issued specifically for it. Two distinct problems produce similar-looking symptoms:
Two different auth failures
Auth flow never completes
Usually a missing or incorrect resource parameter — the client requested a token without properly scoping it to the target server, so the authorization server either rejects the request or issues a token the MCP server won't accept, and the client retries the whole flow without ever succeeding.
A token works for server A, rejected by server B
This is not a bug — it's RFC 8707 audience binding doing its job. A token is valid only for the resource it was requested for; sending it to a different server should fail, and does. The fix is requesting (and using) a separate token per server, not trying to make one token universal.
Fix: For a genuinely stuck flow, check the resource parameter is present and correct on both the authorization and token requests. For "rejected by a different server," there's nothing to fix — request a token for that specific server instead.
Transport-Specific Assumptions That Don't Survive Redeployment
Mechanism: A server built and tested only over stdio can accumulate assumptions that are true locally but false once it's redeployed remotely over Streamable HTTP: that it only ever serves one client at a time, that credentials can come from the local environment implicitly, that there's no need to handle concurrent requests.
| Local (stdio) assumption | Breaks how, once remote |
|---|---|
| "I only serve one client" | Multiple clients now connect concurrently; shared mutable state races |
| "Credentials come from the environment" | No shared local environment anymore — needs real OAuth per Authorization & Security |
| "No auto-reconnect logic needed" | Remote clients do auto-reconnect — the server needs to behave correctly when the same client reconnects mid-operation |
Fix: Treat "will this run remotely eventually" as a design question from the start, even for a server currently running over stdio — retrofitting multi-client and auth assumptions after the fact is significantly more work than building for them from the outset.
Concept Checks
Check yourself
A server's tools never show up in a host. What are the two independent things to check, and why does checking only one sometimes miss the cause?
Both the server's server/discover capability advertisement and the client's own declared _meta.clientCapabilities — a capability only works if both sides agree it's supported. Checking only the server side misses cases where the server is advertising correctly but the client never declared it can handle that capability, which produces the identical symptom of "nothing appears."
A token issued for one MCP server is rejected when sent to a different one. Is this a bug to fix?
No — this is RFC 8707 audience binding working as intended. Tokens are scoped to the specific server they were requested for via the resource parameter, and a server correctly rejects tokens that weren't issued for it. The fix, if one is needed, is requesting a separate properly-scoped token for the second server, not treating the rejection as an error.
Why can a schema validation error be a documentation problem rather than a model-capability problem?
Because tools/call arguments are checked against the declared inputSchema, and an ambiguous schema (missing format, unclear required fields, loose typing) gives the model genuinely insufficient information to construct valid arguments reliably. Tightening the schema — adding explicit formats, enums, and a complete required list — often resolves the failure without the model needing to be any more capable.
Key Concepts Recap
| Concept | One-line summary |
|---|---|
| Triage by layer first | Discovery, schema, transport/auth, or untrusted-server — each needs a different fix |
| Capability negotiation is two-sided | Both client and server must declare a capability for it to work |
| Schema errors are usually schema problems | Tighten inputSchema before assuming the model is at fault |
| Cross-server token rejection is correct | RFC 8707 audience binding, not a bug |
| Notifications missing ≠ client bug | Best-effort delivery is a documented protocol property |
| stdio-era assumptions don't survive redeployment | Design for multi-client and real auth from the start if remote deployment is ever likely |
Next
With the failure modes catalogued, the next page walks through one complete, worked design that gets these decisions right from the start: Designing an MCP-Based System.
Production & Operations
Trust boundaries, output caps, reconnection behavior, best-effort notifications, and honest versioning for MCP servers running for real
Designing an MCP-Based System
A complete worked example — an internal developer-tools MCP server, reachable by engineers, a shared team config, and an on-call agent