ChangeGamer

← All guides · MCP in practice

MCP Server Versioning and Spec Migration: An Operator Playbook

Part 5 of MCP in practice · 1,466 words · ~7 min read · published 2026-08-11 · updated 2026-09-27 · 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 Authentication and the Production MCP Server Playbook 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.

How do you feature-detect capabilities instead of checking 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

Put this corpus inside your own agents

This guide is free and stays free. The reference corpus behind it — machine-readable contracts, verified primary sources, continuously refreshed — is the paid product: a €5 starter key unlocks every premium reference for one agent via API; a €25 corpus license delivers the full corpus as RAG / fine-tuning data with an explicit AI-use grant; the €150 enterprise license adds commercial redistribution rights.

Agents: this page as Markdown · JSON · offers at /api/pricing.json · payment methods at /api/payment.json · single-resource access via HTTP 402 (how that works)