Stateless MCP server: sessions, Mcp-Session-Id, and handles

A stateless MCP server keeps no session between requests. What Mcp-Session-Id does, what 2026-07-28 removed, and where state goes instead.

5 min readSume
All posts

A stateless MCP server keeps no session between HTTP requests: each request carries everything the server needs to answer it. In the 2025-11-25 revision, a Streamable HTTP server may assign a session ID, Mcp-Session-Id, at initialization. The 2026-07-28 revision, the current one, removes protocol sessions and that header, and servers that need state across calls pass explicit handles as ordinary tool arguments.

The protocol facts come from the MCP 2025-11-25 transports page, the 2026-07-28 changelog and tools pages, the versioning page, and the MCP Inspector's protocol eras page, read on 2026-09-28. The worked example is Sume's hosted MCP server, from its current code and the Jobs and results docs; Sume's basics page says hosted MCP still works but is not the primary path today.

What changed between the 2025-11-25 and 2026-07-28 revisions?

In the handshake-era revisions, an MCP session is the set of related interactions between a client and a server, beginning with initialization. The 2026-07-28 revision makes MCP stateless: every request carries its protocol version and client capabilities in _meta, and the MCP Inspector describes a modern connection as sessionless and per-request.

From the MCP 2025-11-25 transports page and the 2026-07-28 changelog, read 2026-09-28.
Topic2025-11-252026-07-28
SessionServer MAY assign MCP-Session-Id at initialization; the client sends it on every later requestNo protocol-level sessions; Mcp-Session-Id removed
Handshakeinitialize, then notifications/initializedRemoved; version and capabilities ride on each request
State across callsCan live in a session the server establishesExplicit, server-minted handles passed as tool arguments
Broken streamA server MAY make it resumable with Last-Event-IDNot resumable; the client re-issues the request with a new ID

How does Mcp-Session-Id work, and why is my session not found?

On a 2025-11-25 Streamable HTTP server, the session ID arrives in an MCP-Session-Id header on the response that carries the InitializeResult. From then on:

  • The client MUST include that header on all of its later HTTP requests.
  • A server that requires a session SHOULD answer a request without the header, other than initialization, with 400 Bad Request.
  • A server may end the session at any time. After that it MUST answer that session ID with 404 Not Found, and the client MUST start a new session with a new InitializeRequest that carries no session ID. So a session-not-found error calls for a fresh initialization, not a retry with the old ID.
  • A client that is done SHOULD send HTTP DELETE with the header; the server MAY answer 405 Method Not Allowed.

How do I keep state without a session?

Return a handle. The 2026-07-28 tools page's guidance: a server that needs state across calls, such as a shopping cart, an open browser context, or a database transaction, returns an explicit handle from a creation tool and accepts it as an argument on later calls. The model carries the handle forward, and the server looks the state up on each call. The spec's design notes for handles come first, then its shopping-cart example:

  • Authorization: a handle is a name, not a capability, so check the caller's rights against it on every call.
  • Lifetime: state the retention policy in the creation tool's description, where the model can see it.
  • Expiry: a call with an expired or unknown handle should return a tool execution error that says so.
// → tools/call
{ "name": "create_basket", "arguments": {} }
// ← result
{
  "content": [{ "type": "text", "text": "Created basket bsk_a1b2c3" }],
  "structuredContent": { "basket_id": "bsk_a1b2c3" }
}
// → tools/call
{ "name": "add_item", "arguments": { "basket_id": "bsk_a1b2c3", "sku": "..." } }

Is Sume's MCP server stateless?

At the protocol level, yes. In current code, Sume's hosted server at https://mcp.sume.com/mcp handles each POST on its own with a 60-second deadline, answers with a JSON body, and sets no session ID. It still speaks the handshake era: it negotiates 2025-03-26, 2025-06-18, or 2025-11-25 through initialize.

Long work already uses handles. In current code generate_video answers with a job id, never a clip URL, and the agent passes that id to jobs_wait and jobs_result. The docs say jobs_wait takes 1 to 20 ids and holds at most 55 seconds per call, and unknown or foreign-workspace ids fail the whole call. MCP tool call timeouts on long-running video jobs covers the wait loop.

Will a 2026-07-28 client connect to a handshake-era server?

Only if it falls back. The 2026-07-28 transports page says clients and servers that interoperate with earlier revisions detect the other side's era and fall back. On Streamable HTTP, the version also travels in the MCP-Protocol-Version header.

Sume's server shows why the fallback matters. In current code it answers an MCP-Protocol-Version header of 2026-07-28 with HTTP 400, so a client pinned to that revision alone fails there, while one that falls back to initialize can connect. The MCP Inspector's legacy, auto, and modern era settings are covered in How to test an MCP server, and the codes Sume answers with in MCP error codes.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume