modelcontextprotocol / modelcontextprotocol/typescript-sdk
Bug: Resumption token dropped when a resumed SSE stream disconnects before any id-bearing event
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
Bug
In StreamableHTTPClientTransport, _handleSseStream() tracks the latest event id in a local lastEventId initialized to undefined. it is never seeded from the resumptionToken the stream was opened with. Both reconnect paths pass resumptionToken: lastEventId when scheduling the next attempt.
So if a stream resumed with Last-Event-ID: e1 disconnects again before any id-bearing event arrives (LB idle timeout, server restart), the reconnect GET is sent with no Last-Event-ID header. The server treats it as a brand-new standalone stream instead of
a resumption, missed events are never replayed and a long-running request hangs until timeout.
Repro
- Open a stream with
resumptionToken: 'event-1'(GET carriesLast-Event-ID: event-1) - Server accepts (200,
text/event-stream) but the stream closes before any event with an id - The scheduled reconnect GET carries no
Last-Event-IDheader.
Verified with a failing unit test: mock the resumed GET with a body that closes immediately,
then assert the second fetch call's headers and last-event-id is null.
Suggested fix
Seed the tracker from the options the stream was opened with:
let lastEventId: string | undefined = options.resumptionToken;
so a reconnect that saw no new events re-sends the same token (replay is idempotent)
instead of silently dropping it.
Happy to submit a PR with the fix + regression test.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in StreamableHTTPClientTransport._handleSseStream() and inspect the existing failing unit test for a resumed GET whose body closes immediately. Verify how resumptionToken initializes the tracker and how both reconnect paths pass it to the next attempt. Done means the second fetch retains the original Last-Event-ID when no id-bearing event arrives, and the regression test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100