[RFC] feat(server): multi-node session hydration (C# SDK parity)#1786
[RFC] feat(server): multi-node session hydration (C# SDK parity)#1786felixweinberger wants to merge 2 commits intomainfrom
Conversation
Adds two pieces for reconstructing a session-aware server on a node
that did not handle the original initialize handshake:
- sessionId option on WebStandardStreamableHTTPServerTransport: when
set, the transport validates incoming mcp-session-id headers and
rejects re-initialization without requiring a fresh handshake.
Mirrors C# SDK's SessionId { get; init; } and python-sdk's
mcp_session_id constructor parameter.
- Server.restoreInitializeState(params): restores negotiated client
capabilities and version from persisted InitializeRequest params,
so capability-gated server-initiated features (sampling, elicitation,
roots) work on hydrated instances. Mirrors C# SDK's
HandleInitializeRequestAsync.
Internal refactor: removes the private _initialized flag. Its checks
are replaced by equivalent sessionId === undefined checks; observable
behavior (error codes/messages) is unchanged.
Closes #1658. Supersedes #1668.
🦋 Changeset detectedLatest commit: e63641e The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
@claude review |
There was a problem hiding this comment.
No bugs found, but this is an RFC with several open design questions (protocol version restoration, McpServer passthrough, sessionId+sessionIdGenerator validation), new public API surface, and changes to core session/initialization logic -- definitely needs human design review.
Extended reasoning...
Overview
This PR adds multi-node session hydration support to the TypeScript MCP SDK, porting patterns from the C# SDK. It touches two core files: server.ts (adds restoreInitializeState public method) and streamableHttp.ts (adds sessionId constructor option, removes _initialized flag). A new integration test file covers the feature, and a changeset is included.
Security risks
The changes touch session management and initialization flow. The sessionId option bypasses the normal initialize handshake, which is the intended behavior for hydration but means the transport trusts the caller to provide a valid, previously-negotiated session ID. The restoreInitializeState method similarly trusts the caller to provide accurate client capabilities. These are reasonable trust boundaries for a server-side API, but the design tradeoffs merit human review.
Level of scrutiny
High scrutiny warranted. This is explicitly an RFC/draft PR ("Not committed to this approach yet"), adds new public API surface that will be hard to change after release, modifies core session validation logic by replacing _initialized with sessionId === undefined checks, and the changeset triggers Major version bumps for express, hono, and node packages. The author has listed 4 open design questions that need maintainer input.
Other factors
- The author explicitly frames this as a draft for discussion, not ready for merge
- The behavioral equivalence of removing
_initializedand replacing withsessionId === undefinedchecks appears correct on inspection, but the semantic shift is subtle - Test coverage is solid with 7 new integration tests
- No existing reviewer comments to address yet
Draft for discussion. Not committed to this approach yet.
Supersedes #1668. Closes #1658 (if we go this direction).
Motivation and Context
The README's "Persistent storage mode" (
examples/server/README.md:96) describes multi-node deployments where any node can handle a session, but the SDK has no way to reconstruct a session-aware transport on a node that didn't handle the originalinitializehandshake. The private_initializedflag blocks all non-init requests until initialize runs on that specific transport instance.This ports the pattern from the C# SDK, which has the most complete solution across SDKs:
SessionId { get; init; }on the transportHandleInitializeRequestAsyncfor protocol-state restorationPython-sdk also takes
mcp_session_idin the constructor and has no_initializedguard.Changes
Transport layer:
sessionId?: stringtoWebStandardStreamableHTTPServerTransportOptions. When set, the transport validates incomingmcp-session-idheaders against it and rejects re-initialization._initializedflag. Its checks are replaced by equivalentsessionId === undefinedchecks; observable behavior (error codes/messages) is unchanged.Server layer:
Server.restoreInitializeState(params)to restore_clientCapabilitiesand_clientVersionfrom persistedInitializeRequestparams. Without this, a hydrated node has no negotiated capabilities, so server-initiated features (sampling, elicitation, roots) silently fail.Open questions
Timing vs SEP-1442: The transports-wg is moving toward stateless-by-default, which may make
Mcp-Session-Idoptional or obsolete. Is it worth adding session-hydration API if it might be short-lived? Counterpoint: C# already has it, the current spec version will coexist for a while regardless.Scope: Should
restoreInitializeStatealso restore the negotiated protocol version? C# stores_negotiatedProtocolVersion; TypeScript'sServerdoesn't appear to track this separately.McpServer passthrough: Should
McpServerexposerestoreInitializeStatedirectly, or is reaching throughmcpServer.server.restoreInitializeState(...)acceptable?Validation: Should we validate that
sessionId+sessionIdGeneratoraren't both set (or that it's coherent if they are)?How Has This Been Tested?
7 new integration tests in
test/integration/test/server/streamableHttp.sessionHydration.test.tscovering transport hydration (accept, mismatched ID, missing header, re-init rejection, default flow unchanged) andServer.restoreInitializeState.Breaking Changes
None.
_initializedwas private; its replacement checks produce identical errors.Types of changes
Checklist
Additional context
Cross-SDK comparison:
SessionId { get; init; }HandleInitializeRequestAsyncmcp_session_idparamsessionIdoptionrestoreInitializeState