MCP Stateless HTTP: Migrate Away from Protocol Sessions Safely
MCP stateless HTTP does not mean your application must forget everything between requests. The final 2026-07-28 Streamable HTTP specification removes protocol-level sessions from the modern transport path, but your application can still persist jobs, permissions, carts, or conversations behind explicit identifiers. Carry that state deliberately, send the required request metadata, and keep a separate compatibility path for older clients.
Separate protocol state from application state
The migration becomes easier when the two kinds of state are named separately:
| State | Older design assumption | Stateless HTTP design |
|---|---|---|
| Protocol capability negotiation | initialize creates a session | Each request declares MCP-Protocol-Version and mirrors it in _meta |
| Transport correlation | Mcp-Session-Id identifies the conversation | Each POST can stand on its own; request-scoped SSE is still possible |
| Request identity | Method and tool name are inferred from the parsed body | Mcp-Method and, where required, Mcp-Name mirror the JSON-RPC request |
| Product state | Hidden in a server-side session | Stored behind an explicit handle such as cart_id, job_id, or tenant_id |
| Request metadata | Inferred from connection state | Sent with the request and its reserved _meta values |
If a tool call needs a cart, job, or tenant, make that value part of the tool input or an authenticated request context. Do not reuse a transport session ID as a business record key; the two lifecycles have different ownership and retention rules.
Send a complete Streamable HTTP request
The modern transport uses one MCP endpoint. Clients send JSON-RPC messages with HTTP POST; the server can answer with JSON or a request-scoped SSE stream. Every POST must include Accept: application/json, text/event-stream, MCP-Protocol-Version, and Mcp-Method. Mcp-Name is also required for methods such as tools/call, resources/read, and prompts/get.
Use the same protocol version in the header and the reserved body metadata. This is the smallest useful shape for a tool call:
POST /mcp HTTP/1.1Accept: application/json, text/event-streamContent-Type: application/jsonMCP-Protocol-Version: 2026-07-28Mcp-Method: tools/callMcp-Name: get_weather
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": {"location": "Seattle, WA"}, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} } }}Treat the example as a protocol boundary, not a complete authentication design. The exact JSON-RPC method, tool arguments, authorization, and response handling still belong to the server contract. The important change is that the request carries what the server needs instead of relying on a previously negotiated transport session.
Handle discovery and application state explicitly
Use the 2026-07-28 discovery mechanisms when a client needs to learn what a server supports. Discovery is a capability decision; it is not a replacement for authorization or durable product state.
The specification reserves _meta values for request-level metadata and mirrors the protocol version at the HTTP boundary. Keep metadata small, non-secret, and scoped to the request. Authentication credentials, tenant authorization, and durable application state should continue to use their intended security and storage mechanisms.
If a tool needs continuity, pass an application-owned job_id, cart_id, or similar handle in its arguments or an authenticated request context. Give that handle its own authorization, expiry, and idempotency rules. Do not reuse a transport session ID as a business record key.
Treat a 400 response as a protocol signal
The final transport spec requires the server to reject missing or mismatched required headers with HTTP 400. A JSON-RPC HeaderMismatch error uses code -32020. This makes an invalid modern request distinguishable from an older server that does not understand the new boundary.
For a client that must support both generations, use this decision tree:
- Send the modern POST with the required headers and body metadata.
- If the response is HTTP 400 with a recognized modern JSON-RPC error, fix the headers, version negotiation, or body rather than falling back.
- If the 400 body is empty or unrecognized, treat the endpoint as a possible legacy server and try the older
initializeplus HTTP+SSE flow. - Return a useful upgrade error when neither lane can provide the requested capability.
- Measure legacy traffic before removing the fallback.
Do not use every 400 response as permission to fall back. That can hide a malformed modern request and turn a client bug into a misleading compatibility path. A modern server may also return 405 for old GET or DELETE session operations; Mcp-Session-Id is not a substitute for the modern request headers.
Cloudflare’s Agents SDK v0.20.0 changelog describes a factory-based createMcpHandler path that can support stateless and legacy ordinary tools on the same route, while marking McpAgent as deprecated and feature-frozen. If your server relies on sessions, pushed requests, or stream replay, use a dual-route migration until those behaviors have a tested replacement.
Test state boundaries instead of only tool calls
A migration test should prove more than “the tool returned 200.” Exercise these cases:
- a fresh stateless request with no protocol session and all required headers;
- a request whose
MCP-Protocol-Versiondisagrees with_meta.io.modelcontextprotocol/protocolVersion; - a request missing
Mcp-Methodor a requiredMcp-Nameand the expected HTTP 400 response; - discovery followed by a tool call with explicit application state;
- two requests from different connections using the same authorized
job_id; - an old client that still sends
initializeand a session header; - an empty or unrecognized 400 body that triggers the legacy fallback;
- an expired or unauthorized application handle;
- a retried request that must not create duplicate side effects.
If you are deploying the handler on Workers, a Cloudflare Workers test harness can help you exercise request and failure paths before switching production traffic.
A safe migration sequence
- Inventory hidden session state. List every tool or resource that reads server memory keyed only by a protocol session.
- Define explicit state handles. Choose the durable store, authorization rule, expiry, and idempotency behavior for each handle.
- Add discovery and stateless request handling. Make the new route observable without deleting the legacy route.
- Run both compatibility paths. Test current and older clients against the same business invariants.
- Move traffic gradually. Track legacy requests, errors, reconnects, and duplicate side effects.
- Remove only unused protocol state. Keep application state and audit records according to their own retention policy.
FAQ
Q: Is MCP stateless HTTP the same as an application with no state?
A: No. It removes a transport-level session requirement from the newer protocol direction. Your application can still persist jobs, carts, permissions, or conversation records behind explicit identifiers.
Q: Do all MCP clients stop sending initialize immediately?
A: No. Older protocol revisions and clients still use the handshake and session model. Try the modern request first when supported, classify 400 responses correctly, retain a tested compatibility path, and remove it only after observing migration data.
Q: What should replace Mcp-Session-Id for a long-running job?
A: Create an application-owned job identifier with explicit authorization, expiry, and idempotency rules. Pass that identifier as part of the request or tool input instead of treating a transport session ID as the job record.
References:
MCP specification: Streamable HTTP transport
MCP 2026-07-28 release candidate context
Report a typo or broken link, or suggest a related topic.