Skip to content

fix(client): close the SSE transport when its stream ends for good - #3140

Open
arjunprabhulal wants to merge 1 commit into
modelcontextprotocol:mainfrom
arjunprabhulal:fix/sse-client-close-on-dead-stream
Open

arjunprabhulal wants to merge 1 commit into
modelcontextprotocol:mainfrom
arjunprabhulal:fix/sse-client-close-on-dead-stream

Conversation

@arjunprabhulal

Copy link
Copy Markdown

Motivation and Context

When an SSEClientTransport loses its stream after connecting and the reconnect fails for good, the transport stays open with nothing behind it. For example, the server restarts and the reconnect GET returns 404 or 401. In-flight requests wait for their timeout, 60 seconds by default, instead of failing, and onclose never fires, so the application is never told it needs to reconnect.

Steps to reproduce:

  1. Connect a Client over SSEClientTransport.
  2. Send tools/list, and have the server end the SSE response before it answers.
  3. Answer the EventSource reconnect GET with 404.

Expected: the pending tools/list rejects with ConnectionClosed and onclose fires.

Observed: EventSource stops reconnecting (readyState is CLOSED), but the request stays pending until it times out, and onclose is never called.

Once start() has resolved, the _eventSource.onerror handler in packages/client/src/client/sse.ts calls reject() (a no-op by then) and onerror, and returns. It never checks whether EventSource has given up, so close() is never called.

The handler now calls close() when the endpoint is known and EventSource is CLOSED. It does this on both terminal error paths, so onclose fires and Protocol rejects pending requests with ConnectionClosed. Errors before start() resolves, transient errors while EventSource is still reconnecting, and the onUnauthorized retry path behave as before.

How Has This Been Tested?

  • New test packages/client/test/client/sseStreamEnd.test.ts runs a real HTTP server. The server ends the SSE stream in the middle of tools/list and answers the reconnect with 404. The test asserts the request rejects with ConnectionClosed and onclose fires.
  • Without the change to sse.ts, the test fails: Error: Test timed out in 15000ms. With it, the test passes.
  • The full @modelcontextprotocol/client suite passes: 45 test files, 1079 tests.
  • pnpm build:all, pnpm typecheck:all and pnpm lint:all pass, and prettier --check passes on the changed files.

Breaking Changes

None. A transport whose stream can no longer be reopened now closes instead of staying open with no stream.

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

v1.x has the same handler (src/client/sse.ts:194), and most SSE users are still on v1. I can open the same fix against v1.x if you'd like it there too.

AI assistance: Claude Code was used for this change.

After start() resolved, the EventSource onerror handler only reported
the error. When the stream dropped and the reconnect failed with a
non-200 status, EventSource stopped retrying but the transport never
called close(), so onclose did not fire and pending requests waited for
their timeout. Close the transport once the EventSource is CLOSED.
@arjunprabhulal
arjunprabhulal requested a review from a team as a code owner October 31, 2026 23:31
@changeset-bot

changeset-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1938f8e

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

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/core-internal Patch

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T23:35:16.588308Z 1938f8e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Oct 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@3140

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@3140

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@3140

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 1938f8e

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1938f8e622

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +371 to +372
if (this._endpoint !== undefined && this._eventSource?.readyState === EventSource.CLOSED) {
void this.close();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid closing again after an error callback closes the transport

If an application closes the client in onerror and starts reconnecting in onclose, this check executes after the error callback has already called close(). Because SSEClientTransport.close() leaves _eventSource in CLOSED and fires onclose unconditionally, the helper closes the old transport again and runs Protocol._onclose() twice; the second call clears _transport for the replacement connection started by the first callback. This also happens for transient errors that become CLOSED only because the application closed them. Guard against an already-closed transport, or make close() idempotent.

Useful? React with 👍 / 👎.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Nov 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant