The MCP changelog told you what's deprecated. It stayed silent on what breaks.
Roots, Sampling, and Logging are only annotation-deprecated; SEP-2567 deletes Mcp-Session-Id and sticky routing on July 28 with no grace period. Audit the session layer first.
You read that MCP is going stateless, opened the changelog, and found three deprecations looking back at you: Roots, Sampling, and Logging. That is where most audits will start. Start there and you spend your audit on the layer that will not break.
Those three are annotation-only. They keep working, unchanged on the wire, in every spec version released for at least a year, and actually removing them needs a spec version dated more than a year out. What breaks on July 28 is a layer the deprecation notice never mentions: SEP-2567 deletes the Mcp-Session-Id header and the protocol-level session outright, with no deprecation window at all.1 If your remote server leans on session identity, or sits behind a gateway that routes by it, that is a hard break, and it lands under four weeks out.
Audit in priority order: first every path that reads Mcp-Session-Id or routes on it, because that is what July 28 removes; put Roots, Sampling, and Logging last, because they still work a year from now. Get the order backwards and you burn your runway hardening three features that are fine, while the one change that quietly drops session state ships unreviewed.
The three deprecated features are the ones that will not break you
SEP-2577 marks Roots (roots/list), Sampling (sampling/createMessage), and Logging (logging/setLevel) as deprecated. Read the actual change and it is smaller than the word implies: a @deprecated JSDoc tag on the schema types, a warning block on the docs pages, and nothing else. No types are removed, no capability negotiation changes, and no existing implementation breaks.2 Methods, capability flags, and wire format all behave exactly as they do today.
For how long is the part that matters to your audit. SEP-2577 keeps the features “fully functional in all specification versions released within one year” of the July 28 release, and the feature-lifecycle policy underneath it guarantees at least twelve months between a deprecation and the earliest version that may remove it.3 Removal cannot land in a spec version dated less than a year out, which means the deprecation you are looking at is a signal to stop building new things on these features. Migrating the code you already have can wait.
Roots, Sampling, and Logging got a
@deprecatedcomment and a year on the clock. The session got a delete key and a date.
There is one genuinely useful thing in the deprecation notice: it tells you where each feature is meant to go. Sampling’s replacement is calling your LLM provider directly instead of asking the client to. Logging’s replacement is stderr or OpenTelemetry, which is the honest admission that structured observability was never the protocol’s job to carry. That framing lines up with a point worth internalizing separately: observability is not evals, and neither one belonged inside the message channel.
The break is the session, and it ships with no grace version
Now the change that actually moves on July 28. SEP-2567 removes the protocol-level session concept: the Mcp-Session-Id header is deleted, and the spec language describing session lifecycle and session-scoped behavior goes with it. A companion change, SEP-2575, removes the initialize handshake and carries protocol version and capabilities on every request instead.4 Together they make MCP stateless at the protocol layer.
SEP-2567 never frames the change as a deprecation. The one time it uses the word is to say there is no deprecation window.1 The only thing standing between you and an instant outage is protocol version negotiation: a client that speaks both versions will talk the old protocol to your unmigrated server and the new one to everyone else. That only buys time; it does not fix anything. The day your clients start negotiating the July 28 version, every code path that assumed a session has nothing to fall back on, and there is no per-feature grace period the way the deprecations get one.
Before you assume the worst, check the odds. SEP-2567 ships with an automated survey of a 1000-repo random sample of open-source MCP servers, labeled per repo by an LLM.1 The figures are LLM-labeled sample counts with no error bars, so treat every number here as a ballpark. About 900 of the 1000 repos reference the session ID nowhere in application code. The uses that genuinely break come to roughly 37 of the 1000, about 3.7 percent: around 25 repos key application state to the session, and the rest split between sticky-routing gateways and session-bound auth, on the order of 5 to 7 repos each. Odds are your server is in the ~900, and the audit is how you confirm it.
The session removal has no deprecation window. Version negotiation buys you time only until your clients speak the July 28 protocol, and then session-dependent code has nothing underneath it.
What to grep your server for, in priority order
Here is the first pass. It covers every category the SEP flags as a real migration; a clean run still earns a test against the release candidate before you trust it:
# Breaks July 28. Audit these first.
grep -rniE 'mcp-session-id|sessionIdGenerator|stateless_http' . # the session header + the SDK switches
grep -rniE 'sessions?\[|Map<[^>]*[Ss]ession' . # session-keyed state maps
grep -rniE 'session.?affinity|sticky' ./deploy ./gateway ./infra # sticky routing in infra config
What a hit means, mapped to the SEP’s own migration categories:
Mcp-Session-Idmeans you read the header the spec deletes. If it is only the TypeScript SDK’s routing boilerplate (aMap<sessionId, Transport>), a sessionless SDK transport migrates it away for you; the survey counts about 35 of the 1000 repos in that boilerplate category, roughly 3.5 percent. If it keys real state, keep reading.sessionIdGenerator(TS) orstateless_http(Python) is the SDK switch itself. If it is alreadysessionIdGenerator: undefinedorstateless_http=True, you are running stateless today and July 28 is a no-op for you. Every official SDK except PHP already ships a stateless mode; this change just makes it the only mode on the new protocol version.1- A session-keyed state map (
sessions[id],Map<sessionId, Cart>) is the session-keyed-state category, about 25 of the 1000 repos and the one that costs real work. Move the state to an explicit handle (next section) or key it on the authenticated principal instead. stickyorsession affinityin your gateway or load-balancer config is the sticky-routing category, on the order of 7 of the 1000 repos, and the SEP singles it out as the one category that needs a designed replacement. It gets its own section below.- Auth bound to the session (a PKCE verifier or JWT claims keyed on session ID) is the rarest category, roughly 5 of the 1000 repos. Replace it with a server-generated nonce or the token subject.
Sticky routing was scaffolding around stateful upstreams
The scariest grep hit is usually the gateway, and it is also the one people most want to re-implement. Resist that. A gateway routes by session ID for exactly one reason: its upstream was stateful, so a follow-up request had to come back to the node that held the state. Remove the state from the node (or move it to a shared store keyed by an explicit handle) and any replica can serve any request. The routing key you are about to lose was holding up something you get to delete.
A gateway that pins requests by session ID is load-balancing around a problem the new spec lets you delete.
This is the orchestrator-worker reliability payoff stated at the protocol layer: a worker you can send any request to is a worker you can lose, replace, and scale without a coordination dance, and MCP just made stateless workers the default. The SEP quantifies a second win for orchestrators that fan out to subagents: with sessions gone, tools/list is cacheable across what used to be session boundaries, which drops a subagent fan-out from O(subagents × servers) list calls to O(servers), so the orchestrator fetches each server’s tool list once and every subagent reuses it.1 If you are building the routing tier for a multi-agent system, the change removes a whole class of stickiness you would otherwise have to engineer around, and the design patterns that get simpler are the ones in multi-agent orchestration patterns.
State survives the session removal, moved into the tool arguments
Removing sessions still lets your server carry state between calls. It moves that cross-call state out of an implicit session and into an explicit, server-minted handle that the model threads through subsequent calls. The canonical shape is a creation tool that returns an ID, and later tools that take it as an ordinary argument:
// create_basket() -> returns a handle
{ "structuredContent": { "basket_id": "bsk_a1b2c3" } }
// add_item(basket_id, ...) -> the model threads the handle back
{ "name": "add_item", "arguments": { "basket_id": "bsk_a1b2c3", "sku": "shoes" } }
The handle is a pure tool-design pattern. There is no handles/* method and no handle type in the schema; from the wire’s point of view basket_id is an ordinary string in a result and an ordinary string in an argument. The spec documents and recommends it, and the widely deployed remote servers already work this way: Linear’s create_issue returns an issue id, Stripe’s create_customer returns a customer id, and every later call carries it.1
The reliability story cuts both ways. On the upside, a handle gets validated against the authenticated caller on every call, so a leaked handle is not the free hijack a leaked session ID could be under an SDK that routed on the session alone. On the downside, the model now carries the handle, which introduces a failure mode sessions did not have: if context compaction drops the tool result that held the basket_id, the state is orphaned, and an orphaned handle feeding a downstream tool is exactly the kind of well-formed-but-wrong value that propagates instead of stopping.
One more thing moved: Tasks is now an extension
If you built on the experimental Tasks primitive from the 2025-11-25 spec, budget a separate migration. Tasks graduates out of the core protocol into an official extension, reshaped around the same handle idea: tools/call can answer with a task handle, and the client drives it with tasks/get, tasks/update, and tasks/cancel.5 The blast radius is smaller than the session change and it only touches you if experimental Tasks is in your server, but it is a third thing to grep for in its own right. If you never adopted the experimental primitive, ignore this one entirely.
Do this before July 28
- Run the session grep today. If the only hits are
sessionIdGeneratororstateless_httpalready set to stateless, you are done: test against the release candidate and move on. - For session-keyed state, move it to an explicit handle or the authenticated principal.
- For a sticky-routing gateway, make the upstream stateless instead of re-implementing affinity.
- For session-bound auth, swap in a server-generated nonce or the token subject.
- Schedule, do not scramble, the Roots, Sampling, and Logging migration. A year-plus of support, and removal cannot land until a spec dated more than a year out.
- Test against the release candidate now. It is already published; the final spec locks on July 28.3
The through-line under all of it is ownership of state. A session was a piece of state the protocol held for you and never defined clearly; an explicit handle is a piece of state you own, validate, and can watch cross-call. That is the reliability upgrade hiding inside a breaking change. Unowned, unlogged state is where agent cascades start, and this change forces you to own the state that used to hide in the session. The playbook for treating that state as a measured reliability lever is in reliable AI agents in production, and the orchestration patterns that get simpler once workers are stateless are in multi-agent orchestration patterns. Audit the session layer first. The deprecations will still be there in a year.
Footnotes
-
Model Context Protocol, “SEP-2567: Sessionless MCP via Explicit State Handles” (Status: Final). Removal of the
Mcp-Session-Idheader and the protocol-level session, the clean-break rollout with no deprecation window, the 1000-repo migration survey and its per-category breakdown (session-keyed state, sticky-routing gateways, session-bound auth), the explicit state-handle pattern, thetools/listcaching win, and the SDK stateless-mode note: https://modelcontextprotocol.io/seps/2567-sessionless-mcp ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Model Context Protocol, “SEP-2577: Deprecate Roots, Sampling, and Logging” (Status: Final). The annotation-only
@deprecatedchange with no wire-level or capability-negotiation change, the “fully functional in all specification versions released within one year” support language, and the deferred-removal timeline: https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging ↩ -
Model Context Protocol Blog, “The 2026-07-28 MCP Specification Release Candidate.” The July 28 final-spec date, the release candidate being available now, the at-least-twelve-months feature-lifecycle floor, and the stateless / load-balancer framing: https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/ ↩ ↩2
-
Model Context Protocol, “SEP-2575: remove the initialize handshake and carry version/capabilities per request” (PR #2575), the companion change that, with SEP-2567, makes MCP stateless: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575 ↩
-
Model Context Protocol, “SEP-2663: Tasks Extension” (PR #2663). Moves Tasks from an experimental core feature to an official extension with a task-handle lifecycle (
tasks/get,tasks/update,tasks/cancel): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663 ↩