Skip to content

fix(client): seed SSE resumption tracker from resumptionToken (#2499)#2509

Open
vishnujayvel wants to merge 1 commit into
modelcontextprotocol:mainfrom
vishnujayvel:fix/2499-resumed-stream-drops-last-event-id
Open

fix(client): seed SSE resumption tracker from resumptionToken (#2499)#2509
vishnujayvel wants to merge 1 commit into
modelcontextprotocol:mainfrom
vishnujayvel:fix/2499-resumed-stream-drops-last-event-id

Conversation

@vishnujayvel

Copy link
Copy Markdown

Seed _handleSseStream()'s resumption-token tracker from the resumptionToken a stream was (re)opened with, so a resumed stream that disconnects again before any new event arrives still reconnects with the same Last-Event-ID.

Motivation and Context

StreamableHTTPClientTransport._handleSseStream() tracks the latest event id in a local lastEventId that always starts undefined. It is never seeded from the resumptionToken the stream was opened with, even though both reconnect paths (onDone / onError) pass resumptionToken: lastEventId when scheduling the next attempt.

So when a stream resumed with Last-Event-ID: e1 disconnects again before any id-bearing event arrives — a load-balancer idle timeout or a server restart, both plausible on a long-running request — the scheduled reconnect GET carries no Last-Event-ID header at all. The server has no way to know this was meant to be a resumption, treats it as a brand-new standalone stream, and the events the client was waiting to replay are never sent; the original request just hangs until timeout.

The fix seeds the tracker from options.resumptionToken at the top of _handleSseStream(). Since replay is idempotent (that's the whole point of Last-Event-ID), re-sending the same token when no new events arrived is exactly the correct behavior — it's what the client would have sent had it never gotten this far in the first place.

This only affects the reconnect path: the initial POST send always constructs a fresh options object with no resumptionToken, so lastEventId there is still seeded undefined as before — no behavior change outside of resumed streams.

How Has This Been Tested?

One new unit test in packages/client/test/client/streamableHttp.test.ts (SSE retry field handling describe block, alongside the existing "should reconnect on graceful stream close" test it mirrors): opens a stream via _startOrAuthSse({ resumptionToken: 'event-1' }), mocks the resumed GET's body closing immediately with no events, and asserts the second fetch call's headers carry last-event-id: event-1 (previously null).

Revert-proofed by reverting the fix in isolation: the new test fails with expected null to be 'event-1', confirming it actually exercises the bug; restoring the fix makes it pass again.

Full @modelcontextprotocol/client suite: 739/739 pass (738 pre-existing + 1 new), no pre-existing failures. pnpm --filter @modelcontextprotocol/client typecheck and lint (eslint + prettier) both clean.

Breaking Changes

None. This only changes the Last-Event-ID header value sent on a reconnect GET that would otherwise have omitted it; no public API or type signature changes.

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

Changeset included (@modelcontextprotocol/client: patch).

Credit to @ayush00git for the report, root-cause analysis, and suggested fix in #2499 — this PR implements exactly the direction proposed there. @jspahrsummers and @harshmathurx also weighed in on the issue confirming the approach.

A resumed stream (Last-Event-ID: e1) that disconnects again before any
new id-bearing event arrives (LB idle timeout, server restart) was
reconnecting with no Last-Event-ID header at all, because
_handleSseStream() tracked the latest event id in a local variable
that always started undefined instead of being seeded from the
resumptionToken the stream was (re)opened with. The server then
treated the reconnect as a brand-new stream instead of a resumption.

Seed the tracker from options.resumptionToken so a reconnect that saw
no new events re-sends the same token.

Fixes modelcontextprotocol#2499
@vishnujayvel
vishnujayvel requested a review from a team as a code owner July 17, 2026 05:12
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f18c728

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/core Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/codemod 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

@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: f18c728

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.

1 participant