{"slug": "what-breaks-mcp-servers-in-production-mcp-best-practicies", "title": "What Breaks MCP Servers in Production (MCP Best Practicies)?", "summary": "A team running multiple MCP servers in production identified common failures including unactionable errors, forced re-logins, disappearing tools, and oversized responses. They compiled their fixes into a standard with a release-gate checklist, emphasizing error messages as part of the API, token refresh rotation issues, and timeout contracts.", "body_md": "Our team runs several MCP servers in production. Two of them are remote multi-user servers. The first is an analytics gateway: a semantic layer for company data, where metrics have one standardized definition, one tested way to compute them, and role-based access. Anyone in the company can ask \"which sources brought the most revenue last week\" and get the same verified numbers, instead of requesting data access for the hundredth time or reinventing the calculation in a spreadsheet. The second is AI-Lens, an analytics tool for AI coding assistants.\n\nRunning these servers for real users surfaced a set of problems that, I suspect, haunt every company-wide MCP deployment: errors the model can't act on, users randomly kicked into re-login, tools silently disappearing from clients, responses blowing past size limits. None of them show up in a demo. All of them show up in week two of production.\n\nEach time we debugged one, the fix turned into a rule. Eventually the rules became a standard: [github.com/r-ms/mcp-server-standard](https://github.com/r-ms/mcp-server-standard) — a normative document with a release-gate checklist, published under CC BY 4.0.\n\nBackend error handling usually assumes a human will read the message and fix the input. An MCP server has a different reader, and this reader makes values up. Ask the model for a table name and it will confidently produce one that almost exists. It also retries automatically, so the only question is whether your error gives it enough to retry correctly.\n\n**An error message is part of the server's API.** It deserves the same design attention as the tool schema.\n\nSo when a parameter misses, enumerate what is valid. Wrong table name returns `available_tables`\n\nplus a fuzzy \"did you mean\", and the hallucinated value becomes a corrected call on the next turn instead of a dead end. This single pattern has saved more round trips than anything else in the standard.\n\nReturn the whole error at once, too. A response like `error_code: 12345`\n\nwith a separate `get_error(code)`\n\nlookup is a design a human tolerates and a model wastes turns on. Everything needed for self-correction goes into the error itself: `isError: true`\n\ninside HTTP 200, a structured hint, concrete next steps. `QUERY_EXHAUSTED_RESOURCES: add a LIMIT, use APPROX_DISTINCT, or filter by partition`\n\nproduces a corrected query on the next attempt; a plain 504 produces a blind retry of the same one.\n\nBetter still, prevent the miss before it happens: put your reference lists (tables, sites, whatever your domain enumerates) into the tool descriptions or the server's `instructions`\n\n. The specifics differ per server; the principle doesn't. The standard formalizes it as three layers (requirement 5.2.4): instructions prevent the error, the docstring reinforces the rule, the hint cures the miss.\n\nThe last piece is a timeout contract (requirement 5.1.4). The MCP client defaults to 60 seconds per request. If your internal query timeout is also 60 seconds, the client cancels first and your structured error never arrives. We cap internal timeouts at about 30 seconds; operations that legitimately run longer return early with an execution id the model can poll.\n\nFor a while, users of the gateway kept getting logged out in the middle of the workday. Cursor would open a browser window and ask them to authorize again. There was no pattern we could see — not tied to deploys, not tied to token lifetime.\n\nThe cause turned out to be Refresh Token Rotation. With rotation enabled, the IdP invalidates the old refresh token every time it issues a new one. That assumes the client reliably persists the new token. MCP clients don't always manage it, and a client that comes back with a stale token gets `invalid_grant`\n\nand falls back to a full browser login.\n\nI measured the difference. With rotation on, token refresh failed 16 times in 21 hours. With rotation off, twice in 17 hours.\n\nThe working configuration (requirement 2.2.3, written for Auth0 but portable): allow offline access on the API, enable the `refresh_token`\n\ngrant on the application, and set rotation to non-rotating for proxied MCP clients. Issuing refresh tokens and rotating them are separate settings; the first two control issuance, the third controls whether your users keep their sessions.\n\nA related fact that surprised me: redeploying the server does not drop sessions. The access token is a JWT verified against the tenant's JWKS, so no server-side state is involved. If your users re-login after every deploy, look at client persistence, not at your deploy pipeline.\n\nAnthropic's API validates JSON Schema property keys against `^[a-zA-Z0-9_.-]{1,64}$`\n\n. If any parameter of any tool has a key outside that pattern, the API responds with 400.\n\nThe expensive part is the blast radius. The 400 rejects the request that carries the entire tools array, so one invalid key in one obscure tool makes every tool disappear. The server stays healthy and keeps serving requests. Your logs show nothing, because the rejection happens upstream, between the client and Anthropic.\n\nI now treat this as a boot-time invariant (requirement 3.1.3 in the standard). On startup, the server spins up a throwaway instance of itself, fetches `tools/list`\n\nthrough `InMemoryTransport`\n\nthe way a real client would, and walks every property key. If anything violates the pattern, the process exits with an error.\n\nThe detail that matters: validate the output of `tools/list`\n\n, not your source schemas. The SDK converts Zod definitions to JSON Schema on the way out, and the converted form is what the client actually sees. The same helper doubles as a contract test in CI.\n\nMCP responses are prompt tokens. Someone pays for them, in dollars and in context window, so the standard requires a token-efficient tabular encoding (I default to TOON) and mandatory truncation.\n\nThe mistake I made first: truncating by row count only. A row cap does nothing when one row is huge — a single JSON blob in one cell pushed a response past the client's size limit, and instead of data the user saw \"File content exceeds maximum allowed size\". The budget has to bind on rows and bytes at the same time. On truncation, set a `truncated`\n\nflag with a hint to narrow the query, and keep `structuredContent`\n\ncomplete; only the text block shrinks.\n\nThe same size discipline applies to the `instructions`\n\nfield of `InitializeResult`\n\n. Claude Code loads it at session start and truncates at 2 KB. Anything you need the model to know about your server has to fit in the first 2 KB.\n\nEvery requirement follows one rule: a MUST fixes a capability, a SHOULD recommends a vendor. For a company-wide server the non-negotiable core is OAuth, HTTP transport, and centralized logging: a per-call audit log with secret redaction, shipped somewhere queryable, plus access logs on the nginx in front of the server. The two views together answer both \"what did the tool do\" and \"what did the client actually send\", and debugging the re-login mystery above would have been impossible without them.\n\nWe ship logs through Vector to Axiom, but that's a SHOULD: substitute your own stack with a stated reason. I've watched standards die because someone objected to a tool choice; this one imposes properties, not tools.\n\nIt also documents its own gap honestly. Zero-downtime rolling deploys for stateful MCP servers need session affinity by `Mcp-Session-Id`\n\nplus a front proxy, and I haven't built that. Until then, the standard accepts healthcheck-gated recreate with autoheal as a conformant option instead of pretending it's temporary.\n\nThe full text is at [r-ms/mcp-server-standard](https://github.com/r-ms/mcp-server-standard): ten sections (transport, auth, tool design, response format, errors, logging, security, server context, deploys, testing) and a release-gate checklist with stable requirement IDs, so a code review comment can say \"violates 5.1.4\" and link to it.", "url": "https://wpnews.pro/news/what-breaks-mcp-servers-in-production-mcp-best-practicies", "canonical_source": "https://dev.to/michael_rakutko/what-breaks-mcp-servers-in-production-mcp-best-practicies-2d1e", "published_at": "2026-07-08 14:33:39+00:00", "updated_at": "2026-07-08 14:41:25.633682+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["MCP", "AI-Lens", "Auth0", "Cursor"], "alternates": {"html": "https://wpnews.pro/news/what-breaks-mcp-servers-in-production-mcp-best-practicies", "markdown": "https://wpnews.pro/news/what-breaks-mcp-servers-in-production-mcp-best-practicies.md", "text": "https://wpnews.pro/news/what-breaks-mcp-servers-in-production-mcp-best-practicies.txt", "jsonld": "https://wpnews.pro/news/what-breaks-mcp-servers-in-production-mcp-best-practicies.jsonld"}}