Changing your MCP server without breaking your users
Your server is published and you want to rework a tool. In 10 steps you make the surface visible, check changes automatically and roll over without other people's setups quietly breaking.
The first release is the easy part. You build tools, you push the server out, people wire it in. It gets interesting at version two. One tool has an awkward name, a parameter is missing, a response should be more structured. And from now on every change has somebody hanging off it that you do not know and cannot ask.
The difference to a normal library: your users are not only developers, they are language models too. When a model has been calling a tool correctly and reaches past it after your update, there is no compiler error. There is an answer that is simply worse. That is what makes broken MCP changes so hard to notice.
This guide shows you how to make your surface visible, check changes automatically and switch over cleanly.
Step 1: Work out what your contract actually is from the user's side
Your contract is not your code. It is exactly what a foreign client gets to see when it connects: the list of your tools with name, description and input schema, plus your server details. Everything underneath that you can rebuild as you like.
Take five minutes and write that surface down once as a list. Per tool: what it is called, which fields are required, which are optional, what comes back, and what it means in domain terms. That list is your reference point from now on.
The last column, the domain meaning, is the one everybody forgets. It appears in no schema and still breaks the hardest.
Step 2: Make the description part of the API
In a classic library a comment is cosmetics. In MCP a tool's description is functional, because the model decides from it whether to call the tool at all.
Rephrase a description and you have changed the routing behaviour, even though no schema was touched. A tool that used to be picked reliably suddenly gets skipped, or another one pulls the calls over to itself.
So treat descriptions like code. They belong in the review, not in a quick tidy-up commit on the side.
Step 3: Freeze the surface as a file
Your server can print its own tool list. That is exactly what you write into a file and put next to your code in the repo, for example as mcp-surface.json.
From now on your contract is no longer an idea in your head but a versioned file. Every change to your tools produces a change to that file, and you see it in the diff before anybody else notices.
This is the one step that moves the whole topic from discipline to mechanics. Without it, all the following steps stay good intentions.
Step 4: Let the diff stop your build
Build a check step that generates the current tool list during testing and compares it with the checked-in file. If they differ, the run aborts.
What matters is the error message. It should not say "file differs", it should say what changed and whether that is harmless or delicate for users. So: new tool added, field went from optional to required, tool disappeared.
The abort is not a ban. You may update the file at any time. You just should not walk past it by accident.
Step 5: Memorise the four changes that always break
One, renaming a tool. Every existing call points into the void.
Two, introducing a new required field. All calls that do not send it fail the schema check.
Three, making a previously optional field required. Same effect, just less obvious.
Four, changing the meaning while keeping the name. A limit field meant rows yesterday and pages today. Schema identical, results wrong, and nobody gets an error. That is the case that haunts you the longest.
Step 6: And the ones that are safe
Adding a new tool is uncritical, as long as it does not crowd out the existing ones.
A new optional field with a sensible default is uncritical, because old calls keep working unchanged.
Extending a result with an extra field is mostly uncritical, as long as the existing fields keep the same meaning.
Everything that consists of removing or reinterpreting belongs in step 5. Everything that consists of adding is at home here. That dividing line is easier to remember than any list of rules.
Step 7: Build alongside instead of bending
You want to rename search_docs to find_documents and tidy up the parameters. The expensive way is to do it in one go.
The calm way: you add the new tool alongside. Both run in parallel, internally they call the same code. The old one stays functional, the new one is the recommended path.
That moves the pain from your release date to a moment you pick yourself. And you keep the option of switching the old tool off only once you can see nobody is using it any more.
Step 8: Announce the shutdown where it gets read
A note in the changelog reaches people who read changelogs. That is few of them, and language models are not among them.
The place that actually lands is the description of the old tool. One sentence is enough: that this tool has been replaced, what the successor is called and from when it goes away. Models read that description on every connect, developers see it while debugging.
Set a concrete date instead of "soon". Without a date nothing happens, and you drag the old tool along for two years.
Step 9: Look after the version number you are sending anyway
When the connection is set up, your server reports its details, among them a version. Many people set it to 1.0.0 once and never touch it again.
Use it as a signal instead. Raise the third position for internal fixes, the second for the additions from step 6, the first for anything from step 5. That way somebody who wires your server in can see without asking how risky an update is for them.
It costs you nothing and it is the only machine-readable warning you have at all.
Step 10: If something changes at runtime, say so
There are servers whose tool list changes while running, for instance because a user unlocks a module. For that case the protocol provides a notification with which the server tells the client that the list has changed.
If your list is dynamic, send it. Otherwise the client keeps working with a state that no longer exists and calls tools that have disappeared.
If your list is fixed at startup, you do not need this. Then your release process is the place where the change lands, and for that you have steps 3 to 8.
What next
If you are standing here, you have a running server with real users. The next sensible extension is access control, because from the moment strangers wire your tool in, you want to know who. That is covered in MCP auth.
And if you are coming from the other side, wiring in other people's servers yourself and wanting to know what to watch out for: the playbook Vetting third-party MCP servers safely is the reverse direction of this one.
Sources
The details on tool definition, input schema and the notification for a changed tool list come from the MCP specification, section Tools: https://modelcontextprotocol.io/specification/2025-06-18/server/tools
The server details sent when the connection is established, including the version field, are in the Lifecycle section: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle