Skip to content

fix: reset relay request-response waits on progress notifications (#2028) - #2363

Merged
cliffhall merged 3 commits into
v2/mainfrom
v2/fix/2028-stdio-delayed-responses
Sep 14, 2026
Merged

cliffhall merged 3 commits into
v2/mainfrom
v2/fix/2028-stdio-delayed-responses

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2028

Problem

A long-running tool that reports progress (ctx.report_progress in FastMCP) times out at ~60s in the web client, while the same tool completes over the CLI/TUI and in other MCP clients. The web relay is the only surface with the defect.

The browser drives its MCP Client through a two-hop relay: the browser's RemoteClientTransport → the Node backend (/api/mcp/send + the /api/mcp/events SSE stream) → the real upstream transport. The relay adds two request-response waits that the direct transports don't have, and both were hardcoded to 60s and blind to notifications/progress:

  • backend RemoteSession.waitForRequestResponse (awaited by /api/mcp/send)
  • browser RemoteClientTransport.waitForSseResponse

The SDK Client's own request timeout resets on progress (resetTimeoutOnProgress, which the Inspector already enables). But the relay's progress-blind waits fire first and shadow it, so on the web path a progress-reporting call is cancelled at 60s no matter how recently progress arrived.

Fix

Make both relay waits reset their deadline on a notifications/progress whose progressToken matches the pending request id — mirroring the SDK's resetTimeoutOnProgress. A shared, browser-safe progressTokenOf helper (core/mcp/remote/progressToken.ts) extracts the token; both waits re-arm their full window when one arrives.

Log messages (notifications/message, i.e. ctx.info) deliberately do not reset the deadline — matching the SDK and every other client, so a purely-logging tool that runs past the timeout still times out as before.

Not addressed (and why)

The issue also reports responses arriving 44–173s late on the reporter's machine while the server's notifications arrive promptly over the same pipe. That is a server-side stall (FastMCP/Python 3.14 on their host), not an Inspector defect: it does not reproduce on the identical package stack (fastmcp 3.4.7 / mcp 1.30.0 / Python 3.14.7) across a raw stdio reader, the CLI, or the full web relay. The Inspector faithfully sends, correlates, and times out per the SDK's 60s default. See the issue comment for the full analysis.

Tests

  • core/mcp/remote/progressToken.ts — unit tests for every branch of the token extractor.
  • backend RemoteSession — a progress note re-arms the wait past its original deadline; a log message does not; no-ops for an unknown id, a timerless wait, and a malformed token.
  • browser RemoteClientTransport — progress keeps the SSE wait alive past sseResponseTimeoutMs; a log-only stream still times out.

Verified end to end against a real FastMCP stdio server over the relay: a 75s progress-reporting call now completes; a 75s log-only call still times out at ~60s.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp

…ns (#2028)

The web client drives its MCP Client through a two-hop relay
(RemoteClientTransport -> Node backend -> upstream transport). The relay
added two request-response waits that the direct transports don't have —
the backend's RemoteSession.waitForRequestResponse and the browser's
RemoteClientTransport.waitForSseResponse — and both were hardcoded to 60s
and blind to notifications/progress. The SDK Client's own request timeout
resets on progress (resetTimeoutOnProgress, which the Inspector enables),
but the relay's progress-blind waits fire first and shadow it, so a
long-running tool that reports progress is cancelled at 60s on the web
client alone while it completes over the CLI/TUI and other MCP clients.

Reset both relay waits on a notifications/progress whose progressToken
matches the pending request id, mirroring the SDK's resetTimeoutOnProgress.
A shared browser-safe progressTokenOf helper extracts the token. Log
messages (notifications/message, i.e. ctx.info) deliberately do not reset
the deadline, matching the SDK and every other client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Progress-token validation accepts invalid numeric tokens, and the new browser timeout tests are timing-dependent.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes #2028 by making both web relay timeout layers restart when matching progress notifications arrive.

Changes:

  • Adds shared progress-token extraction.
  • Re-arms backend and browser SSE response waits.
  • Adds regression and validation tests.
File summaries
File Description
core/mcp/remote/remoteClientTransport.ts Resets browser relay waits on progress.
core/mcp/remote/progressToken.ts Extracts progress tokens from notifications.
core/mcp/remote/node/remote-session.ts Resets backend relay waits on progress.
clients/web/src/test/integration/mcp/remote/remote-session.test.ts Tests backend timeout behavior.
clients/web/src/test/core/mcp/remote/remoteClientTransport.test.ts Tests browser relay timeout behavior.
clients/web/src/test/core/mcp/remote/progressToken.test.ts Tests token extraction.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 5
  • Review effort level: Balanced

Comment thread core/mcp/remote/progressToken.ts Resolved
…ake relay tests (#2028)

- progressTokenOf now delegates to the SDK's ProgressTokenSchema instead of a
  bare typeof check, so fractional and past-MAX_SAFE_INTEGER numbers are
  rejected (matching isProgressToken in inspectorClient.ts); tests cover both.
- Browser relay timeout tests use fake timers with advanceTimersByTimeAsync and
  flush the SSE consumer between pushes, replacing real-sleep windows that a
  loaded runner could race (#1596).
- The log-only timeout test holds and awaits its rejection rather than
  swallowing it in an empty catch.
- progressToken.test.ts gains a file header; the remote-session malformed-token
  fixture uses a single-cast helper instead of `as unknown as`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Thanks for the review — all five comments addressed in fb28b0f (replied inline on each thread):

  1. Timing-dependent browser tests — reworked to fake timers (advanceTimersByTimeAsync), flushing the SSE consumer after each push so a frame is parsed before time moves; no real sleeps.
  2. Empty catch — the log-only test now holds and awaits the expected rejection instead of suppressing it.
  3. progressTokenOf numeric validation — now delegates to the SDK's ProgressTokenSchema.safeParse (same as isProgressToken in inspectorClient.ts), so fractional and past-MAX_SAFE_INTEGER values are rejected; added tests for both plus the safe boundary.
  4. Missing file header — added to progressToken.test.ts.
  5. as unknown as double cast — replaced with a single-cast asMsg helper.

I also mutation-checked both regression tests (disabling each relay's reset makes its test fail), so they genuinely detect the fix rather than passing vacuously.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

A newly added unjustified double cast bypasses type safety and should be removed before approval.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

clients/web/src/test/core/mcp/remote/remoteClientTransport.test.ts:726

  • This new as unknown as typeof fetch is unnecessary because the mock is already declared with vi.fn<typeof fetch>(); it also bypasses the repository's type-safety rule for double casts without a safety justification. Keep the typed mock result directly.
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…er (#2028)

The backendHoldingSend fetch mock is already declared with vi.fn<typeof fetch>(),
so the trailing `as unknown as typeof fetch` was redundant and tripped the
repo's no-double-cast rule. Return the typed mock directly; the test project
typechecks without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Round 2 addressed in 5165316: removed the as unknown as typeof fetch on the backendHoldingSend mock helper. The mock is already declared vi.fn<typeof fetch>(), so it returns the typed result directly and the test project typechecks without the cast (answering the suppressed-comments finding here, since it has no inline thread).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

Both relay layers implement the intended progress-aware timeout behavior with comprehensive deterministic regression coverage.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@cliffhall
cliffhall merged commit 3e728a3 into v2/main Sep 14, 2026
5 checks passed
@cliffhall
cliffhall deleted the v2/fix/2028-stdio-delayed-responses branch September 14, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inspector times out when FastMCP tool emits ctx.info/ctx.report_progress (v2.2.0, LEGACY/STDIO)

2 participants