-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Summary
The issue is for Stateful server Persistent Storage pattern for version 1.27.1 and the problem should also be there in the main branch.
The SDK documentation describes three multi-node deployment patterns in src/examples/README.md, including Pattern 2 (Persistent Storage) where session state is stored in an external database and any node can serve any session. However,
StreamableHTTPServerTransport (and its underlying WebStandardStreamableHTTPServerTransport) provides no public API to reconstruct a session-aware transport instance from externally persisted session data.
To Reproduce
Any client starting a connection to the MCP server in a multi-node environment would fail:
The first request to the initialize method goes through and returns a response with an mcp-session-id. However, the second request to the notifications/initialized method(and any following requests) fails because it hits a different pod/node while carrying the mcp-session-id issued by the first pod/node.
My analysis
WebStandardStreamableHTTPServerTransport stores session state (sessionId, _initialized) as private instance fields that are only set during the initialize handshake on the original transport instance. The validateSession() method
— which checks the mcp-session-id request header against this.sessionId — is also private and cannot be overridden. This means in a multi-node environment, when a request arrives at a different node than the one that handled initialize, there is no supported way to create a transport that:
- Knows the existing session ID
- Validates incoming mcp-session-id headers against it
- Includes mcp-session-id in response headers
Workaround:
- Stateless mode (
sessionIdGenerator: undefined) for non-intialize requests — which disables session validation entirely, violating the MCP spec's requirement that servers validate session IDs - Hydrate/re-construct a transport instance by setting private fields — setting
_webStandardTransport.sessionIdand_webStandardTransport._initializeddirectly, which is what I chose to go with, but of cause it's not robust without SDK official support.
Suggestions
Introduce a public mechanism to construct a transport that is "pre-initialized" with an existing session ID. This could take several forms:
- A
hydrate(sessionId: string)method on the transport that sets sessionId and marks the transport as initialized - Constructor options like existingSessionId that skip the initialization requirement
- A
TransportStore / TransportFactoryabstraction that the SDK's request handling delegates to for session-aware transport creation/retrieval — replacing the simpleMap<sessionId, transport>pattern in the examples