feat(server): add replayInitialization for stateless serverless sessions#1912
Open
kev-flex wants to merge 3 commits intomodelcontextprotocol:mainfrom
Open
feat(server): add replayInitialization for stateless serverless sessions#1912kev-flex wants to merge 3 commits intomodelcontextprotocol:mainfrom
kev-flex wants to merge 3 commits intomodelcontextprotocol:mainfrom
Conversation
…on adoption In stateless serverless deployments (Vercel, Lambda, Workers), each HTTP request creates a fresh Server + Transport. The initialize handshake sets state (_clientCapabilities, _clientVersion, sessionId, _initialized) that is lost between requests, forcing developers to use Reflect.set hacks. Add a `replayInitialization` callback to transport options that restores session state on non-initialize requests. The transport reads the session ID from the request header, calls the callback to fetch cached state, and flows it to the server via a new `oninitialized` callback on the Transport interface. - Transport: `replayInitialization` option, `_tryReplayInitialization` helper called once in `handleRequest` before method dispatch - Transport: fix `validateSession` to recognize replayed sessions - Core: `oninitialized` callback on Transport interface (same pattern as onmessage/onclose/onerror) - Server: override `connect()` to hook oninitialized, seeding _clientCapabilities and _clientVersion via ??= - Node middleware: delegate oninitialized getter/setter to inner transport - Callback returns undefined → 404 per spec (client re-initializes) - Callback throws → 500 internal error - Race condition guard via _replayInProgress flag Closes modelcontextprotocol#1658, modelcontextprotocol#1882
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
9689917 to
3f12390
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add a
replayInitializationcallback toWebStandardStreamableHTTPServerTransportthat restores session state on non-initialize requests in stateless serverless deployments.Closes #1658, closes #1882, closes #776
Motivation and Context
In stateless serverless deployments (Vercel, Lambda, Workers), each HTTP request creates a fresh
Server+Transport. Theinitializehandshake sets private state (_clientCapabilities,_clientVersion,sessionId,_initialized) that is lost when the instance is garbage collected. This means:CapabilityNotSupportedon any non-initialize requestReflect.sethacks to write private fields — fragile and breaks on field renamesThe
replayInitializationcallback lets developers restore session state from an external cache (Redis, KV, Postgres) without reaching into private fields:The replay is invisible to the client — it's an internal server-side optimization that restores state from a prior legitimate handshake.
Changes
packages/core/src/shared/transport.tsoninitializationreplaycallback toTransportinterface (same pattern asonmessage/onclose/onerror)packages/core/src/util/inMemory.tsoninitializationreplayonInMemoryTransportpackages/server/src/server/server.tsconnect()to hooktransport.oninitializationreplay, seeding_clientCapabilities/_clientVersionvia??=packages/server/src/server/streamableHttp.tsreplayInitializationoption,_tryReplayInitializationhelper (called once inhandleRequestbefore method dispatch), fixvalidateSessionto recognize replayed sessions, race condition guardpackages/middleware/node/src/streamableHttp.tsoninitializationreplaygetter/setter to inner transportCallback semantics:
undefined→ 404 "Session not found" per spec (client re-initializes)How Has This Been Tested?
oninitializationreplayhook seeding, overwrite by real init, callback chaining, undefined without callpnpm typecheck:all,pnpm test:all,pnpm lint:allBreaking Changes
None. All new options are optional with
undefineddefaults. Existing behavior is identical whenreplayInitializationis not provided.Types of changes
Checklist
Additional context
Spec compliance: The spec says "The server MAY terminate the session at any time, after which it MUST respond with HTTP 404 Not Found" and "When a client receives HTTP 404, it MUST start a new session." When the callback returns
undefined(session unknown/expired), the transport returns 404 — the protocol self-heals.