The final 2026-07-28 revision of the Model Context Protocol publishes in 8 days — it's still a Release Candidate as I write this — and it deletes the two things that made an MCP server stateful at the wire: the initialize handshake (SEP-2575) and the Mcp-Session-Id header (SEP-2567). This is the server-side migration guide. If you own the client, we wrote the client-side change separately; if you want the why, see what the stateless spec changes for agent builders and what deprecating Sampling, Roots, and Logging means. Here: what to change on your server, in what order, with code.

The one-line triage: if your server is plain request/response tools and never touched a session id, SSE, Sampling, or Roots, this is an SDK bump and you're done. If it kept per-session state, the real work is replacing sessions with explicit handles. Everything below sorts your server into one of those two piles.

You are not on a hard deadline. Deprecated features keep working for at least 12 months (SEP-2596, the lifecycle policy — distinct from the deprecations themselves in SEP-2577). You're migrating ahead of the stable release, not racing a shutoff.

Step 0: Which pile are you in?#

Grep your server for four things. Each hit moves you from "SDK bump" toward "real work":

No hits? Skip to Step 1 and stop there.

Step 1: The SDK bump (the easy 90%)#

The protocol simply stops requiring the handshake, so a stateless tool server barely changes shape. Only the Python and TypeScript v2 betas implement the protocol-level change today; Go and C# keep v1-compatible APIs for now.

Python renames FastMCP to MCPServer but keeps the decorator API:

# AFTER — Python v2 (pip install "mcp[cli]==2.0.0b1")
from mcp.server import MCPServer          # was: from mcp.server.fastmcp import FastMCP

mcp = MCPServer("Demo")                   # was: FastMCP("Demo")

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

The v2 Python HTTP endpoint answers both the new server/discover flow and the legacy initialize handshake, so old clients keep working with zero extra code. TypeScript splits into focused packages and uses a stateless HTTP handler from @modelcontextprotocol/server:

// AFTER — TypeScript v2, minimal server
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

const server = new McpServer({ name: "greeting-server", version: "1.0.0" });
server.registerTool(
  "greet",
  { description: "Greet someone by name", inputSchema: z.object({ name: z.string() }) },
  async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] })
);
// For stateless HTTP, wire createMcpHandler from "@modelcontextprotocol/server".

These betas are moving; pin the exact version and check the shipped package before you deploy. That's the whole migration for a stateless tool server.

Step 2: Sessions → explicit state handles#

This is the change that trips people. Do not reach for the old sessionIdGenerator: undefined / enableJsonResponse: true trick and call it done — that's v1 transport-level statelessness in the current stable SDK, and it is not the new protocol change. The real fix under SEP-2567 is to stop tying state to a connection at all.

The pattern: a tool mints a handle, the model passes it back as a normal argument, and you look the state up in your own store.

// BEFORE (v1): state keyed by the session behind a StreamableHTTP transport
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),   // sticky sessions + a server-side store
});
// the server kept per-session cart state keyed by Mcp-Session-Id

// AFTER (SEP-2567): mint an explicit handle; the model threads it back
server.registerTool("create_cart", { /* ... */ }, async () => ({
  content: [{ type: "text", text: JSON.stringify({ basket_id: newId() }) }],
}));

server.registerTool(
  "add_item",
  { inputSchema: z.object({ basket_id: z.string(), sku: z.string() }) },
  async ({ basket_id, sku }) => {
    // look up state by basket_id in YOUR database — not a session
    await carts.addItem(basket_id, sku);
    return { content: [{ type: "text", text: "added" }] };
  }
);

Two rules fall out of this. First, tools/list, resources/list, and prompts/list must not depend on prior calls or per-connection state — that's what lets a load balancer cache and fan them out. Second, because nothing is sticky, any request can hit any instance: you can finally put MCP behind a plain round-robin balancer with no shared session store.

Step 3: SSE / long work → the Tasks extension#

If you streamed progress or held a connection open for slow work, that moves to the Tasks extension (SEP-2663). A slow tools/call returns a task result (resultType: "task") with a taskId, an initial status, a TTL, and a polling interval. The client then polls tasks/get, and drives with tasks/update and tasks/cancel.

There is deliberately no tasks/list — without sessions, one caller's tasks must not be visible to another, so there's nothing to list. The state machine is small: working and input_required are non-terminal; completed, failed, and cancelled are terminal. Mid-task questions come back through the stateless input_required flow (an input-requests map plus an opaque request-state token you echo back) — that's what replaces SSE-based elicitation.

Step 4: Sampling and Roots → do it yourself#

Both are deprecated under SEP-2577 and stay callable for 12+ months, so migrate deliberately, not in a panic:

The migration, in order#

  1. Bump to the v2 SDK beta; pin the version. Verify old clients still connect (they should — the endpoint answers initialize too).
  2. Grep for Mcp-Session-Id and session-keyed state. Replace each with a minted handle threaded through tool arguments.
  3. Move SSE/elicitation and long-running calls onto Tasks (tasks/get polling).
  4. Swap Sampling for a direct provider call, Roots for tool params, Logging for stderr/OTel — on the 12-month clock, not today.
  5. Drop the shared session store and put the server behind a plain load balancer. That payoff — horizontal scale with nothing sticky — is the entire reason the spec did this.