chore(frontend): upgrade MCP SDK to 1.29.0 + adopt onerror, streaming, OAuth provider#2390
chore(frontend): upgrade MCP SDK to 1.29.0 + adopt onerror, streaming, OAuth provider#2390malinskibeniamin wants to merge 24 commits intomasterfrom
Conversation
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
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.
|
@claude review |
|
Claude finished @malinskibeniamin's task in 2m 35s —— View job PR Review
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
The component-level Bug:
|
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>
|
@claude review |
|
Claude finished @malinskibeniamin's task in 2m 11s —— View job PR 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
Note: Bug:
|
…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>
|
@claude review |
|
Claude finished @malinskibeniamin's task in 3m 20s —— View job PR Review (Round 3)
Both round 2 issues are fixed. Details below, plus two new minor issues. Previous issues — both resolved
Bug: User cancellation of a hanging stream fires an unexpected toastWhen the user calls
The existing test at
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();Minor:
|
- 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>
|
@claude review |
|
Claude finished @malinskibeniamin's task in 3m 18s —— View job PR Review (Round 4)
All three Round 3 issues are fixed. Details below, plus two new minor notes. Previous issues — all resolved
Minor:
|
- 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>
|
Fix the screenshot in the PR description, it's unstyled |
|
@claude review |
|
Claude finished @malinskibeniamin's task in 2m 18s —— View job PR Review (Round 5)
Both Round 4 minor issues are resolved. No new issues found. Previous issues — both resolved
Final stateAll six rounds of feedback have been addressed:
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, This PR is ready to merge. |
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
OAuthClientProviderwrapping the Console JWT. The first consumer is the MCP server inspector tab, which now surfaces tool-call progress live.Why
ConsoleJWTOAuthProviderlets 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.User-visible changes
Hardening
client.getServerCapabilities().tasks?.requests?.tools?.call. Absent → we callclient.callTooldirectly instead ofexperimental.tasks.callToolStream, so older servers cannot produce a silently-hung mutation.streamTimeoutMs). On timeout, the signal is aborted and the mutation rejects withMCP tool stream timed out after <N>ms.resultorerrormessage, the mutation rejects explicitly with a descriptive error rather than resolving undefined or hanging.ConsoleJWTOAuthProvider.tokens()base64url-decodes the payload and readsexp. Already-expired tokens returnundefinedso the SDK surfacesUnauthorizedErrorearly; malformed or exp-less tokens fail open (advisory, not crypto).transport.onerroris wired beforeclient.connect(), logs to the browser console with the server URL, and deliberately does not toast — the mutation'sonErrorowns toast to avoid double-firing.[0, 100]and handles NaN /total=0by going indeterminate, so a misbehaving server cannot push a broken-looking bar.Risks & mitigations
expdecode intokens(), fail-open on any parse failuretransport.onerrorcould double-fire with mutationonErrorAuthorizationheader +authProvidercould disagreeconfig.jwtper-request from the same source, so they agree by constructionTest plan
bun run type:checkbun run lint:check(no new violations over the 984-error baseline)bun run test(unit + integration)bun run test:browser(streaming inspector visual regression)Follow-ups