| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d552564966 |
Network: modern Mcp-* headers, new error codes, cancellation-as-abort (#1628) (#1722)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Network: modern Mcp-* headers, new error codes, cancellation-as-abort (#1628) Upgrade the Network tab for the modern Streamable HTTP transport (SEP-2243 / SEP-2575): - Recognise and highlight the mirrored headers (Mcp-Method, Mcp-Name, Mcp-Param-*, MCP-Protocol-Version), decode =?base64?…?= sentinel values inline, and cross-check each against the request body so a HeaderMismatch is visible before the server rejects it. - Show the new spec error codes distinctly: -32020 HeaderMismatch, -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion (with the server-advertised `supported` versions), and modern -32601 MethodNotFound on HTTP 404 (distinguished from a legacy bare 404 by its JSON-RPC body). - Reflect cancellation as a connection abort ("Cancelled") rather than a hard error, per SEP §7.5. - Confirm and document browser-path header mirroring: the SDK builds the Mcp-* headers client-side and the /api/fetch proxy forwards them verbatim (new integration test). New pure util `mcpNetworkHeaders.ts` holds the recognition/decode/classify logic; new `McpErrorBadge` element renders the distinct error chips. The optional raw-request controls (cacheMode bypass/refresh, single-page fetch) are split to #1721 — they are cross-cutting core + feature-tab work beyond the Network tab. Also refreshes the AGENTS.md board Status option-ID table (the Building / MCP Apps Extension columns were removed). Closes #1628 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network smoke-test showcase + fix redaction clobbering JSON-RPC error codes Adds a composable "modern-network" showcase server for exercising the #1628 Network-tab work end-to-end, and fixes a real bug the smoke test surfaced. Bug fix (core): the fetch-tracking body redaction masked ANY field named `code` (targeting the OAuth authorization code), including the numeric JSON-RPC `error.code`. That destroyed the field the Network tab classifies the modern spec errors on, so -32020/-32021/-32022/-32601 never rendered against real traffic. Redaction now only masks STRING values of a sensitive field (secrets are always strings; a numeric code is never one). Test-server showcase (test-servers): `modern-network-http.json` serves a `get_weather` tool with an `x-mcp-header: "City"` annotation plus four `trigger_*` tools. A new `transport.modern.injectSpecErrors` flag installs a middleware on the modern leg that answers the triggers with real HTTP status + JSON-RPC error bodies (400/-32020, 400/-32021, 400/-32022 w/ `supported`, 404/-32601), so the Inspector's error rendering can be driven live. Mirroring finding (verified live, comment corrected in createRemoteFetch.ts): the SDK builds the STANDARD mirrored headers (Mcp-Method / Mcp-Name / MCP-Protocol-Version) unconditionally — they reach the server through the web proxy — but gates `Mcp-Param-*` on a non-browser environment (`detectProbeEnvironment() !== "browser"`), so the web client never emits them. An x-mcp-header tool is therefore uncallable from the web client against a strict modern server (it answers -32020); it works from the Node CLI/TUI. The earlier "mirroring works through the proxy" note was corrected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network: move the spec-error badge off the crowded compact top row In the narrow monitoring sidebar (embedded layout) the McpErrorBadge shared the top row with the timestamp/method/category/duration/status chips, which truncated the method and category ("PO…", "TRANSP…") and clipped the error name. Render the badge separately from `metaBadges` and place it on the second row, to the right of the URL, so the top row stays readable and the error name shows in full. The full-width layout keeps it in the top-right cluster (where there's room). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network: keep the spec error name's casing in McpErrorBadge (no uppercase) The four spec/SDK error identifiers are PascalCase (`HeaderMismatch`, `MissingRequiredClientCapability`, `UnsupportedProtocolVersion`, `MethodNotFound`). Mantine's Badge uppercases label text by default, which flattened them to a hard-to-read run-together string ("UNSUPPORTEDPROTOCOLVERSION"). Set `tt="none"` so the badge shows the spec's own casing with word boundaries intact; short badges (POST, status) keep the default uppercase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network: in the full-width layout, move the spec-error badge to the second row The full-width entry kept the McpErrorBadge in the top-right cluster, where a long spec name (e.g. "MissingRequiredClientCapability") crowded the top row and truncated the method chips and URL. Move it to the start of the second row (the expand-toggle row) so the top row stays readable; the toggle stays at the end (`space-between` only when the badge is present). The compact sidebar already places it on its second row beside the URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Protocol: show Replay as the right-side icon in the wide layout too The compact (sidebar) ProtocolEntry already renders Replay as an icon next to the pin/expand icons; the wide layout used a left-aligned "Replay" text button instead. Make the wide layout match — Replay as the ReplayButton icon at the start of the right-side action cluster (Replay, Pin, Expand) — so the two layouts are consistent. Drops the now-unused SubtleButton constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Move the modern spec-error chip + alert from the Network tab to the Protocol tab The Network tab is focused on the HTTP transaction; the distinct protocol-error taxonomy (-32020/-32021/-32022/-32601) belongs with the protocol messages. So: - ProtocolEntry now renders the McpErrorBadge chip on the top line (right after the direction badge, both layouts) plus an explanatory alert in the expanded detail, classified by JSON-RPC error code via `classifyProtocolSpecError`. - NetworkEntry drops the chip + alert; it keeps the HTTP-level features (status badge, Mcp-* header highlight/sentinel-decode, header/body mismatch marker, cancellation-as-abort). - The SDK delivers -32020/21/22 (HTTP 400) as JSON-RPC error responses, so they reach the Protocol log directly. -32601 (HTTP 404) and other non-enveloped errors are *thrown* — the request stays pending. `enrichProtocolEntries` (App) correlates the Protocol log to the Network log by JSON-RPC id (parsed from the transport fetch's requestBody) and folds the thrown error onto its pending request, so it too appears in Protocol. This is a derived view; the underlying log is not mutated. Verified live against the modern-network showcase: both a -32020 error response and a thrown -32601 render distinctly in the Protocol tab. Follow-up: a "reveal in Network" link on the Protocol alert (cross-tab scroll-to + expand of the correlated HTTP entry). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Protocol: show the spec-error chip only in the wide layout, after the method Refine the placement of the spec-error chip added in the previous commit: - Wide layout: move it from after the direction badge to right after the method chip (reads as "tools/call -32020 HeaderMismatch"). - Compact sidebar row: drop the chip entirely — the two-line row stays uncluttered and relies on its ERROR status badge. The explanatory alert still appears in the expanded detail in both layouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Protocol: "reveal in Network" link — jump from a spec error to its HTTP entry Adds the cross-tab navigation that was the follow-up to relocating the spec-error rendering. The expanded Protocol alert now shows a "View the HTTP request in the Network tab" link when the entry has a correlated Network request; clicking it: - correlates the Protocol message to its transport fetch by JSON-RPC id (`revealableMessageIds` gates the link; `correlateFetchEntry` finds the target on click), - clears whatever filter would hide the target (network filterText + category, and the embedded monitorSearch), - switches to the Network view in whichever mode is active — the pinned column (`pinMonitor`) when the sidebar is open, or the full-screen header tab (`onActiveTabChange`) when it's closed, - scrolls the matching NetworkEntry into view and force-expands it. The reveal is a one-shot: a new `revealId` threads NetworkScreen → NetworkStreamPanel → NetworkEntry, where the targeted entry scrolls itself into view (rAF, after `useScrollMemory`'s restore) + expands, then clears the signal via `onRevealComplete`. Verified live in both the pinned-sidebar and closed/full-screen layouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Protocol: only show the "reveal in Network" link when it adds value Gate the link on a new `httpRelevant` flag: show it only when the error is either thrown by the SDK (its real HTTP response lives only in the Network log — the Protocol entry is a synthetic fold) OR tied to the HTTP request headers. A delivered, protocol-only error gets no link, since the raw HTTP entry adds nothing over the alert. - -32020 HeaderMismatch → link (the mirrored headers are the problem) - -32601 MethodNotFound → link (thrown; the 404 response is only in Network) - -32021 MissingRequiredClientCapability → no link (delivered, protocol-only) - -32022 UnsupportedProtocolVersion → no link (supported list is in the alert) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network: cite SEP-2575 for cancellation-as-abort, not the internal doc's §7.5 The cancellation note referenced "(SEP §7.5)", but §7.5 is a section of the internal v2_new_spec_impact.md doc, not a SEP. The behavior — closing the Streamable HTTP request stream is the cancellation signal; notifications/cancelled is stdio-only — is defined in SEP-2575 (stateless-mcp). Cite SEP-2575 in the user-facing alert and the two related comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network: fix reveal one-shot canceling its own scroll; review doc/comment nits Address @claude review of #1722: - Reveal one-shot: move onRevealComplete() inside the rAF (after scrollIntoView) so clearing revealId (which flips revealed->false and runs the effect cleanup) can no longer race/cancel the pending scroll. Add a flip-back test. - Comment the intentional double-mount of revealId (pinned monitor + full-screen ScreenStage both match; clear is idempotent). - Doc drift after the Network->Protocol relocation: McpErrorBadge + the mcpNetworkHeaders module header now say the spec-error chip lives in Protocol. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Network: clarify redaction array-of-scalars edge + revealableIds superset Address second @claude review of #1722 (both non-blocking polish notes): - fetchTracking redaction: note that masking assumes a sensitive value is a scalar or object; an array-of-scalars under a sensitive key would slip through (no OAuth/token payload has that shape). - revealableMessageIds: document it's a superset of the linked entries; the spec-error/httpRelevant narrowing is applied downstream at render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Protocol: gate modern -32601 framing to a genuine 404, not in-band 200 Address third @claude review of #1722. classifyProtocolSpecError recognised -32601 from the code alone, so an ordinary in-band 'method not found' returned on HTTP 200 was dressed up as the modern MethodNotFound taxonomy: the spec badge, a 404-asserting alert, and a 'reveal in Network' link that jumped to a plain 200 (claiming diagnostic value that wasn't there). Gate the modern -32601 treatment on the correlated fetch actually being a 404 (mirroring classifyMcpSpecError). -32020/-32021/-32022 are SEP-reserved and unaffected. The correlated HTTP status is threaded from InspectorView (new correlatedFetchStatusById) through ProtocolScreen/ProtocolListPanel to ProtocolEntry; when unknown, -32601 falls back to code-only recognition (the thrown-404 path always carries a 404). Badge, alert copy, and link now resolve together, so a plain in-band -32601 falls through to ordinary error rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Protocol: require a genuine 404 for modern -32601 (drop unknown-status fallback) Address fourth @claude review of #1722. The prior fix only rejected -32601 when the status was *known* and not 404; an unknown status fell back to code-only recognition. But the thrown-404 path always carries a 404 (folded in from a 404 fetch), so the unknown-status branch is only ever reached by an ordinary in-band -32601 with no correlated 404 — most notably a stdio connection (no HTTP at all) — which then wrongly got the modern MethodNotFound badge and 404-asserting copy. Drop the escape hatch: -32601 is the modern taxonomy only on httpStatus === 404. Nothing intended is lost (the genuine case always has its 404). Update the tests (code-only/undefined -32601 now expects null; add a stdio no-correlated-fetch ProtocolEntry case) and mark the MethodNotFound story with correlatedHttpStatus 404. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9b2c188d11 |
MRTR: drive input_required manually to keep pending-request UX (#1704) (#1719)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* MRTR: drive input_required manually to keep pending-request UX (#1704) Follow-up to #1627/#1705. Instead of letting the SDK auto-fulfil the modern (2026-07-28) MRTR retry loop, InspectorClient now drives it itself so the Inspector keeps its pending-request UX, owns the loop, and shows era-accurate semantics. Core (core/mcp/inspectorClient.ts): - Construct the SDK Client with `inputRequired: { autoFulfill: false }` (unconditional; a no-op on legacy, which never returns input_required). - New `requestWithInputRequired()` driver: issues the request with `allowInputRequired` + `withInputRequired(schema)`, and while the result is `input_required`, fulfils each embedded request through the SAME pending sampling/elicitation plumbing, then retries the original call with `inputResponses` + the echoed `requestState` on a fresh id, bounded by MRTR_MAX_ROUNDS. - Route tools/call, prompts/get, and resources/read through the driver. - Embedded roots/list is auto-answered from configured roots (no pending UX); decline/cancel is echoed to the server (not treated as an abort); a tool-call abort mid-round settles the pending request and surfaces ToolCallCancelledError. - Extract shared `enqueuePendingSample`/`enqueuePendingElicitation` helpers used by both the legacy inbound handlers and the driver; add a per-request `origin` ("server-request" | "input-required") to the pending wrappers. Web UI: - New `MrtrOriginNote` element shows an "input_required — your answer returns as a retry" note on a modern MRTR round; legacy panels are unchanged. - Thread `origin` App -> PendingClientRequestModal -> the sampling/elicitation panels (and the inline panels). Test servers: new MRTR presets (multi-round, roots, sampling, loop, edge-case param shaping). Integration tests cover the manual round-trip (asserting the pending UI pauses and the retry carries inputResponses + requestState on a new id), multi-round, roots auto-answer, sampling, decline-echoed, maxRounds bound, abort mid-round, and requestState-only/inputRequests-only param shaping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Review: clarify sampling-reject and between-rounds abort comments (#1704) Address review nits on PR #1719 (comment-only, no behavior change): - enqueuePendingSample docstring: sampling has no decline/cancel action; the panel either sends a result (resolve) or Rejects (reject → fails the call). The shared "declined/cancelled resolves" wording was accurate only for the elicitation case. - callToolWithRetries abort comment: note the between-rounds case — when the abort lands while an MRTR round awaits an embedded pending request there is no in-flight SDK request, so nothing is sent on the wire; the outcome (ToolCallCancelledError, no failed-call record) is identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * test-servers: add mrtr-showcase config exercising every MRTR preset (#1704) A single modern-era streamable-HTTP server (port 3111) bundling all six MRTR presets — mrtr_confirm, mrtr_two_step, mrtr_sample, mrtr_roots, mrtr_edge, mrtr_loop — for manual testing of the manual-driving pending-request UX across single-round, multi-round, sampling, roots, param-shaping, and maxRounds paths. Document it in the README alongside modern-mrtr-http.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
73d9b181fe |
History: MRTR conversation grouping + modern message vocabulary (#1705)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* History: MRTR conversation grouping + modern message vocabulary Teach the Protocol/History view the modern (2026-07-28) message vocabulary, correlating it off the frames the transport already captures — no change to how MRTR is driven (manual driving is deferred to #1704). - Correlation helpers in protocolUtils: extractResultType, extractRequestState, extractSubscriptionId, isModernFrame, and groupProtocolEntries — clustering the contiguous rounds of one logical operation (original call -> input_required -> retried call with a new id -> final result) that share a requestState token. - New MrtrConversation group renders that cluster as one expandable unit with a status derived from the final round, reusing ProtocolEntry for each round. - ProtocolEntry: resultType badge (input required / complete), a "modern" frame badge for server/discover, subscriptions/listen, and the acknowledged notification, and a copyable subscriptionId. - ProtocolListPanel folds each section through the grouping pass and shows an era badge in the header. Era is threaded from the negotiated protocolEra (App -> InspectorView -> ProtocolScreen -> panel) and never inferred from frames, respecting the transcript-classification caveat. - Shared EraBadge element (+ eraUtils); ConnectionInfoContent reuses it. - test-servers/configs/modern-mrtr-http.json serves an elicitation tool over the modern leg for eyeballing a real MRTR round-trip. - Pre-bundle @modelcontextprotocol/client in optimizeDeps.include so the Storybook vitest project doesn't re-optimize mid-run once protocolUtils pulls the client barrel into the browser graph. Closes #1627 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * test-servers: real MRTR tool + live-verified History grouping While driving the modern flow in the browser, the elicitation-based config proved unable to produce MRTR: the modern (2026-07-28) SDK server rejects `server.elicitInput` — server→client requests aren't allowed on that leg; a tool must instead return `inputRequired(...)`. So the shipped `modern-mrtr-http.json` (and its README claim) were wrong. - Add a genuine MRTR tool preset (`mrtr_confirm` / `createMrtrTool`): its handler returns `inputRequired({ inputRequests: { confirm: elicit(...) } })` on the first call and the final result once the client echoes the answer back on the retry. - Teach the composable test-server to support it: the tool wrapper forwards an `InputRequiredResult` verbatim (instead of coercing it to a CallToolResult), and `toHandlerExtra` surfaces `inputResponses`/`requestState` so a handler can branch between the original call and the retry. - Point `modern-mrtr-http.json` at the new preset; correct the README to describe the real mechanism. - Add an integration test that drives the full round-trip through InspectorClient (auto-fulfilling the embedded elicitation) and asserts the final result is only reachable after the input_required round + retry — regression cover that would have caught the original config bug. Verified live in the browser: the History view renders the round-trip as one grouped "MRTR conversation" (Round 1 input_required → Round 2 complete), keyed by the real requestState, under the Modern era badge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address review notes: dedupe status badge, unique MRTR token, import alias Non-blocking review feedback on #1705: - ProtocolEntry: suppress the redundant green "OK" status badge when a `resultType` badge already conveys the outcome (a modern success shows only `complete`/`input required`). Errors/pending have no `resultType`, so their status badge still shows. - createMrtrTool: mint a unique `requestState` per original call (closure counter) so two same-action invocations don't fold into one conversation during manual eyeballing. Real SDK tokens are already unique per operation. - ProtocolEntry: align the `MessageEntry` import to the `@inspector/core` alias used by its siblings (was a deep relative path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Review round 2: green `complete` badge, lighter embedded MRTR container Non-blocking observations from the second review: - ProtocolEntry: `resultType` "complete" now renders green (was gray) so a modern success keeps the at-a-glance green success affordance a legacy "OK" had, now that the redundant OK badge is suppressed. `input_required` stays yellow. - MrtrConversation: in the narrow monitoring (embedded) column, drop the outer container border and tighten padding — the per-round entries are already bordered cards, so this avoids a busy border-in-border nesting. The full-width view keeps the bordered surface that sets a conversation apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Review round 3: module-scope MRTR counter; revert embedded border change - test-servers: hoist the MRTR `requestState` counter to module scope. The modern (2026-07-28) leg is stateless — `createMcpHandler` rebuilds the server (and every tool closure) per request — so the previous per-closure counter reset to 0 on every request and every token ended in `:1`, defeating its purpose. A module-level counter survives the per-request factory recreation and increments monotonically across the process, so same-action invocations get distinct tokens and don't fold into one conversation. (Review finding.) - MrtrConversation: revert the embedded borderless/tighter-padding change — per maintainer feedback from manual testing, dropping the border didn't improve the narrow-column visual, so the group keeps its border in all layouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e06c0a45f7 |
Add modern-era (2026-07-28) test-server preset (#1700) (#1703)
* Add modern-era (2026-07-28) test-server preset (#1700) The era-aware connection model (#1626, PR #1697) added per-server `protocolEra` and the era-aware Connection Info UI, but no bundled test server negotiated the modern era — the modern happy path had only unit/Storybook fixtures, no live coverage. Serve the modern (2026-07-28) protocol era from the HTTP test server via the SDK's `createMcpHandler`. A `modern` flag on `ServerConfig` (and a `transport.modern` field in the JSON config) mounts the handler with a dual-era `legacy: "stateless"` fallback by default, or `legacy: "reject"` for modern-only strict serving. The web-standard `fetch` handler reuses the existing `toWebRequest`/`writeWebResponse` conversion, so no new `@modelcontextprotocol/node` dependency is needed. An SDK v2 client negotiating `protocolEra: "auto" | "modern"` now reaches the modern leg end-to-end: `getProtocolEra() === "modern"`, `getNegotiatedProtocolVersion() === "2026-07-28"`, and a populated `getDiscoverResult()`. Verified live in the Inspector — Connection Info shows Era Modern / Session Sessionless / the Discovery section. - Modern serving path + `listen`/`startModernHttp` helpers in test-server-http.ts; modern-leg teardown in stop(). - `modern` config field threaded through load-config/resolve-config with a streamable-http-only validation guard; modern-http.json demo config. - Integration test covering auto/modern negotiation, the dual-era legacy fallback, a modern tools/call, and the strict `legacy: "reject"` rejection. Closes #1700 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Review nit: gate transport.modern guard on truthiness A falsy `transport.modern` (e.g. `false`) is a no-op that `resolveConfig` normalizes to `undefined`, so it shouldn't trip the streamable-http-only guard. Gate on truthiness instead of `!== undefined` so only *enabling* modern on a non-HTTP transport is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e026051b47 |
Era-aware connection model & capability gating (#1626) (#1697)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Era-aware connection model & capability gating (#1626) Make protocol era (SEP §7.8) a first-class, per-server connection dimension, orthogonal to the transport type, and drive the SDK Client's versionNegotiation from it. Config - Add `ServerProtocolEra` ("legacy" | "auto" | "modern"), `DEFAULT_PROTOCOL_ERA` (legacy, per the SDK's don't-auto-probe guidance), `MODERN_PROTOCOL_VERSION`, and `eraToVersionNegotiation()` in core/mcp/types.ts. - Persist `protocolEra` on `StoredMCPServer` / `InspectorServerSettings`, round-tripping through serverList.ts (omitting the default on disk) and the `/api/servers` read + write validators. InspectorClient / state - Thread `versionNegotiation` through `InspectorClientOptions` (default legacy), replacing the hardcoded pin. After connect, read `getProtocolEra()` / `getDiscoverResult()` / `getNegotiatedProtocolVersion()`, expose them via getters, dispatch `protocolEraChange` / `discoverResultChange`, and clear on disconnect. `useInspectorClient` surfaces `protocolEra` / `discoverResult`. Web UI - ServerSettingsForm: a Protocol Era selector bound to the per-server setting. - ConnectionInfoContent: era-aware — Era badge, Session ("Sessionless" on modern), and a Discovery section (supported versions + extensions) from the `server/discover` result. CLI/TUI also map the mcp.json `protocolEra` into versionNegotiation so the field applies uniformly. Version negotiation stays fully client-side; modern connect works over the existing remote tunnel and disconnect is sessionless (the SDK skips DELETE when there is no session id). Mcp-* header fidelity is left to the History/Network card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address @claude review: era docstrings, era validation, stdio session label, AGENTS.md rule - Fix stale docstrings/comments in inspectorClient.ts, useInspectorClient.ts, inspectorClientEventTarget.ts, and ConnectionInfoContent.tsx: getProtocolEra() returns "legacy" (not undefined) on a plain legacy connect; it is undefined only when not connected. - Validate protocolEra at the storedFieldsToInspectorSettings read boundary (new shared isProtocolEra guard) so a hand-edited mcp.json read directly by the CLI/TUI drops an unknown era instead of passing it to eraToVersionNegotiation. Keeps the exhaustive switch intact. - ConnectionInfoContent: the Session row is HTTP-only — show "N/A (stdio)" for stdio instead of the misleading "Session-based". - AGENTS.md: add a rule against unjustified `as unknown as` double casts, and drop the one such cast introduced in the serverList test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Address re-review: dedupe isProtocolEra guard in remote server Replace the local isProtocolEra in remote/node/server.ts with the shared guard now exported from serverList.ts (already imported here), so the era literal set lives in one place and can't drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7fecebed5e |
OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1694)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1625) Re-key the persisted OAuth store to (server, issuer) and add the SEP-837/2350/2352 authorization-hardening UX. The SDK v2 auth() drives the SEP-2352 flow off the provider's ctx.issuer; the Inspector's job is to honor it and round-trip the issuer stamp. SEP-2352 — issuer-bound credentials - ServerOAuthState nests issuer-bound credentials under byIssuer[issuer] = { clientInformation, clientRegistrationKind, tokens }, with an activeIssuer pointer answering the transport's ctx-less per-request bearer read. - Fix a latent bug: OAuthStorageBase re-parsed tokens/client-info through OAuthTokensSchema/OAuthClientInformationSchema, which strip the non-RFC `issuer` field and silently defeat the SDK's discardIfIssuerMismatch. The store re-attaches the stamp from the byIssuer key. - BaseOAuthClientProvider threads ctx.issuer and implements discoveryState()/saveDiscoveryState() (activates the SDK's callback-leg AuthorizationServerMismatchError binding check) and invalidateCredentials(). - Lazy, migration-free upgrade: a legacy pre-1625 blob's top-level clientInformation/tokens are returned as an unkeyed fallback until the first issuer-stamped save promotes them into byIssuer. SEP-837 — application_type - DCR declares application_type: "native" (localhost Inspector). - ClientSettingsForm renders RegistrationRejectedError detail and reflects DCR's deprecated-in-favor-of-CIMD (SEP-991) status. SEP-2350 — step-up scope union - Per-server onInsufficientScope (reauthorize | throw) setting wired ServerSettingsForm -> StoredMCPServer.oauth -> StreamableHTTPClientTransport. - StepUpAuthModal visualizes the prior-union-challenged scope set, tagging each scope `new` vs `already granted`. mcpAuth simplification - Delete the hand-rolled authorizeWithoutRefresh (mishandled issuer stamping); forward forceReauthorization/skipIssuerMetadataValidation to native SDK auth() so both the initial and step-up paths get SEP-2352 stamping. Vocabulary + docs - OAuthStep gains cimd_fetch / issuer_comparison / scope_step_up (recorded on a step-up redirect); the Network tab labels auth requests by OAuth phase. - v2_auth_hardening.md gains an as-built status section. Deferred to the era-aware card: the SdkError(EraNegotiationFailed) -> UnauthorizedError unwrap and finishAuth(URLSearchParams) form (only fire under non-legacy negotiation). Closes #1625 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address @claude review: issuer-stamp source gating + clear/activeIssuer + phase over-match - SEP-2352: stamp the issuer only when the credential actually came from the byIssuer slot (`fromSlot`), not merely when a slot exists. A partially-populated slot alongside a surviving legacy field would otherwise stamp a legacy (possibly cross-AS) credential with the current issuer, defeating discardIfIssuerMismatch. - Issuer-scoped clears no longer promote the cleared issuer to `activeIssuer` (updateIssuerSlot gains a `setActive` flag; save paths keep the promotion). - oauthNetworkPhase matches register/token/authorize as the final path segment (trailing slash tolerated) so `/token/refresh` or `/api/register/foo` no longer over-match. - Tests for all three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f4fb4b50c9 |
Forward RFC 9207 iss to the v2 SDK (fixes OAuth + EMA leg 1)
SDK v2 added RFC 9207 §2.4 authorization-response issuer validation
(`validateAuthorizationResponseIssuer` / `IssuerMismatchError`) as a mix-up
attack defense. It is strict: when AS metadata advertises
`authorization_response_iss_parameter_supported: true` and no `iss` reaches
`exchangeAuthorization`/`auth()`, the exchange throws.
v1.29.0 had no `iss` parameter and no RFC 9207 validation at all, so dropping
the callback `iss` was harmless. The migration swapped in the stricter SDK
without feeding it the new input, breaking the authorization-code exchange
against any RFC 9207 authorization server (Okta/xaa.dev advertise it):
Issuer mismatch in authorization response (RFC 9207):
expected "https://idp.xaa.dev", received undefined
Found by smoke-testing EMA against live xaa.dev staging; not caught by CI
because the mock IdP never advertised the parameter, so the SDK took its
lenient path (a missing `iss` is only fatal when advertised).
`iss` was dropped in three places, with the plumbing half-built —
`oauthManager`/`inspectorClient` already accepted `iss`, but nothing supplied
or forwarded it:
- `parseOAuthCallbackParams` never extracted `iss` from the callback query
- `mcpAuth` never forwarded `options.iss` to SDK `auth()` — its own comment
("forwarded on v2 upgrade; ignored by v1 SDK auth()") predicted this exact
gap, so standard OAuth was broken identically, not just EMA
- EMA leg 1 (`completeIdpOidcAuthorization`) never passed `iss` to
`exchangeAuthorization`
Thread `iss` end to end: callback query -> CallbackParams -> resumeAfterOAuth
-> completeOAuthFlow -> {mcpAuth | EMA leg 1} -> SDK. The Node callback server
and runner carry it for CLI/TUI.
Tests: `ema-mock-servers.ts` now advertises
`authorization_response_iss_parameter_supported: true` so the mock is at least
as strict as real IdPs — with it absent the SDK silently takes the lenient path
and a dropped `iss` goes unnoticed. Flipping it made three existing tests
reproduce the failure. Adds regression coverage for forwarding and for
rejecting a mismatched `iss` before any network call.
Verified end to end against live xaa.dev staging: EMA legs 1-3 (IdP SSO ->
ID-JAG mint -> resource token) now complete and the MCP session initializes.
web 3099, cli 212, tui 274 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjMemXZohSR2EndBQYif2b
|
||
|
|
8a7a38f800 |
Fix MCP App load regression: translate ext-apps v1 setNotificationHandler API to v2
An MCP App (e.g. NightRider's drum-pad) failed to load with "'[object Object]' is not a spec notification method; pass schemas as the second argument to setNotificationHandler()". `@modelcontextprotocol/ext-apps` still peers on SDK v1 and, during the AppBridge connect handshake, subscribes to tools/resources/prompts list_changed with the v1 schema-first API `setNotificationHandler(NotificationSchema, handler)`. SDK v2 requires a method STRING and throws on a schema object — the migration's compat shim covered the types but not this runtime API difference. Fix in the app-renderer client Proxy (`getAppRendererClient`): translate a schema-first `setNotificationHandler` call to v2's method-string form by reading the method literal off the schema's `shape.method.value`; native string-first calls pass through, and an unrecognized first arg falls through to the SDK's clear error rather than being swallowed. Only `setNotificationHandler` needed this — AppBridge's `request(req, schema, …)` calls work as-is (the v1 SDK schemas expose `~standard`, so v2 accepts them). Remove when ext-apps#702 ships a v2 peer. Added an integration test covering the schema-first translation end-to-end (handler fires on a real tools/list_changed), plus the string-passthrough and unrecognized-schema branches. Coverage ≥90 holds; typecheck + App tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
19a6df7ad3 |
SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624)
Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.
Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
@modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
rewrite (schemas -> core, everything else -> client); the codemod misroutes
client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
2026-era goes on the wire.
Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
implemented over raw client.request + explicit deprecated schemas;
experimental.tasks.callToolStream replaced by a local pollTaskToolCall
async generator (with _progressHandlers revival so task-execution progress
still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
validates a sampling/elicit handler result and rejects the legacy { task }
response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
entry in the private _requestHandlers map for the task-augmented branch so
{ task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
for single-page behavior (v2's client.listX() auto-aggregate all pages).
Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
(remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
vi.mock intercepts the SUT's import.
Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.
npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).
Closes #1624
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a029e42a19 |
cli: Wave 3 — programmatic review path (rollup of #1573–#1575) (#1655)
* feat(cli): programmatic ergonomics — initialize, tool-args-json, connect-timeout, env-var fixes Adds the #1573 programmatic-ergonomics surface to the CLI, re-implemented informed by PR #1510 and adapted to the v2/main OAuth-wired cli.ts: - `--method initialize` — connect-only probe emitting `{serverInfo, protocolVersion, capabilities, instructions}`. - `--tool-args-json '{"k":"v"}'` — tool args passed verbatim as JSON (no key=value coercion); mutually exclusive with `--tool-arg`, with clear errors for combined use, malformed JSON, and non-object values. - `--connect-timeout <ms>` — default 15000 for ad-hoc `--server-url`/target runs, file-level timeout otherwise; `0` disables. Backed by the exported `withConnectTimeout()` helper. - MCP_CATALOG_PATH is ignored when an ad-hoc target is given, so a shell that exports it can still run one-off ad-hoc invocations. - MCP_STORAGE_DIR is honored by core `getStateFilePath` (`<dir>/oauth.json`), below the per-file MCP_INSPECTOR_OAUTH_STATE_PATH override. Tests: new programmatic-ergonomics.test.ts (CLI, in-process runCli + direct withConnectTimeout unit tests) and getStateFilePath resolution cases in the web storage integration suite. Per-file coverage gate stays ≥90. README: documents the new flags and env-var semantics. Closes #1573 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * feat(cli): --app-info probe + --format json envelope Adds the #1574 app-probing and machine-readable output surface, re-implemented informed by PR #1510 and adapted to the v2/main OAuth-wired cli.ts. - `--method tools/call --tool-name <t> --app-info` — prints one JSON line describing the tool's app posture (hasApp, resourceUri, csp, permissions, domain, …) WITHOUT invoking the tool; exit 0 when it has an app, 2 (no_app) when it doesn't. A missing tool exits 5 (tool_not_found) — distinct from the no-app exit so a typo isn't mistaken for "no app". - `--method tools/list --app-info` — one NDJSON line per tool over a single connection (`| jq -c 'select(.hasApp)'`). - `--format json` — a single stdout envelope (`{result}` plus `{appInfo}` as a sibling key for App tools), no banner, so any method pipes into jq. - A tools/call returning isError:true now exits 5 (tool_is_error) after printing its payload, so && chains stop on a failed call. Implementation: `collectAppInfo()` (core `extractAppInfo` + a tolerated resource read that reports `resourceError`), an NDJSON loop in tools/list, and a single `emitResult()` that owns format + exit-code handling. `runMethod` now returns a `MethodOutcome` discriminated union so the NDJSON path can signal "already emitted". Tests: app-info.test.ts (exit-0/2/5 + NDJSON + no-invoke), format-json.test.ts (envelope shapes + bad --format), emit-result.test.ts (direct unit coverage of the isError→exit-5 and resourceError branches via exported emitResult/ collectAppInfo). Updated the tools.test "nonexistent tool" case to the new exit-5 tool_not_found contract. Per-file coverage ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines). README documents the flags, NDJSON shape, and exit semantics. Closes #1574 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * feat(cli): stored-auth suite — --use-stored-auth, --wait-for-auth, --list-stored-auth, --print-handoff Adds the #1575 web→CLI OAuth handoff surface, re-implemented informed by PR #1510 and adapted to v2/main. - `--use-stored-auth` — read the shared oauth.json and inject `Authorization: Bearer <token>` for --server-url; exit 3 (no_stored_token), listing stored keys, when none matches. - `--wait-for-auth <sec>` — poll oauth.json (500ms) until a token for --server-url appears, then proceed; timeout → exit 3 (auth_wait_timeout). - `--list-stored-auth` — print `{oauthStatePath, storedServerUrls}` and exit (short-circuit, no connect). - `--print-handoff` — emit `{serverUrl, deepLink, portForwardCmd, oauthStatePath, apiToken, note}` for --server-url and exit. Adaptations vs. the reference: - State-path resolution reuses core `getStateFilePath()` (the MCP_INSPECTOR_OAUTH_STATE_PATH → <MCP_STORAGE_DIR>/oauth.json → default precedence added in #1573) instead of a CLI-local resolver. - The state file is read through the shared isomorphic `parseOAuthPersistBlob`, so both the plain `{servers,idpSessions}` layout the web backend writes and the legacy `{state,version}` envelope are accepted. - `parseArgs` now returns a ParseResult union so the short-circuit modes return without a server connection. Blind injection: the token is passed as-is; a stale one surfaces as HTTP 401 → exit 3. Documented in the README along with all four flags, the state-path precedence, and a follow-up note that --print-handoff's deepLink shape is interim pending #1576's canonical handoff format. Tests: stored-auth.test.ts drives injection, --header coexistence, missing --server-url, no-token exit-3, URL-normalisation matching, MCP_STORAGE_DIR resolution, listing, handoff (+note), wait-for-auth polling/timeout, and normalizeServerUrl units — all against a real in-process HTTP test server. Per-file coverage ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines). Closes #1575 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * docs(core): scope MCP_STORAGE_DIR comment in getStateFilePath to the OAuth backend Address review of PR #1651: the doc comment claimed MCP_STORAGE_DIR was honoured "across the app, so setting it once points every persist backend at the same directory", but the branch only relocates oauth.json. Narrow the comment to state it is OAuth-backend-only (client.json/mcp.json still resolve from getDefaultStorageDir → HOME) and record why the resolution lives here rather than in getDefaultStorageDir (the default-path constant is evaluated at module load, so a runtime-set MCP_STORAGE_DIR — as the CLI stored-auth / --wait-for-auth flows set it — must be read at call time). Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review(cli): per-tool-tolerant app-info listing + drop dead helper (PR #1653) Address review of PR #1653: - collectAppInfo now never throws: a malformed `_meta.ui.resourceUri` (extractAppInfo throws) folds into `{hasApp:false, resourceError}` just like a resources/read failure. This makes `tools/list --app-info` per-tool tolerant — one bad tool no longer aborts the whole NDJSON listing with a generic exit-1. Guarding inside collectAppInfo (vs. a loop-local try/catch) keeps the branch unit-testable; added an emit-result.test.ts case for the malformed-URI path. - Documented that `tools/list --app-info` always emits NDJSON regardless of `--format` (the per-tool list shape is fixed; --format json only reshapes the single-result paths) — in both the code comment and the README. - Removed the now-unused `expectJsonError` helper (its only caller moved to the exit-5 tool_not_found contract). Coverage stays ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review(cli): stored-auth follow-ups — TODO(#1576), wait-for-auth guard test, docs (PR #1655) Address review of PR #1655: - Added an explicit TODO(#1576) on buildHandoff's deepLink: `transport:"http"` is hardcoded even for SSE, and `autoConnect=<token>` is not one of the three token sources the web app reads (per CLAUDE.md) — both to be reconciled with #1576's canonical handoff format. - Added a test for `--wait-for-auth` without `--server-url`, covering the previously-untested `--wait-for-auth` arm of the shared missing-server-url guard's message ternary. - Documented that `--list-stored-auth` / `--print-handoff` are mutually exclusive short-circuit modes (list takes precedence) and that a stored refresh_token is not yet used (noted as a follow-up in code + README). No behavior change; coverage stays ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0ddb79203a |
web: Wave 2 — spec-conformant MCP Apps host (rollup of #1565–#1572) (#1660)
* web: sandbox hardening — opaque-origin inner frame, srcdoc-only, frame-ancestors Harden the MCP Apps sandbox isolation model: - The inner (untrusted-widget) iframe is sandboxed WITHOUT `allow-same-origin` (default "allow-scripts allow-forms") and any server-supplied sandbox value has `allow-same-origin` stripped case-insensitively, so the widget runs under an opaque origin and cannot reach the proxy's DOM to bypass its own CSP via the parent realm. - Widget delivery is `srcdoc`-only (no document.write), which respects the opaque origin. - The relay accepts the inner frame's "null" origin with `event.source === inner.contentWindow` as the unforgeable identity check. - sandbox-controller.ts serves the proxy with `Content-Security-Policy: frame-ancestors http://127.0.0.1:* http://localhost:*`; fetch directives are deliberately omitted so a srcdoc iframe's inherited policy container does not override the per-app CSP. Updated the isolation-model comments and the sandbox-controller integration test asserts the frame-ancestors header (and the absence of fetch directives). Closes #1565 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: enforce per-app _meta.ui.csp in the app bridge + surface resource-read failures - createAppBridgeFactory now builds each app's `_meta.ui.csp` host-side via the sandbox-csp library (approveCspSources → buildSandboxCspPolicy) and wraps the untrusted widget HTML with wrapSandboxedHtml so the CSP <meta> is always the first <head> child before the HTML reaches the sandboxed iframe. The approved (post-filter) csp + permissions are echoed back via hostCapabilities.sandbox so the view sees what was granted. The csp is no longer sent inline through sendSandboxResourceReady — it is enforced via the wrapped meta. - A per-app copy of HOST_CAPABILITIES prevents the sandbox echo from mutating the shared constant. - New optional `onResourceError` dep on AppBridgeFactoryDeps routes resource read/post failures to the caller (also always console.error'd) instead of leaving a silent blank frame. Closes #1566 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: full hostContext delivery + live host-context-changed updates - createAppBridgeFactory now seeds the full hostContext via snapshotHostContext(iframe, HOST_AVAILABLE_DISPLAY_MODES): theme, styles (Mantine design tokens), displayMode ("inline"), availableDisplayModes, and containerDimensions — replacing the theme-only seed. The local currentTheme helper is dropped in favor of hostContext.ts. - AppRenderer gains `displayMode` and `containerRef` props and pushes live host-context changes via the SDK's sendHostContextChange (partial host-context-changed notifications), never a hand-maintained snapshot: - theme + styles via a MutationObserver on <html data-mantine-color-scheme>, - containerDimensions via a ResizeObserver on the host container (or the iframe fallback), gated on `initialized`, skipping 0x0 and value-equal repeats, plus a one-shot push on initialize once layout settles, - displayMode whenever the prop changes (Maximize/Restore), gated on init. Adds AppRenderer host-context tests (theme flip incl. resolved styles, displayMode push + init gate, container-dimensions on init and resize, observer teardown) and updates the factory + story mocks for the new hostContext shape and sendHostContextChange method. Closes #1567 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: honor ui/notifications/size-changed and ui/request-display-mode AppRenderer: - New `onSizeChange` prop forwards the view's ui/notifications/size-changed reports (via the bridge `sizechange` event). - New `onRequestDisplayMode` prop backs an `onrequestdisplaymode` bridge handler that returns the mode the host actually applied; with no handler the request is declined by returning the current host-side mode (falling back to inline). AppsScreen: - Adds a host-controlled RendererContainer (measured for containerDimensions) with an inner box sized by the view-reported height (`appHeight`), so host→view container size and view→host size-changed don't couple. - `handleSizeChange` records the reported height (ignored while maximized); `displayMode` is derived from the maximize toggle and pushed to the view; `handleRequestDisplayMode` filters against HOST_AVAILABLE_DISPLAY_MODES (declining e.g. "pip") and maps fullscreen↔inline onto the maximize state. Adds AppRenderer tests (size forwarding, request-display-mode apply/decline/ fallback) and AppsScreen tests (frame sizing on report, no-height ignore, fullscreen/restore/decline via request-display-mode). Story mocks gain sendHostContextChange/addEventListener. Closes #1568 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: honor ui/download-file with confirmation and http(s) allowlist createAppBridgeFactory now advertises the `downloadFile` host capability and attaches an `ondownloadfile` handler: - Requires a host-mediated confirmation (window.confirm) listing the requested file(s); a declined prompt or an empty payload returns isError without acting. - Inline EmbeddedResources (text or base64 blob) are written to disk via the shared downloadBlob object-URL anchor; base64 is decoded via base64ToBytes. - ResourceLinks are opened in a new tab, restricted to http(s) by the shared isHttpUrl allowlist (javascript:/data:/file:/malformed are rejected). - Labels shown in the confirmation are sanitized (control + format chars stripped, length clamped) so a server-supplied filename/URI can't inject newlines or reflow the prompt. - Partial-batch success is surfaced (isError only when nothing downloaded), with skipped items warned. Adds factory tests for the full download surface (text/blob/link, decline, empty, throw, non-http rejection, label sanitization/clamping, partial batch). Closes #1569 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: surface ui/message and app logging in panels below the running app AppRenderer: - New `onMessage` prop backs an `onmessage` bridge handler that surfaces the view's ui/message content and returns the spec-required empty result; with no handler the submission is declined (isError). - New `onLog` prop forwards MCP `notifications/message` log entries via the bridge `loggingmessage` event, honoring the advertised `logging` capability. AppsScreen: - Adds `messages`/`appLogs` state (cleared with the reported height on select/open/close/back via resetAppChannels) and `handleMessage`/`handleLog`. - Renders a "Messages from app" panel (data-testid="apps-messages", reusing ContentViewer) and a default-expanded collapsible "App logs" panel (data-testid="apps-logs", reusing LogLevelBadge) with logger names and a Clear button, both as pinned panels below the running widget. Adds AppRenderer tests (log forward + no-handler no-throw, ui/message route/empty-result/decline) and AppsScreen tests (message log render + empty result + clear-on-close, log panel expand/collapse/clear). Closes #1570 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: stage partial input — replay tool-input-partial before final tool input AppRenderer: - New `partialInputs` prop (ordered fragments), snapshotted into pendingPartials at bridge-build time (read via a ref so prop churn never rebuilds the iframe) and cleared on dispose. flushPending replays them via bridge.sendToolInputPartial BEFORE the complete tool-input, per spec. AppsScreen: - New `partialStages` state + `handleStagePartialInput` (snapshots current form values). resetAppChannels gains a `keepPartials` option so Open App preserves the staged fragments (they're consumed by the renderer) while select/close/ back clear them. - Adds a "Stage partial input" control (fielded apps only) with a staged count and "Clear staged", wired to AppRenderer's `partialInputs`. Adds AppRenderer tests (ordered replay before tool-input; none when omitted) and AppsScreen tests (stage/count/clear gated on fields; staged partials survive Open and are replayed). Closes #1571 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * web: app lifecycle status + machine-readable data attributes on the Apps screen AppRenderer: - New AppRendererStatus ("loading" | "ready" | "error") + onAppStatusChange callback. Fires "loading" at the start of every (re)build, "ready" on the view's initialized signal, and "error" on a factory throw/rejection. AppsScreen: - appStatus/appError state; handleAppError captures the reason locally and forwards to the parent onError. resetAppChannels clears both. - Surfaces the contract as data-testid="apps-form" carrying data-app-status and data-app-error, renders an error panel (data-testid="apps-error") in place of the blank frame, and tags the stage button data-testid="apps-stage". - AppDetailPanel's Open App button gains data-testid="open-app". App.tsx: - Wires the bridge factory's onResourceError to a red toast so a malformed/404 UI resource is no longer console-only (the renderer separately drives data-app-status, so a driver times out on never-reaching-ready and reads it). Documents the data-testid/data-* automation contract in clients/web/README.md. Adds AppRenderer status-transition tests and AppsScreen status/error-panel tests. Closes #1572 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: warn when server sandbox drops allow-scripts + revert incidental lockfile churn Addresses the claude[bot] review on PR #1650: - sandbox_proxy.html: log a console.warn when a server-supplied `sandbox` value lacks `allow-scripts` after the `allow-same-origin` strip, so a fail-closed (app-never-runs) sandbox is diagnosable instead of silent. - Restore clients/{cli,launcher,tui,web}/package-lock.json to their v2/main state — the earlier churn (@emnapi/@napi-rs optional deps, a dropped version field) was incidental `npm install` regeneration unrelated to the sandbox change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: document CSP-vs-permissions filtering asymmetry (PR #1652) Address the claude[bot] review's finding #2: add a comment explaining why `csp` is injection-filtered (approveCspSources) while `permissions` is passed through verbatim — permissions is a structured boolean object consumed only by the sandbox proxy's buildAllowAttribute() (fixed key→token mapping), so it never reaches the iframe sandbox/allow attribute as raw text. No logic change. Also merges v2/1565-sandbox-hardening (review fixes) up the stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * test: lock in per-app HOST_CAPABILITIES isolation (PR #1652 round 2) Add a test asserting the shared HOST_CAPABILITIES.sandbox stays undefined after a sandboxready run sets hostCapabilities.sandbox — guards against a silent regression if the per-app spread copy were ever dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: gate theme observer on initialized + document seed/dep assumptions (PR #1654) Address the claude[bot] review: - Finding #1 (applied): gate the theme+styles MutationObserver on the view's `initialized` signal, matching the container/displayMode pushes — a theme flip in the construction→handshake window no longer races ui/initialize. The factory seeds the construction-time theme, and the first post-init flip carries the current value. Added tests: pre-init flip is dropped; the live flip test now completes the handshake first. - Finding #2 (applied, comment): note in snapshotHostContext that the `displayMode: "inline"` seed assumes inline-at-open (AppsScreen always opens inline; #1568's displayMode push carries transitions). - Finding #4 (applied, comment): explain why `containerRef` is a build-effect dep yet doesn't force a rebuild (sameInputs ignores it; read lazily). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: guard 0-height size reports, distinct RendererFrame, shared payload type (PR #1656) Address the claude[bot] review: - #1 (applied): handleSizeChange ignores a non-positive height — a view's ResizeObserver can transiently fire 0 pre-layout/teardown, which would collapse the frame; mirrors AppRenderer's 0×0 skip. Added a test. - #2 (applied): reinstate a distinct `RendererFrame` constant for the inner content-sized box so its role reads clearly vs. the outer RendererContainer. - #3 (declined + comment): keep carrying `appHeight` across maximize→restore — clearing it would flash the frame to full-card height until the next size-changed; the carry-over restores at the prior size. Documented. - #4 (applied): type handleSizeChange's param as AppBridgeEventMap["sizechange"] so the payload shape has a single source of truth with AppRenderer's prop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: distinguish links, cap batch size, comment truncation (PR #1657) Address the claude[bot] review: - #1 (applied): the confirmation now reads "download or open N item(s)" and resource_links are prefixed "↗" in the summary, so a link that opens in a tab is distinguishable from an embedded file that saves to disk. - #2 (applied): cap a single ui/download-file batch at MAX_DOWNLOAD_ITEMS (20); an oversized batch is rejected (isError) before the prompt, with a warn. - #3 (declined): can't check window.open's return value to detect a blocked popup — we pass `noopener`, and per spec window.open returns null even on a successful noopener open, so null can't distinguish blocked from succeeded. - #4b (applied, comment): note the start-of-label truncation is intentional so a link's scheme+host stays visible for the trust decision. Adds tests: oversized batch rejected without confirming/acting; resource-link ↗ prefix in the summary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: skip embedded resource with neither text nor blob (PR #1657 round 2) Address the round-2 observation: an untrusted EmbeddedResource carrying neither `blob` nor a string `text` previously produced `new Blob([undefined])` — a file containing the literal text "undefined", counted as success. Now such an item is skipped (returns false, like a rejected link), so isError reflects it. Added a test asserting no object URL is created and the batch reports isError. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: type-honest formatLogData, cap channels, log-panel a11y (PR #1658) Address the claude[bot] review: - #1 (applied): formatLogData coalesces JSON.stringify(undefined) → "" so the `: string` return type is honest for a data-less log. - #2 (applied): cap retained messages/logs at MAX_APP_CHANNEL_ENTRIES (500) via appendCapped — a chatty widget no longer grows the panels/DOM without bound between Clear/close; oldest entries drop. - #3 (declined): keep the Messages panel without a Clear/collapse — messages are sparser and already cleared on close/switch; keeping it simple. - #4 (applied): the App-logs toggle now has aria-controls pointing at the Collapse region (id="apps-logs-region") for assistive tech. Adds tests: no-data log renders (coalesce branch), 501 logs cap at 500 (oldest dropped), aria-controls assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: stable id key for Messages panel (PR #1658 round 2) Address the round-2 nit: give AppMessage a stable `id` (assigned via nextMessageIdRef in handleMessage) and key the Messages panel on it instead of the array index — so the appendCapped front-drop can't renumber keys once a run exceeds the cap, matching AppLogEntry's treatment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: note partialStages intentionally survives handleOpen (PR #1659) Address the claude[bot] review's observation #2: add a comment at the handleOpen call site clarifying that partialStages is intentionally NOT cleared there — AppRenderer snapshots the fragments into its own pendingPartialsRef at build time, and the staging UI only renders while not running, so the surviving state is invisible until the next reset drains it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw * review: align error-panel docs with behavior + gate data-app-error on running (PR #1660) Address the claude[bot] review: - #1 (applied, docs): the error panel renders BELOW the (blank) frame, not in place of it — the renderer stays mounted so a factory error still shows the reason alongside the iframe (gating the frame off on error would drop the auto-launched iframe an integration test relies on). Reworded the README row and the test comment to say "below the frame"; added a code comment. - #2 (applied): gate data-app-error on `running` (like data-app-status) so the two attributes can't desync if a future path sets an error without running. - #3 (no change): resource-read failures staying at data-app-status="loading" (toast only) is intentional and documented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e6718e2fd |
test(web): disable Mantine transition timers in the unit env
CI failed with an unhandled `ReferenceError: window is not defined` thrown from a Mantine `Transition` `setTimeout` (`use-transition.mjs`) firing after the test environment was torn down — every test passed, but the leaked timer failed the run. happy-dom has no `matchMedia`, so `useReducedMotion` resolves to "motion allowed" and every Transition (ScreenStage, Modal, …) schedules real enter/exit timers; a timer that outlives its test throws post-teardown. This surfaced now because MonitoringScreen started wrapping its screens in ScreenStage (a Transition) where it previously rendered them directly. Mock `window.matchMedia` in the test setup to report `prefers-reduced-motion: reduce`, so Mantine transitions render instantly (no timers). All other queries return `false`, matching the prior absent-matchMedia behavior, so `useMediaQuery`-driven layout is unchanged. Full unit suite passes with no unhandled errors across repeated runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw |
||
|
|
a26cbac609 |
review: harden version read + fix MonitoringScreen docstring
Address PR #1641 review feedback: - Add readInspectorVersionSafe() (non-throwing) in core/node/version.ts and use it in the web backend, so a failure to resolve the root manifest hides the (cosmetic) version badge instead of crashing the backend at startup. Covered by tests for both the resolved and unresolvable paths. - Update MonitoringScreen's docstrings: it now wraps every supplied screen in a ScreenStage (only the active tab mounts, cross-fading) rather than rendering just the active tab's node. The third fetch-consolidation suggestion (one useInitialConfig instead of three GET /api/config hooks) is tracked as a follow-up in #1643. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw |
||
|
|
650e5bd020 |
feat(web): show the Inspector version in the lower-right corner (#1639)
Display the single-source version (root package.json) in the web UI's lower-right corner, in grey — matching the CLI and TUI. The browser can't read the filesystem, so the version flows backend → browser via GET /api/config: - Add `version?: string` to InitialConfigPayload; the web backend (webServerConfigToInitialPayload) reads it once via the shared readInspectorVersion() and tags it onto the payload. - Add a `useInspectorVersion` hook mirroring useSandboxUrl/useServerListWritable. - Add a presentational `VersionBadge` element rendered fixed lower-right, grey, non-interactive; styling lives in a `versionBadge` Text theme variant (no inline styles, --inspector-* tokens). - Wire it into App.tsx. Verified live: /api/config serves `version: "2.0.0"` and the badge renders `v2.0.0` fixed at the lower-right in grey. Tests: useInspectorVersion (full branch coverage), VersionBadge (unit + Storybook), web-server-config payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw |
||
|
|
7815548300 |
refactor(version): single-source the version from the root package.json
The Inspector ships as one package with one version, but four client package.jsons each carried their own `version` and were read at runtime (cli.ts, tui App.tsx), and core hardcoded a third, stale value (0.20.0) that the TUI and web actually reported to servers. Consolidate onto the root package.json as the single source of truth. - Add core/node/version.ts: shared, Node-only readInspectorVersion() that walks up to the root package.json (always shipped in the tarball). 100% covered by a new integration test. - CLI and TUI read the version via the shared reader instead of their own manifest; TUI now also passes a real clientIdentity (was falling through to the stale core default). - Drop the `version` field from all four client package.jsons; the root keeps the only version. TUI header name/description become local display constants. - core default client identity: replace the stale 0.20.0 hardcode with a neutral 0.0.0 placeholder (real clients pass clientIdentity; web will source its version from GET /api/config, see #1639). - Stop shipping clients/cli/package.json and clients/tui/package.json (no longer read at runtime) — supersedes the earlier "ship them" fix; update pack:verify and the root README packaging invariants accordingly. - Add core/node/** to the web coverage include so the new module is gated. - Add the shared-code architecture diagram to the root README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw |
||
|
|
ace4250d31 |
refactor(web): rename "History" tab/feature to "Protocol" (#1623)
Rename the web client's History tab and its supporting code to Protocol so the feature reads as the MCP "Protocol" view. - Tab renders as "Protocol"; INSPECTOR_TAB_IDS, InspectorView tab constants/MonitorTab, TAB_UI_REGISTRY key, and navigation.ts tab id all updated to Protocol. - Rename components + folders: HistoryScreen -> ProtocolScreen, HistoryListPanel -> ProtocolListPanel, HistoryEntry -> ProtocolEntry, HistoryControls -> ProtocolControls, historyUtils -> protocolUtils (plus their tests and stories, and exported types). - Rename App/InspectorView plumbing: historyUi, pinnedHistoryIds, on*History* handlers/props, replayHistoryRequest, isReplayableHistoryMethod, sort/compact scopes, etc. -> Protocol names. - Export filenames use the "protocol" slug (buildExportFilename + ExportKind). Preserved (per issue #1623 decisions): - The in-panel section discriminator keeps its "pinned" | "history" values, and the sub-sections stay labeled Pinned / History. - window.history and core message-log/replay concepts are untouched. npm run ci passes (format, lint, build, unit tests, >=90 per-file coverage gate, smoke, Storybook). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw |
||
|
|
32a3610737 |
Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts: # clients/cli/package-lock.json # clients/cli/package.json # clients/web/src/App.tsx # clients/web/src/lib/environmentFactory.test.ts # clients/web/src/lib/environmentFactory.ts # clients/web/src/lib/remoteOAuthStorage.test.ts # clients/web/src/lib/remoteOAuthStorage.ts # clients/web/src/test/core/auth/connection-state.test.ts # clients/web/src/test/core/auth/providers.test.ts # clients/web/src/test/core/auth/storage-browser.test.ts # clients/web/src/test/core/auth/storage-remote.test.ts # clients/web/src/test/core/mcp/oauthManager.test.ts # clients/web/src/test/integration/auth/node/storage.test.ts # clients/web/src/test/integration/storage/adapters.test.ts # clients/web/src/utils/clearServerOAuthState.test.ts # clients/web/src/utils/clearServerOAuthState.ts # core/auth/connection-state.ts # core/auth/oauth-storage.ts # core/auth/providers.ts # core/auth/storage.ts # core/auth/store.ts # core/storage/adapters/remote-storage.ts |
||
|
|
98517c2758 |
test(auth): fix type error in remote backend write() test
`tsc -b` (run in CI's validate step, but not by vitest locally) rejected `ok.mock.calls[0][1].body` — under the project's strict indexing the untyped mock's call tuple is `[]`, so `[0]` is possibly-undefined and `[1]` is out of range. Capture the request body inside a `vi.fn<typeof fetch>` implementation instead of indexing into `mock.calls`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4c30dc6e2e |
test(auth): make the browser-safe guard transitive
Extend the Node-free guard beyond oauth-persist.ts to every module reachable from the browser's RemoteOAuthStorage (storage-remote, oauth-storage, store, oauth-persist, store-serialize), so a forbidden store-io/node:/atomically import added one hop away is caught too — not just a direct import in oauth-persist.ts. Addresses the non-blocking hardening note on Claude review of #1612. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e6a2825d4 |
fix(auth): keep oauth-persist browser-safe (move Node file backend out)
The refactor routed the browser's RemoteOAuthStorage through `oauth-persist.ts`, which value-imported the Node-only `store-io.js` (`atomically` -> `stubborn-fs` -> `node:process.getuid`, plus `node:fs/promises`). That dragged Node built-ins into the browser bundle and the web app crashed to a blank page at runtime — a regression from v2/main, where browser OAuth storage used a Zustand JSON adapter with no Node imports. Unit/integration tests run in node/happy-dom and `smoke:web` only checks the served HTML, so none of them execute the app in a real browser and the break slipped through. Fix, following the existing `store-id.ts` precedent: - Split the pure JSON (de)serializers into Node-free `core/storage/store-serialize.ts`; `store-io.ts` re-exports them so existing importers are unaffected. - Move the Node-only `createFileOAuthPersistBackend` into `core/auth/node/oauth-persist-file.ts`; update `storage-node.ts` and the storage adapters test to import from there. - `oauth-persist.ts` now imports only the Node-free serializers, so the browser bundle no longer pulls in `node:fs`/`atomically`. Tests: - oauth-persist-browser-safe.test.ts: source-level guard that `oauth-persist.ts` never imports store-io/node:/atomically. - Direct unit tests for the isomorphic remote + session backends (restoring oauth-persist.ts coverage after the file backend moved out). - store.test.ts: OAuthMemoryStore unit coverage incl. replace() fallbacks. - storage.test.ts: clearAllOAuthClientState null-snapshot (no file) path. Verified end-to-end in a real browser (Playwright): DCR OAuth flow completes, tokens persist via the remote store, and after a full page reload the server reconnects with no re-authentication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cdeb2316d0 |
test(auth): assert parseOAuthPersistBlob({}) returns null
Document the contract the remote read() path leans on — the server's
missing-file response is `c.json({}, 200)`, and the backend now passes
that empty object straight to parseOAuthPersistBlob. Addresses the
optional note on Claude review of #1612.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
edc7dbb1c1 |
Merge remote-tracking branch 'origin/v2/main' into v2/1549-oauth-storage-no-zustand
# Conflicts: # core/auth/store.ts |
||
|
|
ba48e06ffd |
fix(auth): guard OAuth-storage async paths against unhandled rejections
Review follow-ups on the RemoteOAuthStorage web migration: - useEmaIdpLoginState.logout(): add a .catch to the now-async clearEmaIdpSession call so a storage failure no longer surfaces as an unhandled rejection; leave loginState unchanged on failure (the session was not actually cleared). Add a test asserting no unhandled rejection. - createOAuthStore: attach a no-op catch to the eager persistLoadPromise so a hydration read that fails before any load() awaits it does not become an unhandled rejection; real awaiters still observe the rejection via the shared reference. - storage-remote.test: replace `as unknown as typeof fetch` double-casts with the typed vi.fn<typeof fetch>() pattern the same PR established. - remoteOAuthStorage: correct the doc comment to describe the cache-of-one accurately instead of implying a keyed map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af174666d6 |
fix(auth): address Claude review on OAuth persist serialization
Serialize persist() in OAuthStorageBase so overlapping remote POSTs cannot reorder and clobber state. Regenerate launcher package-lock.json to drop stale zustand entries. Let parseOAuthPersistBlob accept parsed objects from res.json() without a redundant JSON round-trip. |
||
|
|
3c65bdb4c6 | Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage. | ||
|
|
512f45d357 | fix(auth): reject load() when persist read fails | ||
|
|
cfbdfdf2bd |
Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts: # AGENTS.md # clients/cli/README.md # clients/web/src/App.test.tsx # clients/web/src/App.tsx # clients/web/src/test/core/auth/utils.test.ts # core/mcp/node/transport.ts # test-servers/src/composable-test-server.ts |
||
|
|
a24091484c | fix(auth): await OAuth storage load() to fix remote OAuth races | ||
|
|
7bd26c938e | refactor: migrate to RemoteOAuthStorage for web OAuth handling | ||
|
|
f4b6095719 |
test: close ≥90% per-file coverage gaps for mid-session auth
The mid-session auth work passed `validate`/CI but failed the local-only
`npm run test:coverage` per-file gate (≥90% lines/statements/functions/
branches) on 12 files. Add targeted unit/integration tests so every changed
file clears the gate; annotate provably-dead defensive branches with justified
`/* v8 ignore */` comments rather than relaxing the gate.
Files brought to ≥90 on all four dimensions:
- core/auth: mcpAuth, oauthUx, utils, challenge, node/runner-interactive-oauth
- core/mcp: oauthManager, inspectorClient, node/authChallengeFetch,
remote/remoteClientTransport, remote/node/{remote-session,server}
- clients/web/src/utils: oauthResume, browserTabVisibility
`npm run test:coverage` now passes (3575 tests, 0 threshold failures);
`npm run validate` green across all four clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
|
||
|
|
a066873e37 | Fixes for remaining issues | ||
|
|
8f1ed7588d | Prettier fix on last commit | ||
|
|
52023ff1c3 |
fix(auth): clear OAuth callback timer and reorder EMA scope persist
Clear the loopback callback timeout in runner-interactive-oauth finally so already_authorized and error paths do not hang the CLI/TUI. Check EMA step-up satisfaction against minted token scope before persisting the requested union, matching the standard OAuth path. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
0635d09e83 | Fixed CI failure (test fixture) and added local CI script | ||
|
|
562bdd00f1 | All of the mid-session and step-up auth work | ||
|
|
775b98a019 |
docs(core): clarify redaction scope + add nested request-body test
Address round-1 review feedback on fetchTracking redaction: - Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader understands why two identical "[REDACTED]" constants coexist. - Document that redactBody deliberately scopes to form-encoded + JSON (multipart/binary bodies pass through; OAuth never uses multipart). - Add a tracker-path test asserting a nested sensitive key in a JSON request body is redacted in the recorded copy while the live outbound body stays byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
||
|
|
524bf2d543 |
feat(core): redact sensitive body + URL-query fields in fetch log (closes #1593)
Extends the header redaction from #1585. Before a fetch entry reaches any sink, mask known-sensitive credentials in the request/response bodies and the URL query string, in addition to headers: - redactBody(): form-urlencoded + JSON bodies, recursive over nested objects/arrays, best-effort (never throws), values-only. - redactUrlQuery(): masks sensitive query params, preserves path, non-sensitive params, and any fragment. - Sensitive fields: client_secret, code, refresh_token, access_token, id_token, code_verifier, client_assertion, assertion, password, token (case-insensitive), reusing the [REDACTED] sentinel. Applied to the request success + error entries and the async response-body callback. Live outbound requests are byte-identical — only the logged copy is redacted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
||
|
|
c6b5af3b81 | merge: v2/1596-test-stability (#1597) | ||
|
|
e425602295 |
merge: v2/1548-web-remote-oauth-storage (#1592)
# Conflicts: # clients/web/src/test/core/auth/utils.test.ts |
||
|
|
cc37a755ae | merge: v2/1562-oauth-state-hardening (#1589) | ||
|
|
52cd90bd50 | merge: v2/1560-downloadfile (#1586) | ||
|
|
9257c849b9 | merge: v2/1557-mcp-app-demo (#1587) | ||
|
|
4e3cd591a4 | merge: v2/1563-node-proxy (#1590) | ||
|
|
2d1a99ab37 | merge: v2/1561-redact-headers (#1585) | ||
|
|
4bad110942 |
test(web): eliminate timeout flakiness in the web test suite (closes #1596)
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.
Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.
Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
past the 300ms completion debounce (await setTimeout(400)) — which race
the debounce timer under load — with awaited conditions (findBy/waitFor)
on the real rendered outcome, plus delay:null. Negative assertions now
lean on the component's synchronous state/timer teardown instead of a
timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
piped out-of-band from the tool's JSON-RPC response, so reading the log
synchronously after callTool raced the stderr chunk. Wrapped the assertion
in vi.waitFor so it polls until the line lands.
Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
|
||
|
|
4e149719bd |
fix(auth): route clearClientInformation through clearAfterHydration too
Round-2 @claude review of #1592 caught a seventh member of the clear family that the first pass missed: clearClientInformation still wrote to the store synchronously, carrying the same pre-hydration clobber risk the clearAfterHydration helper was written for (a clear before hydration lands persists a near-empty blob, overwriting the whole on-disk oauth.json). Wrap its mutation the same way and add a clear-before-hydration test proving a sibling token (and thus every other server's blob) survives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
||
|
|
8c10b8eb8e |
fix(auth): make clear* hydration-safe, migrate raw-key blobs, doc keepalive/memo caveats
Address round-1 @claude review of #1592: - OAuthStorageBase.clear* no longer mutate the pre-hydration (empty) store — a clear issued before hydration is deferred until it lands, so it merges onto the real persisted state instead of (a) being resurrected by the late rehydrate merge or (b) persisting a near-empty blob that clobbers every other server's on-disk credential. Already-hydrated clears stay synchronous. - store.setServerState migrates a pre-normalization raw-key blob onto the canonical key on first partial write (was: shadowed it with a fresh canonical entry, orphaning the raw blob's other fields). clearServerState now also drops the raw-key orphan. - Document the keepalive 64KB combined-body ceiling on the remote-storage write, the getRemoteOAuthStorage memo key intentionally omitting fetchFn, and the real ordering invariant behind the sync getScope. - Tests for the clear-before-hydration and raw-key-migration paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
||
|
|
e5dbdca65c |
test(test-servers): export MCP_APP_DEMO_URI, dedupe in integration test
Address code-review DRY note on PR #1587: MCP_APP_DEMO_URI was defined in both the fixture and the integration test. Export it from the fixture and import it in the test so the two stay in lockstep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
||
|
|
c299b6e9d8 |
feat(web): migrate OAuth store to shared /store API (RemoteOAuthStorage parity) (closes #1548)
Migrate the web client's OAuth storage from BrowserOAuthStorage (sessionStorage) to RemoteOAuthStorage, which POSTs through the backend's /api/storage/oauth route to ~/.mcp-inspector/storage/oauth.json (mode 0600). A credential obtained in the browser is now the same blob the TUI/CLI read on the same host, giving web ⇄ TUI ⇄ CLI parity (Wave 1 of #1579). Core (shared with TUI/CLI): - OAuthStorageBase now drives a single async hydration (`ready()`, `getHydrationError()`); every post-redirect read (getClientInformation, getTokens, getCodeVerifier, getServerMetadata, getIdpSession) and every save awaits it so a late hydration merge cannot clobber a fresh write. getCodeVerifier/getServerMetadata become async; the store is created with skipHydration:true so there is no auto-hydration to race the explicit one. - store.ts adds normalizeServerUrl so a token saved under https://Example.com/mcp is found when the CLI asks for https://example.com/mcp/; getServerState falls back to the raw key. - remote-storage adapter POSTs with keepalive so the write survives the OAuth authorize redirect, surfaces swallowed persist failures, and gives richer read/write error messages. - generateOAuthState throws instead of silently degrading to Math.random. - NodeOAuthStorage honors MCP_INSPECTOR_OAUTH_STATE_PATH for isolated fixtures. Provider codeVerifier()/getServerMetadata() + the EMA idpOidc and connection-state readers await the now-async storage. Web: - environmentFactory + a new shared lib/remoteOAuthStorage accessor (memoized per {baseUrl, authToken}) wire RemoteOAuthStorage into the connection path, the EMA IdP hook, and the per-server "clear OAuth" action so all three share one in-memory view of oauth.json. Tests: async-hydration + normalizeServerUrl suite, keepalive/error-path coverage, and the sync→async migration across existing storage/provider/EMA tests. Web validate + coverage gate + integration suite green; CLI/TUI/ launcher validate green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
||
|
|
d5be4929e9 |
feat(core): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY in the Node transport (closes #1563)
Wrap the Node transport's fetch with an undici EnvHttpProxyAgent dispatcher when a standard proxy env var is set, so the CLI and web backend can reach remote MCP servers through corporate proxies with zero new flags. undici is imported lazily (only when a proxy var is present) and added as a dependency at the repo root and in the CLI client; a missing undici raises an actionable error. Documents the behavior and dependency in the CLI README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX |
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d552564966 |
||
|
|
9b2c188d11 |
||
|
|
73d9b181fe |
||
|
|
e06c0a45f7 |
||
|
|
e026051b47 |
||
|
|
7fecebed5e |
||
|
|
f4fb4b50c9 | ||
|
|
8a7a38f800 | ||
|
|
19a6df7ad3 | ||
|
|
a029e42a19 |
||
|
|
0ddb79203a |
||
|
|
1e6718e2fd | ||
|
|
a26cbac609 | ||
|
|
650e5bd020 | ||
|
|
7815548300 | ||
|
|
ace4250d31 | ||
|
|
32a3610737 | ||
|
|
98517c2758 | ||
|
|
4c30dc6e2e | ||
|
|
1e6a2825d4 | ||
|
|
cdeb2316d0 | ||
|
|
edc7dbb1c1 | ||
|
|
ba48e06ffd | ||
|
|
af174666d6 | ||
|
|
3c65bdb4c6 | ||
|
|
512f45d357 | ||
|
|
cfbdfdf2bd | ||
|
|
a24091484c | ||
|
|
7bd26c938e | ||
|
|
f4b6095719 | ||
|
|
a066873e37 | ||
|
|
8f1ed7588d | ||
|
|
52023ff1c3 | ||
|
|
0635d09e83 | ||
|
|
562bdd00f1 | ||
|
|
775b98a019 | ||
|
|
524bf2d543 | ||
|
|
c6b5af3b81 | ||
|
|
e425602295 | ||
|
|
cc37a755ae | ||
|
|
52cd90bd50 | ||
|
|
9257c849b9 | ||
|
|
4e3cd591a4 | ||
|
|
2d1a99ab37 | ||
|
|
4bad110942 | ||
|
|
4e149719bd | ||
|
|
8c10b8eb8e | ||
|
|
e5dbdca65c | ||
|
|
c299b6e9d8 | ||
|
|
d5be4929e9 |