Skip to content

[RFC] feat(server): multi-node session hydration (C# SDK parity)#1786

Draft
felixweinberger wants to merge 2 commits intomainfrom
fweinberger/session-hydration-csharp-parity
Draft

[RFC] feat(server): multi-node session hydration (C# SDK parity)#1786
felixweinberger wants to merge 2 commits intomainfrom
fweinberger/session-hydration-csharp-parity

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

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 original initialize handshake. The private _initialized flag 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:

Python-sdk also takes mcp_session_id in the constructor and has no _initialized guard.

Changes

Transport layer:

  • Add sessionId?: string to WebStandardStreamableHTTPServerTransportOptions. When set, the transport validates incoming mcp-session-id headers against it and rejects re-initialization.
  • Remove private _initialized flag. Its checks are replaced by equivalent sessionId === undefined checks; observable behavior (error codes/messages) is unchanged.

Server layer:

  • Add Server.restoreInitializeState(params) to restore _clientCapabilities and _clientVersion from persisted InitializeRequest params. Without this, a hydrated node has no negotiated capabilities, so server-initiated features (sampling, elicitation, roots) silently fail.

Open questions

  1. Timing vs SEP-1442: The transports-wg is moving toward stateless-by-default, which may make Mcp-Session-Id optional 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.

  2. Scope: Should restoreInitializeState also restore the negotiated protocol version? C# stores _negotiatedProtocolVersion; TypeScript's Server doesn't appear to track this separately.

  3. McpServer passthrough: Should McpServer expose restoreInitializeState directly, or is reaching through mcpServer.server.restoreInitializeState(...) acceptable?

  4. Validation: Should we validate that sessionId + sessionIdGenerator aren'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.ts covering transport hydration (accept, mismatched ID, missing header, re-init rejection, default flow unchanged) and Server.restoreInitializeState.

Breaking Changes

None. _initialized was private; its replacement checks produce identical errors.

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

Additional context

Cross-SDK comparison:

SDK sessionId at construction Protocol-state restoration
C# SessionId { get; init; } HandleInitializeRequestAsync
Python mcp_session_id param (none)
Go (none) (none)
TypeScript (before) (none) (none)
TypeScript (this PR) sessionId option restoreInitializeState

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-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: e63641e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@modelcontextprotocol/server Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 27, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1786

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1786

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1786

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1786

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1786

commit: e63641e

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _initialized and replacing with sessionId === undefined checks 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

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.

StreamableHTTPServerTransport has no public API to reconstruct a session-aware transport from persisted session data

1 participant