Skip to content

fix(client): preserve Streamable HTTP request provenance - #2667

Open
edenbuilds wants to merge 8 commits into
modelcontextprotocol:mainfrom
edenbuilds:codex/fix-stream-provenance-2659
Open

fix(client): preserve Streamable HTTP request provenance#2667
edenbuilds wants to merge 8 commits into
modelcontextprotocol:mainfrom
edenbuilds:codex/fix-stream-provenance-2659

Conversation

@edenbuilds

@edenbuilds edenbuilds commented Aug 15, 2026

Copy link
Copy Markdown

Summary

  • preserve the originating client request ID across a Streamable HTTP POST SSE response stream and resumed GET
  • expose it through the existing ctx.mcpReq.relatedRequestId client-handler context
  • preserve resumption callbacks, request abort, and stream-end settlement across resumed GETs
  • keep standalone GET messages and normal responses backward-compatible
  • add client documentation, regression tests, and a patch changeset

Verification

  • Regression proof: before the source change, the focused provenance assertion received no attributed server request.
  • Regression proof: removing resumed callback forwarding makes the direct resumption-token stream-end test fail with zero callback calls.
  • After the fixes: focused client suite passes 76/76.
  • @modelcontextprotocol/client typecheck passes.
  • ESLint, Prettier, and git diff --check pass.

Fixes #2659

…tprotocol#2659)

Expose the originating client request ID when a server-initiated JSON-RPC
request arrives on that request's SSE response stream.

Fixes modelcontextprotocol#2659
@edenbuilds
edenbuilds requested a review from a team as a code owner August 15, 2026 23:55
@changeset-bot

changeset-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 913689e

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 Aug 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 913689e

Refresh the PR base after upstream's cancellation request ID 0 fix while
preserving the Streamable HTTP provenance change.
@edenbuilds

Copy link
Copy Markdown
Author

Refreshed this branch onto the current upstream/main without rewriting history (d363e64). The diff against upstream remains limited to the Streamable HTTP provenance fix, its regression test, and the changeset. All 14 reported CI checks are green; ready for maintainer review.

Merge the latest upstream MCP protocol-version validation change without
altering the four-file stream provenance diff.
@edenbuilds

Copy link
Copy Markdown
Author

Refreshed the branch onto the latest upstream main in non-rewriting merge commit d0f8ca2b; the diff against current upstream remains limited to the existing four provenance files, and the expected six-package changeset is still detected. GitHub CI has restarted on the refreshed head. The local focused test remains blocked by this clone’s unresolved @modelcontextprotocol/core-internal package entry; no source changes were made in the refresh.

The send(..., { resumptionToken }) path built its _startOrAuthSse options
without relatedRequestId, so a server-initiated request replayed on a
resumed stream arrived unattributed -- the long-running-call case the
original fix was aimed at.

Add negative tests pinning that the standalone GET stream and the
response terminating a POST stream stay unattributed.
@KKonstantinov

Copy link
Copy Markdown
Contributor

@claude review

Comment thread packages/core-internal/src/types/types.ts
Comment thread packages/client/src/client/streamableHttp.ts
Comment on lines +1141 to +1142
const hasRequests = messages.some(msg => 'method' in msg && 'id' in msg && msg.id !== undefined);
const relatedRequestId = messages.length === 1 && isJSONRPCRequest(messages[0]) ? messages[0].id : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [quality] nit: the new relatedRequestId derivation introduces a second, divergent "is this a request" predicate (isJSONRPCRequest) directly beneath the existing hand-rolled hasRequests check ('method' in msg && 'id' in msg && msg.id !== undefined); both should derive from one messages.filter(isJSONRPCRequest) pass.

Extended reasoning...

Concrete cost: two adjacent lines now encode the same concept with different semantics — hasRequests accepts a message lacking jsonrpc: '2.0' while isJSONRPCRequest rejects it — so a maintainer must reason about and keep in sync two request-detection definitions, and a payload that satisfies one but not the other silently opens the SSE-handling branch with relatedRequestId undefined. Simpler form: const requests = messages.filter(isJSONRPCRequest); const hasRequests = requests.length > 0; const relatedRequestId = messages.length === 1 && requests.length === 1 ? requests[0].id : undefined; — one predicate, same behavior for well-formed traffic.

Verification: nit — the factual claim is true. At packages/client/src/client/streamableHttp.ts:1141-1142 the diff adds const relatedRequestId = messages.length === 1 && isJSONRPCRequest(messages[0]) ? messages[0].id : undefined; directly beneath the pre-existing hand-rolled predicate const hasRequests = messages.some(msg => 'method' in msg && 'id' in msg && msg.id !== undefined);. The two predicates do dive

Comment thread packages/client/src/client/streamableHttp.ts
Merge the latest upstream changes without rewriting the maintainer follow-up
already on the pull request branch.
@edenbuilds

Copy link
Copy Markdown
Author

Refreshed the PR after the maintainer follow-up 5d42a7f4.

d237edae is a non-rewriting merge of current upstream main (3e90449f); the diff against upstream remains limited to the existing provenance changes, regression tests, and changeset. Local client verification now passes: focused tests 73/73, typecheck, ESLint, and Prettier. GitHub CI is rerunning on the refreshed head.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Surface relatedRequestId in client handler context and preserve the
resumption token, callbacks, and stream-end lifecycle across resumed GETs.
Keep request classification on one JSON-RPC predicate.

Add regression coverage for handler visibility, token preservation, and
resumption callback delivery.
Refresh the PR onto the latest upstream changes without rewriting its
existing commits.
@edenbuilds

Copy link
Copy Markdown
Author

Addressed the open Claude findings and pushed e7c7e36.

  • surfaced relatedRequestId in ctx.mcpReq for client request handlers
  • preserved the initial Last-Event-ID across reconnects
  • carried resumption-token and stream-end callbacks through resumed GETs
  • unified request classification on isJSONRPCRequest

Verification: each new regression assertion failed on the pre-fix source, then the focused set passed 5/5; core-internal and client typechecks, client lint, and git diff --check pass. The branch is refreshed onto current upstream/main without rewriting history. GitHub Actions is rerunning on the new head.

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

2 participants