Skip to content

[Server] Serve handshake-era clients without a session #644

Open

Description

@ruudk

Problem

Our server targets the stateless 2026-07-28 revision, but many clients still connect with the initialize handshake (Linear, Devin, rmcp 3.2.0, some Claude Code builds). For them the SDK always creates a session on initialize, and Protocol::resolveSession() refuses every later request without one:

A valid session id is REQUIRED for non-initialize requests.

So a horizontally scaled server has to keep a shared session store (e.g. Psr16SessionStore on Redis) plus a SymfonyLockSessionLock, only to serve those clients. We tried to drop that by refusing the handshake, and those clients could no longer connect.

Proposal

An opt-in for the handshake-era leg that serves each request without a session:

  • initialize is answered without an Mcp-Session-Id header. The 2025-06-18 transport spec makes this optional: "A server using the Streamable HTTP transport MAY assign a session ID at initialization time".
  • Later requests without a session id are served on their own instead of refused.
  • GET answers 405, which the spec allows for a server that "does not offer an SSE stream at this endpoint". DELETE answers 405 as well.

The TypeScript SDK does this: without a sessionIdGenerator "session management is disabled (stateless mode)" (streamableHttp.ts), and createMcpHandler() serves handshake-era traffic that way by default (legacy: 'stateless', GET and DELETE answered with 405, see createMcpHandler.ts).

Trade-off

Without a GET stream the server cannot send messages outside a request, such as list_changed notifications. A server request sent during a POST, such as sampling or elicitation, gets its answer in a new POST ("Every JSON-RPC message sent from the client MUST be a new HTTP POST request"), so matching it needs state shared across requests. Servers that need these keep using sessions.

Related

The same slice was proposed in a comment on #341, which asked whether it should get an issue of its own. #341 was closed as implemented via #444 and #455, which cover the stateless 2026-07-28 leg; the handshake-era leg still requires a session.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions