modelcontextprotocol / modelcontextprotocol/typescript-sdk

Bug: Resumption token dropped when a resumed SSE stream disconnects before any id-bearing event

Open Beginner friendly
#2,499 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P2 ready for work v1 v2
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

  1. Open a stream with resumptionToken: 'event-1' (GET carries Last-Event-ID: event-1 )
  2. Server accepts (200, text/event-stream) but the stream closes before any event with an id
  3. The scheduled reconnect GET carries no Last-Event-ID header.

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.