You changed a tool's output. You did it carefully. You added a field, left everything that was already there exactly where it was, ran the schema validator, watched it pass. Nothing your code parses moved. And the agent that depended on the tool got measurably worse.
This is the failure mode nobody warns you about, because it doesn't look like a break. Every guide to tool design treats a tool the way you'd treat a REST endpoint: a request schema, a response schema, a compatibility contract you can reason about with the ordinary rules. Those rules are real and they are worth knowing. They are also only half of what a tool actually is.
The machine layer is the solved part#
Start with the half that behaves. A tool's schema is a contract with the code that calls it, and that contract follows the same discipline as any API you've ever versioned. Additive changes are safe: a new optional output field, a brand-new tool, a widened return. A caller written against the old shape ignores what it doesn't recognize and keeps working. Breaking changes are the familiar list: removing or renaming a field the caller reads, adding a required input parameter, changing a type, tightening validation so calls that used to pass now fail. These break loudly, in a parser, on the next call — which is the good kind of break, because a test catches it before your users do.
The Model Context Protocol just wrote this discipline into the standard. The 2026-07-28 release candidate ships SEP-2596, the protocol's first formal feature-lifecycle policy. Every feature is Active, Deprecated, or Removed, and the policy guarantees a floor: at least twelve months between the revision that marks a feature deprecated and its earliest eligible removal. The only shortcut is a shortened window of ninety days, available solely for a feature that "presents an active security risk" backed by a published advisory. A deprecated feature earns a @deprecated annotation in the schema and a changelog entry, and it keeps working the whole time. That is exactly the deprecate-then-remove cadence a careful engineer already runs by hand — now with a number attached.
Notice the anchor MCP chose for the clock. The twelve months are measured from the release of the revision that deprecates a feature, not from the version identifier and not from when the proposal was finalized. MCP names its versions with dates — 2025-11-25, the RC's 2026-07-28 — where the date marks the last backwards-incompatible change. But the guarantee is deliberately decoupled from that string, so it holds whether the project keeps dating its releases or someday switches to semver. That is a standards body being honest that a version number and a compatibility promise are two different things. (MCP put the policy to work in the same release, deprecating three original primitives on exactly this clock.)
The layer no validator sees#
Here is the part that should change how you work. SEP-2596's sibling proposal, SEP-2577, spells out what a deprecation actually is at the wire level: "advisory only, no wire-level protocol changes." The spec can hang a @deprecated tag on a feature, ship it in the changelog, and light up a client's linter. What it cannot do is make the model notice.
Because a tool is not only a schema. A tool is a name, a description, and an output shape, and all three of those are tokens that land in the context window on every call. They are a prompt. The model chose this tool over the others because of how the description read; it reasoned over the last result because of which fields were there and what they were called. Change any of that and you have changed the prompt — silently, with no request that fails validation, no parser that throws.
A tool schema is a contract with two consumers that version differently: the code, which you can pin and migrate, and the model, which only ever reads the latest copy and can't be told it changed.
Walk the seam. You rename an output field from status to state and, being diligent, you keep status populated for backward compatibility. The validator is thrilled; the code is fine. But the model still keys off status, so your new field is dead weight — and if you ever stop populating the old one, the agent degrades with the schema still green. Or you trim a bloated 40-field response down to the six that matter — a genuine improvement for the context window — and the agent's accuracy dips, because it had quietly been reasoning over one of the thirty-four you removed. Or, most invisibly of all, you reword a description to be clearer, and tool-selection shifts across your whole eval set. Nothing in any schema moved. You just rewrote the prompt and shipped it without an eval.
Version tools the way you version prompts#
The protocol's own versioning underlines the gap. MCP moved version negotiation to a per-request field and returns a crisp -32022 when a client asks for a protocol version the server won't serve. That versions the transport. It says nothing about whether the meaning of a tool drifted underneath a version that stayed the same.
So carry two habits, one for each consumer. For the code, keep the ordinary compatibility rules and borrow MCP's cadence: default to additive, and when you must break, deprecate before you remove, with a real migration note living inside the tool's description — the one place the model will actually read it. For the model, treat a tool-schema change the way this publication has argued you should treat a prompt change: as a deploy. Snapshot the schemas your agent was evaluated against, diff them on every release, and when a description or an output shape moves, re-run the behavioral evals, not just the validation tests — because the layer that matters most is the one that never fails loudly enough to page you.
The validator will keep telling you the contract holds. Believe it about the code. Don't believe it about the model.



