Production & Operations
Trust boundaries, output caps, reconnection behavior, best-effort notifications, and honest versioning for MCP servers running for real
Production & Operations
TL;DR
An MCP server that works in a five-minute local demo has not yet faced the things that break it in production: untrusted third-party servers, an uncapped tool response, a stdio process that silently stopped reconnecting, a notification that never arrived, or a protocol version bump that breaks an already-connected client. None of these are exotic — they're the normal operating conditions of a real deployment.
| Property | Value |
|---|---|
| Level | Intermediate |
| Reading time | ~20 minutes |
| Prerequisites | MCP Agents & Tool Design |
| You will understand | The operational habits that separate a working demo server from one safe to run unattended |
The Areas That Matter
| Area | The thing you must get right |
|---|---|
| Trust | Treat every third-party server as untrusted by default — same posture as any API you don't control |
| Output size | Cap tool responses explicitly; an unbounded one is a silent context-budget risk |
| Reconnection | Remote and stdio servers behave differently on disconnect — design for both, deliberately |
| Notifications | Delivery is best-effort — poll to backstop anything you can't afford to miss |
| Versioning | Advertise supported versions honestly; a mismatch is a normal, handled case, not a crisis |
| Timeouts | Idle timeout budget should match the tool's actual expected duration |
Treat Every Third-Party Server as Untrusted
The same posture you'd take toward a third-party API you don't operate applies directly to MCP servers, and for the same reason: you don't control what it returns, and you can't fully verify what it does internally.
What 'untrusted by default' means concretely
Least-privilege OAuth scopes
RecommendedRequest only the scopes a server's tools actually need for the operations you intend to perform — not the broadest scope available "in case it's needed later." A narrower token limits the blast radius if a server or its response is ever compromised.
Sandbox where output could be adversarial
RecommendedA tool result that could plausibly contain crafted content (fetched web pages, user-submitted files, output from a server you don't operate) shouldn't flow unchecked into a pipeline with further automated actions downstream. Validate or sandbox before that content can trigger another tool call.
Reviewing tool descriptions once, at connection time only
A server's tool descriptions can change on a later tools/list — treating a one-time review as sufficient forever misses that. Re-review after any list_changed notification from a server you don't fully trust, not just before the first connection.
This is the operational half of the trust boundary introduced in MCP Agents & Tool Design — that page covers the design decision; this section covers enforcing it continuously once the server is live.
Cap Tool Output Size
An MCP tool can return arbitrarily large content — there's nothing in the protocol that limits it. Left uncapped, a single call can consume a disproportionate share of a host's context budget, silently.
This isn't theoretical — it's exactly why Claude Code enforces a default. Claude Code caps MCP tool output at 25,000 tokens per call by default, configurable via the MAX_MCP_OUTPUT_TOKENS environment variable. If you're building your own host or client, adopt the same discipline: decide an output cap deliberately, rather than discovering the problem when one large tool result crowds out everything else in a conversation.
# Raise Claude Code's default cap for a server known to return large results
export MAX_MCP_OUTPUT_TOKENS=50000If you're building the server side, the better fix is often upstream of any cap: paginate large result sets, summarize instead of returning raw data, or expose a resource template that lets the caller ask for a bounded slice (logs://incident/{id}?tail=200) instead of everything at once.
Reconnection: Remote vs stdio
The two transports behave differently when a connection drops, and a server or client built assuming one will misbehave under the other.
What happens when the connection drops
Remote (Streamable HTTP)
Reconnects automatically with exponential backoff. A transient network blip or a server restart is usually invisible to whoever's using the host — the client keeps retrying until it succeeds.
stdio
Does not auto-reconnect. If the local server process dies, the connection is gone until something explicitly restarts it. A long-running host session with a stdio server that crashed partway through can silently lose that server for the rest of the session.
Design a stdio server's expected lifecycle accordingly: it should either be robust enough not to crash mid-session, or the host/client wrapping it needs its own restart logic, since the protocol itself won't provide one. A remote server gets that resilience for free from the transport; a stdio server has to earn it.
Notifications Are Best-Effort, Not Guaranteed
The spec is explicit about this: "there are no guarantees that every notification will be sent or received, particularly across transport reconnects. Clients should also rely on polling to preserve freshness of results." A system that assumes a notifications/tools/list_changed or notifications/resources/updated event will always arrive has a real gap — not a rare edge case, a documented protocol property.
Treat any subscription-driven update as an optimization — get the change faster when it works — not as the sole mechanism for anything you can't afford to miss. If a client genuinely needs to know a resource changed, poll it periodically as a backstop, and let the notification stream shorten the average delay rather than being the only path to correctness.
Correct: poll resources/list every N minutes + react faster when a notification arrives
Incorrect: wait for notifications/resources/updated ← the only source of truthVersion Your Server Honestly
A server's server/discover response advertises supportedVersions — the protocol versions it accepts. This is the mechanism that lets a server evolve without breaking every client that's already connected to it.
{
"result": {
"supportedVersions": ["2026-07-28", "2026-01-15"],
"capabilities": { "tools": {}, "resources": {} }
}
}If a client requests a version the server no longer supports, it gets an UnsupportedProtocolVersionError listing what is supported, and the well-behaved client retries with a mutually agreeable version.
Treat UnsupportedProtocolVersionError as a normal, handled case during a migration window — not an outage. Supporting two or three recent protocol versions simultaneously, and giving real clients time to update, is what makes a breaking spec change (or a breaking change to your own server's contract) survivable instead of a coordinated flag day everyone has to hit at once.
Idle Timeouts Should Match the Work
Every long-running tool call needs a timeout budget that fits what it's actually supposed to do — too short, and legitimate slow operations get killed; too long, and a hung call ties up resources indefinitely.
# Claude Code's defaults: 5 minutes for HTTP/SSE/WebSocket, 30 minutes for stdio
export CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=1800000 # 30 minutes, in msA tool that kicks off a genuinely long operation (a large re-index, a slow external API) either needs a timeout budget sized for that reality, or — better — should use the Tasks extension to return a durable handle immediately and let the caller poll for completion, rather than holding a single request open for the full duration.
Concept Checks
Check yourself
Why does 'treat every server as untrusted' need to be an ongoing practice, not a one-time check?
Because a server's tool descriptions and behavior can change after you've connected — a later tools/list call, following a list_changed notification, can return different descriptions than the ones you originally reviewed. A one-time review at connection time misses anything that changes afterward, so the trust posture has to be re-applied continuously, not just at the start.
Why does a stdio server need different resilience design than a remote server?
Because remote (Streamable HTTP) connections reconnect automatically with exponential backoff on the transport level, while stdio connections do not auto-reconnect at all — if the local process dies, the connection stays dead until something explicitly restarts it. A stdio server either needs to avoid crashing, or whatever wraps it needs its own restart logic, since the protocol provides none.
A client relies solely on notifications/resources/updated to know when to re-fetch a resource. What's the risk, per the spec?
Notification delivery is explicitly best-effort, with no guarantee every notification is sent or received — especially across a transport reconnect. A client depending solely on notifications can silently miss updates and serve stale data indefinitely; the spec's own guidance is to poll as a backstop, using notifications only to shorten the average delay, not as the only source of freshness.
Key Concepts Recap
| Concept | One-line summary |
|---|---|
| Untrusted by default | Least-privilege scopes, sandboxed adversarial output, ongoing re-review after list_changed |
| Cap tool output | 25,000 tokens is Claude Code's default; decide your own cap deliberately regardless of host |
| Remote vs stdio reconnection | Remote auto-reconnects with backoff; stdio does not — design each accordingly |
| Notifications are best-effort | Poll as a backstop for anything you can't afford to miss |
supportedVersions and UnsupportedProtocolVersionError | The normal mechanism for a server to evolve without breaking connected clients |
| Idle timeouts match the work | Size the budget to the tool's real duration, or use the Tasks extension for genuinely long operations |
Next
Even with good operational discipline, things still break. Here's how to trace a symptom back to its real cause: Failure Modes & Debugging.
MCP Agents & Tool Design
What changes about agent tool design once tools come from MCP servers you don't fully control, and how to keep a federated tool list usable
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