Versioning an MCP tool surface without breaking connected clients
•6 min read
There is no negotiated version number for a tool surface. That makes the rules simple and strict: add freely, never redefine a name, and put the migration note where the model will read it.
The Model Context Protocol versions itself, not your tools. Protocol revisions carry dates, are negotiated per request, and are the subject of an UnsupportedProtocolVersionError when a server does not implement the one it was asked for. Nothing in that machinery describes your tool names, their parameters or what they mean. So a tool surface is versioned by discipline rather than by a number: add tools and optional parameters freely, never change what an existing name does, and when a change of meaning is unavoidable, ship it as a new name while the old one carries a deprecation note in the place the client actually reads.
What the protocol versions, and what it leaves to you
MCP protocol revisions are dated rather than numbered, and the version is not incremented when a change keeps backwards compatibility. Negotiation happens per request: each request declares its protocol version, the server accepts or rejects it independently, and a client that wants to know up front can ask the server to describe the versions, capabilities and identity it supports in a single call. Clients and servers may support several revisions at once. All of that is about the envelope. What the tools are called and what they accept is your API, versioned by you.
12 monthsMinimum time a deprecated feature stays in the MCP specification before it is eligible for removal, or 90 days under the expedited-removal exceptionMCP specification, Versioning
That policy is worth copying rather than admiring. The specification's own deprecation rule is that a deprecated feature documents a migration path, or states that none is required, and then remains in place for a stated minimum before it can be removed. A tool surface that does the same thing gives an integrator a window measured in months and a written instruction, instead of a call that started returning an error.
Which changes actually break a client?
The same taxonomy any API uses, with one addition that is specific to tools driven by a model.
Change
Breaking?
How to ship it
Adding a new tool
No
Ship it; announce a tool-list change if you declared that capability
Adding an optional parameter
No
Ship it; give it a default that preserves today's behaviour
Making an optional parameter required
Yes
New tool name, or accept both and warn in the result
Removing a parameter or a tool
Yes
Deprecate first, remove after a stated window
Narrowing an accepted enum or a length limit
Yes
Treat as removal; the calls it now refuses used to succeed
Changing what a field means or what units it uses
Yes, and silently
New field name; never redefine one in place
Rewriting a tool's description
Often, in practice
Review it like code; the description is what the model reads
When an assistant works through dozens of your tools in a single turn, there is no transaction around them. Everything before the failure has already happened, and the turn can still end by reporting success.
Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
No card to start. Cancel from the settings screen, not from an email.
The last row is the one that surprises people. For a client driven by a language model, the description is the interface: it decides whether the tool is called at all, when, and with what arguments. A rewrite that changes no schema can change behaviour across every connected client at once, which makes tool descriptions a place for careful edits and a changelog entry, not a place for casual copy improvements.
How to deprecate a parameter in practice
Keep the old parameter, mark it deprecated in its own description, and say what to send instead. Market4's changelog tool does exactly this: create_changelog still accepts a markdown content field whose description begins "Deprecated" and points the caller at structured blocks, and it still accepts an older cover image URL field marked deprecated because that field carries no alt text while its replacement requires it. Both old fields keep working. Neither is what a new integration will choose, because the description says so at the point of use.
Putting the note in the description rather than in external documentation is the part that matters for an agent-driven client. Documentation is read by the person integrating once; the description is read by the model on every call. A deprecation nobody reads is a deprecation that turns into an outage on removal day.
Prefer a section argument to a new tool name
Growing a surface by adding names has a cost that is easy to miss: every new name is another thing a client can fail to call. Market4 puts six Google Tag Manager reads behind one tool with a section argument, and when three more content checks were added they became new sections of an existing check tool rather than three new tools, on the reasoning that the failure mode of a guardrail is not that it is wrong but that nobody runs it. Fewer names means fewer things to skip, and one description can explain when each section applies.
The counter-case is just as instructive. One originality check was deliberately split out of that same tool into a name of its own, because it reconciles an index and therefore is not a read, while every other section is. A client deciding whether a call is safe to repeat reads the tool's annotation, so grouping a write with a set of reads would have made that annotation a lie about one of them. Group by what a call costs and what it changes, not by what it is about.
Announcing a change to the tool list
A server that supports tools declares the tools capability, and it may declare that it emits a notification when the list of available tools changes. Declaring that capability is a promise: clients that trust it will not re-list on their own, so a server that adds a tool and stays silent leaves those clients working from a stale list. If you declare the capability, emit the notification on every change; if you do not want the obligation, do not declare it, and accept that clients will discover new tools whenever they next list.
Can I put a version number in my MCP tool names?
You can, and it is worth resisting until you need it. A name such as get_report_v2 is a permanent scar on a surface a model reads: it invites the model to guess which version applies, and it doubles the descriptions to maintain. Version the name only when a genuine break has no additive form, and when you do, make the old tool's description say plainly that it is deprecated and which name replaces it.
How do I know which clients are still using a deprecated tool?
Log calls by tool name and by whatever identity your authorisation layer establishes, and count them per week. That gives you the two facts a removal decision needs: whether anything still calls the old name, and who. Without it, the removal date is a guess, and the usual outcome is either removing something still in use or keeping a dead tool for years because nobody could prove it was dead.
Does adding a field to a tool's result break clients?
Usually not, because clients generally ignore fields they do not know. It does break the small number of clients that validate results strictly, and it can change behaviour for a model-driven client that now sees extra information in the result and reasons about it. Treat added result fields as low risk rather than no risk: ship them, and describe them, so the change is visible to whoever reads the surface.
Should the tool surface follow the protocol revision it runs on?
No. They move for different reasons and on different schedules. A protocol revision changes the envelope and is negotiated between client and server; a tool surface changes because your product changed. Tying the two means a protocol upgrade forces a tool review, and a tool addition raises a question about the protocol that nobody asked. Keep the two changelogs separate and say which one each entry belongs to.
Both can hand a model a block of JSON, so the choice looks cosmetic. It is not. A tool call is a decision the model makes with arguments it invents; a resource read is a fetch the client performs against an address it already holds.
A tool error that says "invalid input" ends the run. One that quotes the rejected value, lists the accepted set and names the next move lets the assistant repair the call itself. Four real messages from one codebase, read closely.
McpError handlingApi design
Versioning an MCP tool surface without breaking clients