1import{j as e}from"./react-vendor-BpWNMKAs.js";import{S as s}from"./SiteBlogArticleLayout-DmDeeH0a.js";import"./SiteBlogArticleLayoutV2-fohQwcFm.js";import"./router-qViEkJfC.js";import"./HeroFilmGrain-YdVvaUC8.js";import"./icons-P5DuwcRk.js";import"./motion-z_4S-VG1.js";import"./ContentAside-BbSYqH8Q.js";import"./ArticleClosingCta--5IEYr03.js";import"./index-DTSFlFpw.js";import"./query-95q6k4md.js";import"./radix-Clmhrnrt.js";const u=()=>e.jsxs(s,{title:"Monitoring MCP Servers in Production: The Layer Your Stack Forgot",image:"/images/bg-nature-blue.png",category:"Monitoring",readTime:"11 min read",closing:{eyebrow:"The Bottom Line",headline:"Your MCP server is one renamed tool away from a customer-visible regression.",body:"When MCP breaks, your agent doesn't fail â it lies. Traditional uptime monitoring catches none of it. You need monitoring designed for the protocol, not the URL it lives behind. The only question is whether you'll find out from a validate or a support ticket."},children:[e.jsx("p",{children:"Your agent works. Your model provider is up. Your prompt evals pass. Your dashboard is green."}),e.jsxs("p",{children:["Then a customer pings you: ",e.jsx("em",{children:`"The 'lookup_order' tool keeps failing."`})]}),e.jsx("p",{children:"You check the agent endpoint, 200 OK. You check the LLM API, green. You check your retrieval store, fine. Two hours later you discover the problem wasn't in any of those layers. The Model Context Protocol (MCP) server that exposes your tools to the agent had silently changed transports during a deploy. The agent was reaching it. The handshake was failing. Every tool call was returning an error wrapped in a perfectly valid agent response."}),e.jsx("p",{children:"This is the layer almost no monitoring stack covers, and the one most likely to break next."}),e.jsx("hr",{}),e.jsx("h2",{children:"Why MCP Is Different (And Dangerous)"}),e.jsx("p",{children:"MCP is the protocol that exposes tools, resources, and prompts to LLM agents. In 2025 it was a curiosity. By mid-2026 it's everywhere, Claude Desktop, Cursor, agentic IDEs, internal LLM gateways, and most production agent stacks now sit on top of one or more MCP servers."}),e.jsx("p",{children:"Three properties make MCP failures uniquely invisible to traditional monitoring:"}),e.jsxs("ol",{children:[e.jsxs("li",{children:[e.jsx("strong",{children:"The agent is the consumer, not the user."})," A broken MCP call doesn't return a 5xx to your user-facing API. It returns a ",e.jsx("em",{children:"successful"})," agent response that contains an error message, an apology, or worse, a hallucinated answer."]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Transports drift."})," A server can switch between HTTP, SSE, and Streamable HTTP between deploys. Your agent SDK may downgrade silently. Your monitoring sees a 200 because tools/list returned ",e.jsx("em",{children:"something"}
1),", even if that something is unusable."]}),e.jsxs("li",{children:[e.jsx("strong",{children:"The contract isn't a URL."})," It's a JSON-RPC method set. ",e.jsx("code",{children:"tools/list"}),", ",e.jsx("code",{children:"resources/list"}),", ",e.jsx("code",{children:"prompts/list"}),", plus per-tool ",e.jsx("code",{children:"tools/call"}),". None of those show up in URL-level uptime checks."]})]}),e.jsx("p",{children:"If you're monitoring MCP servers the way you monitor REST APIs, you're not monitoring them at all."}),e.jsx("hr",{}),e.jsx("h2",{children:"Failure Mode 1: The Transport Swap"}),e.jsxs("p",{children:[e.jsx("strong",{children:"What happens:"})," A server upgrade switches from HTTP+JSON-RPC to SSE (Server-Sent Events) or Streamable HTTP. Old clients stop working. New clients work. Your reachability check still passes because something responds at the URL."]}),e.jsx("p",{children:e.jsx("strong",{children:"Why it happens:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"MCP transport spec evolved through 2025 to 2026"}),e.jsx("li",{children:"Server frameworks default-changed (e.g. fastmcp, mcp-python)"}),e.jsx("li",{children:"Reverse proxies (Cloudflare, NGINX) buffer SSE incorrectly"}),e.jsx("li",{children:"Edge functions force-flush in ways that break chunked responses"})]}),e.jsxs("p",{children:[e.jsx("strong",{children:"Real example:"})," A team deployed a routine MCP server upgrade. The server now responded with ",e.jsx("code",{children:"Content-Type: text/event-stream"}),". Their agent's SDK didn't support SSE. Tool calls hung for 30 seconds, then timed out, then the agent returned, ",e.jsx("em",{children:`"I'm having trouble accessing your account information right now."`})," Customers thought it was an outage. The dashboard was green for 11 hours."]}),e.jsx("p",{children:e.jsx("strong",{children:"How to prevent it:"})}),e.jsxs("ul",{children:[e.jsxs("li",{children:["Validation transport explicitly. Don't trust the URL, assert the ",e.jsx("code",{children:"Content-Type"})," is what your client supports."]}),e.jsx("li",{children:"Detect SSE vs JSON-RPC vs Streamable HTTP at validate time and fail loudly when the contract changes."}),e.jsxs("li",{children:["Track ",e.jsx("code",{children:"transport_supported = false"}),' as a first-class verdict, not as "degraded latency."']})]}),e.jsx("p",{children:"In the Agent Status validate path, this is the difference between:"}),e.jsx("pre",{children:e.jsx("code",{children:`result['error'] = 'transport_not_supported: sse' 2result['transport_type'] = 'sse' 3result['transport_supported'] = False`})}),e.jsx("p",{children:"and the silent-200 your dashboard would otherwise report."}),e.jsx("hr",{}),e.jsx("h2",{children:"Failure Mode 2: The Vanishing Tool"}),e.jsxs("p",{children:[e.jsx("strong",{children:"What happens:"})," A tool gets renamed, removed, or temporarily disabled in the MCP server's ",e.jsx("code",{children:"tools/list"})," response. The agent now silently can't access it. Responses degrade. No HTTP error fires."]}),e.jsx("p",{children:e.jsx("strong",{children:"Why it happens:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"Feature flags toggled in the MCP server"}),e.jsx("li",{children:"Tool names refactored without coordinating with the agent's prompt"}),e.jsx("li",{children:"Tool registration race conditions on cold starts"}),e.jsx("li",{children:"Auth context restricts which tools are visible per-session"})]}),e.jsxs("p",{children:[e.jsx("strong",{children:"Real example:"})," A travel agent depended on ",e.jsx("code",{children:"search_flights"}),". A backend team renamed it to ",e.jsx("code",{children:"flights_search"}),". Both teams reviewed the diff. Neither noticed the agent's system prompt still said ",e.jsx("code",{children:"search_flights"}),". For four days the agent told customers, ",e.jsx("em",{children:'"I can help with hotels but not flights right now."'})," Bookings dropped 18%. Nobody alerted on it."]}),e.jsx("p",{children:e.jsx("strong",{children:"How to prevent it:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"Snapshot the tool inventory on every validate and diff against the previous run."}),e.jsxs("li",{children:["Alert on ",e.jsx("strong",{children:"tool removal"}),", ",e.jsx("strong",{children:"tool addition"})," (you may want to know), and ",e.jsx("strong",{children:"schema change"})," (input/output contract drift)."]}),e.jsxs("li",{children:["Run at least one ",e.jsx("code",{children:"tools/call"})," per critical tool, discovery alone isn't enough."]})]}),e.jsx("p",{children:"A real validate should answer three distinct questions:"}),e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Question"}),e.jsx("th",{children:"Validation"})]})}),e.jsxs("tbody",{children:[e.jsxs("tr",{children:[e.jsx("td",{children:"Is the server reachable?"}),e.jsxs("td",{children:[e.jsx("code",{children:"tools/list"})," returns 200 with valid JSON-RPC"]})]}),e.jsxs("tr",{children:[e.jsx("td",{children:"Are the right tools present?"}),e.jsxs("td",{children:["Diff ",e.jsx("code",{children:"result.tools"})," against expected set"]})]}),e.jsxs("tr",{children:[e.jsx("td",{children:"Do the tools actually work?"}),e.jsxs("td",{children:[e.jsx("code",{children:"tools/call"})," with a known input then assert known output"]})]})]})]}),e.jsx("p",{children:"A tool that exists in the listing but errors on every call is the most expensive kind of broken: it looks healthy and acts dead."}),e.jsx("hr",{}),e.jsx("h2",{children:"Failure Mode 3: The Slow Discovery"}),e.jsxs("p",{children:[e.jsx("strong",{children:"What happens:"})," ",e.jsx("code",{children:"tools/list"})," returns in 800ms instead of 80ms. The agent's first turn now takes 4 seconds longer because every cold session pays the discovery tax. Users churn. No threshold trips because nobody alerted on discovery latency."]}),e.jsx("p",{children:e.jsx("strong",{children:"Why it happens:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"Server now lists 200 tools instead of 12 (catalog bloat)"}),e.jsx("li",{children:"Dynamic tool registration runs N+1 queries against a backing store"}),e.jsx("li",{children:"Auth enforcement was added inline to the discovery handler"}),e.jsx("li",{children:"Cold-start path of the host process is in the discovery path"})]}),e.jsxs("p",{children:[e.jsx("strong",{children:"Real example:"}),` A code-assistant team added 60 new MCP tools to their server in Q1. By Q2, the IDE plugin's "first reply" latency had crept from 1.2s to 4.7s. Eng blamed the model. The model wasn't the problem, discovery was. They fixed it by paginating tool listings and caching at the gateway.`]}),e.jsx("p",{children:e.jsx("strong",{children:"How to prevent it:"})}),e.jsxs("ul",{children:[e.jsxs("li",{children:["Track ",e.jsx("code",{children:"latency_ms"})," on ",e.jsx("code",{children:"tools/list"}),", ",e.jsx("code",{children:"resources/list"}),", ",e.jsx("code",{children:"prompts/list"})," as separate SLAs."]}),e.jsx("li",{children:"Alert when discovery latency drifts more than 1.5x over a 7-day baseline."}),e.jsx("li",{children:"Treat discovery as part of the user experience, because it is."})]}),e.jsx("hr",{}
3),e.jsx("h2",{children:"Failure Mode 4: The Auth Boundary"}),e.jsxs("p",{children:[e.jsx("strong",{children:"What happens:"}),` The MCP server requires a token. Your agent has the token. Your monitoring doesn't. Validations pass anonymously, returning a smaller (or empty) tool catalog. The "agent" experience is completely different from what you measure.`]}),e.jsx("p",{children:e.jsx("strong",{children:"Why it happens:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"MCP servers commonly gate tools by OAuth scope, API key, or signed bearer"}),e.jsx("li",{children:"Some tools are visible only to authenticated clients"}),e.jsxs("li",{children:["The same server returns ",e.jsx("em",{children:"different"})," tool sets per principal"]}),e.jsx("li",{children:"Validations that don't authenticate look like generic visitors"})]}),e.jsxs("p",{children:[e.jsx("strong",{children:"Real example:"})," A customer-support agent could call ",e.jsx("code",{children:"refund_order"})," only when authenticated. The team's monitoring was anonymous. For six months, monitoring proved the server was up, but it was up ",e.jsx("em",{children:"as a stranger"}),". The first time they actually tested the authenticated path, three tools had been broken for weeks."]}),e.jsx("p",{children:e.jsx("strong",{children:"How to prevent it:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"Validation MCP servers using the same auth surface your real agent uses."}),e.jsx("li",{children:"Maintain at least two validate identities: anonymous (catalog you expose publicly) and authenticated (catalog the agent actually sees)."}),e.jsx("li",{children:"Diff the two catalogs continuously. Drift between them is a leak indicator."})]}),e.jsx("hr",{}),e.jsx("h2",{children:"Failure Mode 5: The Smithery / Gateway Hop"}),e.jsxs("p",{children:[e.jsx("strong",{children:"What happens:"})," Your MCP server isn't direct, it's behind a hosted gateway (Smithery, an internal proxy, or a multi-tenant connector). The gateway's connection state can be ",e.jsx("code",{children:"pending"}),", ",e.jsx("code",{children:"failed"}),", ",e.jsx("code",{children:"expired"}),", or ",e.jsx("code",{children:"degraded"})," independent of your server's actual health."]}),e.jsx("p",{children:e.jsx("strong",{children:"Why it happens:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"Hosted MCP gateways introduce a connection lifecycle (open, validate, route)"}),e.jsx("li",{children:"Gateway-to-server links can be down while the gateway itself looks fine"}),e.jsx("li",{children:"Authorization happens at the gateway, not the server"}),e.jsx("li",{children:"Connection IDs expire and need re-establishment"})]}),e.jsxs("p",{children:[e.jsx("strong",{children:"Real example:"})," A team using a popular MCP hosting provider had perfect server-side metrics. The provider's connection layer was returning ",e.jsx("code",{children:"connection_state: error"})," on 12% of calls during a peak window. Server logs showed nothing because the calls never reached the server. They only caught it by validating through the gateway with the same flow real agents used."]}),e.jsx("p",{children:e.jsx("strong",{children:"How to prevent it:"})}),e.jsxs("ul",{children:[e.jsx("li",{children:"Validation through the same gateway your agents use, not directly to origin."}),e.jsx("li",{children:"Capture and surface gateway connection state as a distinct error code."}),e.jsxs("li",{children:["Treat ",e.jsx("code",{children:"Smithery Connect PUT failed: HTTP <code>"})," and ",e.jsx("code",{children:"connection state: error"})," as actionable verdicts, not noise."]})]}),e.jsx("hr",{}),e.jsx("h2",{children:'What "Real" MCP Monitoring Looks Like'}),e.jsx("p",{children:"A complete MCP validate answers six questions on every cycle:"}),e.jsxs("ol",{children:[e.jsxs("li",{children:[e.jsx("strong",{children:"Reachability"}),", did ",e.jsx("code",{children:"tools/list"})," succeed?"]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Transport"}),", is the response in a transport my agent's SDK supports?"]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Inventory"}),", are the expected tools/resources/prompts present and correctly named?"]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Schema"}),", does each tool's input/output contract match what the agent prompt expects?"]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Behavior"}),", does at least one ",e.jsx("code",{children:"tools/call"})," per critical tool return the expected output for a known input?"]}),e.jsxs("li",{children:[e.jsx("strong",{children:"Performance"}),", is discovery + invocation latency within the user-experience SLA?"]})]}),e.jsx("p",{children:"A failure of any one is a customer-visible defect, even when the URL still returns 200."}),e.jsxs("p",{children:["This is exactly the validate surface Agent Status runs against your MCP servers. The verdict isn't ",e.jsx("code",{children:"UP/DOWN"})," based on HTTP. It's:"]}),e.jsxs("ul",{children:[e.jsxs("li",{children:[e.jsx("code",{children:"UP"}),", reachable, transport supported, all tool validations pass"]}),e.jsxs("li",{children:[e.jsx("code",{children:"DEGRADED"})," (",e.jsx("code",{children:"mcp_tool_fail"}),"), reachable but one or more tool calls fail"]}),e.jsxs("li",{children:[e.jsx("code",{children:"DEGRADED"})," (",e.jsx("code",{children:"mcp_transport_unsupported: <type>"}),"), server changed transports out from under your client"]}),e.jsxs("li",{children:[e.jsx("code",{children:"DOWN"})," (",e.jsx("code",{children:"mcp_unreachable"}),"), handshake failed entirely"]})]}),e.jsx("p",{children:"Each verdict is attributable, alertable, and actionable."}),e.jsx("hr",{}),e.jsx("h2",{children:"The Validation Profiles That Matter"}),e.jsx("p",{children:"Different MCP servers need different validate depths. We use four:"}),e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Profile"}),e.jsx("th",{children:"What it does"}),e.jsx("th",{children:"When to use"})]})}),e.jsxs("tbody",{children:[e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("code",{children:"health_only"})}),e.jsxs("td",{children:[e.jsx("code",{children:"tools/list"})," only"]}),e.jsx("td",{children:"Public servers, cheap continuous checks"})]}),e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("code",{children:"full_discovery"})}),e.jsx("td",{children:"tools + resources + prompts"}),e.jsx("td",{children:"Servers exposing more than just tools"})]}),e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("code",{children:"tool_contract"})}),e.jsxs("td",{children:["Discovery + named ",e.jsx("code",{children:"tools/call"})," validations"]}),e.jsx("td",{children:"Production servers with known critical tools"})]}),e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("code",{children:"full_validation"})}),e.jsx("td",{children:"Discovery + auto-generated validations from listings"}),e.jsx("td",{children:"Servers under active development"})]})]})]}),e.jsxs("p",{children:["Most teams default to ",e.jsx("code",{children:"health_only"}),", which is roughly equivalent to a TCP ping. It's better than nothing, and it misses everything that matters."]}),e.jsx("hr",{}
3),e.jsx("h2",{children:"Quick Wins"}),e.jsx("p",{children:"If you only do three things this week:"}),e.jsx("h3",{children:"1. Diff your tool inventory daily"}),e.jsxs("p",{children:["Snapshot ",e.jsx("code",{children:"tools/list"})," once a day. Diff against yesterday. Alert on any change you didn't ship."]}),e.jsx("h3",{children:"2. Validation at least one critical tool end-to-end"}),e.jsx("p",{children:'Pick the single MCP tool whose failure would hurt customers most. Call it on every validate cycle with a known input. Assert known output. This catches 80% of "broken in a way nobody notices" cases.'}),e.jsx("h3",{children:"3. Validation through the same path your agent uses"}),e.jsx("p",{children:"If your agent calls MCP through a gateway, validate through the gateway. If it authenticates, your validate authenticates. The validate's job is to be indistinguishable from the agent, anything else is theater."})]});export{u as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.