MCP Server Versioning and Spec Migration: An Operator Playbook
A migration runbook for MCP server operators: feature-detecting via capabilities instead of hard protocolVersion branching, a dual-version fleet rollout with rollback triggers, a compatibility shim for legacy clients still sending initialize, and a deprecation calendar built off the 12-month SEP-2577 floor.
- The 2026-07-28 MCP spec revision removed the initialize/initialized handshake entirely (SEP-2575) along with the Mcp-Session-Id header (SEP-2567); client capabilities, previously sent only during that handshake, now travel in a
_metafield on every request instead. Any guide describing "pinning protocolVersion during the initialize handshake" is describing pre-2026-07-28 behavior, not the current mechanism. - Feature-detect off declared capabilities, not off a hard-coded protocolVersion string comparison — a server or client that branches on an exact version string breaks the moment a new patch-level revision ships with the same capability set, while capability-based branching keeps working.
- A server fleet migrating across a spec revision should run old and new code side by side behind version-tagged, canaried traffic, with an explicit rollback trigger (a spike in malformed-request or protocolVersion-mismatch rates, not a fixed timer) rather than a single flag-day cutover.
- SEP-2577's 12-month deprecation floor is a concrete calendar item, not just a policy statement: Sampling, Roots and Logging were deprecated on 2026-07-28, so the earliest the spec permits their removal is 2027-07-28 — treat that date as a deadline to have migrated off them, not a date to start thinking about it.
- There is no published, spec-defined mechanism for how a post-2026-07-28 stateless server should respond to a legacy client that still opens with an initialize call — any compatibility shim for that case is an operator-built workaround, not a documented protocol feature, and should be tested against a real legacy client rather than assumed to work.
The pillar's versioning section tells you what changed across MCP's three dated revisions and lays out a baseline discipline for staying current with the spec. This article is what happens after you have absorbed that discipline and actually have to migrate a running server across a revision boundary — the mechanics of feature detection, a fleet rollout that does not take your server down for every client mid-upgrade, a concrete answer for the legacy-client compatibility gap the pillar names but does not solve, and a deprecation calendar you can put dates on. It assumes you have already read the pillar's revision table and failure mode #8 (spec-transition breakage); this does not repeat either.
Feature-detect on capabilities, not on a protocolVersion string
Before the 2026-07-28 revision, a client declared its capabilities once, during the initialize exchange, and the server replied with its own protocolVersion, capabilities, and serverInfo in the same handshake — the negotiated version then governed the whole session. As of the 2026-07-28 revision, that handshake is gone: SEP-2575 removes initialize/initialized outright, and client capabilities — previously sent only during the now-removed handshake — travel in a _meta field on every request instead. There is no longer a single negotiation moment; capability information arrives, potentially differently, on each call.
That shift makes a hard-coded if (protocolVersion === '2026-07-28') branch a worse idea than it already was. A version string changes on every dated revision, including ones that add nothing relevant to the code path you are guarding — you would be updating that branch on every spec bump, whether or not it affects you, and a server built pre-2026-07-28 that only checks protocolVersion has nothing to read at all once a client stops sending it inside a handshake. Branch on the capability itself:
// Illustrative — not copied from an SDK. Feature-detect off declared
// capabilities in _meta, falling back to a conservative default rather
// than assuming a specific protocolVersion implies a specific capability.
function handleToolCall(req: JsonRpcRequest, meta: Record<string, unknown> | undefined) {
const clientCaps = (meta?.capabilities as string[] | undefined) ?? [];
if (clientCaps.includes('structuredOutput')) {
return respondWithSchema(req); // JSON Schema 2020-12 output (SEP-2106)
}
return respondWithLegacyShape(req); // plain content array, safe default
}
The one place a hard protocolVersion check is still correct: rejecting a session outright when a client asks for a version your server does not support at all — a fast, loud failure, not a silent downgrade. Everything short of that outright rejection should key off what a specific caller declares it supports, not off which dated string it happens to be sending.
Backward compatibility for a legacy client's initialize call
The pillar names spec-transition breakage as failure mode #8 without solving the specific case of a legacy client landing on a post-2026-07-28 server. Here is the honest state of that problem: nothing in the shipped spec defines server behavior for an initialize request, because the 2026-07-28 revision removed the method entirely. That is not an oversight to work around quietly — it means any fix you build is your own compatibility shim, not a documented protocol feature, and you should treat it that way in code review and in testing.
JSON-RPC's own method dispatch does not forbid a server from still recognizing initialize as a method name even though the current spec no longer describes it, so a workable shim looks like:
// Operator-built compatibility shim, not a spec-defined behavior.
if (req.method === 'initialize') {
return {
jsonrpc: '2.0',
id: req.id,
result: {
protocolVersion: negotiateVersion(req.params.protocolVersion),
capabilities: SERVER_CAPABILITIES,
serverInfo: { name: 'example-server', version: '2.0.0' },
// No Mcp-Session-Id is issued: the server has nothing to key one to.
},
};
}
// Every other request, from any client generation, is handled statelessly.
Two things this shim does not solve, and cannot: whether the legacy client tolerates a response with no session ID rather than erroring or looping into a fresh initialize attempt is entirely up to that client's own implementation, and nothing in the corpus confirms behavior either way. Test the shim against a real old client build before shipping it, not just against your own mock — an untested assumption here fails exactly at the seam between "responded successfully" and "behaved the way the old client expected next."
A dual-version fleet rollout
Migrating a running fleet across a spec revision is a deployment problem, not just a code change, and it deserves the same discipline as any other breaking dependency upgrade: run both versions side by side, canary a small slice, and roll back on a real signal.
- Tag releases by the spec revision they target, and keep the previous version deployable and runnable, not just in source control, for the length of your rollout window.
- Route a small percentage of traffic to the new version first rather than a flag-day cutover across every instance simultaneously — a stateless architecture (true of every server since 2026-07-28) makes this easier than it would be for a stateful service, because there is no session affinity to preserve across the split.
- Define the rollback trigger before you start, and make it a rate, not a timer: a rise in
protocolVersion-mismatch responses, a rise in malformed-request rates from clients that assumed pre-migration behavior, or a spike in reconnect attempts from clients repeatedly retrying a call the new version handles differently. Any of those crossing a set threshold should revert the canary slice automatically, the same way you would gate any other production rollout on an error-rate regression rather than a fixed soak time. - Widen the rollout only after the canary slice has run through your actual traffic patterns, not just a quiet overnight window — spec-transition bugs tend to show up on the request shapes your regular clients send, not on synthetic smoke traffic.
None of this replaces the CI-level protocolVersion assertions covered in testing an MCP server in CI — those catch a mismatch before you ship a build at all. This is what to do once a build you have already tested is going out to a fleet of instances that different clients are actively connected to.
A deprecation calendar with real dates on it
The pillar's advice to "subscribe to the spec changelog" is correct and underspecified. Concretely: the changelog for a given revision lives at a fixed, versioned path in the spec repo, and the spec project also publishes a release list and per-SDK release pages for the four official SDKs — those are the feeds worth watching, not just a generic "check the website periodically" habit (MCP goes stateless).
Two operational anchors turn that subscription into an actual calendar rather than a vague intention:
- A revision typically has a release-candidate lock before it ships final. The 2026-07-28 revision's RC locked on 2026-05-21 — roughly two months before the final spec shipped. Treat an RC lock as your trigger to start migration work, not the final release date; by the time a revision is final, you are already behind if you have not started.
- SEP-2577's 12-month floor is a hard deadline, not a suggestion. Sampling, Roots and Logging were deprecated, final and not draft, on 2026-07-28 (MCP primitives). Removal is barred for at least twelve months from that date, which puts 2027-07-28 as the earliest possible removal — plan the migration work to land well before that date, since "at least twelve months" gives the maintainers room to extend the floor, never to shorten it.
When a new SEP shows up in a changelog, triage it against three questions before deciding it needs action: does it change what crosses the wire on every request (like SEP-2243's routing headers did), does it deprecate or remove something your server currently relies on (as of August 2026), and does it change what a client is required to send you (like capabilities moving into _meta). A SEP that fails all three is worth noting and moving on from; one that hits any of them is worth a canaried rollout using the runbook above, not a same-day production edit.
Where this leaves you
Feature-detect on capabilities rather than a version string, treat legacy-client compatibility as your own tested shim rather than a spec guarantee, roll out a spec migration across a fleet the same way you would any breaking dependency upgrade — canaried, with a rate-based rollback trigger — and put the SEP-2577 twelve-month floor on an actual calendar rather than a mental note. For what changed across the three revisions and the four-point baseline discipline this builds on, see MCP server in production; for catching a protocolVersion regression before it ships, see testing an MCP server in CI; for the OAuth-specific SEPs from the same 2026-07-28 hardening set, see implementing OAuth 2.1 for an MCP server.
Frequently asked questions
- Should I branch my server logic on protocolVersion or on capabilities?
- Capabilities, wherever the choice exists. protocolVersion is a single string that changes on every dated revision, including ones that add nothing your server cares about — branching on it directly means you have to update that branch on every spec bump whether or not it affects you. Capabilities describe what a specific client or server actually supports, which is the thing your code needs to know to decide how to respond. Reserve a protocolVersion check for the one place it genuinely matters: rejecting a session outright when a client requests a version your server does not support at all.
- What happens if an old client sends an initialize request to my post-2026-07-28 server?
- Nothing is specified, because the 2026-07-28 spec revision removed the initialize/initialized handshake entirely — there is no defined server behavior for a method the current spec no longer describes. JSON-RPC itself does not forbid a server from still answering a method call it recognizes, so a server can choose to special-case `initialize` and reply with a synthesized response for backward compatibility, but that is an operator-built shim, not something the spec guarantees will work end-to-end with every legacy client. Test it against an actual old client before relying on it — the client's post-handshake expectations (like waiting for a session ID that will never arrive) are exactly where an untested shim breaks.
- How long do I have to migrate off a deprecated MCP primitive like Sampling or Roots?
- At least 12 months from the deprecation date, per SEP-2577. Sampling, Roots and Logging were marked deprecated — an annotation-only change, so all three still work today — on 2026-07-28, which makes 2027-07-28 the earliest date the spec permits their removal, not a fixed removal date. Treat the floor as your outside deadline: plan the migration work well before it, because "at least 12 months" leaves the maintainers room to extend the window, but no room to shorten it.
- How should I roll out a spec-migration change across a fleet of server instances?
- The same way you would roll out any breaking dependency upgrade: side by side, not as a single cutover. Deploy the migrated code as a version-tagged release alongside the version it replaces, route a small percentage of traffic to it, and watch for the specific failure signal a spec migration produces — a rise in protocolVersion-mismatch responses, malformed-request rates from clients that assumed the old behavior, or a spike in reconnect/re-initialize attempts. Trigger rollback on that signal crossing a threshold, not on a fixed timer, and only widen the rollout once the canary slice has run clean through your normal traffic patterns, not just a quiet period.