Detecting Tool + Schema Drift in a Remote MCP Server
An MCP server can ship a change that breaks every agent calling it, and nothing in your monitoring will notice. The endpoint still answers 200. The initialize handshake still completes. tools/list still returns a result. Every signal a conventional uptime check knows how to read stays green — and an agent that memorized last week's tool contract starts failing anyway, because the contract underneath it moved. This is drift: a server's tool inventory or a tool's input schema changing between two points in time, with no transport-level symptom at all.
Drift is not a hypothetical. MCP servers are young, most are maintained by small teams, and a tool's inputSchema is just a JSON object in a deploy — there is no compiler stopping someone from renaming a required field, tightening an enum, or dropping a tool nobody remembered an agent still called. The only way to catch it is to have looked at the server before and remember what you saw.
What Actually Counts as Drift
Drift is anything about a server's advertised capability contract that differs from the last time you checked. Concretely:
- A tool disappears. It was in yesterday's
tools/list, it is not in today's. Any agent that calls it now gets a JSON-RPC error mid-flight, not at startup — the failure shows up wherever the agent happens to reach for that tool. - A tool appears. Informational on its own, but worth recording — it is also how you notice a server quietly forking its capability set per client or per deploy.
- A tool's contract changes shape. Same name, different
inputSchema— a field renamed, a type narrowed, a new required parameter, a changeddescriptionthat alters how an LLM decides to call it. The tool is still callable, which is what makes this the dangerous case: nothing errors immediately, calls just start failing validation or getting silently misinterpreted. - The capability set changes. The server stops advertising
resourcesorprompts, or starts. Anything built against the old capability list breaks the moment it tries to use what is gone. - The protocol version or server version changes. Not breaking by itself, but a marker that something was deployed — worth correlating against the other signals.
- The server crosses the stateful/stateless spec boundary. A server that answered the classic
initializehandshake yesterday and answers only statelesstools/listtoday (or the reverse) has changed its entire transport contract, covered in more depth in Streamable HTTP vs SSE and why MCP initialize fails.
None of these are visible from a single check. They only exist as a diff between two snapshots — which is exactly why one-shot health probes can't catch this class of failure no matter how thorough the probe is.
Why This Breaks Agents While Every HTTP Signal Stays Green
Agents don't call MCP tools blind. They call tools/list once (often cached for the session, sometimes cached across sessions), read each tool's name, description, and inputSchema, and use that to decide which tool to call and how to shape the arguments. That contract is the entire interface. If it changes underneath an agent that already has it cached, or underneath a client that pins a tool call it built weeks ago, the agent isn't talking to a broken server — from the transport's point of view the server is perfectly healthy — it's talking to a server that quietly agreed to a different deal.
Compare that to a REST API: a removed field or a changed type is usually caught by a build against a generated client, or shows up as a 4xx your monitoring already watches for. MCP tool schemas have no equivalent gate today. There's no compiler between "someone edited the tool definition" and "every connected agent is now calling it wrong."
How to Detect It From the Outside
Since drift is a diff, detecting it requires two things: a way to fingerprint a server's contract at a point in time, and a place to keep the previous fingerprint so the next check has something to compare against.
The fingerprint has to be more than the tool names. Two tools/list calls returning the same five names tells you nothing about whether tool #3's schema changed underneath that name. A usable snapshot needs, per tool, a digest computed over the parts of the contract an agent actually reads: the tool's name, its description, and its inputSchema. Hash those together and you get a value that only changes when the callable contract changes — a cosmetic reordering of unrelated response fields elsewhere in the payload won't false-positive it, but a narrowed enum or a renamed parameter will.
// Run N
{ "name": "create_ticket", "description": "Open a support ticket",
"inputSchema": { "properties": { "priority": { "enum": ["low","med","high"] } }, "required": ["title"] } }
// Run N+1 — same tool name, silently different contract
{ "name": "create_ticket", "description": "Open a support ticket",
"inputSchema": { "properties": { "priority": { "enum": ["p1","p2","p3","p4"] } }, "required": ["title","priority"] } }
Nothing about that second response is malformed. tools/list succeeds either way. An agent that built a call using the old enum values, or that omitted priority because it used to be optional, starts failing — and the failure looks like an agent bug, not a server change, unless you have the previous digest to compare against.
How Merlonix Detects It
Merlonix's MCP health checker builds exactly this kind of fingerprint. Each run's parseToolsList walks the tools/list result and, for every tool, hashes its name, description, and inputSchema (to.inputSchema ?? to.input_schema) into a per-tool digest, stored in the tool_digests map on that run's McpCapabilitySnapshot alongside the server's protocol_version, server_version, transport, advertised capabilities, and spec_generation. That snapshot is what the monitored check persists between runs.
detectMcpDrift is the pure diff over two snapshots: it compares the digest maps' key sets to find tools present now but not before (added_tools), before but not now (removed_tools), and present in both with a changed digest (changed_tools). It separately diffs the capabilities arrays for added_capabilities/removed_capabilities, flags protocol_version_changed and server_version_changed when either value differs between two known values, and flags spec_generation_changed when a server crosses stateful_2025 ⇄ stateless_2026 — deliberately only firing between two known generations, so an auth-gated run or a pre-migration snapshot missing the field never produces a false alarm. With no prior snapshot — first run ever, or a run where the handshake failed and there was nothing to fingerprint — detectMcpDrift returns no drift by design; it only alerts on a genuine change between two known-good fingerprints.
describeMcpDrift turns that structured diff into one human-readable alert line, and it orders it worst-first: removed tools and changed schemas lead, because those are what break an agent already calling the server; a stateful/stateless migration and version changes come next; new tools and new capabilities — informational, nothing breaks — trail at the end. This is the same drift precedent Merlonix already applies to site-level agent-readiness discovery, extended to the live protocol layer, and it's covered from the health-check side in how to health-check a remote MCP server.
What to Do About It
If you operate the server:
- Treat
inputSchemachanges like an API contract change, because that's what it is. A required-field addition, a type narrowing, or an enum restriction is breaking even though the wire format around it — JSON-RPC,tools/list— never complains. - Version or namespace breaking tool changes rather than mutating a tool in place.
create_ticket_v2alongside a deprecatedcreate_ticketcosts you a little clutter and costs your callers nothing. - Publish a changelog for your tool inventory, the same instinct you'd apply to a public API. If you also ship a
/.well-known/mcp.jsonserver card, keep it in sync — a card that still declares a removed tool is its own integrity problem, separate from drift but caught by the same continuous-check habit. - Don't remove tools that clients might still be calling without a deprecation window. A tool count that only grows is safer for your callers than one that occasionally shrinks; if you're wondering whether your inventory has grown too large in the other direction, that trade-off is covered in how many MCP tools is too many.
If you consume someone else's server:
- Don't treat a passing
tools/listcall as proof the contract you built against still holds. It proves the server answered, not that it answered with the schema you coded to. - Monitor continuously, not once. A single health check, however thorough, cannot see drift — it has no prior snapshot to compare against. Only a check that runs on a schedule and remembers what it saw last time can.
- Alert on removed and changed tools as breaking, and on added tools as informational — the same severity split the digest comparison surfaces, so the page you get matches the actual blast radius.
Run a one-off check with Merlonix's free MCP health checker against any endpoint — it captures the handshake, transport, and tool inventory in one probe. Continuous drift detection between runs, with the digest comparison described above, is part of the monitored check on paid plans. The MCP directory is a useful place to see how a range of live servers currently present their tool inventories, and if you build or operate MCP servers as part of your job, MCP server developers collects the rest of the toolchain in one place.