Traditional API versioning assumes the caller is code someone wrote against a fixed contract, tested once, and deployed deliberately. MCP breaks that assumption on both ends: the caller is an agent that reads the tool catalog at the start of every session, and the "code" it writes exists only for the length of a task. Rename a tool, tighten a parameter, or change what an error means, and the breakage does not show up in any build. It shows up as an agent that suddenly cannot complete a workflow it handled last week, with no deploy to bisect.
The goal is not to freeze your tools forever. It is to make change visible, additive where possible, and explicit when it cannot be.
What actually counts as a breaking change
Because agents select tools by reading names, descriptions, and JSON Schemas, the surface area is wider than a typed client's. Treat the following as breaking, always:
- Removing or renaming a tool.
- Removing or renaming a parameter.
- Making an optional parameter required.
- Narrowing a parameter's type or enum set.
- Changing the units or semantics of a value (cents to dollars,
status: 3from "shipped" to "returned"). - Changing an error code that agents branch on.
- Removing a field from the result the agent commonly reads.
The following are safe and should be your default move:
- Adding a new tool.
- Adding an optional parameter with a documented default.
- Adding new enum values (assuming agents handle unknown values gracefully; make yours do so).
- Adding result fields.
- Clarifying description prose without changing meaning.
Notice that "the HTTP API behind the tool stayed compatible" does not matter if the tool description changed. The MCP surface is its own contract.
The deprecation window
When a tool must change incompatibly, ship the replacement before removing the old one and make the old one announce itself:
{
"name": "search_orders_legacy",
"description": "[DEPRECATED, removal 2027-01-31] Use search_orders_v2, which takes date filters as a range object instead of two string fields. This tool now proxies to v2 with converted arguments.",
"inputSchema": { "type": "object", "properties": {} }
}
Three things are happening there, and all three matter: the word DEPRECATED in the description (models weight leading tokens heavily), a concrete removal date, and a proxy implementation that keeps old call patterns working during the window. Agents that already learned the old tool keep functioning; agents reading the catalog fresh learn the new one.
Keep the old tool for at least one full model-memory cycle. In practice that means weeks, not days: prompts, saved instructions, and shared playbooks all contain tool names people copy and paste.
Versioned names: coarse but honest
For a genuinely different behavior rather than a reshaped parameter, a version suffix is the clearest signal available:
-
create_invoice(v1, net-30 terms implied) -
create_invoice_v2(explicit terms object, multi-currency)
Run both side by side during migration. Resist the urge to version everything from day one; a catalog of foo_v1, bar_v1, baz_v1 teaches the model nothing about which to use. Version suffixes are for incompatibility, not for pride in iteration count.
Capabilities and schema negotiation
Use the protocol's own versioning facilities for structural changes:
- Bump the server version in the
initializeresponse so clients and logs can correlate behavior. - Gate large new capabilities behind advertised server capabilities rather than exposing half-working tools.
- When the input schema itself needs a breaking redesign that cannot be proxied, that is the case for a new tool name, not an in-place edit.
Catch the break in CI, not in a demo
A tool catalog generated from an OpenAPI document gives you a diffable artifact. Snapshot tools/list and fail the build on removed or narrowed surface area, exactly as you would for an OpenAPI breaking change:
# Pseudocode for a CI step: generate the catalog from the merged spec
# and compare against the main branch, allowing additions only.
npx @acme/oas-to-mcp build openapi.yaml --out tools/
node scripts/assert-no-breaking-tool-diff.js \
--before main:tools/list.json \
--after tools/list.json
The same OpenAPI diff gate that protects REST consumers protects agents; the mechanics are identical to detecting breaking API changes with OpenAPI diffs in CI. A removed operation, a tightened field, or a deleted enum value all show up as a tool catalog break before merge.
Description churn is real churn
One subtlety unique to agents: editing a description can change tool selection even when schemas are untouched. Rewriting "refunds a payment" to "cancels a pending payment and returns funds" changes which situations a model considers the tool appropriate for. That is usually why you are rewriting it, but treat behavioral edits to descriptions with the same review discipline as schema edits, and include them in the snapshot diff so reviewers see the change.
When tools are a build artifact, versioning gets cheap
Hand-maintained MCP servers make all of this painful because the tool catalog and the API it calls evolve independently. When the catalog is generated from the spec, a single source of truth drives both:
- The OpenAPI document declares what is deprecated (
deprecated: true), and generated tool descriptions inherit the marker automatically. - Additive spec changes produce additive tool changes with no extra work.
- The breaking-change gate runs against one artifact.
- Hosting a new server version alongside an old one is a deployment decision, and stdio users pin a build version the same way they pin any CLI.
That generated-server model, and why the MCP server should be treated as compiled output rather than maintained code, is laid out in the MCP server is a build artifact. You can generate a versioned local server from any spec in the online demo and inspect the exact tool catalog an agent would receive.
Top comments (0)