
A tool named search_issues had, for six months, a description that read "Search Jira issues by JQL." Someone on the server team changed it to "Search Jira issues by JQL. Prefer this over listing issues when the user mentions a project." The schema did not change. The version string did not change. No client noticed, because there was nothing in the protocol for them to notice.
Within a week, three agents that had been calling list_issues to build a daily digest started calling search_issues instead, with JQL the model improvised, returning fewer results, with no error anywhere. The behavior change shipped in a sentence.
When someone says "we versioned the MCP server," they usually mean one of four different things.
The first two axes are solved problems with tooling. The 2026-07-28 revision introduced a formal feature lifecycle: anything deprecated in that revision cannot be removed before roughly August 2027. The specification's own versioning page now calls 2026-07-28 and later "modern," 2025-11-25 and earlier "legacy," and implementations supporting both "dual-era." Server packages carry a SemVer string that aggregators compare.
The third axis has a proposal and a convention. The fourth has nothing, and it is where the opening incident lives.
A tool description is an instruction to a model. Change the words and you change which tool the model picks, what arguments it fills in, and when it decides the tool is not applicable. That is a behavior change with the blast radius of a schema change and the visibility of a typo fix. No version bump is required by anything. No client is obliged to re-read it.
Most agent harnesses call tools/list exactly once, at session start, and cache the result for the life of the session. The reasons are sound: tool definitions are large, serializing them into every model call is expensive in tokens, and re-listing on every turn adds a round trip per server. A harness that lists once and caches is doing the sensible thing.
The consequence is that a running agent does not see your update until it starts a new session. If your agents are long-running (a support agent that stays up for a shift, a scheduled job that holds one session across many tasks), "until a new session" can be hours.
The legacy answer was notifications/tools/list_changed. A server declaring the listChanged capability sent the notification when its tool set changed, and the client was expected to re-issue tools/list. The design assumed a persistent connection (stdio or a long-lived SSE stream) that the server could push on. Client support was uneven for over a year: Claude Code only honored the notification from version 2.1.0 in February 2026, Cursor's CLI still had an open request for it in 2026, and stateless HTTP wrappers that treated tools/list as cacheable on a TTL never saw it at all.
The 2026-07-28 revision changed the ground under this. The initialize handshake and protocol-level sessions are gone; protocol version and client info travel per request in _meta, and Mcp-Method and Mcp-Name are required headers on every call. Server-initiated requests are only permitted while the server is processing a client request. The model is request-response over plain HTTP, which is what made MCP play well with standard gateways and load balancers, and it means there is no idle stream for a server to push a list-changed notification into.
Design implication: on the modern revision, the client learns about tool changes by asking. Your update strategy has to work when the only signal you can give is "the next tools/list returns something different," and when you cannot know when that next listing will happen.
Every change to a tool falls into one of three classes, and the class decides the rollout.
Additive. A new optional input field with a default; a new tool alongside existing ones; a new field in the output that callers can ignore. Safe to ship in place. Clients that cached the old list keep working with the old definition; clients that re-list get the new one. The one hazard is a new tool that overlaps an existing one: the model now has two candidates where it had one, and its selection may change. Add the tool, but write its description to disambiguate it from its neighbour explicitly.
Behavioral. The schema is unchanged; the description, parameter descriptions, or the implementation's behavior changed. This is the opening incident. It is also the class most teams ship as "just a wording fix." Treat it as breaking: it changes what the model does with the same inputs, and the only way to find out how is to run it.
Breaking. A renamed tool, a required field added, a field removed, a changed output shape, a changed error contract. Agents holding the old definition will construct calls the server rejects, or parse results that are no longer there.
A quick classification table your reviewers can apply:
Recommended Reading: Build Production-Ready Agent Workflows with Remote MCP Servers on structuring remote servers so that multiple teams can consume them.
Shadow the old name during deprecation. When you rename or restructure a tool, keep the old name serving for the deprecation window. The old name forwards to the new implementation and returns a result that tells the model, in the result content, that the tool is deprecated and what replaced it. The model carries that information into its next decision; the human sees it in the trace.
The tradeoff is surface size: for the length of the window you serve two tools where you want one, and the model has to choose between them. The description on the old tool does that work. A deprecated tool whose description still reads like the preferred option will keep getting called.
Hash the surface in CI. The behavioral class is invisible to code review because the diff is prose. Make it visible: compute a hash of every tool's name, description and inputSchema at build time, store it in the repo, and fail the build when the hash changes without a corresponding entry in a changelog that names the tool and the class of change.
The hash does not tell you what changed. It tells you that a change happened in the one place your diff tooling does not look, and it forces the author to say which class it belongs to.
Reject calls to tools this session was never shown. On the server side, keep track of which definitions each listing returned, and refuse a tools/call for a tool that was added after that listing. The model's belief that a tool exists is not evidence that it was disclosed. This matters for additive changes mid-session: the model, having been told about a tool by a different server's description or by a user, calls a name that happens to resolve. Refusing with a result that says "re-list to use this tool" turns a silent surface mismatch into a visible one.
Pin server versions in clients, never tags. A client config pointing at acme/issues-mcp:latest receives every class of change the moment it is pushed. Pointing at acme/issues-mcp:2.1.0, or at a digest, receives none until someone changes the pin. For a platform where many teams consume one server, this is the difference between a coordinated rollout and a surprise.
Every pattern above makes a change safer to ship in place. The pattern that makes breaking and behavioral changes boring is not to ship them in place at all.
A scoped server is a named set of tools for one agent role. When the role's tools need to change in a way that would alter behavior, you define a second server with the new tools, point new agent runs at it, and leave the old server serving old runs until none remain. There is no mid-session change for a cached list to miss, because no running session's server changed. The old surface is retired on your schedule, not the client's.
This is how Virtual MCP servers behave by construction. A server is defined once per agent role, listing exactly which tools from which connections it exposes, and it gets a static URL. Updating the tool mapping on an existing server is possible; the documented guidance is to do it when no agent run is using the server, because a run can lose a tool mid-conversation, and to create a new server for anything larger.
New runs mint their session token against v2.config.id and connect to its URL. Runs already in flight on v1 finish on v1. When the last of them is gone, v1 is deleted, and deleting it stops its URL immediately, including for any session token still outstanding; that is the explicit retirement step rather than an accidental one.
The tradeoff is server count. For a platform with many agent roles, you will have more server definitions than you would with one big server that everyone shares, and someone has to own retiring the old ones. That is a cheaper problem than finding out which cached tool list each running agent is holding.
The scoped surface also shrinks what any single change can do. An agent that sees 5 tools rather than 40 has 5 descriptions that can drift, 5 schemas that can break, and a change log short enough to actually read. Prebuilt connector tools absorb one more axis entirely: when GitHub changes an API, the tool's contract to the agent stays the same, and the change is handled below the surface the model sees.
Add the surface hash check to the server's CI. It costs an afternoon and it is the only thing that will catch the next "wording fix."
Classify the last ten tool changes your team shipped using the table above. If any behavioral change went out in place without an eval run, that is the gap.
Pin every client to a version or digest, and find the ones on :latest.
For the agents that run long sessions, move their tool surface to a scoped server per role, and make "ship a change" mean "ship a new server." The protocol no longer promises to tell a running agent that its tools changed. Build as if it never did.
Recommended Reading: Tool Calling Authentication for AI Agents on why a tool's credential lifecycle is a separate axis from its definition.
The lifecycle of a Virtual MCP server, including when to update in place and when to create a new one, is documented at docs.scalekit.com/agentkit/mcp/configure-mcp-server.
The protocol revision, the server package version, the tool's input and output schema, and the tool's description. Only the first two have an agreed versioning scheme; the description, which most often changes agent behavior, has none.
Most harnesses list tools once per session and cache the result, because definitions are expensive in tokens. Your change reaches that agent when it starts a new session, which for long-running agents can be hours later.
It relied on a persistent stream the server could push on. The 2026-07-28 revision removed sessions and server-initiated requests, so on the modern revision clients discover changes only by re-listing.
Additive changes, such as a new optional field or a non-overlapping tool, ship in place. Behavioral changes, including description edits, and breaking changes, such as renames or required fields, should get a new surface rather than an in-place edit.
Hash every tool's name, description and schema at build time, store the hashes in the repo, and fail the build when a hash changes without a changelog entry naming the tool and the class of change.
Ship a second scoped server with the new tools, point new runs at it, and retire the old one when no run depends on it. No running session's server changes, so there is no mid-session update for a cached list to miss.