Skip to content

chore(frontend): upgrade MCP SDK to 1.29.0 + adopt onerror, streaming, OAuth provider#2390

Open
malinskibeniamin wants to merge 24 commits intomasterfrom
chore/mcp-sdk-upgrade
Open

chore(frontend): upgrade MCP SDK to 1.29.0 + adopt onerror, streaming, OAuth provider#2390
malinskibeniamin wants to merge 24 commits intomasterfrom
chore/mcp-sdk-upgrade

Conversation

@malinskibeniamin
Copy link
Copy Markdown
Contributor

@malinskibeniamin malinskibeniamin commented Apr 19, 2026

What

We upgraded the MCP TypeScript SDK (1.26 → 1.29) and adopted three new capabilities across the Console MCP servers feature and the AI agents path: transport-level error observability, task-based streaming tool calls, and a first-class OAuthClientProvider wrapping the Console JWT. The first consumer is the MCP server inspector tab, which now surfaces tool-call progress live.

Why

  • Richer tool call UX. Long-running tool calls now stream incremental progress with a status line rather than appearing as a single opaque spinner — the user sees the tool working.
  • Centralized auth. ConsoleJWTOAuthProvider lets the SDK's internal request machinery (retries, auth recovery) drive off the same JWT source as Console's transport, instead of having to be taught each callsite separately.
  • Observable transport errors. SSE drops and HTTP failures that happen outside the request/response cycle are now logged with the server URL instead of silently disappearing.

User-visible changes

  • The MCP server inspector tab shows a Progress bar and a live status line while a tool call is executing.
  • Cancelling a running tool call aborts the underlying request (no orphan fetches against the MCP gateway).
  • Tool calls against older MCP servers that do not yet advertise task-streaming continue to work unchanged — the client transparently falls back to the classic non-streaming path.

MCP streaming inspector

Hardening

  • Capability fallback. Post-connect, we inspect client.getServerCapabilities().tasks?.requests?.tools?.call. Absent → we call client.callTool directly instead of experimental.tasks.callToolStream, so older servers cannot produce a silently-hung mutation.
  • Streaming timeout. The stream is wrapped in a 120s composed-signal timeout (configurable per-call via streamTimeoutMs). On timeout, the signal is aborted and the mutation rejects with MCP tool stream timed out after <N>ms.
  • Watchdog. If the stream closes without a terminal result or error message, the mutation rejects explicitly with a descriptive error rather than resolving undefined or hanging.
  • JWT expiry detection. ConsoleJWTOAuthProvider.tokens() base64url-decodes the payload and reads exp. Already-expired tokens return undefined so the SDK surfaces UnauthorizedError early; malformed or exp-less tokens fail open (advisory, not crypto).
  • Transport error logging. transport.onerror is wired before client.connect(), logs to the browser console with the server URL, and deliberately does not toast — the mutation's onError owns toast to avoid double-firing.
  • Progress clamping. The inspector clamps the computed percent to [0, 100] and handles NaN / total=0 by going indeterminate, so a misbehaving server cannot push a broken-looking bar.
  • Tool-switch reset. Switching the selected tool mid-stream now clears in-flight progress state so stale percentages do not bleed into the new selection.

Risks & mitigations

Original risk Shipped mitigation
Streaming against an older server could hang the mutation Capability check + non-streaming fallback + 120s timeout + watchdog
OAuth provider has no expiry awareness Best-effort exp decode in tokens(), fail-open on any parse failure
transport.onerror could double-fire with mutation onError Transport callback logs only; contract pinned by a regression test
Manual Authorization header + authProvider could disagree Both read live config.jwt per-request from the same source, so they agree by construction

Test plan

  • bun run type:check
  • bun run lint:check (no new violations over the 984-error baseline)
  • bun run test (unit + integration)
  • bun run test:browser (streaming inspector visual regression)
  • Manual smoke: create an MCP server, inspect a tool, run it, observe the Progress bar + status line move
  • Manual smoke: rotate the console JWT mid-session, confirm the next MCP call picks up the new token

Follow-ups

  • Elicitation UI — SDK 1.29 delivers elicitation through server-initiated requests handled via registered handlers, requiring a new dialog component.
  • OAuth refresh flow with backend coordination, once the expiry detection lands and we decide on refresh semantics.
  • Streaming adoption in the knowledge-base playground (currently on the non-streaming path).

malinskibeniamin and others added 6 commits April 18, 2026 18:39
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lures

Attach an onerror handler to StreamableHTTPClientTransport before
connect() so transport-level failures (SSE stream drops, HTTP errors
outside the request/response cycle) are surfaced via console.error
instead of silently disappearing.

Logging only — not toasting — to avoid double-firing with the
existing mutation onError path that already formats a toast via
formatToastErrorMessageGRPC.
Adds useStreamMCPServerToolMutation, which consumes the experimental
client.experimental.tasks.callToolStream async generator introduced
in SDK 1.29.0. Exposes an onProgress callback that surfaces both
RequestOptions.onprogress progress tokens and taskCreated/taskStatus
messages, and returns the final CallToolResult for drop-in use.

Wired into the MCP server inspector so users see a live progress bar
and status message while a long-running tool executes. Non-streaming
callers (knowledge base playground) are untouched; useCallMCPServerToolMutation
is retained for them.

Elicitation is intentionally not surfaced: SDK 1.29.0 routes elicitation
through server-initiated requests handled via registered handlers, not
the ResponseMessage stream, so it would require a separate dialog
component. Tracked as follow-up.
Introduces ConsoleJWTOAuthProvider, a minimal OAuthClientProvider
implementation that wraps the externally managed JWT held on the
shared config object. tokens() always reads the latest JWT so the
SDK's auth layer can inspect it without a refresh cycle; PKCE and
redirect-based flows throw because the console never drives
interactive OAuth.

Passed to StreamableHTTPClientTransport as authProvider. The manual
Authorization header on the custom fetch is kept as belt-and-suspenders
because the SDK only uses authProvider for 401 challenge recovery and
token lookups, not for injecting the token into a caller-supplied
fetch implementation.
Pure formatting: import sort order, async generator shorthand,
constructor param layout, multi-line type import wrapping.
- suppress noConsole on the transport.onerror logger
- replace redirectUrl getter with property to satisfy useGetterReturn
- hoist regex literals in OAuth provider tests
- drop parameter properties and async-without-yield shapes in mocks
- use Boolean() guard on streamProgress render
- add scoped biome-ignore for the existing complex IIFE that now hosts
  the progress indicator branch
@malinskibeniamin malinskibeniamin self-assigned this Apr 19, 2026
@malinskibeniamin malinskibeniamin requested review from a team, SpicyPete, datamali, jvorcak and yougotashovel and removed request for a team April 19, 2026 19:08
Covers default clientName fallback, empty redirect_uris, and 'none' token
endpoint auth method so the jwt-bearer public-client contract is pinned.
createMCPClientWithSession:
- transport.onerror assigned before client.connect
- onerror handler logs without throwing
- ConsoleJWTOAuthProvider passed to transport constructor
- custom fetch still injects Mcp-Session-Id + Authorization headers
- clientName forwarded, SDK client version pinned at 1.0.0
- returns { client, transport } shape

useStreamMCPServerToolMutation:
- forwards numeric progress from the SDK onprogress callback
- passes AbortSignal through to the SDK
- resolves with the terminal result payload
- rejects on error events
- routes non-abort errors through formatToastErrorMessageGRPC
  with action=call, entity=MCP tool
- suppresses toast pipeline on AbortError
Pins the inspector tab's streaming behavior:
- Registry Progress bar and status line render while a tool call is
  pending and a progress event has arrived
- Progress UI hides once the stream resolves
- Non-abort errors surface via the sonner toast pipeline
- Clicking Cancel aborts the mutation's AbortSignal and does not toast
- Capability fallback: if server does not advertise tasks.requests.tools.call,
  fall back to client.callTool non-streaming instead of starting a stream that
  will never terminate against older servers.
- Stream timeout: compose caller signal with a 120s timeout controller so a
  hung stream rejects with a descriptive error instead of blocking forever.
- Watchdog: reject explicitly if the stream closes without a terminal result
  or error message, rather than letting the mutation hang.
Decode the base64url payload of the current JWT and check `exp`. When the
token is already expired, return undefined so the SDK raises
UnauthorizedError early instead of the server rejecting the call.

Fail-open on any parse failure, missing exp, or non-numeric exp — decode is
advisory, never cryptographic, and we never lock out a user on a well-formed
token we could not read.
…toast

Prevents regressions where a future change might route transport-level
failures through formatToastErrorMessageGRPC and double-fire with the
mutation onError path.
- Clamp MCP inspector Progress value to [0, 100] and guard against NaN/total=0.
- Reset progress UI when the user switches tools mid-stream so stale
  percentages from a cancelled stream do not bleed into the new selection.
- New coverage: two parallel streaming calls are isolated (separate clients,
  separate AbortControllers), cancelling one never cancels the other, and the
  factory never returns a shared singleton client.
- New coverage: connect() failure propagates cleanly with no half-initialized
  client.
- New coverage: integration-style happy path (listTools then streaming call
  with progress + terminal result) pins the full consumer contract.
- New coverage: ConsoleJWTOAuthProvider instance state is per-instance.

Also extracts buildStreamAbortControl and drainMCPStream helpers in
remote-mcp.tsx to keep the streaming mutationFn below the cognitive
complexity budget.
Co-locates a browser-mode visual regression covering the mid-stream state
of the MCP inspector tab: Progress bar and status line ("Processing batch
6/10...") rendered after a mocked onProgress callback.

Baseline PNG is checked in under frontend/docs/screenshots/ so it can be
linked from the PR description.
That PNG was emitted by a failed first-run of the browser test before the
reference existed; the stable baseline is mcp-streaming-inspector-chromium-darwin.png.
@malinskibeniamin
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Apr 19, 2026

Claude finished @malinskibeniamin's task in 2m 35s —— View job


PR Review

  • Read CLAUDE.md and understand repo conventions
  • Review changed source files
  • Review test files
  • Formulate and post review feedback

This is a solid, well-scoped upgrade. The architecture choices — capability fallback, composed abort signals, fail-open JWT expiry, transport error segregation — are all correct. Test coverage is thorough and the concurrency suite is particularly good. A few issues worth addressing before merge:


Bug: Double toast on non-abort tool errors

useStreamMCPServerToolMutation has a mutation-level onError that calls formatToastErrorMessageGRPC. The component's executeToolRequest also passes a per-call onError that calls toast.error(error.message). TanStack Query fires both callbacks, so a "server blew up" error shows two toasts.

remote-mcp.tsx:475-487 — mutation-level handler calls formatToastErrorMessageGRPC for every non-abort error.

remote-mcp-inspector-tab.tsx:378-382 — per-call handler calls toast.error(error.message) unless the message is the exact string 'Request was cancelled'.

The component-level onError should either be removed (relying entirely on the mutation-level handler) or the mutation-level handler should be removed and error formatting owned by the call site. The mixed strategy causes doubles.

Fix this →


Bug: throw message.error when error is undefined

In drainMCPStream (remote-mcp.tsx:422), message.error is typed as Error | undefined. When the stream yields { type: 'error' } with no error payload, throw undefined produces an unhelpful undefined rejection.

// Current:
throw message.error;

// Safer:
throw message.error ?? new Error('MCP stream yielded an error event with no payload');

Fix this →


Connection leak: MCP client is never closed

createMCPClientWithSession establishes an SSE/HTTP transport session and returns { client, transport }, but neither the mutation nor the component ever calls client.close() or transport.close(). Every tool invocation holds an open connection until the server times it out. The AbortController cancels the stream but not the underlying transport session.

remote-mcp.tsx:437createMCPClientWithSession called but result's transport is never torn down in useStreamMCPServerToolMutation's finally block.

The simplest fix is a finally { await client.close?.() } around the streaming block, or closing the transport after both the streaming and fallback paths complete.

Fix this →


Minor: useCallMCPServerToolMutation is now dead code

The component switched to useStreamMCPServerToolMutation but useCallMCPServerToolMutation is still exported from remote-mcp.tsx:319. If no other consumer uses it, remove it to keep the surface clean. If it's kept as a public API, it should be noted.


Minor: streamTimeoutMs is silently ignored on the capability-fallback path

When the server doesn't advertise tasks.requests.tools.call, the code falls back to client.callTool and passes the raw signal — but the composed timeout signal is never built for this path. The caller's streamTimeoutMs parameter has no effect on the fallback. Worth either documenting clearly or wiring the same timeout composition.


Notes / praise

  • buildStreamAbortControl signal composition is correct — the { once: true } listener avoids double-removal and the signal?.aborted pre-check is a nice edge case handle.
  • ConsoleJWTOAuthProvider is clean. The fail-open behaviour on malformed tokens is correctly documented and tested.
  • Dual auth (authProvider + manual Authorization header in custom fetch) is acknowledged in the PR and both read live config.jwt, so they agree by construction. Fine as a belt-and-suspenders; worth collapsing in a follow-up once the SDK's auth layer is confirmed as the canonical path.
  • Test suite is comprehensive: capability fallback, timeout, watchdog, pre-aborted signal, concurrency isolation, onerror-vs-toast contract regression — all covered.

malinskibeniamin and others added 5 commits April 19, 2026 21:57
Fixes the lint failure on PR #2390 CI. Biome's useTopLevelRegex rule flags
inline /run tool/i usage inside a click handler; hoisting to a module-level
const also satisfies the formatter's post-lint pass.
…utation onError

The streaming MCP tool mutation's onError was formatting an error string but
not toasting it; the inspector component was toasting error.message. The
mutation-level onError is the contract (mirrors transport.onerror). Move the
toast.error call into the mutation and remove the component-level onError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rror event

drainMCPStream threw message.error directly, but that field is Error | undefined.
Servers that emit { type: 'error' } without a payload would surface an undefined
rejection, which masks the failure. Fall back to a descriptive Error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion leak

createMCPClientWithSession opens a StreamableHTTP session that the caller
must release. Both useStreamMCPServerToolMutation and the (still-used)
useCallMCPServerToolMutation now wrap their SDK calls in try/finally and
invoke client.close() on every exit path — success, stream error, caller
abort, and the capability-fallback non-streaming path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…path

When the server lacked the tasks.requests.tools.call capability, the
non-streaming fallback received the raw caller signal, so streamTimeoutMs
was silently ignored and a hung callTool would block indefinitely. Build
the composed (caller ∪ timeout) signal before the capability branch and
pass it to both the streaming and fallback calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses lint/performance/useTopLevelRegex for the empty-error-payload
regression test and avoids an Error cast by narrowing via instanceof.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malinskibeniamin
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Apr 19, 2026

Claude finished @malinskibeniamin's task in 2m 11s —— View job


PR Review

  • Read CLAUDE.md and understand repo conventions
  • Review changed source files (current state after recent fixes)
  • Check if prior review issues are resolved
  • Identify any remaining or new issues
  • Post updated review

All four issues raised in the first review pass are now fixed. Details below, plus one new issue exposed by reading the current code.


Previous issues — all resolved

Issue Fix commit
Double toast on non-abort errors f3fcfdf — component-level toast.error removed; mutation onError is the single toast owner
throw message.error when error is undefined d8847fa — now throw message.error ?? new Error('MCP stream yielded an error event with no payload')
MCP client never closed in useStreamMCPServerToolMutation 45452ed — outer finally { await client.close?.() } added
streamTimeoutMs ignored on capability-fallback path 8984e2a — composed signal now applied to callTool fallback

Note: useCallMCPServerToolMutation (previously flagged as dead code) is a live consumer — knowledge-base-playground-tab.tsx uses it for the non-streaming knowledge-base path. ✅


Bug: listMCPServerTools leaks the MCP client connection

Every tool-list fetch opens a transport session and leaves it open. The MCP client is created but never closed:

remote-mcp.tsx:308-312

export const listMCPServerTools = async (serverUrl: string) => {
  const { client } = await createMCPClientWithSession(serverUrl, 'redpanda-console');
  return client.listTools(); // transport held open until server timeout
};

The same pattern fixed in useStreamMCPServerToolMutation and useCallMCPServerToolMutation applies here. Fix:

export const listMCPServerTools = async (serverUrl: string) => {
  const { client } = await createMCPClientWithSession(serverUrl, 'redpanda-console');
  try {
    return await client.listTools();
  } finally {
    await client.close?.();
  }
};

Fix this →


Minor: Empty Mcp-Session-Id header on initial request

In createMCPClientWithSession, the custom fetch wrapper always sends 'Mcp-Session-Id': client?.transport?.sessionId ?? ''. On the initial connect, the session hasn't been established yet, so this sends Mcp-Session-Id: '' (an empty string rather than omitting the header). Some servers may treat an empty session ID differently from an absent one.

remote-mcp.tsx:287-293

headers: {
  ...init?.headers,
  'Content-Type': 'application/json',
  ...(config.jwt && { Authorization: `Bearer ${config.jwt}` }),
  'Mcp-Session-Id': client?.transport?.sessionId ?? '',  // ← always present
},

More defensively:

...(client?.transport?.sessionId && { 'Mcp-Session-Id': client.transport.sessionId }),

This matches the conditional pattern already used for Authorization.


Notes / praise

The buildStreamAbortControl composition and drainMCPStream watchdog logic are solid and handle all the edge cases correctly (pre-aborted signal, timeout vs user-cancel disambiguation, empty-error stream). The ConsoleJWTOAuthProvider is clean with correct fail-open semantics. The test suite is thorough — capability fallback, concurrency isolation, onerror-vs-toast contract, and the new streaming inspector visual regression are all well covered.

…ders

- close client in listMCPServerTools via try/finally (connection leak)
- omit Mcp-Session-Id header when no session established yet

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malinskibeniamin
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Apr 19, 2026

Claude finished @malinskibeniamin's task in 3m 20s —— View job


PR Review (Round 3)

  • Read CLAUDE.md and understand repo conventions
  • Read current state of changed files
  • Check if round 2 issues are resolved
  • Identify any remaining or new issues
  • Post updated review

Both round 2 issues are fixed. Details below, plus two new minor issues.


Previous issues — both resolved

Issue Fix
listMCPServerTools leaks MCP client connection try/finally { await client.close?.() } added (remote-mcp.tsx:308-316). Two new lifecycle tests added (remote-mcp.test.tsx:982-997).
Empty Mcp-Session-Id on initial request Changed to conditional spread ...(client?.transport?.sessionId && {...}) (remote-mcp.tsx:290). New test added (remote-mcp.test.tsx:999-1017).

Bug: User cancellation of a hanging stream fires an unexpected toast

When the user calls cancelToolRequest() while a stream is in progress, the following happens under the mock (and potentially the real SDK if it returns cleanly rather than throwing on abort):

  1. controller.abort()onUserAbort fires → timeoutController.abort()composedSignal.aborted = true
  2. The generator observes the signal and returns without yielding a terminal message
  3. drainMCPStream returns undefined
  4. The condition composedSignal.aborted && !signal?.aborted is true && false = false (user signal is also aborted)
  5. Falls through to throw new Error('MCP tool stream ended without a terminal result or error message')
  6. onError receives this watchdog error — its message doesn't contain 'aborted' and name !== 'AbortError', so a toast fires

The existing test at remote-mcp.test.tsx:939 verifies client close but doesn't assert on toastErrorMock, so the toast fires undetected. In production the real SDK likely throws an AbortError from callToolStream directly (bypassing this path), but the code has no defensive fallback.

remote-mcp.tsx:483-487 — add a user-abort guard before the watchdog throw:

if (composedSignal.aborted && !signal?.aborted) {
  throw new Error(`MCP tool stream timed out after ${streamTimeoutMs}ms`);
}
// NEW: distinguish user cancellation from a genuine watchdog
if (signal?.aborted) {
  throw Object.assign(new Error('Request was cancelled'), { name: 'AbortError' });
}
throw new Error('MCP tool stream ended without a terminal result or error message');

And add a corresponding assertion to the abort test:

expect(toastErrorMock).not.toHaveBeenCalled();

Fix this →


Minor: setStreamProgress replaces rather than merges state

Both progress channels call the same onProgress callback with different fields:

  • drainMCPStream emits { taskId, status, statusMessage } from task messages
  • The SDK onprogress callback emits { progress, total } for numeric progress

In the inspector tab, both paths call setStreamProgress(update) which replaces the full state. If a taskStatus message arrives after an onprogress update, the numeric progress/total fields are cleared, reverting the bar to indeterminate. The reverse also holds.

remote-mcp-inspector-tab.tsx:375:

// Current:
onProgress: (update) => setStreamProgress(update),

// Suggested:
onProgress: (update) => setStreamProgress((prev) => ({ ...prev, ...update })),

Minor: No client lifecycle tests for useCallMCPServerToolMutation

listMCPServerTools and useStreamMCPServerToolMutation both have tests confirming client.close() is called on success and failure (remote-mcp.test.tsx:903-997). useCallMCPServerToolMutation has the same try/finally pattern but no equivalent tests.


Notes / praise

  • Both round 2 fixes are correct and well-tested. The Mcp-Session-Id conditional spread matches the Authorization header pattern exactly.
  • The listMCPServerTools lifecycle tests (lines 982-997) cover both the success and rejection paths — good coverage parity with the streaming mutation tests.
  • buildStreamAbortControl's { once: true } listener and signal?.aborted pre-check remain solid.
  • ConsoleJWTOAuthProvider fail-open semantics (base64url decode with padding tolerance, isJwtExpired returning false on any parse error) are correct and well-documented.

- distinguish user cancellation from watchdog in stream completion path
  so toast is suppressed when caller aborts a hanging stream
- merge functional setState for onProgress so numeric progress and task
  status updates compose instead of replacing each other
- add lifecycle tests for useCallMCPServerToolMutation close-on-success
  and close-on-reject, asserting parity with streaming mutation
- pin no-toast-on-abort contract in caller-abort-mid-stream test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malinskibeniamin
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Apr 19, 2026

Claude finished @malinskibeniamin's task in 3m 18s —— View job


PR Review (Round 4)

  • Read CLAUDE.md and understand repo conventions
  • Read current state of changed files
  • Check if round 3 issues are resolved
  • Identify any remaining or new issues
  • Post updated review

All three Round 3 issues are fixed. Details below, plus two new minor notes.


Previous issues — all resolved

Issue Fix
User cancellation fires unexpected toast via watchdog path remote-mcp.tsx:488-490if (signal?.aborted) { throw Object.assign(new Error('Request was cancelled'), { name: 'AbortError' }); } added before the watchdog throw
setStreamProgress replaces instead of merges state remote-mcp-inspector-tab.tsx:375 — changed to setStreamProgress((prev) => ({ ...prev, ...update })). New test added (remote-mcp-inspector-tab.test.tsx:264-287) verifying statusMessage from task messages persists after numeric onprogress fires.
No lifecycle tests for useCallMCPServerToolMutation Two tests added (remote-mcp.test.tsx:1007-1040) covering success and rejection paths with close() call assertions.

Minor: nextListToolsRejection and nextCallToolRejection not reset in beforeEach

beforeEach resets nextConnectRejection = undefined (line 206) but omits the two sibling variables declared on lines 84-85. The mock self-clears them on first use, so in practice there's no observed flakiness — but if a test throws before the mock fires, the stale rejection bleeds into the next test. Worth adding for consistency:

// remote-mcp.test.tsx beforeEach
nextListToolsRejection = undefined;
nextCallToolRejection = undefined;

Minor: No abort-path test (no-toast assertion) for useCallMCPServerToolMutation

useStreamMCPServerToolMutation's abort test (remote-mcp.test.tsx:946-971) pins expect(toastErrorMock).not.toHaveBeenCalled(). useCallMCPServerToolMutation has the same AbortError guard in onError (lines 344-346) but its lifecycle tests don't cover the abort path. Parity test would be:

test('does not fire a toast when callTool rejects with AbortError', async () => {
  nextCallToolRejection = Object.assign(new Error('aborted'), { name: 'AbortError' });
  // ...mutateAsync...
  expect(toastErrorMock).not.toHaveBeenCalled();
});

Notes / praise

  • The abort-guard fix (signal?.aborted check before watchdog throw) is exactly right and well-tested. The mock in remote-mcp.test.tsx correctly simulates the hang-then-signal-abort path: the generator resolves when composedSignal.aborted, the for await exits with no messages, and the new guard throws an AbortError instead of the watchdog error.
  • State-merge fix is clean: spreading null is a JS no-op so { ...null, ...update } on the first progress event is safe.
  • useCallMCPServerToolMutation lifecycle tests match the parity pattern from the streaming mutation tests.
  • Overall the code is solid, all critical bugs from prior rounds are resolved, and the hardening strategy (capability fallback + composed abort + watchdog + fail-open JWT) is correct and well-tested. Ready to merge modulo the two minor test notes above.

- reset nextListToolsRejection and nextCallToolRejection in beforeEach
  so stale rejection flags cannot bleed across tests
- add no-toast-on-AbortError test for useCallMCPServerToolMutation to
  match the existing parity contract with the streaming mutation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malinskibeniamin
Copy link
Copy Markdown
Contributor Author

Fix the screenshot in the PR description, it's unstyled

@malinskibeniamin
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Apr 19, 2026

Claude finished @malinskibeniamin's task in 2m 18s —— View job


PR Review (Round 5)

  • Read CLAUDE.md and understand repo conventions
  • Read current state of changed files
  • Check if round 4 issues are resolved
  • Identify any remaining or new issues

Both Round 4 minor issues are resolved. No new issues found.

Previous issues — both resolved

Issue Fix
nextListToolsRejection / nextCallToolRejection not reset in beforeEach Added to beforeEach at remote-mcp.test.tsx:206-207. ✅
No abort-path (no-toast) test for useCallMCPServerToolMutation Test added at remote-mcp.test.tsx:1043-1061 — covers AbortError rejection, verifies close() called once and toastErrorMock not called. ✅

Final state

All six rounds of feedback have been addressed:

Round Issues raised Status
1 Double toast; throw undefined; connection leak in streaming mutation; dead code; streamTimeoutMs ignored on fallback All fixed
2 listMCPServerTools connection leak; empty Mcp-Session-Id on initial request All fixed
3 User-cancel watchdog fires unexpected toast; setStreamProgress replaces instead of merges; no lifecycle tests for useCallMCPServerToolMutation All fixed
4 nextList/CallToolRejection not reset in beforeEach; no abort no-toast test for useCallMCPServerToolMutation All fixed

The hardening strategy (capability fallback + composed abort signal + watchdog + fail-open JWT + transport error segregation) is correct and fully tested. The test suite is comprehensive across all paths: streaming success/failure, timeout, user abort, capability fallback, concurrency isolation, client lifecycle, onerror-vs-toast contract, and progress state merge.

This PR is ready to merge.

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