MCP Went Stateless: Inside the July 28, 2026 Protocol Revision
On July 28, 2026, the Model Context Protocol shipped its largest revision since launch. The change is not incremental — it’s architectural. Sessions are gone. The handshake is gone. Every request is now self-describing, cacheable, and routable without server-side state. This is a deep dive into what actually changed under the hood, the wire format, SDK v2 breaking changes, and what it means for operations.
The handshake is dead
The most visible change is the removal of initialize and notifications/initialized methods, per SEP-2575. The Mcp-Session-Id header and session DELETE are also gone (SEP-2567). Every request is now required to carry protocolVersion and clientCapabilities inside _meta 1.
This means a client can fire a request at any MCP endpoint without prior negotiation. The server reads the _meta block, validates the version, and responds. No state, no sticky connections, no session lifecycle.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "fetch_url",
"arguments": {"url": "https://example.com"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {"tools": {}}
}
}
}
Routing moved into headers
SEP-2243 standardized Mcp-Method and Mcp-Name routing headers. The Mcp-Method header is required on all requests, and Mcp-Name is required on tools/call, resources/read, and prompts/get 2. The MCP-Protocol-Version header is also mandatory on every request 1.
The critical enforcement: if the header doesn’t match the body method, the server returns 400 -32020 HeaderMismatch. This is a hard fail, not a warning.
POST /mcp HTTP/1.1
Host: mcp.example.com
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: fetch_url
Content-Type: application/json
Why headers? Because intermediaries — proxies, gateways, load balancers — can now inspect and route requests without parsing the JSON body. This is a massive win for edge routing and observability 3.
server/discover replaces the handshake
The new required server method is server/discover 1. It returns server capabilities, supported protocol versions, and available tools/resources. This is the health check and capability probe in one.
A critical ops note: a GET to /mcp now returns 405 Method Not Allowed. The health check pattern is a POST to server/discover 4.
curl -X POST https://mcp.example.com/mcp \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
Elicitation became round-trips
SEP-2322 reworked elicitation. When a tool needs more input, the server returns resultType: "input_required" along with an opaque state token. The client must re-call the same tool with an inputResponses array 2.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "input_required",
"stateToken": "eyJhbGciOiJIUzI1NiJ9...",
"inputRequests": {
"type": "object",
"properties": {"apiKey": {"type": "string"}}
}
}
}
The client then re-calls with:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "fetch_private_data",
"arguments": {"url": "https://api.internal"},
"inputResponses": {"apiKey": "sk-1234"},
"requestState": "eyJhbG...NiJ9...",
"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}
}
}
The SDK impact is significant: ctx.elicit() now raises NoBackChannelError on modern connections, because there is no back channel — no session — to push elicitation prompts through 3.
List results are cacheable
SEP-2549 added ttlMs and cacheScope to tools/list, prompts/list, and resources/list. Results must use deterministic ordering to make caching safe 3.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"tools": [
{"name": "fetch_url", "description": "Fetch a URL"},
{"name": "search_web", "description": "Search the web"}
],
"cacheScope": "public",
"ttlMs": 300000
}
}
This is a token-cost win. Stable prompt caches mean the client can persist tool listings and skip re-fetching on every conversation turn. For long-running agents, this reduces latency and spend.
Deprecations and the offramp
Three major features are deprecated: roots, sampling, and logging (SEP-2577/2596) 1. The legacy HTTP+SSE transport is also deprecated. The spec mandates a 12-month minimum offramp policy — deprecated features must remain functional for at least 12 months from the revision date 4.
This deprecation is a signal: the protocol is moving toward pure request-response semantics. Streaming will still exist, but not through the legacy SSE transport.
State without sessions
The big question: how do you handle stateful workflows without sessions? The answer is server-minted opaque handles passed back as ordinary tool arguments 2.
The server creates a handle (e.g., job_1234), returns it as a tool result, and the client passes it back as an argument on the next call. Cursors must encode and sign their own position — the server doesn’t track where you are.
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "list_records",
"arguments": {
"cursor": "eyJwb3NpdGlvbiI6IDQyLCAic2lnIjogImFiYzEyMyJ9"
},
"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}
}
}
This is a deliberate trade-off: stateless servers are simpler to scale, but clients must manage opaque state tokens carefully.
Ops changes: round-robin and no sticky sessions
The ops implications are immediate. Load balancers no longer need sticky sessions. Plain round-robin works because every request is independent 4.
Two nginx settings are now critical:
location /mcp {
proxy_pass http://mcp_backend;
proxy_buffering off;
proxy_read_timeout 1h;
}
proxy_buffering off is required for streaming responses to flow through immediately. proxy_read_timeout 1h accommodates long-running tool calls without session state 4.
Python SDK v2 breaking changes
The Python SDK v2 is a hard break. FastMCP is renamed to MCPServer, and transports are now configured on run() 3.
from mcp.server.mcpserver import MCPServer
server = MCPServer("my_server")
@server.tool()
def fetch_url(url: str) -> str:
return f"Fetched {url}"
server.run(transport="streamable-http", stateless_http=True, json_response=True)
pip install mcp now installs 2.x by default. The old FastMCP import will fail. Migration requires updating transport configuration and handling the new input_required flow.
Auth hardening
Two auth changes arrived with this revision. iss validation is now required per RFC 9207 (SEP-2468), preventing issuer confusion attacks. Credential-to-issuer binding (SEP-2352) ensures credentials are only valid for the issuer they were minted for 3.
These changes matter because stateless requests are more likely to cross trust boundaries. Every request must be independently authenticated and authorized.
The bottom line
The July 28 revision is a fundamental redesign. Sessions are gone, replaced by self-describing requests and server-minted handles. The handshake is gone, replaced by server/discover. Elicitation is now a round-trip dance. List results are cacheable. Auth is stricter.
The win is operational simplicity — stateless servers scale horizontally with zero coordination. The cost is client complexity: opaque tokens, signed cursors, and the input_required flow. For teams running MCP at scale, this revision is a net positive: simpler load balancing, better caching, and a wire format that intermediaries can route without parsing JSON.
The 12-month deprecation window means teams have time to migrate, but the direction is clear. Stateless is the default, and the protocol is better for it.
← Back to all posts


