# Detecting Tool + Schema Drift in a Remote MCP Server

> Source: <https://dev.to/merlonix/detecting-tool-schema-drift-in-a-remote-mcp-server-1p9i>
> Published: 2026-08-21 09:40:18+00:00

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.

Drift is anything about a server's advertised capability contract that differs from the last time you checked. Concretely:

`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.`inputSchema`

— a field renamed, a type narrowed, a new required parameter, a changed `description`

that 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.`resources`

or `prompts`

, or starts. Anything built against the old capability list breaks the moment it tries to use what is gone.`initialize`

handshake yesterday and answers only stateless `tools/list`

today (or the reverse) has changed its entire transport contract, covered in more depth in 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.

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."

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.

Merlonix's MCP health checker builds exactly this kind of fingerprint. Each run walks the `tools/list`

result and, for every tool, hashes its name, description, and `inputSchema`

into a per-tool digest, stored alongside the server's protocol version, server version, transport, advertised capabilities, and spec generation. That snapshot is what the monitored check persists between runs.

The drift detector is a pure diff over two snapshots: it compares the digest maps' key sets to find tools present now but not before (added), before but not now (removed), and present in both with a changed digest (changed). It separately diffs the capabilities arrays, flags protocol-version and server-version changes when either value differs between two known values, and flags a spec-generation change when a server crosses stateful ⇄ stateless — 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 — it returns no drift by design; it only alerts on a genuine change between two known-good fingerprints.

The alert line is ordered 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. It's covered from the health-check side in [how to health-check a remote MCP server](https://merlonix.com/blog/how-to-health-check-a-remote-mcp-server/).

If you operate the server:

`inputSchema`

changes like an API contract change`tools/list`

— never complains.`create_ticket_v2`

alongside a deprecated `create_ticket`

costs you a little clutter and costs your callers nothing.`/.well-known/mcp.json`

server 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.If you consume someone else's server:

`tools/list`

call as proof the contract you built against still holds.Run a one-off check with Merlonix's [free MCP health checker](https://merlonix.com/tools/mcp-health/) 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](https://merlonix.com/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](https://merlonix.com/for/mcp-server-developers/) collects the rest of the toolchain in one place.
