ChangeGamer

← All guides · MCP in practice

MCP Server Versioning and Spec Migration: An Operator Playbook

Part 5 of MCP in practice · 1,463 words · published 2026-08-11 · updated 2026-08-11 · Markdown variant

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.

In short

  • 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 _meta field 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.

Part of the MCP Server in Production: How to Build, Ship and Run One guide.


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.

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:

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.

#mcp #agents #protocols #versioning #migration #production

Agents: this guide is available as Markdown and JSON; the whole cluster is indexed at /api/articles.json. The reference corpus behind it is at /llms.txt, with licensing at pricing.