Serving two MCP protocol eras: deprecations and a migration checklist

Moving a server to the 2026-07-28 revision of the Model Context Protocol (MCP) doesn’t mean you can leave older clients behind. For the foreseeable future, real servers will meet clients from two protocol eras: the initialize-handshake era (2025-11-25 and earlier) and the stateless era (2026-07-28). What surprised me in the deprecation rules is that features don’t all run on the same clock, and that no clock removes anything by itself.
This is part 22 of my series on what an MCP server does under the 2026-07-28 specification, after the threat model in part 21. It turns the changes of part 2 into a plan for your own server. The facts are as I read them in October 2026.
In brief
- Your server will meet clients from both protocol eras, so serve both from one codebase.
- Treat the era as a property of each request, not a server-wide mode.
- A deprecated feature keeps working for at least twelve months, with two shorter exceptions, and even after that it only becomes eligible for removal.
- Much is already gone, not merely deprecated, from the handshake and
Mcp-Session-Idto server-initiated requests. - My advice: drop the legacy path when your traffic says you can, not when the specification does.
One server, two eras
A clean approach, and the one several software development kits (SDKs) take, is to decide the era per request, not for the whole server:
server/discover needs no handshake; part 3 shows how clients use it to pick a version or detect an older server. A request whose _meta carries the protocol version gets the stateless lifecycle, its envelope validated every time (part 5).
What the SDKs do
Examples, as of October 2026:
| SDK | Serving both eras |
|---|---|
| Ruby SDK | Its bundled transports serve server/discover and the per-request envelope alongside the initialize handshake, with no configuration. Its client probes server/discover and falls back to the handshake. |
| C# SDK v2 | Supports the versions from 2024-11-05 through 2026-07-28. Pinned to 2026-07-28, it rejects initialize; pinned to an earlier version, it rejects the per-request envelope. Its sampling application programming interfaces (APIs) are marked obsolete. |
| TypeScript SDK 2.0 | Adds the 2026-07-28 revision. The v2 MCP runtime of Claude Code is built on it, and uses the newer revision with servers that support it. |
Python SDK, mcp 2.3.0 | MCPServer serves both eras with no configuration, as the sample below shows. |
One Python server, both eras
One MCPServer, with nothing configured for either era, has a tool that reports the protocol version its request arrived on. Two in-memory clients call it: the default mode probes server/discover, and mode="legacy" uses the initialize handshake. Part 3 used the same two modes to read discovery; here the point is the server.
import anyio
from mcp import Client
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("acme-docs")
@mcp.tool()
def which_era(ctx: Context) -> str:
"""Report the protocol version this request arrived on."""
return ctx.protocol_version
async def main():
for client in (Client(mcp), Client(mcp, mode="legacy")):
async with client:
how = "discover" if client.session.discover_result else "initialize"
result = await client.call_tool("which_era", {})
print(f"{client.mode}: {how}, {client.protocol_version}, "
f"tool saw {result.content[0].text}")
anyio.run(main)
It prints:
auto: discover, 2026-07-28, tool saw 2026-07-28
legacy: initialize, 2025-11-25, tool saw 2025-11-25
The same tool answers both clients without a line for either era; ctx.protocol_version is there for the rare handler that must tell them apart. From the package’s source and short probes:
- Routing. Over Streamable HTTP,
mcp2.3.0 routes each request by itsMCP-Protocol-Versionheader. On standard input and output (stdio), the first request settles the era for the whole connection, so there the decision is per connection rather than per request: a laterinitializeon a modern connection gets-32022(UnsupportedProtocolVersion), and a request carrying the 2026-07-28_metaenvelope on a legacy connection gets-32600(Invalid Request). - Asking the user. A
Resolvequestion (part 13) works in both eras; the push-stylectx.elicit()raisesNoBackChannelErroron a 2026-07-28 request. - Logging.
ctx.info()and its siblings run in both eras, with anMCPDeprecationWarning, but on a 2026-07-28 request the message is sent only when the request carriesio.modelcontextprotocol/logLevelin_meta.
What is deprecated, and what is already gone
The 2026-07-28 revision adopted a formal feature lifecycle, SEP-2596 (a Specification Enhancement Proposal, or SEP), with a registry of deprecated features. When a SEP marks a feature deprecated, it stays fully functional, but new implementations should not adopt it. It stays for at least twelve months, or at least 90 days when it presents an active security risk, and even then it only becomes eligible for removal: taking it out is a decision for the core maintainers.
Deprecated, still working
Part 2 gave the rule in brief; here is each feature with its earliest removal. One of them gets less than twelve months: the HTTP with Server-Sent Events (SSE) transport, HTTP+SSE.
| Feature | Deprecated | Use instead | Earliest removal |
|---|---|---|---|
| Roots | 2026-07-28 | Pass directories or files as tool parameters, as resource Uniform Resource Identifiers (URIs) or in server configuration | First revision on or after 2027-07-28 |
| Sampling | 2026-07-28 | Call your own model provider from the server | First revision on or after 2027-07-28 |
Logging (notifications/message) | 2026-07-28 | stderr on stdio, or OpenTelemetry (part 20) | First revision on or after 2027-07-28 |
| Dynamic Client Registration | 2026-07-28 | Client ID Metadata Documents; registration stays for older authorization servers | First revision on or after 2027-07-28 |
| HTTP+SSE transport | Soft-deprecated 2025-03-26, formally 2026-07-28 | Streamable HTTP | Three months after SEP-2596 reaches Final |
includeContext values thisServer and allServers | Soft-deprecated 2025-11-25 | Omit the field, or use "none" | Follows sampling’s schedule |
Removed outright in 2026-07-28
- the
initializeandnotifications/initializedhandshake; - the
Mcp-Session-Idheader; pingandlogging/setLevel;notifications/roots/list_changed;- the HTTP GET endpoint;
resources/subscribeandresources/unsubscribe;- SSE resumability (
Last-Event-ID); - server-initiated requests, replaced by Multi Round-Trip Requests (MRTR);
notifications/elicitation/completeandelicitationId;- core experimental tasks,
tasks/resultandtasks/listincluded; tasks now live in the redesigned Tasks extension (a later part), without those two methods.
Migration checklist: 2025-11-25 to 2026-07-28
Most of the move swaps an old pattern for a new one; where an earlier part explains the new one, the row links to it.
If you used to… now you…
| Old pattern | New pattern |
|---|---|
Stored per-session state keyed by Mcp-Session-Id | Return an opaque handle from a “create” tool, accept it as an argument, and keep the state behind it in a shared store (part 5) |
Read the version and capabilities once at initialize | Read them from _meta on every request (part 5) |
Sent elicitation/create from inside a tool call | Return input_required, let the client retry (part 13), and protect requestState (part 14) |
Called sampling/createMessage or roots/list | Call your own model provider, and take directories or files as tool parameters. Until both features are removed, an input_required result can still ask the client for them. |
Pushed notifications over the GET stream, or after resources/subscribe | Deliver them on subscriptions/listen: acknowledge first, and tag every message with subscriptionId (part 17) |
Relied on SSE Last-Event-ID to resume | Make tools idempotent, or move long work to Tasks |
The checklist
- Implement
server/discover: versions, capabilities, instructions,ttlMsandcacheScope. - Make each change in the table above.
- Add
resultTypeto every result. - Add
ttlMsandcacheScopeto complete results of the list methods andresources/read(part 6; aninput_requiredresult carries none, per the caching page), and sort lists deterministically. - Require and verify the Streamable HTTP headers (part 7):
MCP-Protocol-VersionandMcp-Methodon every request,Mcp-Nameonly ontools/call,prompts/getandresources/read(the Streamable HTTP page). Return-32020when a header and the body disagree. - Change resource-not-found from
-32002to-32602(part 10). - Move experimental tasks to the Tasks extension:
tasks/getpolling plustasks/update, with notasks/resultortasks/list. - Stop emitting logs unless the request sets
logLevel, and move to OpenTelemetry. - Plan the removals of roots, sampling, logging and HTTP+SSE, on the schedule in the table of deprecated features above.
- Move from Dynamic Client Registration to Client ID Metadata Documents (part 18).
- Test against at least one modern and one legacy client. In my review of MCP testing tools, the official conformance suite “carries per-revision requirement sets for 2025-11-25 and 2026-07-28”, and the official MCP Inspector “speaks stdio, SSE and Streamable HTTP in both protocol eras”.
When to drop the legacy path
The deprecation windows say when a feature may leave the specification, not when your server should stop serving older clients. My advice: don’t rush. Hosts adopt new protocol revisions at very different speeds, and a server that only speaks 2026-07-28 will look broken in any host that hasn’t caught up. Track the protocol versions you see in your clientInfo and protocolVersion telemetry, and remove the legacy path when your traffic says you can, not when the specification does.
Method and caveats
- Built from two chapters of my guide, on deprecations and on what changed, and written against the 2026-07-28 specification. The SDKs’ and Claude Code’s behaviour is as I read it in October 2026.
- The Python sample ran against
mcp2.3.0 on 9 October 2026 with the in-memory client; the output is what it printed, and the notes under it come from the package’s source and short probes. - Where the specification is more precise than my notes (how long deprecated features last, that the move from Dynamic Client Registration belongs in the migration, which requests need
Mcp-Name, which results carry caching hints), this article follows the specification’s registry of deprecated features, its Streamable HTTP page and its caching page, as checked on 8 October 2026.