Migrating to sessionless MCP (2026-07-28)
What the 2026-07-28 MCP spec changes for server teams, a migration checklist, and how Spanly captures both protocol eras.
The MCP 2026-07-28 spec release makes the protocol stateless at the
transport layer. It is a clean break rather than a deprecation: existing
servers keep working on the older protocol versions through version
negotiation, and the ecosystem supports the 2025-11-25 generation for at
least a year. But new servers, and servers that adopt the v2 SDKs, work
differently enough that a migration is real engineering work. This guide
covers what changed, a checklist for server teams, and where Spanly fits.
Every claim below traces to a linked SEP or the official release post. Read those before making changes; this page is an orientation, not a substitute for the spec.
What changed and why
- Sessions are gone (SEP-2567).
The
Mcp-Session-Idheader is removed. New servers do not mint or echo it, andGET/DELETEon the MCP endpoint return405. Cross-call state moves to "explicit state handles": a tool result carries an opaque id, and the model threads it back into later tool calls. Handles are invisible to the protocol, so a gateway or middleware cannot see them. - The initialize handshake is gone (SEP-2575).
Instead, every request carries
_metakeysio.modelcontextprotocol/protocolVersion,io.modelcontextprotocol/clientInfo, andio.modelcontextprotocol/clientCapabilities(all required; a missing set is a400). Aserver/discoverRPC returns what the old initialize response carried. The long-livedGETSSE stream is replaced by asubscriptions/listenPOST whose response is an event stream.Last-Event-IDresumability,ping, andlogging/setLevelare removed; the log level is now a per-request_metaopt-in and servers must not emitnotifications/messagewithout it. - Header standardization (SEP-2243).
MCP-Protocol-Version(every POST),Mcp-Method(every request), andMcp-Name(tools/call,resources/read,prompts/get) are mandatory and mirrored from the body, so gateways route and authorize without parsing the body. Servers must reject a header/body mismatch withHeaderMismatchError(-32020). Non-ASCII header values use a=?base64?...?=sentinel. - Server-initiated requests are multi round-trip
(SEP-2322). A tool can no
longer
awaitan elicitation mid-execution. It returnsresultType: "input_required"with an opaquerequestState, and the client retries the same method with the answer plus the echoedrequestStateunder a new request id. - Sampling, Roots, and Logging are deprecated (SEP-2577), with at least a 12-month window.
- List results must declare caching
(SEP-2549).
ttlMsandcacheScope("public"or"private") are required ontools/list,prompts/list,resources/list,resources/read, andresources/templates/list, and a list result must not vary per connection. Variance by authenticated principal is allowed; deterministictools/listordering is recommended. - Full JSON Schema 2020-12 (SEP-2106)
in
inputSchema/outputSchema: composition (oneOf/anyOf/allOf), conditionals, and internal$refare all legal. - W3C trace context (SEP-414)
reserves
traceparent,tracestate, andbaggagein_meta.
Source: the 2026-07-28 release candidate post and the SDK v2 beta post.
Migration checklist for server teams
-
Upgrade to the v2 SDK. The maintainers stated that "existing code keeps compiling" is a non-goal, so plan for source changes rather than a drop-in bump.
-
Move session state to explicit state handles. Anywhere you relied on
Mcp-Session-Idto key server-side state, mint an opaque handle in the tool result and read it back from the next call's arguments:// Before: state keyed on the session id. // After: the tool returns a handle the model threads forward. async function openDataset(args) { const handle = createHandle(); // e.g. "ds_a1b2c3" store.set(handle, await loadDataset(args)); return { content: [{ type: 'text', text: `Opened as ${handle}` }] }; } async function queryDataset(args) { const dataset = store.get(args.handle); // handle came from the model return { content: [{ type: 'text', text: run(dataset, args.query) }] }; }Treat a handle as model-visible: it can appear in transcripts and be replayed, so scope it to the authenticated principal and expire it.
-
Stop varying
tools/listper connection. A toolset that changes as a side effect of other calls (for example aconnect_database()that reveals more tools) is forbidden. Serve identical list results per authenticated principal, and setcacheScope: "private"when the list is principal-specific. -
Replace Sampling with direct LLM calls. Call your model provider's API from the server instead of asking the client to sample.
-
Refactor mid-call elicitation to the
input_required+requestStateshape, or gate the feature on a client capability during the transition. Against a dual-era server, old clients keep working and simply lose elicitation until they upgrade. -
Emit and validate the new headers. Mirror
Mcp-Method/Mcp-Namefrom the body and reject a mismatch with-32020. -
Add cache directives (
ttlMs,cacheScope) to every list result, and sort list results deterministically. -
Move server logs off the MCP channel. Emit to stderr (stdio) or your APM/OTel pipeline, and only send
notifications/messagewhen a request opted in via the per-request log level.
Timeline facts: the spec is final on 2026-07-28; the 2025-11-25
generation is supported for at least a year; new clients fall back to
initialize against old servers, but old clients cannot talk to a
modern-only server, and a modern-only client cannot talk to a legacy
server. Expect mixed-era fleets for the whole window.
How Spanly helps
Spanly's producers capture both protocol eras with no configuration
change. The CLI, the Docker sidecar, and the TypeScript and Python
middleware read raw request and response frames, so they capture legacy
initialize traffic and modern per-request _meta identity the same way.
On the product side:
- Protocol version per server and session. The server dashboard shows which protocol versions your fleet is on, and for a mixed fleet it breaks the traffic down by client family, which is the go/no-go signal for dropping legacy support. Session detail shows each session's version.
- Migration-readiness scan checks. The static and live scans flag the
concrete blockers: a session-based protocol version with the migration
ahead of it, Sampling/Roots/Logging usage that SEP-2577 deprecates,
tools/listresults that vary per connection, missing cache directives, mid-call elicitation that SEP-2322 reshapes, clients leaning on the removed SSE resumability, and version-negotiation failures from a client family that cannot reach your server. - Sessionless traffic stays grouped. For servers with no session id, Spanly synthesizes a telemetry-only session key from the request's credential and idle gap, so sessions, dashboards, and session-scoped checks keep working. See Session tracking.
None of this requires you to migrate first. Spanly captures the old and new protocols side by side, so you can watch your fleet's era mix move and verify each migration step against real traffic.