Two ways a tool description goes stale

Why a discovered tool definition is not permission to call it: re-checking catalog, policy and capability at call time, and deriving every format from one source.

November 20267 min readAgent systemsSecurityInterfacesEvaluation

Two blurred ochre and green planes lean together at the bottom of a charcoal ground, beneath a chalk arc painted twice.
On this page
  1. Has it changed?
  2. Can it be reached?
  3. Who said yes?
  4. Did it happen?
  5. The same drift across formats
  6. What a grade can’t tell you

Progressive discovery solves a real problem. A large tool catalog doesn’t belong in the initial prompt, so the agent gets a way to search for tools and read their definitions when it needs them. It also opens an interval that a fixed tool list mostly hides. The model reads a definition at one moment and calls the tool at a later one, perhaps several tool results later, and by then the MCP server may have restarted with a different catalog, its maintainer may have reworded what the tool does, an administrator may have blocked it, or the credential behind it may have lapsed. A cached definition cannot establish that a call is still available or permitted.

In the August 2026 version of the agent harness on the platform I work on, the tool dispatcher is built around that interval. The runtime descends in part from pi, and the dispatcher’s header states the rule in one sentence: “a mounted or discovered definition is never an authorization grant.” Every call goes through a single execution path that re-resolves the catalog revision, the definition fingerprint, availability, policy, approval and live capability before anything runs, and host failures come back as structured outcomes the model can read, never as exceptions. In practice that path asks four questions.

Has it changed?

The MCP client pool hashes each server’s raw tools, resources and prompts into a catalog revision, and fingerprints each tool over its server id, name, description and input schema. A caller can pass back the values it saw, much like If-Match on an HTTP resource. A call made against revision 1 when the server has moved to revision 2 comes back as “catalog changed” without running, a wrong fingerprint comes back as “stale definition,” and the pool checks the revision once more inside the call itself. The inspection tool behaves the same way and returns the fresh fingerprint with its refusal, so the model can read the new definition and decide again.

This catches a server that tells you something changed. It can’t catch one that doesn’t. A server can change what a tool does beneath a byte-identical name, description and schema, and every fingerprint will still match.

Can it be reached?

Availability has four separate answers, connection required, authorization required, blocked and circuit open, because each calls for a different response. The circuit opens for 30 seconds after three failures and a success resets it, and an invalid header declaration on one tool rejects that tool and leaves the rest of the server alone, so one unavailable integration need not hide every other tool. Some tools never appear at all: a research source registers only when its credentials exist, and image generation, which that source doesn’t provide, is simply absent from the list instead of being imitated.

Migration between protocol revisions sharpens the question. The 2026-07-28 revision of MCP removed initialize and went stateless, so a client has to work out which era of server it is talking to. The pool probes server/discover and falls back to the old handshake only on positive evidence of an older server: a JSON-RPC -32601, or an HTTP 400, 404, 405, 406 or 501. It never falls back on a 401, a 403, a TLS failure or any other 5xx response. The code names the risk as a “downgrade attack”: anything able to make a probe fail could otherwise push the client onto an older protocol.

Who said yes?

Discovery has an awkward consequence for approvals. When the model reaches a tool through a generic call wrapper, a gate keyed on the tool it named would be approving the wrapper, which is to say everything. So the gate receives the target tool, and the host wires it to that tool’s own policy. With no gate configured the call fails closed, a gate that throws counts as a denial, and a blocked tool is refused before any gate is asked. Approval receipts bind the tool name, an input digest, the profile revision, the thread, the turn and the environment, and a replay store consumes each one once, so a yes given to one push in one turn can’t be spent on another. Internal registrations can’t be reached from an id the model supplies at all.

The last check resolves the live capability, and an effectful call without one is denied. This narrows the interval without closing it. A capability can expire a moment after the check, so the system at the far end still has to enforce its own authority.

Did it happen?

The interval continues after the call. Reads get 120 seconds and anything side-effectful or unknown gets 300, and a timeout means only that the abort signal fired and the dispatcher stopped waiting. A tool that ignores the signal can still finish its work. For a push, that means the remote may have changed even though the caller received a timeout. The error therefore tells the model to “VERIFY the remote state before retrying.”

The same drift across formats

The second way a description goes stale has nothing to do with time. An application that wants to be usable by people and agents ends up describing the same routes and actions over and over: HTML for browsers, Markdown for models, OpenAPI for generated clients, an agent card, an MCP tool list, an llms file, a skills index. Written by hand, these start to disagree by about the second edit.

On Ferrite, my own application platform and a separate codebase from the harness above, the answer is one declared surface. An application lists its routes and operations once, and the robots file, sitemap, llms file, Markdown twins, problem+json errors, OpenAPI document, agent card, MCP tools, manifest, skills index, pricing, x402 and Web Bot Auth declarations are all derived from it. The README calls them “projections, not documents anyone authors.” An operation’s name becomes its OpenAPI operationId, and its read-only and idempotent flags become the hints an MCP client sees, so renaming an operation can’t leave one format behind. A declaration remains a claim, of course: declaring Web Bot Auth verifies no signature and declaring x402 settles no payment.

Getting the Markdown to the right reader is ordinary HTTP done carefully. The router ranks Accept according to RFC 9110, including wildcards at different q-values, q=0 and a malformed q=nope, keeps HTML on a tie, and sends Vary: Accept because, as the comment puts it, “HONEST HTTP says so.” The header is easy to over-read, though. It tells a cache that the response depends on Accept. It says nothing about whether the Markdown and the HTML still mean the same thing, and no header could.

A single source doesn’t protect you from the protocol moving underneath it either. At the end of August the MCP face of this layer spoke revision 2025-11-25, with initialize, while a separate MCP package in the same repository targeted 2026-07-28, which had removed it. I had unified my descriptions and still ended up with two implementations of one protocol at two revisions, which is a fair joke at the expense of this section.

Query strings showed the same pattern one layer down. The repository had grown eight query decoders, and the differences among them produced two real defects in a collaboration app: a stream cursor with its colon encoded as %3A, and a query like ?debug&token=…, where a parameter with no value came first. Those are exactly the resume and authentication paths where parsers ought to agree. I extracted one total decoder, with a decision table and a test that walks every edge, and its own documentation calls it a stopgap.

What a grade can’t tell you

I built a readiness scanner to catch regressions in all of this before publication, with twenty checks and a local harness that forwards discovery, payment and auth headers to an application’s real handlers. A sample application scores 100 and an A, which means it satisfies this scanner and nothing more. A separate developer evaluation asks whether real coding clients, Codex, Claude and Pi, can create, test and deploy working applications with the platform’s MCP tooling, and it grades source artifacts, admission, deployment, probes, browser behavior, task completion and effort separately, because the “primary result is working apps built by real clients, not a protocol score.” Eight fresh attempts at the end of August produced five apps that passed the browser checks without an app repair and three that needed repair. All three repaired apps later passed, but only two repair sessions completed, one with operator intervention; the other ended on a provider error after deploying the working app.

That is a diagnostic sample and no kind of model ranking. The repairs addressed mobile overflow, hidden results and a missing displayed value, problems the discovery grade was not testing. A working application and a successfully completed agent session are separate results too.

Re-checking at the last moment notices when a description has changed, and deriving every format from one source stops the copies disagreeing. Neither makes a description true. What a tool says about itself and what it does remain separate facts, and the second only shows up when something runs.

Chris Chabot, November 2026