Skip to content

fix(server): replay request-scoped terminal response after closeSSEStream#2153

Open
adityasingh2400 wants to merge 1 commit into
modelcontextprotocol:mainfrom
adityasingh2400:fix-closesse-terminal-replay
Open

fix(server): replay request-scoped terminal response after closeSSEStream#2153
adityasingh2400 wants to merge 1 commit into
modelcontextprotocol:mainfrom
adityasingh2400:fix-closesse-terminal-replay

Conversation

@adityasingh2400
Copy link
Copy Markdown

Fixes #2151.

Problem

closeSSEStream(requestId) is meant to support the SEP-1699 flow where the server disconnects a request-scoped POST SSE stream, keeps working, and replays missed events after the client reconnects. On main this breaks when the terminal response is produced while the stream is disconnected:

  • closeSSEStream removes the stream from _streamMapping but keeps _requestToStreamMapping.
  • In send(), request-scoped events are only stored inside if (!this._enableJsonResponse && stream?.controller && stream?.encoder), so once the controller is gone the result/error is never written to the event store.
  • When all responses for the request are ready and no live stream exists, send() throws No connection established for request ID: ....

So the documented "close SSE, reconnect, replay final result" path is not reliable for request-scoped POST SSE streams. The standalone GET SSE path already does the opposite (stores while disconnected, returns without throwing when no controller), so the two paths were asymmetric.

Fix

Mirror the standalone GET SSE behavior for request-scoped responses:

  • store request-scoped events whenever an event store is configured, regardless of whether a controller is currently attached;
  • write to the controller only when one is present;
  • when all responses are ready but the stream is gone, and an event store can replay the persisted response, clean up the request mappings and return instead of throwing. Without an event store there is no way to deliver the response, so the original error is preserved.

No behavior change when no event store is configured.

Testing

Added should replay the terminal response after closeSSE and reconnect to packages/middleware/node/test/streamableHttp.test.ts (the file that previously only asserted the stream closes, not that the result is replayable). It calls ctx.http?.closeSSE?.(), completes the tool while disconnected, reconnects with Last-Event-ID, and asserts the terminal tools/call result is replayed. The test fails on main (reconnect never receives the result) and passes with this change.

pnpm --filter @modelcontextprotocol/server test (65 passed) and pnpm --filter @modelcontextprotocol/node test for the streamableHttp suite (75 passed) are green; typecheck and lint pass.

…tream

After closeSSEStream() removes the active stream, send() only stored the
event while a controller was attached, so the terminal response produced
during the reconnect window was never written to the event store and could
not be replayed. With all responses ready and no live stream, send() then
threw "No connection established for request ID".

Store request-scoped SSE events whenever an event store is configured
(regardless of controller presence) and write to the controller only when
one is attached, mirroring the standalone GET SSE path. When the stream is
gone but the response is persisted for replay, clean up the request mappings
and return instead of throwing; keep throwing when no event store can deliver
the response.

Fixes modelcontextprotocol#2151
@adityasingh2400 adityasingh2400 requested a review from a team as a code owner May 24, 2026 12:43
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 24, 2026

⚠️ No Changeset found

Latest commit: 582cea8

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 May 24, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2153

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2153

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 582cea8

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.

closeSSEStream() breaks request-scoped SSE resumability for terminal responses

1 participant