Spanly Docs

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-Id header is removed. New servers do not mint or echo it, and GET/DELETE on the MCP endpoint return 405. 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 _meta keys io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo, and io.modelcontextprotocol/clientCapabilities (all required; a missing set is a 400). A server/discover RPC returns what the old initialize response carried. The long-lived GET SSE stream is replaced by a subscriptions/listen POST whose response is an event stream. Last-Event-ID resumability, ping, and logging/setLevel are removed; the log level is now a per-request _meta opt-in and servers must not emit notifications/message without it.
  • Header standardization (SEP-2243). MCP-Protocol-Version (every POST), Mcp-Method (every request), and Mcp-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 with HeaderMismatchError (-32020). Non-ASCII header values use a =?base64?...?= sentinel.
  • Server-initiated requests are multi round-trip (SEP-2322). A tool can no longer await an elicitation mid-execution. It returns resultType: "input_required" with an opaque requestState, and the client retries the same method with the answer plus the echoed requestState under 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). ttlMs and cacheScope ("public" or "private") are required on tools/list, prompts/list, resources/list, resources/read, and resources/templates/list, and a list result must not vary per connection. Variance by authenticated principal is allowed; deterministic tools/list ordering is recommended.
  • Full JSON Schema 2020-12 (SEP-2106) in inputSchema/outputSchema: composition (oneOf/anyOf/allOf), conditionals, and internal $ref are all legal.
  • W3C trace context (SEP-414) reserves traceparent, tracestate, and baggage in _meta.

Source: the 2026-07-28 release candidate post and the SDK v2 beta post.

Migration checklist for server teams

  1. 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.

  2. Move session state to explicit state handles. Anywhere you relied on Mcp-Session-Id to 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.

  3. Stop varying tools/list per connection. A toolset that changes as a side effect of other calls (for example a connect_database() that reveals more tools) is forbidden. Serve identical list results per authenticated principal, and set cacheScope: "private" when the list is principal-specific.

  4. Replace Sampling with direct LLM calls. Call your model provider's API from the server instead of asking the client to sample.

  5. Refactor mid-call elicitation to the input_required + requestState shape, 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.

  6. Emit and validate the new headers. Mirror Mcp-Method/Mcp-Name from the body and reject a mismatch with -32020.

  7. Add cache directives (ttlMs, cacheScope) to every list result, and sort list results deterministically.

  8. Move server logs off the MCP channel. Emit to stderr (stdio) or your APM/OTel pipeline, and only send notifications/message when 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/list results 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.

On this page