In September 2026, the Model Context Protocol passed a threshold that changes what it means to ship a server. Ecosystem commentary now puts MCP past 10,000 public servers and 97 million monthly SDK downloads, governed as a neutral standard under the Linux Foundation's Agentic AI Foundation — which this month launched a vendor-neutral MCP certification at AGNTCon + MCPCon Europe in Amsterdam. MCP is no longer the new integration toy. It is infrastructure with pinned clients.
That is the part most server authors have not internalized. The moment your deploy-from-chat endpoint serves its first tools/list, your tool schemas become a public API. Agent clients cache that response, hardcode argument names into prompts and configs, and keep calling them long after you have renamed the parameter. This post is about what breaking changes do to those pinned agents — worked end to end, twice — and the versioning discipline a self-hosted MCP deploy endpoint needs before its first breaking rename.
What counts as breaking for an MCP tool
MCP has protocol-level versioning — the initialize handshake negotiates a protocolVersion, both sides declare capabilities, and the server reports a serverInfo name and version — but none of that versions your tools. Your tool names, input schemas, and output shapes are your own contract, and the ecosystem is already learning this the hard way: compatibility checkers have been caught missing breaking parameter-schema changes entirely, passing narrowed enums as backward compatible.
The table below is the core artifact of this post. The left column is the change; the right column is what a pinned agent does about it. Note the split that matters: some breaks fail closed (loud error, no deploy), and some fail open (a 200 with the wrong real-world effect).
| Change | Breaking? | Pinned-agent outcome |
|---|---|---|
Rename a tool (deploy → trigger_deploy) | Yes | Fail-closed: tools/call names a tool that no longer exists |
| Rename or remove an input field | Yes | Fail-closed: stale args fail validation, deploy never starts |
| Add a new required field | Yes | Fail-closed: old clients omit it and get rejected |
| Narrow an enum (remove an accepted value) | Yes | Fail-closed if validated — but see §3b for the silent variant |
| Remove or rename a structured-output field | Yes | Often silent: client parses a missing field and misreads success |
| Change meaning without changing schema | Yes | Silent: identical calls, different real-world effect, no diff to catch |
| Add a new optional field | No | Safe: old clients omit it, server applies the default |
| Add a new tool | No | Safe: old clients never call what they never listed |
| Add a new enum value | No | Safe: old clients keep sending the values they know |
| Clarify a description without changing semantics | No | Safe — and the cheapest place to announce a deprecation |
Two rows deserve emphasis because they slip through every automated check. Silent behavior changes — same schema, different behavior — pass semver, pass schema diffing, and pass re-listing, because there is no diff to catch. And output-shape changes are silent whenever the agent treats a missing field as anything other than an error, which in practice means most agents most of the time.
Two breaking changes, worked end to end
Theory is cheap here, so let us break a real deploy tool twice: once loudly, once silently.
3a. The loud break: renaming env to environment
Suppose your endpoint exposes a deploy tool whose input schema starts like this:
{
"name": "deploy",
"inputSchema": {
"type": "object",
"properties": {
"app": { "type": "string" },
"env": { "type": "string", "enum": ["staging", "production"] }
},
"required": ["app", "env"]
}
}An agent pins against it the way agents do: it caches tools/list, its operator writes env: staging into a config or a system prompt, and daily deploys flow. Then you ship the rename — env becomes environment — because the new name reads better next to twelve other tools:
{
"name": "deploy",
"inputSchema": {
"type": "object",
"properties": {
"app": { "type": "string" },
"environment": { "type": "string", "enum": ["staging", "production"] }
},
"required": ["app", "environment"]
}
}What the pinned agent does: it sends {"app": "api", "env": "staging"} against a schema that now requires environment. Validation rejects the call. The deploy fails loudly, the operator sees the error, and somebody has to touch the config at 2am if that is when the cron-triggered agent runs. Painful, but fail-closed: nothing deployed to the wrong place.
The safe rollout is a four-step sequence, and the order is the whole point:
- Alias, don't rename. Ship a schema that accepts both
envandenvironment, with the old field marked deprecated in itsdescription— the one channel every agent already reads. - Advertise the change. Bump your
serverInfoversion and emitnotifications/tools/list_changedso clients that honor it re-fetch the schema instead of trusting cache. - Hold the window. Keep the alias across at least two minor releases, with the removal date in a changelog agents and operators can actually find.
- Remove once, loudly. Drop the old field in a major bump, and keep the error message for the old name actionable ("
envwas removed in v3; useenvironment") rather than a bare validation failure.
3b. The silent break: same schema, different target
Now the shape that should keep you up at night. Same deploy tool, same schema — but your fleet consolidates regions, and the value "staging" quietly starts resolving to a different cluster. Or, equivalently, you change the default of an optional strategy field from rolling to recreate. The schema diff is empty. Here is the client-behavior matrix:
| Client | Loud rename (§3a) | Silent retarget (§3b) |
|---|---|---|
Cached schema, ignores list_changed | Hard error, no deploy | 200, deploys to the wrong target |
| Fresh session, re-lists tools | Adapts to the new name | 200, deploys to the wrong target |
Read that second column twice: re-listing does not save the client from a silent change. Both rows get a 200, and the agent reports success.
The only observable signals left are the serverInfo version bump and the changelog entry — which is exactly why the discipline in §6 treats "bump the version and write the changelog for behavior changes too" as a rule, not a courtesy. And if your contract tests only assert schema shape, add assertions on observable behavior per tool: same input, same target, same strategy, every release.
What agents can actually observe
A deprecation window only works if agents can see it. Here is the complete inventory of version signals MCP gives you today, roughly in the order a client encounters them:
protocolVersionnegotiation ininitialize— client and server agree on a dated spec revision (recent eras include2025-06-18,2025-11-25, and2026-07-28). This versions the wire protocol, not your tools, but it determines which features exist at all.- The
capabilitiesobject — each side declares what it supports. For tools, the load-bearing sub-capability istools.listChanged: your promise that you will emit a notification when the tool list changes, so clients know cached schemas may be stale. serverInfoname and version — the closest thing to a software version in the handshake. Clients that record it can change their behavior across your releases; clients that ignore it are flying blind. Bump it on every contract-affecting change, including behavior-only ones.notifications/tools/list_changed— the runtime signal. Well-behaved clients re-fetchtools/list(following pagination cursors) when they see it. Many clients cache aggressively and never re-fetch mid-session — design your windows for the clients you have, not the clients the spec imagines.
The cautionary tale for all of this arrived with the 2026-07-28 spec revision, which removed protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport and dropped the initialize handshake from the wire format entirely. Servers built directly on the session model broke against clients speaking the new revision — real issues, real breakage, from a change that was versioned and announced and still caught implementations pinned to the old shape.
If a dated, negotiated, spec-level version bump can do that to careful engineers, imagine what an unannounced env-to-environment rename does to a prompt-cached agent.
dry_run fights and stateless fights are the same missing contract
Two debates keep recurring in the MCP server community, and they look unrelated until you see them as symptoms. The first is the dry_run debate: should every mutating tool accept a preview flag, and what does it mean when a server overrides or ignores it? The second is the stateless turn: servers flipping Streamable HTTP to stateless mode for horizontal scaling, breaking clients that assumed sessions. Both are arguments about behavior the server never declared.
The fix for both is the same contract thinking, and MCP already ships half of it as tool annotations. Every deploy tool should carry the full set:
readOnlyHint: false— this tool changes the world; say so explicitly.destructiveHint: trueon anything that overwrites or destroys (production deploys qualify).idempotentHintset honestly — a retry-safe deploy and a retry-unsafe one need different client behavior, and the hint is how the client knows.openWorldHintreflecting whether the tool touches state outside its own sandbox.
Annotations are advisory, not enforcement — a compliant host uses them to gate calls, but nothing stops a raw client. Treat them as the machine-readable half of the contract and your descriptions, versions, and changelogs as the human-readable half. A dry_run flag whose override semantics are documented, versioned, and annotated is a feature; one that silently changes meaning between releases is §3b wearing a trench coat.
The 6-rule discipline checklist
Before your first breaking rename, adopt these six rules. They are ordered by when they pay off:
- Additive-only by default. New optional fields, new tools, new enum values — these never break pinned clients. Make them the path of least resistance in review.
- Alias, then remove. Every rename or removal ships as an alias with a deprecation note in the field
description, held across at least two minor releases before removal. - Advertise every contract change. Bump
serverInfoversion and emittools/list_changed— including for behavior-only changes where the schema diff is empty. - Put the window where agents look. Removal dates go in the tool description and a machine-fetchable changelog, not just a GitHub release note nobody's agent reads.
- Annotate every mutating tool. Full
readOnlyHint/destructiveHint/idempotentHint/openWorldHintcoverage on the deploy path, kept accurate as behavior evolves. - Contract-test the wire shape and the behavior. Assert the schema agents pin against, and assert observable behavior per tool (same input → same target), so silent changes fail CI instead of failing at 2am.
At 10,000 servers, your MCP endpoint is no longer a sidecar to your platform — it is the control plane your agents fly. Version it like one.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.
Sources
- MCP ecosystem scale (10,000+ servers, 97M monthly SDK downloads) and AAIF governance — agentic-AI briefing, April 2026, MCP adoption report
- AAIF launches MCPA certification, September 2026
- Apicurio: McpToolCompatibilityChecker misses breaking parameter schema changes
- MCP spec lifecycle: capability negotiation and list_changed
- 2026-07-28 spec removes sessions and Mcp-Session-Id (SEP-2567/SEP-2575) and downstream breakage
- ADR: tool-schema evolution policy



