The 2026-07-28 Model Context Protocol revision makes the core stateless: no initialize handshake, no Mcp-Session-Id header, any request served by any instance. Every migration guide this week — ours included — points at the same next step: install the beta v2 SDKs and start rewriting.

Here's the part that gets lost: you don't need the beta to go stateless. Statelessness wasn't invented by the 2026-07-28 spec — the spec makes it the default. It was already an opt-in mode in the stable SDK. Which means you can adopt the architecture today, on a shipped stable release, and treat the eventual v2 upgrade as a dependency bump instead of a rewrite. This walkthrough shows the one-line flag in both SDKs, then spends its time on the part that actually matters: getting your state out of the transport.

The flag is one field. The spec is one idea.#

Stateful MCP kept a session: on initialize, the server minted an Mcp-Session-Id, handed it back, and expected the client to echo it on every later request. That id let the server pin you to one instance and remember things about you between calls. Stateless deletes that. No id, no pinning, no server-side memory of "you" — each request carries everything it needs and can land on any replica.

The stable SDK has always let you turn the session off. Here's how, in each language.

TypeScript: sessionIdGenerator: undefined#

On the stable @modelcontextprotocol/sdk (1.x), the Streamable HTTP transport takes a sessionIdGenerator. Give it a UUID factory and you get sessions; give it undefined and you get a stateless server. That's the whole switch.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

const server = new McpServer(
  { name: "stateless-streamable-http-server", version: "1.0.0" },
  { capabilities: { logging: {} } }
);

server.registerTool(
  "greet",
  { description: "Greet someone", inputSchema: { name: z.string() } },
  async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] })
);

// STATELESS: no session id is ever issued, so any request stands alone.
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined,
});
await server.connect(transport);
// then hand each HTTP request to transport.handleRequest(req, res, body)

The one line that matters is sessionIdGenerator: undefined. Swap it for sessionIdGenerator: () => randomUUID() and you're back to a session-pinned server. This pattern is straight out of the SDK's own stateless example at tag 1.30.0 — nothing pre-release about it.

Because no session is created, a common stateless deployment goes one step further and builds a fresh transport (and even a fresh McpServer) per incoming request, then closes it when the response is sent. There's no session to reuse, so there's nothing to keep alive between requests — which is exactly the property that lets a load balancer send consecutive calls to different instances.

Python: stateless_http=True#

On the stable mcp package (1.x), FastMCP exposes the same idea as a constructor flag. Pass stateless_http=True, and pair it with json_response=True so each call returns a plain JSON response instead of holding an SSE stream open:

from mcp.server.fastmcp import FastMCP

# stateless: no session, plain JSON responses — any worker can answer any call
mcp = FastMCP("Demo", stateless_http=True, json_response=True)

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

The minimal server and the streamable-http transport are exactly as documented in the Python SDK README; the two flags are the stateless toggle. (Flag names have been stable, but it's worth a glance at your installed version's docs — pip show mcp — before you ship.)

The flag is the easy 10%. Here's the 90%.#

Turning off the session is trivial. What's not trivial is that the session was quietly holding things for you, and now nothing does. Every one of those things has to move. There are three usual suspects:

1. Auth and identity. If you resolved the caller's identity once at initialize and cached it for the connection, that cache is gone. Resolve identity from the credentials on each request instead. Under the 2026-07-28 spec, servers are positioned as OAuth 2.1 resource servers precisely because per-request auth is the stateless-safe model — see our note on authenticating a remote MCP server.

2. Conversation and tool state. Any per-client scratch you kept in a server-side dictionary — a running total, an uploaded file handle, a multi-step tool's progress — has to leave process memory and move to a store every replica can reach: Redis, Postgres, a durable KV. Key it by an identifier from your application domain (a user id, a workspace id, a conversation id you generate and return to the client), not by a transport session. The client passes that id on each request; any instance loads the state, does the work, writes it back.

3. Long-lived round-trips. Holding an SSE stream open to ask the client a question mid-request assumes the follow-up comes back to the same instance. Statelessly, it might not. The stateless pattern is to return a result that says "I need this input," carry a small piece of requestState in it, and accept the answer as a fresh request that any replica can pick up.

The test for whether you've actually gone stateless is blunt: if two consecutive requests from the same client could be served by two different replicas and still behave correctly, you're stateless. If they can't, you still have hidden session state to externalize. That's also the property that unlocks the operational wins — you can blue-green deploy without draining sessions and autoscale replicas freely — and it's what a statelessness conformance test actually checks.

What still waits for the stable v2 SDK#

Adopting the architecture today doesn't mean speaking the full 2026-07-28 wire format today. The removed initialize handshake, the new Mcp-Method / Mcp-Name routing headers, and handle-based Tasks are what the beta/RC v2 SDKs (@modelcontextprotocol/server 2.0.0-beta, mcp 2.0.0rc1) implement — and those APIs can still shift before the stable v2 release. Don't pin a pre-release in production.

The pragmatic sequence: go stateless on the stable SDK now, move your state into a shared store, verify with a conformance check, and ship. When the stable v2 SDKs land, upgrading is a dependency bump that picks up the new wire format — because you already did the hard part, which was never the flag. It was the state.