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
Address round-1 @claude review of #1588 (no behavior change to the
enforced policy):
- approveCspSources / buildSandboxCspPolicy: document that
resourceDomains intentionally feeds script-src/style-src per the
McpUiResourceCsp contract, and that approve screens injection-safety
only (not breadth — a bare `*` is accepted; safe under the
opaque-origin sandbox).
- CSP_KEYS: replace the `satisfies` (proves listed keys valid) with an
exhaustiveCspKeys() helper that ALSO fails to compile if the upstream
ext-apps type gains a domain key CSP_KEYS omits, so a requested
restriction can never be silently dropped.
- SAFE_CSP_SOURCE: accept case-insensitive schemes (URL schemes are
case-insensitive); previously over-rejected `HTTPS://…`. Adds tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
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
Add a pure, DOM-free CSP-builder library at
clients/web/src/lib/sandbox-csp.ts that validates app-supplied
`_meta.ui.csp` requests, filters them to safe sources, builds the
locked-down Content-Security-Policy string, and wraps untrusted widget
HTML in a fixed host shell whose first <head> child is the CSP meta.
- SAFE_CSP_SOURCE: strict regex that rejects directive/attribute
breakouts (`;`, `"`, `<`, `>`, whitespace).
- approveCspSources(): drops unsafe entries (with a warning) and omits
empty keys; echoes back only what the host will enforce.
- buildSandboxCspPolicy(): `default-src 'none'` catch-all, `form-action
'none'`, source-allowlist filtering, `'unsafe-inline'` for the app's
own inline script/style.
- escapeHtmlAttr() / wrapSandboxedHtml(): defense-in-depth so untrusted
bytes never precede the applied policy.
Re-implements the sandbox-csp portion of PR #1510 (Wave 1 of #1579) as a
standalone library; wiring into the app bridge is a follow-up issue.
Adds full unit tests (100% lines/statements/functions/branches) and
lists the file in the web coverage `include` so it is gated (the
untested legacy src/lib siblings stay out until they get tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Extend the browser download library beyond JSON so the Apps host's
ui/download-file support can download arbitrary embedded resources and
open http(s) links safely, with no UI wiring in this change:
- downloadBlob(): temp-anchor download core with a setTimeout(...,0)-
deferred revokeObjectURL so a synchronous revoke can't abort the
scheduled download (Firefox/Safari, intermittently Chrome).
- downloadJsonFile(): refactored on top of downloadBlob; callers unchanged.
- fileNameFromUri(): last path segment sanitized (control/format chars
stripped, disallowed filename chars -> _, capped at 255, "download"
fallback).
- isHttpUrl(): parse-and-allowlist http(s) only, else null.
Re-implementation of the downloadFile.ts slice of PR #1510 as Wave 1 of
the #1579 decomposition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
* fix(web): don't show a permanent PENDING badge on history notifications (#1439)
`extractStatus` derived the history status badge purely from whether a
`response` was attached, so a fire-and-forget notification (no id, no
response ever) — and any unmatched standalone response — was labelled
"Pending" forever. The pending → OK/Error lifecycle only applies to
request entries (messageLogState attaches the response by JSON-RPC id),
so scope the status to `direction === "request"` and render no
request-style badge for notifications/responses. The method badge already
labels them.
Part of the History-screen work on #1438.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): track outgoing responses so server→client requests resolve in history (#1440)
MessageTrackingTransport.send only tracked outgoing requests, so the
client's response to a server→client request (roots/list, sampling,
elicitation) was never recorded. The inbound request entry never got its
response folded in, leaving it stuck "Pending" with no response body.
Make send symmetric with onmessage: track outgoing responses (by id +
result/error) as well as outgoing requests. The response now correlates
to its request entry, which resolves to OK/Error with the body. Outgoing
notifications remain untracked (separate gap).
Part of the History-screen work on #1438.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): wire up History Pin and Replay (#1438)
Both HistoryEntry actions were rendered but stubbed to todoNoop in App.
- Pin: App now owns a session-scoped Set<string> of pinned entry ids,
toggled by onTogglePinHistory and passed down as pinnedHistoryIds.
HistoryListPanel already sorts pinned entries to the top. The set resets
with the rest of the per-screen state on disconnect/server-switch.
- Replay: onReplayHistory re-issues the entry's original request by method
(tools/call → callTool, prompts/get → getPrompt, resources/read →
readResource). The call flows through InspectorClient → tracked
transport → message log, so the fresh request+response surface as a new
History entry (history-local) without touching the Tools/Prompts/
Resources panels. Unsupported methods / a removed tool surface a toast.
Tests: pin toggle propagation, replay dispatch for each supported method,
unsupported-method and missing-tool toasts.
Closes#1438.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make history Replay support list/discovery requests
Replaying a tools/list entry reported "Replay isn't supported". Extend
replayHistoryRequest beyond the call-style requests to the read/discovery
methods, re-issuing each via its InspectorClient method (preserving the
pagination cursor):
- tools/list → listTools, prompts/list → listPrompts,
resources/list → listResources,
resources/templates/list → listResourceTemplates, ping → ping.
Tests: tools/list replay (cursor preserved) and an unsupported-method
toast (logging/setLevel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): replay tasks/list; reorder + conditionally show History buttons
- Add tasks/list to the replayable set (re-issued via listRequestorTasks),
completing the client-issued */list methods.
- Reverse the HistoryEntry actions to Pin then Replay, and hide Replay for
entries whose method can't be replayed.
- Introduce a single source of truth — REPLAYABLE_HISTORY_METHODS /
isReplayableHistoryMethod in historyUtils — used by HistoryEntry to
show/hide the button and by App's replayHistoryRequest to gate dispatch,
so the two can't drift.
Tests: tasks/list replay, Replay hidden for a non-replayable method, and
Pin-before-Replay ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): add client↔server direction badge to History and Network (#1437)
A dual-state badge at the top-left of each entry shows which way it
traveled: "client → server" (green) for inspector-originated, "client ←
server" (yellow) for server-originated.
- New shared element MessageDirectionBadge (Elements).
- History: MessageEntry gains an `origin` ("client" | "server") set at
tracking time — MessageTrackingTransport tags `send` as client and
`onmessage` as server, threaded through the track callbacks. HistoryEntry
maps origin → badge. (direction stays the request/response/notification
message-type used for response correlation; origin is the flow.)
- Network: fetches are always inspector-originated, so NetworkEntry renders
the badge as outgoing.
Tests + stories for the badge; HistoryEntry direction tests; transport
test asserts the origin argument.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): use purple (not yellow) for the client ← server direction badge
Yellow + green read as caution/ok status; purple vs green reads as
direction. Incoming (server → client) is now violet; outgoing stays green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make History Pinned/History sections collapsible toggles
The "Pinned Requests (N)" and "History (N)" section headers are now
clickable toggle buttons that expand/collapse their entries — styled like
the LogControls level toggles (UnstyledButton listItem variant with the
active background) and wrapping the entries in a Collapse. Both default
open and collapse independently.
Tests assert the per-section aria-expanded toggling and independence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): replace Expand/Collapse text button with an icon toggle
History, Network, and Task cards now use a shared ExpandToggle ActionIcon
instead of the "Expand"/"Collapse" text button: LuArrowDownToLine when
collapsed (expand), LuArrowUpFromLine when expanded (collapse). The
aria-label stays "Expand"/"Collapse" so the control reads the same.
- New shared element ExpandToggle (+ test + stories).
- HistoryEntry: actions split into a left group (Pin/Replay) with the
toggle pushed right via space-between.
- NetworkEntry / TaskCard: drop their now-unused SubtleButton const.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): pin icon toggle on HistoryEntry, moved to the right
Replace the Pin/Unpin text button with a shared PinToggle ActionIcon
(TiPinOutline when unpinned, TiPin when pinned; aria-label stays
Pin/Unpin). Move it to the right action group, just left of the
expand/collapse toggle — Replay stays on the left.
- New shared element PinToggle (+ test + stories).
- HistoryEntry action row: Replay left; Pin + expand toggle right.
- Drop the now-unused formatPinLabel helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): per-section Clear/Export on the Pinned and History headers
Each section header now shows Clear and Export links on the right, but
only when both sections are present (a single section is already covered
by the panel-level Clear/Export). They act on just that section's entries:
- Clear: removes that section's entries by pin membership (clearing the
Pinned section also drops the now-stale pinned id set).
- Export: downloads just that section's entries
(inspector-history-pinned/-unpinned-...json).
Threaded onClearSection/onExportSection App → InspectorView → HistoryScreen
→ HistoryListPanel; App computes each section from messages + pinned ids.
Extracted a CollapsibleSection subcomponent and a SectionActions links
group. Extended ExportKind with history-pinned/history-unpinned.
Tests cover both-sections visibility gating and per-section dispatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): Filter by Message Direction on the History controls
Add a "Filter by Message Direction" section to HistoryControls, modeled on
LogControls' "Filter by Level": a Select/Deselect All control plus two
listItem toggles — "client → server" and "client ← server" — that show or
hide entries by origin.
- HistoryUiState gains visibleDirections (Record<MessageOrigin, boolean>),
defaulting to both visible; persists with the other per-screen state.
- HistoryScreen wires the toggles (single + all) through onUiChange and
passes the filter to HistoryListPanel, where matchesFilters drops entries
whose direction is off (entries without a recorded origin are never
filtered out).
Tests cover the controls (render, single toggle, select/deselect all), the
HistoryScreen onUiChange wiring, and the list-level direction filtering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): section Clear/Export as subtle buttons, not link anchors
Match the Select/Deselect All control in HistoryControls: the per-section
Clear/Export on the History/Pinned headers now use a subtle Button
(variant subtle, size xs) instead of an Anchor link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): color the direction filter labels to match the badge
The "client → server" / "client ← server" toggles in HistoryControls now
color their labels green / violet, matching the MessageDirectionBadge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): track outgoing client notifications in history too
Review follow-up (#1441): MessageTrackingTransport.send now also tracks
the client's own notifications (initialized, progress,
roots/list_changed) as origin "client", making send fully symmetric with
onmessage. Previously only id-bearing outgoing messages (requests,
responses) were tracked, so client notifications never reached History
while incoming server notifications did.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): Filter by Message Direction on the Network screen
Add the same "Filter by Message Direction" section to NetworkControls as
on HistoryControls. Extracted the shared section into a MessageDirectionFilter
group component (used by both HistoryControls and NetworkControls) so they
stay identical — a Select/Deselect All control plus green/violet
client → server / client ← server toggles.
- NetworkUiState gains visibleDirections (default both visible), persisted
with the rest of the per-screen state; NetworkScreen wires the toggles and
NetworkStreamPanel filters on them.
- Network fetches are always inspector-originated, so all entries are
client → server: the client toggle gates them all and the server toggle
matches nothing (documented in NetworkStreamPanel). The section mirrors
History for parity.
Tests cover the shared filter, both controls' direction sections, the
Network list filtering, and the NetworkScreen wiring; ambiguous "Deselect
All" selectors (now two sections) disambiguated by order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert(web): drop message direction from the Network tab
Network traffic is always client → server — even SSE is a client-initiated
stream tagged outgoing — so a direction badge/filter there is meaningless.
Remove the MessageDirectionBadge from NetworkEntry and the Filter by Message
Direction section (and its NetworkUiState.visibleDirections wiring) from the
Network screen.
History keeps its direction badge + filter; the shared MessageDirectionBadge
and MessageDirectionFilter components stay (still used by History).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): a lone History section isn't a collapsible accordion
When only one of Pinned / History is present, its header is now a plain
title and its entries always show — instead of a `listItem` accordion
toggle that could hide them based on a stale collapsed state from when
both sections were on screen. The accordion (and per-section Clear/Export)
only applies when both sections are present.
Example fixed: collapse History while pins exist, then unpin everything —
History becomes the sole section and shows its entries under a plain
header rather than staying collapsed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): put section Clear/Export on the header pleat itself
The per-section Clear/Export sat to the right of the collapsible header
bar, on the panel background. Make the whole header row the "pleat":
a new Group `sectionHeader` theme variant (rounded + hover, active bg
passed per instance) wraps a flex-1 toggle button (the title) on the left
and the Clear/Export buttons on the right — so the actions live on the
pleat. The toggle is a separate button since the action buttons can't nest
inside it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): entry expand toggle uses the same icons as ListToggle
Swap ExpandToggle (History / Network / Task cards) from the LuArrow icons
to RiExpandVerticalLine / RiCollapseVerticalLine — the same expand/collapse
icons the list-level ListToggle uses. Same ActionIcon size, so it matches
the other small buttons on the cards; aria-labels unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): panel Clear keeps pinned entries (history-only)
Review follow-up (#1441): the panel Clear button is gated on the unpinned
count (implying history-only), but onClearHistory wiped everything
(pinned included) and left pinnedHistoryIds stale. Make Clear history-only
— clearMessages((m) => !pinnedHistoryIds.has(m.id)) — so pinning protects
an entry, matching the gating and the per-section model; the pin set stays
valid since its entries survive.
Also drop the stale "and Network" mention from the MessageDirectionFilter
docstring (Network direction filter was reverted).
Test: panel Clear's predicate keeps pinned ids and removes unpinned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378)
Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…`
query string) with empty sessionStorage made every `/api/*` request 401 —
the browser had no way to recover the backend's auth token.
Embed the token into `index.html` on every page load so the browser no
longer depends on the query string surviving navigation:
- New shared helper `clients/web/server/inject-auth-token.ts` embeds
`<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against
`</script>` injection; no-op when auth is dangerously omitted).
- Dev: the Vite plugin injects via `transformIndexHtml`.
- Prod: the Hono server injects on the `/` route.
- `App.tsx` `getAuthToken()` now reads the injected global first, then the
query string, then sessionStorage (both fallbacks preserved).
- Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL`
(`core/mcp/remote/constants.ts`).
Tests: helper unit coverage + an integration test exercising the real
prod server's `/` → `/api/*` flow (injected token authenticates; missing
token 401s). AGENTS.md documents the token-recovery order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): wire OAuth authorization-code flow into App.tsx (#1379)
OAuth-protected MCP servers could not be connected to from the v2 web
client: the core OAuth pipeline exists, but App.tsx never invoked it, so a
connect attempt 401'd and surfaced "Remote send failed (401): … Missing
Authorization header" as a toast.
Wire the two missing entry points (all core primitives already in place):
- Auto-trigger on 401: in onToggleConnection's catch, detect an upstream
401 (isUnauthorizedError) and call client.authenticate(), which runs
discovery + DCR (backend-proxied) and redirects the page to the auth
server via BrowserNavigation. The initiating server id is persisted to
sessionStorage first, since the OAuth `state` carries only mode+authId
and the full-page redirect wipes React state.
- /oauth/callback handler: a mount effect that, once `servers` hydrate,
parses the callback params, recovers the pending server, rebuilds its
InspectorClient, runs completeOAuthFlow(code) (PKCE verifier + DCR client
info survive in BrowserOAuthStorage), replaceState("/") so a reload can't
replay the single-use code, then connect(). An `error=` callback toasts
instead of retrying.
connect() already attaches the OAuth provider to the transport
(inspectorClient.ts), so once tokens land in BrowserOAuthStorage the
outbound request carries the bearer token.
Extracted the pure pieces (constants + isUnauthorizedError) to
src/utils/oauthFlow.ts with unit tests. Verified end-to-end in a real
browser against the MCP SDK demo OAuth server: Connect -> redirect ->
auto-approve -> callback -> Connected, with the access token shown in the
Connection Info modal (#1377).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(auth): persist OAuth pre-redirect Network log across the redirect (#1384)
After #1379, the Network tab showed only the post-redirect auth HTTP
(discovery re-run + POST /token); the pre-redirect discovery and the DCR
POST /register that run during authenticate() were lost when the page
navigated to the auth server.
Root causes:
1. Ordering — BrowserNavigation set `window.location.href` before the
client's `saveSession` event fired (OAuthManager calls onBeforeOAuthRedirect
*after* auth() already navigated), so the save raced the unload and was
dropped. Fix: BrowserNavigation now runs a synchronous `beforeNavigate`
hook immediately before assigning location.href; App wires it through
createWebEnvironment to flush the active fetch log to RemoteInspectorClient
Storage (keyed by the authId parsed from the auth URL) via a keepalive POST
that outlives the unload.
2. Illegal invocation — RemoteInspectorClientStorage defaulted to
`this.fetchFn = globalThis.fetch` and called `this.fetchFn(...)`, which
re-binds `this` and makes native fetch throw "Illegal invocation"
(swallowed by the catch). This silently broke *all* session save/load.
Fix: default to a wrapper that preserves the global receiver.
3. Restore race — hydrateFetchRequests replaced the list, so a load that
resolved after the resuming connect appended live entries would clobber
them. Fix: merge restored (older) entries ahead of live ones, dedupe by id.
saveSession also now uses keepalive: true.
Verified end-to-end against the MCP SDK demo OAuth server: the connected
page's Network tab shows the full handshake — pre-redirect discovery + DCR
/register plus post-redirect discovery + /token as `auth`, alongside
`transport`. Added unit tests for the beforeNavigate ordering and the
hydrate merge/dedupe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378)
Addresses code-review feedback on the token-injection PR:
- Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously
served the raw index.html off disk via serveStatic, bypassing injection — so
a bookmark/reload at a non-`/` route with empty sessionStorage would 401.
Route the SPA fallback through the same `serveIndexHtml` (inject) helper;
real static assets (paths with a dot) still serve verbatim. Dev already
injected on every HTML serve via Vite `transformIndexHtml`.
- `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__`
to sessionStorage (not just the URL-param branch), priming the backstop for
any later navigation that loses the global.
- Injected HTML responses now send `Cache-Control: no-store`, so a page
carrying a token isn't cached and served stale after a restart regenerates
the token.
Integration tests added: SPA fallback (`/oauth/callback`) carries the token,
`Cache-Control: no-store` on injected HTML, real assets served verbatim, and
unknown `/api` routes 404 rather than falling through to the HTML shell.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(auth): address #1383 review — split OAuth/connect toasts, tighten 401 match (#1379)
Code-review feedback on the OAuth-wiring PR:
- Callback effect: split completeOAuthFlow vs connect() into separate
try/catch blocks. A token-exchange failure now reads "OAuth token exchange
failed … Please try connecting again." (the single-use code is spent and the
URL was cleared, so a reload can't retry); a post-OAuth connect failure reads
"Failed to connect" since OAuth actually succeeded and re-clicking Connect
reuses the persisted tokens.
- isUnauthorizedError: anchor the message fallback on the transport's
`failed …(401)` wording instead of a bare `(401)`, so an unrelated `(401)`
spliced into an error message can't trip the OAuth flow. Added a test.
- Documented that clearing the pending id + URL before the server lookup is
intentional (deleted/renamed server mid-flow → require a fresh Connect).
Also merges the squash-merged #1382 base from v2/main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(auth): address #1385 review — clarify double-save + keepalive cap + test fetch default (#1384)
Code-review feedback on the OAuth Network-log persistence PR:
- Documented the double-save: `FetchRequestLogState`'s `saveSession` listener
is the backstop; `BrowserNavigation`'s `beforeNavigate` hook is the primary
flush for the redirect case. Notes the listener may lose the navigation race
and is harmless when it duplicates (last-writer-wins, identical payload).
- Reworded the keepalive comment in `RemoteInspectorClientStorage.saveSession`:
the 64KB cap is general (the method is also reachable from the listener with
the full session log), so a long session could exceed it and drop silently —
acceptable since the persisted log is best-effort, not load-bearing.
- Added a regression test that constructs `RemoteInspectorClientStorage`
without a `fetchFn`, stubs `globalThis.fetch`, and asserts the default
wrapper calls it (locks in the "Illegal invocation" fix, which callers
otherwise swallow).
Optional items (logger.warn on swallowed save errors; setupClientForServer dep
churn) acknowledged on the PR, not changed.
Also merges the squash-merged #1383 base from v2/main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(export): wire History + Logs export JSON handlers (#1354)
Replaces the `todoNoop` placeholders in App.tsx with real handlers that
serialize the current entries and trigger a browser download via the
existing `downloadJsonFile` helper.
- Adds `buildExportFilename(kind, serverId, now?)` next to the download
helper. Produces sortable, filesystem-safe names like
`inspector-history-alpha-2026-03-17T10-00-42.123Z.json` (URL-encodes
server ids that contain slashes / spaces / colons, swaps `:` for `-`
in the ISO timestamp so the file is safe on Windows).
- Wires `onExportHistory` (full MessageEntry list) and `onExportLogs`
(the LogEntryData list the screen renders).
- Routes the existing Network export through the same filename helper
for consistency.
Unit tests cover the filename builder, including the no-server-id
fallback, special-character encoding, and the default-`now` shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(export): tighten buildExportFilename signature + docs
- Introduce an `ExportKind = "history" | "logs" | "network"` union and
use it as the type of the `kind` parameter. Typos like "hisotry" now
fail at compile time and the supported export kinds are documented
inline.
- Clarify the JSDoc: the segment is omitted when `serverId` is falsy
(undefined OR empty string), matching the existing `serverId ? ... :
undefined` check. New test pins the empty-string branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(logging): remove Copy All button from LogStreamPanel
Export now produces the same JSON the Copy All button used to dump to
the clipboard, so the button is redundant. History and Network panels
have no equivalent — dropping Copy All also brings Logs into parity.
Removes the prop from LogStreamPanel + LoggingScreen + InspectorView +
App.tsx, and updates the matching tests/stories. Tests now assert the
button is *not* in the document so a future re-introduction would have
to update the assertion deliberately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(servers): export server list as mcp.json download (#1346)
Adds an Export button to the Servers screen, next to "Add Servers".
Clicking it downloads the current server list as a canonical mcp.json
file (2-space indent, same shape serializeStore writes), so the user
can hand it off, sync it to another machine, or symlink into Claude
Desktop / Cursor / Cline.
Wiring:
- ServerListControls: new Export Button (variant=default), disabled
when serverCount === 0 (nothing to download).
- ServerListScreen / InspectorView: thread `onExport` / `onServerExport`
through.
- App.tsx: onServerExport runs serverEntriesToMcpConfig on the in-memory
servers list, JSON.stringify with 2-space indent, and triggers a Blob
download via a temporary anchor (appended to body for Firefox
compatibility, then removed; object URL revoked).
Tests:
- ServerListControls unit tests cover button-count (toggle hidden vs
shown, Export always present), disabled-on-empty, click fires onExport.
- ServerListControls.stories grows play functions for both stories —
WithServers asserts Export is enabled + click fires the spy in real
Chromium, WithoutServers asserts Export is disabled.
- Existing screen + view tests / stories updated for the new prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR #1351 review (extract helpers + exception safety + guard)
Substantive items from the review:
1. Serialization format parity (review #1): added
`serializeMcpConfig(entries)` to core/mcp/serverList.ts — a
browser-safe helper that runs `serverEntriesToMcpConfig` then
stringifies with 2-space indent. Single source of truth for export
formatting; can't drift from serializeStore's on-disk shape.
2. Exception safety on the anchor cleanup (review #2) + a direct test
for the App.tsx download wiring (review #6): extracted
`downloadJsonFile(filename, json)` to clients/web/src/lib/
downloadFile.ts. removeChild + revokeObjectURL run in a `finally`
so a thrown click() doesn't leak the DOM node or the object URL.
Unit-tested under happy-dom with URL.createObjectURL mocked
(happy-dom doesn't ship it).
3. Empty-list guard (review #5): App.tsx onServerExport bails when
`servers.length === 0`. The button's disabled prop already prevents
the UI path, but the handler is now locally correct against any
future programmatic caller (storybook, keyboard shortcut, etc.).
Acknowledged but not changed:
- Tooltip on the disabled Export button (review #3) — Mantine's
disabled-button + Tooltip wart wants a span wrapper + custom events
config. Optional per the reviewer; deferred.
- Repeated exports → `mcp (1).json` browser suffix (review #4) — a
deliberate trade-off since `mcp.json` is the conventional name a user
would expect to symlink into Claude Desktop / Cursor / etc.
Tests: 1647 unit (+6) + 410 integration + 306 storybook = 2363 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView
App.tsx mounts InspectorView with live data from the v2 hook layer instead
of the title-page placeholder. InspectorView becomes pure prop-driven; the
demo handshake stub and connection state machinery are removed.
Connection lifecycle:
- App.tsx lazily instantiates `InspectorClient` on the connect edge,
rebuilding it (and its state managers) when the user picks a different
server. State managers (Managed{Tools,Prompts,Resources,ResourceTemplates,
RequestorTasks}State + MessageLog/FetchRequestLog/StderrLog) are
destroyed on switch.
- `useInspectorClient` + the per-primitive `useManaged*` hooks drive the
view's data. `latencyMs` is captured at the connecting → connected edge
via a ref so the intermediate rerenders don't lose the start timestamp.
- `errorMessage` is captured from `client.connect()` rejection; status-
machine-driven errors that don't surface through the promise are a known
follow-up (no `error` event in the v2 InspectorClientEventMap yet).
Action handlers:
- `onCallTool` / `onGetPrompt` / `onReadResource` await the corresponding
InspectorClient method and reflect pending → ok/error in `ToolCallState`
/ `GetPromptState` / `ReadResourceState` panel props. Manually verified
end-to-end against `npx @modelcontextprotocol/server-filesystem` —
the Tools result panel renders the real "Allowed directories: /private/tmp"
output rather than a stub.
- `onSetLogLevel` calls `client.setLoggingLevel(level)` and optimistically
bumps `currentLogLevel` locally (the request has no echo notification).
- `onSubscribeResource` / `onUnsubscribeResource` / `onCancelTask` route to
their respective client methods. Server CRUD (`onServerAdd`/`onServerEdit`
/…) and history pinning are intentionally `todoNoop` for now; the seed
server list is hardcoded per the agreed scope (a `useServers` v2-only
hook is a separate effort).
- `notifications/message` notifications flowing through `MessageLogState`
are filtered into `LogEntryData[]` and passed to the Logs screen, so
`logging/setLevel` and the resulting log stream round-trip.
InspectorView (`InspectorView.tsx`):
- Removed: `STUB_*` constants, `handshakeTimer` ref, `handleToggleConnection`
/ `disconnect` functions, stub `bridgeFactory`/`sandboxPath`, internal
connection state, `logLevel` state.
- Added 38 props covering connection state, panel states, log level, and
action callbacks. The view is pure presentational.
- `availableTabs` is derived from `connectionStatus`; `activeTab` is
clamped to whatever's currently available (avoiding the `set-state-in-effect`
lint and keeping the previously-selected tab on reconnect).
Stories + tests:
- `InspectorView.stories.tsx` adds spy callbacks for every action prop and
new `Connected` / `ConnectionError` stories for the connected/error
narratives.
- `InspectorView.test.tsx` is rewritten around the prop-driven contract
(10 tests covering the empty list, server cards, toggle dispatch,
connected header, error banner, tab snap-back on disconnect, app
filtering, log-level dispatch, autoScroll local toggle).
Dev backend port wiring (`vite.config.ts`):
- Vite's dev server is now pinned to `CLIENT_PORT` (default 6274) with
`strictPort: true`, matching the dev backend's `allowedOrigins`. Without
this, Vite would fall back to 5173 while the Hono origin check still
required 6274 — breaking every browser `/api/*` fetch out of the box.
- Top-level `resolve.alias` now spreads the same bare-module aliases the
vitest projects use. Rolldown can't reach `pino/browser.js` /
`zustand/middleware` from `core/`'s parent (no node_modules there); the
wired App pulls those modules into the browser dep graph for the first
time, so the aliases need to be visible at build time too.
createWebEnvironment helper (`src/lib/environmentFactory.ts`):
- Mirrors v1.5's `lib/adapters/environmentFactory.ts`: builds
`InspectorClientEnvironment` from `createRemoteTransport` +
`createRemoteFetch` + `createRemoteLogger` + `BrowserOAuthStorage` +
`BrowserNavigation`. Returns the assembled environment plus the logger
for direct app-level use.
Closes#1244.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): wire dev-backend auth token into createWebEnvironment + show full stdio command in ServerCard
Two issues surfaced after the #1244 wiring landed locally:
1. **401 on `/api/mcp/connect`** when running `npm run dev` with the
default auth (no `DANGEROUSLY_OMIT_AUTH`). The dev backend generates a
random token and prints `http://localhost:6274?MCP_INSPECTOR_API_TOKEN=…`
in the banner, but App.tsx was calling `createWebEnvironment(undefined, …)`
so the browser never sent `x-mcp-remote-auth`. The Hono middleware
correctly rejected with "Authentication required. Use the
x-mcp-remote-auth header with Bearer token."
Adds a small `getAuthToken()` helper in App.tsx that reads
`MCP_INSPECTOR_API_TOKEN` off `window.location.search`, falls back to
`sessionStorage`, and persists fresh values into sessionStorage so
client-side navigation / OAuth round-trips don't drop the token. The
resolved value is passed to `createWebEnvironment` whenever a new
InspectorClient is built.
2. **Misleading ServerCard display** — the seed server's `config` is
`{ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem",
"/tmp"] }` but the card only rendered `"npx"`, so it looked like the
command was incomplete. `getCommandOrUrl` now joins `command + args`
for stdio configs, so the card shows the same string that gets spawned.
SSE / streamable-http paths unchanged.
Manual end-to-end re-verified against `npm run dev` (no DANGEROUSLY_OMIT_AUTH):
- Banner prints the auth-token URL.
- Browser navigates to that URL → `getAuthToken()` picks the token off the
query string, persists to sessionStorage, threads it through to every
`/api/*` request.
- Server card shows `npx -y @modelcontextprotocol/server-filesystem /tmp`.
- Toggle connect → connection succeeds; ViewHeader shows
`Connected (~1.3s)`; Tools tab populates with the 14 filesystem tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): use dark-9 instead of blue-9 for dark-mode page background
`main.tsx` overrode `--mantine-color-body` to `--mantine-color-blue-9` in
dark mode, while `.storybook/preview.tsx` used `--mantine-color-dark-9`.
The dev app then rendered with a blue background while every story (and
the design intent visible in storybook) used dark grey.
Switch main.tsx to `--mantine-color-dark-9` so the dev app matches the
storybook reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): seed the Servers screen with the "everything" reference server
Adds `@modelcontextprotocol/server-everything` to the hardcoded seed list
alongside the filesystem server. The everything server exposes tools,
prompts, resources, sampling, and completion — covering most of the
surface a developer might want to exercise against the wired-up
InspectorView without configuring anything.
Comment updated to explain the two-seed shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): address #1244 PR review feedback (JsonValue cast, client lifecycle, dead code)
- **#1 — Misleading `Record<string, never>` cast**: Imported `JsonValue`
from `@inspector/core/mcp/index.js` and changed `args as Record<string,
never>` to `args as Record<string, JsonValue>` in `onCallTool`. The new
cast honestly narrows from the screen-level `Record<string, unknown>`
to what `InspectorClient.callTool` actually requires. Added a comment
explaining the boundary.
- **#2 + #3 — Previous InspectorClient not disconnected on server switch
or unmount**: Added a `useEffect` keyed on `inspectorClient` whose
cleanup calls `inspectorClient.disconnect()`. One effect covers both
the swap-on-switch leak (prior session's transport stayed open until
GC) and the unmount-during-connected leak (HMR / tests).
`setupClientForServer`'s existing state-manager `destroy()` calls
handle the listener side; the new effect handles the transport side.
- **#4 — Dead `void` block at bottom of file**: Deleted the trailing
`void FetchRequestLogState; void StderrLogState;` block. The state
managers are already constructed via `new FetchRequestLogState(client)`
/ `new StderrLogState(client)`, so the imports are referenced by the
linter's reckoning. The `void` pair was dead code with a comment that
no longer matched reality.
- **#5 / #7 / #8 — Follow-up TODOs**: Added TODO refs to the new
follow-up issues:
- TODO(#1323): `error` event in InspectorClientEventMap so mid-session
transport failures surface (connect-only catch is incomplete).
- TODO(#1324): negotiated `protocolVersion` through `useInspectorClient`
(currently hard-coded `"2025-06-18"`).
- TODO(#1325): `useResourceSubscriptions` hook so subscribe/unsubscribe
buttons reflect their server-side effect.
Items #6 (useCallback dep churn), #9 (helper unit tests), and #10
(shared no-op bridge factory fixture) are acknowledged in the PR reply
as deferred — no functional impact and out of scope for a "wire the
hook layer" PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stands up the type-only prerequisites for the v2 UX component
interfaces refactor per specification/v2_ux_interfaces_plan.md.
Phase 0.1 — MCP SDK dependency:
- Add @modelcontextprotocol/sdk ^1.29.0 and zod ^4.3.6 to both the
root package.json and clients/web/package.json so component files
can import Tool / Prompt / Resource / Task / LoggingLevel / etc.
directly from the SDK.
Phase 0.2 — Inspector wrapper-types module:
- Create core/mcp/types.ts mirroring the type-only subset of v1.5's
core/mcp/types.ts: MCPServerConfig (discriminated union), ServerType,
MCPConfig, ConnectionStatus, StderrLogEntry, MessageEntry (Date
timestamps, not ISO strings), FetchRequestEntry(+Base), ServerState,
and the *Invocation types. Adds v2-only wrappers
InspectorResourceSubscription and InspectorServerJsonDraft.
- Add core/mcp/taskNotificationSchemas.ts for the Inspector-owned
notifications/tasks/list_changed signal (not in the SDK).
- Add core/mcp/elicitationCreateMessage.ts re-exporting ElicitRequest
and ElicitResult plus an InspectorPendingElicitation interface for
typing the pending-queue.
- Add core/json/jsonUtils.ts for the shared JsonValue / JsonObject
types used inside core/mcp/types.ts.
- Copy v1.5's clients/web/src/utils/jsonUtils.ts (JsonSchemaType) and
schemaUtils.ts (AJV-backed validators) verbatim so SchemaForm can
switch off its local JsonSchema type.
- Copy v1.5's clients/web/src/lib/types/customHeaders.ts verbatim for
ServerSettingsForm and the experimental panel.
- Add clients/web/src/types/navigation.ts with InspectorTab (UI-routing
concept, lives with the web client rather than in core/).
- Wire tsconfig.app.json + vite.config.ts with the @inspector/core/*
path alias so component files can import from core/ without
relative-path hell.
Phase 0.3 — Vet (likely) hedges in v2_ux_interfaces.md:
- Follow-up sweep on commit 2ef257da. Replaces the remaining
useMcpClient references with useInspectorClient, ties pending
elicitation / sampling callbacks to the in-InspectorClient queue
rather than a non-existent useElicitation / useSampling hook,
corrects Task references to the SDK type (not an InspectorTask
wrapper), corrects log hook wiring to useMessageLog, and strikes
all remaining v2-only hooks with explicit "no v1.5 analog"
annotations. grep "likely" specification/v2_ux_interfaces.md now
returns zero results.
Phase 0.4 — Local-redeclaration inventory:
- specification/v2_ux_interfaces_phase_0_4_inventory.md enumerates
every ad hoc local type (JsonSchema, LogLevel, TaskStatus,
PromptItem, ResourceItem, ToolListItemProps, RootEntry, KeyValuePair,
inline ConnectionStatus / transport unions) that will be replaced
in Phases 1–4, including the file it lives in, every site that
re-imports it, and the SDK / wrapper type that replaces it. Intended
for the Phase 0 PR description and as a running checklist during
the refactor.
Build state: npm run build / lint / format:check all pass in
clients/web.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>