Skip to content

Isolate Stateless Requests in Ephemeral Sessions per SEP-2567#415

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:stateless_session_isolation
Open

Isolate Stateless Requests in Ephemeral Sessions per SEP-2567#415
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:stateless_session_isolation

Conversation

@koic

@koic koic commented Jun 16, 2026

Copy link
Copy Markdown
Member

Motivation and Context

SEP-2567 (modelcontextprotocol/modelcontextprotocol#2567, merged for the 2026-07-28 spec release) makes MCP sessionless: every Streamable HTTP POST must be fully self-contained, with no protocol-level session state shared between requests.

StreamableHTTPTransport already implements the SEP's transport surface via stateless: true (no Mcp-Session-Id issued or required, GET returns 405, DELETE is a no-op, server-to-client requests raise), but its dispatch had a state leak: stateless POSTs were handled with session: nil, so Server#init wrote @client and @client_capabilities onto the shared Server instance. Concurrent stateless requests could therefore observe another client's identity, and the data persisted across requests, which is exactly what SEP-2567 forbids. The TypeScript SDK's stateless prototypes (the closed typescript-sdk#2058/#2131/#2251 stack) solve this with per-request dispatch; this change applies the equivalent fix within the existing architecture:

  • Stateless handle_initialization, handle_regular_request, and dispatch_notification now run handlers against an ephemeral per-request ServerSession (with session_id: nil), so client info, logging level, and initialized state live only for the duration of that POST. Repeated initialize requests are naturally permitted because each POST gets a fresh, never-initialized session.
  • send_notification in stateless mode now returns false (non-delivery) instead of raising. With ephemeral sessions in place, a tool calling server_context.report_progress or notify_log_message would otherwise route every call into the exception reporter; non-delivery matches how these helpers already degrade when no session exists. send_request (server-to-client requests) still raises, as those are genuinely unsupported without a stream.

Resolves #388.

How Has This Been Tested?

New tests in test/mcp/server/transports/streamable_http_transport_test.rb:

  • a stateless initialize POST leaves Server#client_capabilities and the server's @client untouched (the leak regression)
  • repeated initialize POSTs both succeed with 200 and no Mcp-Session-Id header
  • a tool calling server_context.report_progress under stateless mode returns its result normally and the exception reporter is never invoked
  • the existing "stateless mode does not support server-sent events" test is updated to assert the new false return instead of the removed raise

All other existing stateless-mode tests pass unchanged.

Breaking Changes

StreamableHTTPTransport#send_notification in stateless mode now returns false instead of raising RuntimeError. The raise message was not a documented contract, broadcasting in stateless mode was always a non-deliverable operation, and the boolean return matches the method's documented semantics in every other non-delivery case. Default (session-oriented) mode is unchanged.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

SEP-2567 (modelcontextprotocol/modelcontextprotocol#2567, merged for the 2026-07-28 spec release)
makes MCP sessionless: every Streamable HTTP POST must be fully self-contained,
with no protocol-level session state shared between requests.

`StreamableHTTPTransport` already implements the SEP's transport surface via
`stateless: true` (no `Mcp-Session-Id` issued or required, GET returns 405,
DELETE is a no-op, server-to-client requests raise), but its dispatch had a state leak:
stateless POSTs were handled with `session: nil`, so `Server#init` wrote `@client` and
`@client_capabilities` onto the shared `Server` instance.
Concurrent stateless requests could therefore observe another client's identity,
and the data persisted across requests, which is exactly what SEP-2567 forbids.
The TypeScript SDK's stateless prototypes (the closed typescript-sdk#2058/#2131/#2251 stack)
solve this with per-request dispatch; this change applies the equivalent fix within the existing architecture:

- Stateless `handle_initialization`, `handle_regular_request`, and `dispatch_notification` now
  run handlers against an ephemeral per-request `ServerSession` (with `session_id: nil`),
  so client info, logging level, and initialized state live only for the duration of that POST.
  Repeated `initialize` requests are naturally permitted because each POST gets a fresh,
  never-initialized session.
- `send_notification` in stateless mode now returns `false` (non-delivery) instead of raising.
  With ephemeral sessions in place, a tool calling `server_context.report_progress` or `notify_log_message`
  would otherwise route every call into the exception reporter; non-delivery matches how
  these helpers already degrade when no session exists. `send_request` (server-to-client requests) still raises,
  as those are genuinely unsupported without a stream.

Resolves modelcontextprotocol#388.

## How Has This Been Tested?

New tests in `test/mcp/server/transports/streamable_http_transport_test.rb`:

- a stateless `initialize` POST leaves `Server#client_capabilities` and the server's `@client` untouched
  (the leak regression)
- repeated `initialize` POSTs both succeed with 200 and no `Mcp-Session-Id` header
- a tool calling `server_context.report_progress` under stateless mode returns its result normally and
  the exception reporter is never invoked
- the existing "stateless mode does not support server-sent events" test is updated to assert
  the new `false` return instead of the removed raise

All other existing stateless-mode tests pass unchanged.

## Breaking Changes

`StreamableHTTPTransport#send_notification` in stateless mode now returns `false` instead of raising `RuntimeError`.
The raise message was not a documented contract, broadcasting in stateless mode was always a non-deliverable operation,
and the boolean return matches the method's documented semantics in every other non-delivery case.
Default (session-oriented) mode is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SEP-2567: Sessionless MCP via Explicit State Handles

2 participants