Skip to content

fix(server): thread responseMode into the legacy stateless fallback - #2621

Open
dielduarte wants to merge 1 commit into
modelcontextprotocol:mainfrom
dielduarte:legacy-response-mode
Open

fix(server): thread responseMode into the legacy stateless fallback#2621
dielduarte wants to merge 1 commit into
modelcontextprotocol:mainfrom
dielduarte:legacy-response-mode

Conversation

@dielduarte

@dielduarte dielduarte commented Aug 6, 2026

Copy link
Copy Markdown

Summary

createMcpHandler's responseMode option lets the modern (2026-07-28) leg defer the HTTP response until a tool call's result is known (responseMode: 'json', backed by PerRequestHTTPServerTransport). The legacy leg's transport, WebStandardStreamableHTTPServerTransport, has the equivalent knob (enableJsonResponse), but createLegacyStatelessFallback never constructed it with the option set — a legacy-classified request always answered over SSE, flushing its 200 before the tool call resolved, with no way to defer it.

This PR threads responseMode into the legacy leg too:

  • responseMode: 'json' now sets enableJsonResponse: true on the legacy leg's transport, buffering the reply until the result is known — the same guarantee the modern leg already had.
  • 'auto' and 'sse' are equivalent on this leg (both leave its SSE-first default unchanged) — the transport has no partial-upgrade concept to distinguish them.
  • legacyStatelessFallback(factory, onerror, responseMode) (the standalone building block for hand-wired compositions) gains a third, optional parameter mirroring the entry's option.

Why this matters: a consumer that needs to turn a tool-call failure into a different HTTP status (e.g. a 401/403 challenge instead of the SDK's default 200 with isError: true) does so by rewriting the response once a handler-recorded failure is known. That rewrite only has a window to fire if the response hasn't already been sent — exactly what enableJsonResponse guarantees. Previously, getting that guarantee on the legacy leg meant bypassing legacy: 'stateless' / legacyStatelessFallback entirely and hand-rolling a WebStandardStreamableHTTPServerTransport + server.connect() + manual abort-teardown. Both now support it directly.

Per CONTRIBUTING.md's note that straightforward bug fixes with tests can skip the issue-first step, I'm opening this directly: the production change is a few lines (a new parameter threaded through createLegacyStatelessFallback/legacyStatelessFallback, one spread into the transport constructor, a call-site pass-through), with tests demonstrating the gap and the fix. Opening as a draft in case a maintainer would rather see this filed as an issue first.

Test plan

  • pnpm --filter @modelcontextprotocol/server test — 474/474 pass (6 new tests covering: unchanged SSE-first behavior for the default/'auto'/'sse', buffered-JSON behavior for 'json' on both the standalone legacyStatelessFallback and end-to-end through createMcpHandler on a legacy-classified request, and dropped mid-call notifications under 'json')
  • pnpm --filter @modelcontextprotocol/server typecheck and lint — clean
  • pnpm check:all (typecheck + lint + docs build across the whole workspace) — clean
  • pnpm test:all — clean aside from one pre-existing, unrelated flaky e2e test (protocol:timeout:max-total [sse 2025-11-25], a fake-timer scenario) that reproduces identically on a clean main checkout
  • Added a changeset (patch on @modelcontextprotocol/server)

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0134d9a

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

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/server Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/client 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

@dielduarte
dielduarte force-pushed the legacy-response-mode branch from 1f394ce to 2720097 Compare August 6, 2026 18:12
@dielduarte
dielduarte marked this pull request as ready for review August 6, 2026 18:15
@dielduarte
dielduarte requested a review from a team as a code owner August 6, 2026 18:15
@dielduarte
dielduarte force-pushed the legacy-response-mode branch from 2720097 to 8ca3df0 Compare August 6, 2026 18:17
createLegacyStatelessFallback built a WebStandardStreamableHTTPServerTransport
per legacy request but never passed it enableJsonResponse, so a legacy-
classified request always answered over SSE, flushing its 200 before a tool
call resolved. The modern leg already solved the equivalent problem via
responseMode: 'json' (backed by PerRequestHTTPServerTransport); it just never
reached the legacy leg's transport, which has the same knob under a different
name.

responseMode: 'json' now sets enableJsonResponse: true on the legacy leg's
transport too, buffering the reply until the result is known - the same
guarantee the modern leg already had. 'auto' and 'sse' are equivalent on this
leg (both leave its SSE-first default unchanged), since the transport has no
partial-upgrade concept to distinguish them.

legacyStatelessFallback(factory, onerror, responseMode) gains a third,
optional parameter mirroring the entry's option, so hand-wired compositions
that use the standalone building block get the same guarantee without
reimplementing the legacy transport wiring by hand.
@dielduarte
dielduarte force-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9a Compare August 6, 2026 18:25
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