Skip to content

feat(server): add replayInitialization for stateless serverless sessions#1912

Open
kev-flex wants to merge 3 commits intomodelcontextprotocol:mainfrom
kev-flex:kev/serverless-replay-support
Open

feat(server): add replayInitialization for stateless serverless sessions#1912
kev-flex wants to merge 3 commits intomodelcontextprotocol:mainfrom
kev-flex:kev/serverless-replay-support

Conversation

@kev-flex
Copy link
Copy Markdown

@kev-flex kev-flex commented Apr 16, 2026

Summary

Add a replayInitialization callback to WebStandardStreamableHTTPServerTransport that 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. The initialize handshake sets private state (_clientCapabilities, _clientVersion, sessionId, _initialized) that is lost when the instance is garbage collected. This means:

  • Tools that check capabilities (elicitation, sampling, roots) throw CapabilityNotSupported on any non-initialize request
  • Session validation fails because the fresh transport doesn't recognize the session
  • Developers must use Reflect.set hacks to write private fields — fragile and breaks on field renames

The replayInitialization callback lets developers restore session state from an external cache (Redis, KV, Postgres) without reaching into private fields:

const transport = new WebStandardStreamableHTTPServerTransport({
  sessionIdGenerator: () => crypto.randomUUID(),     // init request: generates ID

  // Subsequent requests: transport reads mcp-session-id from the client's
  // request header and passes it here to look up cached state
  replayInitialization: async (sessionId) => {
    const cached = await sessionStore.get(sessionId);
    if (!cached) return undefined; // → 404, client re-initializes
    return {
      clientCapabilities: cached.capabilities,
      clientVersion: cached.clientVersion,
    };
  },
});

The replay is invisible to the client — it's an internal server-side optimization that restores state from a prior legitimate handshake.

Changes

File Change
packages/core/src/shared/transport.ts Add optional oninitializationreplay callback to Transport interface (same pattern as onmessage/onclose/onerror)
packages/core/src/util/inMemory.ts Declare oninitializationreplay on InMemoryTransport
packages/server/src/server/server.ts Override connect() to hook transport.oninitializationreplay, seeding _clientCapabilities/_clientVersion via ??=
packages/server/src/server/streamableHttp.ts Add replayInitialization option, _tryReplayInitialization helper (called once in handleRequest before method dispatch), fix validateSession to recognize replayed sessions, race condition guard
packages/middleware/node/src/streamableHttp.ts Delegate oninitializationreplay getter/setter to inner transport

Callback semantics:

  • Returns data → session restored, request proceeds
  • Returns undefined404 "Session not found" per spec (client re-initializes)
  • Throws → 500 internal error

How Has This Been Tested?

  • 4 new server tests: oninitializationreplay hook seeding, overwrite by real init, callback chaining, undefined without call
  • 13 new transport tests: callback invocation, session adoption, 404 on unknown session, 500 on throw, no-ops (no callback, no header, already initialized, init request), session mismatch, GET/DELETE replay, async callback
  • All existing tests pass: pnpm typecheck:all, pnpm test:all, pnpm lint:all

Breaking Changes

None. All new options are optional with undefined defaults. Existing behavior is identical when replayInitialization is not provided.

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

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.

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

changeset-bot bot commented Apr 16, 2026

⚠️ No Changeset found

Latest commit: 0ae44bf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 16, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1912

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1912

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1912

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1912

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1912

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1912

commit: 0ae44bf

@kev-flex kev-flex force-pushed the kev/serverless-replay-support branch from 9689917 to 3f12390 Compare April 16, 2026 05:42
@kev-flex kev-flex marked this pull request as ready for review April 16, 2026 05:45
@kev-flex kev-flex requested a review from a team as a code owner April 16, 2026 05:45
@kev-flex kev-flex changed the title feat(server): add replayInitialization for stateless serverless sessi… feat(server): add replayInitialization for stateless serverless sessions Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant