Ship a REST API and you have decades of convention for versioning it: a /v2/ path, a deprecation header, a changelog someone actually reads before flipping a switch. Ship an MCP server and none of that exists yet. The spec has no version field for an individual tool, no standard way to say “this argument is deprecated,” and no built-in negotiation for “give me the old shape of this response.” So when a tool’s inputs or behavior change, the only client that finds out is whichever one calls it next — and it finds out by getting an error, or worse, a result it silently misinterprets.
This is the builder’s side of a problem this blog has covered from the user’s side before: a tool that changes underneath an approval you already gave is exactly what MCP rug pulls are about. Versioning well is how you avoid causing one by accident.
Why MCP makes this harder than a REST API
An MCP tool is identified by name, and a client discovers its current shape by calling tools/list. There’s no per-tool version number in the spec, and no content-negotiation mechanism like an HTTP Accept header. In practice, most MCP clients cache the tool list from a session or a short-lived poll, then keep calling tools by name with whatever arguments the model constructs. If you rename a required field, change what a field means, or alter the shape of a response, every client that hasn’t re-fetched the tool list is now calling your tool with a schema it thinks is still current.
The result isn’t always a clean error. A renamed field usually does fail loudly, which is the easy case. A field whose meaning changed — a status enum that gains a new value, a timestamp that switches time zones, an amount that switches from cents to a decimal — often doesn’t fail at all. It just returns a plausible, wrong answer that the model reports with full confidence.
What actually breaks when you change a tool
- Renaming or removing an input field. Any client whose model was “taught” the old shape in an earlier turn, or whose prompt hardcodes an argument name, starts failing schema validation immediately. This is the loud, easy failure.
- Narrowing what a field accepts. A
statusstring that used to take any value and now enforces an enum will reject calls that used to work, with no warning before the change shipped. - Changing a response’s meaning without changing its shape. The quiet one. The JSON still parses, the field is still called
amount, but it now means something different. Nothing errors. The model just reasons from a number that no longer means what it did yesterday. - Changing what a tool actually does. Same name, same schema, different side effect — a
send_messagetool that used to post to one channel and now posts to two. This is the case a tool description can’t catch, because the description didn’t need to change for the behavior to.
Patterns that actually work today
Since the protocol doesn’t give you a version negotiation mechanism, the fixes all live in how you name, structure, and roll out tools — not in anything MCP enforces for you.
- Version the tool name, not a hidden parameter.
send_emailandsend_email_v2as two distinct tools is unglamorous, but it means an existing client keeps calling the tool it already knows, unchanged, while new clients pick up the new one deliberately. Nothing breaks silently because nothing was rewritten in place. - Additive changes over breaking ones. Adding a new optional field, or a new enum value that old logic can safely ignore, doesn’t require a version bump. Removing a field, renaming one, or narrowing what’s accepted almost always does. Treat “can an old caller ignore this and still work correctly” as the test.
- Keep the old tool alive through a deprecation window. Retire
send_emailon a published date, not the momentsend_email_v2ships. Say so in the tool’s own description — it’s the only “changelog” a model reliably reads, since there’s nowhere else in the protocol to put one. - Bump a visible server version, even without a spec requirement for it. MCP’s
initializehandshake carries aserverInfo.versionfield. It doesn’t gate anything on its own, but a client or gateway that logs it gives you a real signal to correlate against — “this started failing right after the server version changed” is a debuggable sentence. - Change response shape behind a schema check, not silently. If a response’s meaning has to change, change its field name too. A caller reading
amount_centsinstead ofamounteither updates deliberately or fails loudly — it doesn’t keep working with the wrong unit.
Test the old shape before you ship the new one
A pre-publish checklist for a brand-new server is one thing — we covered that ground in how to test an MCP server before you publish it. Versioning adds a second question on top of it: does a client that only knows the previous tool list still get correct results after this deploy? That means keeping a fixture of the last published tool list and response shapes around, and running it against the new server before rollout, not just testing the new behavior in isolation.
Where this connects to security, not just compatibility
A tool that’s redefined without a name change is indistinguishable, from the client’s side, from a server that’s been compromised and started behaving differently on purpose. That’s the whole premise behind drift detection: a gateway that hashes each tool’s description and schema on every sync will flag your own legitimate version bump the same way it flags a hostile one. That’s not a false positive to silence — it’s the mechanism working. The fix is to version deliberately enough that a flagged change is always one you meant to make, with a name and a description that explain it, not a discovery someone else makes first.
The bottom line
MCP gives you no version negotiation, no deprecation header, and no schema diffing for free — all of that is on the builder. The practical response is boring on purpose: version the tool name instead of rewriting a live one, prefer additive changes, keep the old tool around through a real deprecation window, and test against the previous shape before you ship the new one. None of it is exotic. All of it is the difference between a client that adapts on its own schedule and one that finds out your tool changed by getting a wrong answer it never questioned.