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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(server): thread responseMode into the legacy stateless fallback by dielduarte · Pull Request #2621 · modelcontextprotocol/typescript-sdk · GitHub
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

@dielduartedielduarte 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-botBot 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
NameType
@modelcontextprotocol/serverPatch
@modelcontextprotocol/corePatch
@modelcontextprotocol/clientPatch
@modelcontextprotocol/server-legacyPatch
@modelcontextprotocol/codemodPatch
@modelcontextprotocol/core-internalPatch

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
dielduarteforce-pushed the legacy-response-mode branch from 1f394ce to 2720097CompareAugust 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 ownerAugust 6, 2026 18:15
@dielduarte
dielduarteforce-pushed the legacy-response-mode branch from 2720097 to 8ca3df0CompareAugust 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
dielduarteforce-pushed the legacy-response-mode branch from 8ca3df0 to 0134d9aCompareAugust 6, 2026 18:25
@claudeclaudeBot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dielduarte