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":
- Do you read
Mcp-Session-Id, or keep state keyed by a session? → you need explicit handles (Step 2). - Do you stream mid-call with SSE / elicitation? → that moves to the Tasks extension (Step 3).
- **Do you call
sampling/*** (server asking the host's model to generate)? → call a provider API directly (Step 4). - **Do you use
roots/*** to learn allowed directories? → pass paths as tool params or resource URIs (Step 4).
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:
- Sampling (server asks the host's model to generate) → call an LLM provider's API directly from your tool, server-side. The host owns the model again; you own the key.
- Roots (how a server learned allowed directories) → accept a path or resource URI as a tool parameter, or read it from server config.
- Logging →
stderrfor stdio transports, OpenTelemetry for anything structured.
The migration, in order#
- Bump to the v2 SDK beta; pin the version. Verify old clients still connect (they should — the endpoint answers
initializetoo). - Grep for
Mcp-Session-Idand session-keyed state. Replace each with a minted handle threaded through tool arguments. - Move SSE/elicitation and long-running calls onto Tasks (
tasks/getpolling). - Swap Sampling for a direct provider call, Roots for tool params, Logging for stderr/OTel — on the 12-month clock, not today.
- 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.



