Sessions are gone, the handshake is gone, and server-initiated requests work differently. Here is what actually changed, and what it costs to migrate.
Updated August 2026 · 10 min read · Sourced from the spec diff & SEPs
TL;DR
- Sessions and the
Mcp-Session-Idheader are gone — no sticky routing needed- The
initializehandshake is removed; version negotiation happens per request- Server-initiated requests (sampling, elicitation, roots) become client-driven retries via Multi Round-Trip Requests
- SSE streams no longer resume — a broken stream means a fresh request, and repeat-safe tools are now your responsibility
- Roots, Sampling, and Logging are deprecated (12-month window), not removed
- Python SDK v2 requires FastMCP→MCPServer, snake_case fields, and pinning
mcp<2if you are not ready
Most protocol changes look simple on paper. The real question is not what changed in the spec, but how that change reshapes the systems that depend on it. With MCP stateless design now formalized in the 2026-07-28 release, developers need to rethink how they handle context, retries, connections, and deployment.
Stateful systems are hard to scale and harder to operate. Every session held in a server's memory is a liability the moment you restart a pod, route around a failed node, or scale horizontally under load. The Model Context Protocol carried that burden from its first revision — and rather than patch around it, the maintainers removed it.
This is not a cosmetic API update. The MCP 2026-07-28 spec removes protocol-level sessions, deletes the initialization handshake, replaces server-initiated requests with a new pattern, deprecates three long-standing features, and rewrites large parts of the SDK surface. Everything below is traceable to the official changelog, a numbered Spec Enhancement Proposal, or the SDK migration guide.
SCOPE NOTE
This article is a technical explainer written from the specification diff, the SEP discussions, and the official SDK migration guide. Where a described pattern is an implementation choice rather than a protocol requirement, it says so explicitly.
Why the MCP stateless shift matters
Sessions were tied to a server instance
Under earlier revisions, a client opened a connection, ran an initialize handshake, and received an Mcp-Session-Id. Every subsequent request carried that ID, and the server was expected to remember the negotiated protocol version, the client's capabilities, and whatever state the connection had accumulated.
That model works on a laptop. In the cloud it forces sticky routing: every request from a client must land on the same instance that holds its session.
The 2026-07-28 revision removes the constraint
REMOVED
Protocol-level sessions and theMcp-Session-Idheader are gone from the Streamable HTTP transport entirely. List endpoints —tools/list,resources/list,prompts/list— no longer vary per connection. SEP-2567
The operational payoff, as the maintainers described it in the SDK beta announcement, is that an MCP server can now sit behind an ordinary round-robin load balancer — no sticky sessions, no shared session store.
The tradeoff is explicit
Statelessness does not mean state disappears. It means the protocol stops managing it for you. Servers that need cross-call state now issue explicit, server-minted handles that the client passes back as ordinary tool arguments. The state becomes visible to the model rather than hidden in a connection object.
What actually changed between stateful and stateless MCP?
| Aspect | 2025-11-25 and earlier | 2026-07-28 |
|---|---|---|
| Connection setup |
initialize handshake required |
No handshake; server/discover
|
| Session identity |
Mcp-Session-Id header |
Removed |
| Protocol version | Negotiated once per connection | Sent per request in _meta
|
| Cross-call state | Implicit, server-held | Explicit handles as tool arguments |
| Server → client requests | Direct callbacks | Multi Round-Trip Requests |
| Change notifications | HTTP GET stream + resources/subscribe
|
subscriptions/listen |
| Stream recovery | SSE resumability via Last-Event-ID
|
Removed; client re-issues |
| Load balancing | Sticky routing required | Any instance serves any request |
Key technical changes
01 — The handshake is gone
The initialize / notifications/initialized handshake has been removed. Every request now carries its own protocol version and client capabilities in _meta, using the keys io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. Clients should identify themselves per request via io.modelcontextprotocol/clientInfo, and servers should return io.modelcontextprotocol/serverInfo in each result's _meta. A version mismatch returns UnsupportedProtocolVersionError. SEP-2575
A request under 2026-07-28:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_ticket",
"arguments": { "title": "Checkout returns 500" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "my-agent",
"version": "1.4.0"
}
}
}
}
Illustrative shape based on the keys named in the changelog — check the published schema for the normative definition.
02 — server/discover replaces negotiation
Servers must implement server/discover, which advertises supported protocol versions, capabilities, and identity. Clients may call it before any other request for up-front version selection, or use it as a backward-compatibility probe on STDIO. SEP-2575
03 — Server-initiated requests become Multi Round-Trip Requests
This is the change most likely to affect application logic. Previously a server could call back to the client mid-request — for sampling, elicitation, or roots/list. With no persistent back-channel, that is no longer possible.
Instead, the server returns an InputRequiredResult with resultType: "input_required", whose inputRequests field carries what it needs. The client answers by retrying the original request with inputResponses attached. SEP-2322 · pattern docs

Fig 1 — The multi round-trip request cycle. Neither leg depends on hitting the same instance.
Every result now carries a required resultType field: "complete" for ordinary results, "input_required" for interim ones. Clients must treat a missing resultType from an older server as "complete".
04 — Notifications move to subscriptions/listen
The HTTP GET endpoint and resources/subscribe / resources/unsubscribe are replaced by a single long-lived POST-response stream. Clients opt in to specific notification types — toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions — and the server tags each notification with io.modelcontextprotocol/subscriptionId.
Request-scoped notifications such as notifications/progress and notifications/message continue to flow on the response stream of the request they belong to, not on the subscriptions/listen stream. SEP-2575
05 — Streams no longer resume
REMOVED
SSE stream resumability and message redelivery are gone — both theLast-Event-IDheader and SSE event IDs. A broken response stream loses the in-flight request, and the client must re-issue it as a new request with a new request ID. SEP-2575
This deserves emphasis because it cuts against the intuition that statelessness makes failure handling free. It does simplify routing — any instance can serve the retry — but it shifts recovery responsibility to the client, and it means a re-issued request may execute a tool a second time. The spec does not define an idempotency mechanism; designing tools so that a repeat call is safe is now an application-level concern.
06 — Features removed and deprecated
Removed outright: ping, logging/setLevel, and notifications/roots/list_changed. Log level is now set per request via io.modelcontextprotocol/logLevel in _meta, and servers must not emit notifications/message for requests that did not opt in.
Deprecated — still functional, but not for new implementations. SEP-2577
| Feature | Suggested replacement |
|---|---|
| Roots | Pass directories via tool parameters, resource URIs, or server config |
| Sampling | Integrate directly with the LLM provider API |
| Logging | Log to stderr (stdio), or use OpenTelemetry |
Tasks moved out of the core protocol into an official extension, io.modelcontextprotocol/tasks, with polling via tasks/get and a new tasks/update. SEP-2663
YOU HAVE TIME
The project adopted a feature lifecycle and deprecation policy defining Active, Deprecated, and Removed states with a minimum twelve-month deprecation window. SEP-2596 That window is not, however, a compatibility guarantee across mismatched client and server revisions.
07 — Smaller changes worth knowing
- List and read results now require
ttlMsandcacheScopevia aCacheableResultinterface, letting clients cache and reduce polling. SEP-2549 - Servers should return tools from
tools/listin a deterministic order, to improve client caching and LLM prompt-cache hit rates. - OpenTelemetry trace context propagation is documented for
_meta—traceparent,tracestate,baggage. - Resource-not-found changes from
-32002to-32602to align with JSON-RPC. - On auth: authorization servers should include
issper RFC 9207 and clients must validate it SEP-2468; credentials must be keyed by issuer and never reused across authorization servers SEP-2352; and RFC 7591 Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents.
Architectural implications for developers
The protocol changes above land differently depending on which layer of your system you own. These are the seven areas most likely to need a decision.
| Area | What changes for you |
|---|---|
| Session management | Nothing to manage. There is no session object, no session ID, and no session lifecycle to clean up. |
| Context propagation | Version, capabilities and client identity travel per request in _meta, so the client library must construct that envelope on every call. |
| Retries and recovery | No resumability. A dropped stream means re-issuing with a new request ID, and the protocol defines no idempotency key — safe repeat execution is your design problem. |
| Auth and authorization | Validated per request. Credentials are bound to their issuer and must not be reused across authorization servers. |
| Observability | Protocol logging is deprecated. Use stderr or OpenTelemetry, with trace context carried in _meta. |
| State persistence | Server-minted handles are the protocol-level answer. Where those handles resolve to is entirely your choice. |
| Multi-instance deployment | No sticky routing, no shared session store, no session migration on scale-up. Any instance can serve any request. |
The through-line is that responsibility moved rather than disappeared. Each row above describes something the protocol used to do implicitly that an application now does explicitly — which is more code, but code you can see, test, and reason about.
What this means in code: Python SDK v2
The protocol rewrite forced a matching SDK rewrite. The migration guide documents every breaking change; these are the ones almost every project hits.
Pin before you migrate. pip install mcp now installs 2.x.
# pyproject.toml
# Before
dependencies = ["mcp==1.28.1"]
# Not ready to migrate — stay on v1
dependencies = ["mcp>=1.28,<2"]
# Migrating
dependencies = ["mcp>=2,<3"]
FastMCP is now MCPServer, and transport parameters moved off the constructor onto run().
# Before (v1)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo", json_response=True, stateless_http=True)
mcp.run(transport="streamable-http")
# After (v2)
from mcp.server.mcpserver import MCPServer, Context
mcp = MCPServer("Demo")
mcp.run(transport="streamable-http", json_response=True, stateless_http=True)
Fields are snake_case. The JSON wire format is unchanged, but Python attribute access is not.
# Before
if result.isError: ...
schema = tools.tools[0].inputSchema
# After
if result.is_error: ...
schema = tools.tools[0].input_schema
# If you serialize yourself, you now need by_alias
tool.model_dump(by_alias=True, mode="json") # camelCase wire format
Lowlevel handlers moved from decorators to constructor parameters.
# Before (v1)
server = Server("my-server")
@server.list_tools()
async def handle_list_tools(): ...
# After (v2)
async def handle_list_tools(
ctx: ServerRequestContext,
params: PaginatedRequestParams | None,
) -> ListToolsResult:
...
server = Server("my-server", on_list_tools=handle_list_tools)
Other high-frequency breaks: McpError → MCPError; httpx and httpx-sse replaced by httpx2; streamablehttp_client removed; the WebSocket transport removed; resource URIs are str rather than AnyUrl; and on 2026-era connections, server-initiated sampling, elicitation, and roots raise NoBackChannelError. Full list in the v2.0.0 release notes.
GOOD NEWS
A v2 server still answers the legacyinitializehandshake alongsideserver/discover, so upgrading your server does not strand clients still on 2025-11-25.
How do I migrate to MCP 2026-07-28?

Fig 2 — Pin first. Everything else can happen on your own schedule.
1. Pin first, migrate later. Before the stable release, 84% of the 10,000+ PyPI packages depending on mcp declared no upper bound — meaning a routine rebuild can pull a project onto v2 unintentionally. Add <2 to every manifest today, then migrate deliberately.
2. Audit connection-scoped assumptions. Look for anything that assumes a request follows a previous request on the same connection: in-memory dictionaries keyed by session or connection ID, middleware attaching state to a connection rather than a request, caches scoped to a socket.
3. Externalize what remains. The spec's answer is server-minted handles passed back as tool arguments. Where that handle needs to resolve to real data, most teams will put it in a shared store.
# Implementation pattern, not mandated by the spec
def resolve_handle(handle: str) -> dict:
"""Resolve a server-minted handle from a shared store.
The spec requires the handle; the storage choice is yours.
"""
raw = store.get(f"mcp:handle:{handle}")
return json.loads(raw) if raw else {}
4. Rewrite server-initiated calls. Any place your server called back to the client needs to become an input_required result plus a client retry.
5. Design for repeat execution. Since a broken stream means the client re-issues the request, mutating tools should tolerate being called twice.
6. Test against both eras. Run your suite against a 2025-11-25 client and a 2026-07-28 client. The SDK supports in-process testing by passing an MCPServer instance directly to Client, so this needs no deployed infrastructure.
Common pitfalls
Assuming "stateless" means "no state." It means the protocol no longer manages state. Your application still can — the difference is that the state is now explicit and visible rather than hidden in a connection.
Missing the implicit dependencies. A codebase with no session object can still be stateful: per-client cached connections, in-process rate-limit counters, and module-level dictionaries all break the moment two requests land on different instances.
Treating deprecation as removal. Roots, Sampling, and Logging still work through at least a twelve-month window. Do not rip them out in a panic; do stop building new features on them.
Assuming version compatibility is automatic. A 2026-07-28 server may not work with older clients, and vice versa. The deprecated features registry tracks exactly what is in which state — check it rather than guessing.
Ignoring the ecosystem layer. If your agent reaches MCP through an adapter rather than directly, the adapter is a third moving part. On langchain-mcp-adapters, Dan Leehr opened an issue on July 21, 2026, asking directly whether the library is being tested against the v2 SDK — as of this writing, still open. On IBM's mcp-context-forge, the migration epic opened by jonpspri describes the work as a major version upgrade requiring comprehensive code changes and thorough testing, scoped across nine phases with a total estimate of 11–16 weeks. Check your adapters before your server.
Frequently asked questions
Is MCP 2026-07-28 backward compatible with older clients?
Partially, and the compatibility runs in one direction more than the other. A 2026-07-28 server still answers the legacy initialize handshake alongside server/discover, so upgrading your server does not strand clients still on 2025-11-25. Going the other way, a client that speaks 2026-07-28 falls back to the initialize handshake when it reaches an older server, so old servers and new clients keep interoperating too. What is not guaranteed is behavior that depends on features removed outright — a client relying on SSE stream resumability or the old subscription model will not find it on a 2026-07-28 server.
Do I need to migrate immediately?
No. The project adopted a feature lifecycle and deprecation policy with a minimum twelve-month deprecation window SEP-2596, so Roots, Sampling, and Logging keep working during that period. The more urgent action is defensive, not migratory: pin mcp<2 in your manifest now, since pip install mcp installs 2.x by default and an unpinned rebuild can pull your project onto v2 without anyone deciding to migrate.
What replaces server-initiated sampling and elicitation?
Multi Round-Trip Requests. Previously a server could call back to the client mid-request for sampling, elicitation, or roots/list. With no persistent back-channel, the server instead returns an InputRequiredResult (resultType: "input_required") describing what it needs, and the client retries the original request with inputResponses attached SEP-2322. See Fig. 1 above for the full cycle.
Does removing sessions mean my server can't keep any state?
No — it means the protocol no longer manages that state for you. Servers that need cross-call state issue explicit, server-minted handles that the client passes back as ordinary tool arguments SEP-2567. Where that handle resolves to is entirely an implementation choice — the spec only requires the handle mechanism, not a specific storage backend.
What happens to Roots, Sampling, and Logging?
They are deprecated, not removed SEP-2577. They remain fully functional during the deprecation window, but new implementations should not build on them. The suggested replacements: pass directories via tool parameters, resource URIs, or server config instead of Roots; integrate directly with the LLM provider API instead of Sampling; and log to stderr or OpenTelemetry instead of the Logging feature.
Will pip install mcp break my existing project?
Only if your dependency is unpinned. pip install mcp now installs 2.x, and before the stable release, 84% of the 10,000+ PyPI packages depending on mcp declared no upper bound — meaning a routine rebuild can pull a project onto v2 unintentionally. Add mcp>=1.28,<2 (or similar) to your manifest today if you are not ready to migrate; v1.x remains in maintenance mode and continues to receive security fixes.
Conclusion
The 2026-07-28 revision is the largest change to MCP since launch, and the direction is coherent: push state out of the protocol, make it explicit where it survives, and let servers behave like ordinary stateless HTTP services.
For most teams the practical sequence is short. Pin mcp<2 everywhere today so nothing migrates by accident. Read the changelog against your own server surface. Then migrate deliberately, starting with the SDK renames and finishing with the pieces that genuinely change behaviour — multi round-trip requests and stream retries.
The 2026 roadmap suggests this is a foundation rather than a preview of more churn, and the new lifecycle policy exists specifically to prevent another abrupt rewrite. That makes this migration a one-time cost worth paying early.
Further reading
- MCP 2026-07-28 changelog
- Official release announcement
- Release candidate post — design rationale
- Full specification diff
- Multi Round-Trip Requests pattern
- Streamable HTTP transport
- Extensions overview
- Deprecated features registry
- Python SDK v2.0.0 release notes
- Python SDK v1 to v2 migration guide
Every protocol claim in this article is sourced from the official changelog, a numbered Spec Enhancement Proposal, or the Python SDK migration guide.
Top comments (0)