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>
* refactor(web): update OAuth E2E tests to use new authentication method
- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.
Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.
* Guided-auth removal, OAuth smoke testing, and TUI/CLI support
* Added CLI OAuth runner flags tests
* Added clarifying comment on port 0 binding
* README notes on TUI oauth callback port, prettier fixes for CLI tests
* CIMD validation uses inline like EMA IdP
* Fixed TUI test failures from merge
* test: bring 5 OAuth files over the 90 per-file coverage gate
The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:
- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
provably-dead guards (empty host, out-of-range port, String(err)
fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
the stale-onChange-after-close prev===null guard, and the
timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
kinds, the expired IdP session, and the configured-scope/empty-scope
branches of formatScopes.
npm run coverage now passes (exit 0) across web, cli, tui, launcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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).
* feat(web,core): import client configs and registry server.json (#1348)
Wire up the two server-import affordances that were previously todoNoop:
- Import config: source picker (Claude Desktop / Cursor / Cline / VS Code)
read on the backend via a new GET /api/import-source, plus a file-upload
fallback. Per-server review with import/skip on new servers and
overwrite/skip/rename on id conflicts, then a per-server outcome summary.
- Import server.json: parse the MCP registry single-server format
(npm/pypi/oci packages + remotes, env vars, package/runtime args, URL
template vars) into a runnable config, with a file picker and debounced
live validation; Add Server is disabled until validation passes.
Core gains an isomorphic import strategy layer (core/mcp/import) with a
strategy registry, client-config + server.json parsers, a strategy-agnostic
merge, and a dependency-injected source resolver. useServers exposes
importSource(). Modal headers are bold and the redundant server.json section
header is folded into the modal title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): collapse the server.json textarea into a File Contents disclosure
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): auto-collapse File Contents disclosure after content loads
Once content is pasted or a file is loaded, collapse the File Contents
disclosure after ~1s with Mantine's built-in accordion animation, signaling
the input was accepted and surfacing the validation/env-var sections. The
disclosure becomes controlled by the modal; clearing the textarea re-opens it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): flash File Contents hover highlight before auto-collapse
Before the File Contents disclosure auto-collapses, paint it with its hover
background for 250ms, then run the collapse over a 325ms animation, so the
collapse reads as intentional.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): rename "Import server.json" to "Import from registry config"
Rename the Add Servers menu entry and the modal header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): rename "Import config" to "Import client config"
Rename the Add Servers menu entry and the modal header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web,core): move import orchestration into core helpers + web hooks
Make the import logic reusable by CLI/TUI and keep the modals display-only:
- core/mcp/import: add parseClientConfig (mcpServers→servers fallback),
resolveServerId, selectServerJsonOption, and buildServerConfigForSelection,
so the full import (parse → select → build → merge) is callable from any
environment.
- core/storage: extract the pure validateStoreId into a Node-free store-id
module (store-io re-exports it) so the isomorphic import layer can validate
ids without pulling Node deps into the browser bundle.
- clients/web/hooks: useServerJsonImport and useImportClientConfig own all the
wizard/parse/debounce/apply state; ServerImportConfigModal and
ServerImportJsonModal are now thin display shells over the dumb panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): rename "Import client config" to "Import from client config"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): remove the Cancel button from the registry import modal
The modal header X (and Escape) still dismiss it; drop the redundant Cancel
button (and the panel onCancel prop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): hide validation results + name override until content is present
In the registry import modal, the Validation Results section and the Server
Name override field stay hidden until something has been pasted/loaded into the
textarea.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make Validation Results a collapsible disclosure
Wrap the registry import modal's validation results in a Mantine accordion
disclosure (default open), matching the File Contents section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): Server Name disclosure with read-only derived name + override
Make Server Name a collapsible disclosure containing a read-only "From
configuration" field (the id derived from the server.json name) and the
editable Override field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): scroll to and highlight a freshly-added server
After a manual add/clone or an import (client config / registry), the new
server's card scrolls into view and draws an animated border (Mantine Border
Animate); clicking anywhere on the card dismisses it.
- App tracks highlightedServerId; addServerHighlighted sets it (edits and
conflict-overwrites don't). Threaded through InspectorView -> ServerListScreen
-> ServerCard.
- ServerCard scrolls into view on highlight and wraps in <BorderAnimate>;
onClick clears the highlight.
- Pin @gfazioli/mantine-border-animate@2.0.3 (Mantine 8 compatible; 2.0.4+
requires Mantine 9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): highlighted card fills its cell and its controls work on first click
- The BorderAnimate wrapper is display:flex, so the card was shrinking to
content width while the border spanned the cell. Stretch the card with
w=100% (and the wrapper).
- Always render BorderAnimate and toggle show/animate instead of conditionally
wrapping: swapping the element type on clear remounted the card, swallowing
the click that triggered it (e.g. the connect toggle fired clear but not
connect, needing a second click). Verified: one click now connects + clears.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web,core): address review — OCI env, untrack fixture, rename validation
- Untrack the stray inspector-v1-multi.server.json (accidentally committed) and
gitignore root *.server.json fixtures.
- core/mcp/import/serverJson: forward declared env vars into OCI containers via
'-e KEY' docker args (they previously only set the docker CLI process env);
add a test. Clarify buildServerConfig (destructure env out instead of
spread-then-delete) and document value-vs-default env handling.
- useImportClientConfig: validate 'rename' targets inline (invalid charset /
collision with existing, additions, or other renames) and gate the Import
button on canImport; surface the error on the field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): highlight all servers from a batch import, not just the last
Track highlighted servers as a set of ids instead of one. addServerHighlighted
accumulates each id (a multi-server client-config import highlights them all);
opening an add/import modal starts a fresh batch. Clicking a card clears only
that card's highlight; only the first highlighted card scrolls into view.
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(web): catalog vs read-only session launch model + apply --header (#1481, #1483)
Per specification/v2_catalog_launch_config.md, `mcp-inspector --web` now selects
the backend's server-list source and a session-wide writability, instead of
seeding an inert `initialMcpConfig` the UI never read:
- `--catalog <path>` (and `MCP_CATALOG_PATH`) → active WRITABLE catalog
(seed/CRUD/watch), like the default catalog.
- `--config <path>` → READ-ONLY session file: served in the UI but never
written, seeded, or plaintext-secret-migrated — safe to point at a foreign
config (e.g. claude_desktop_config.json).
- ad-hoc `--server-url`/command (+ `--header`) → one server held IN MEMORY
(no file written); `--header` is lifted onto the entry so the seeded
connection actually uses it (closes the #1483 warn-only no-op).
- nothing → default catalog (writable), unchanged.
Backend (`createRemoteApp`): new `writable` + `initialServers` options. GET
/api/servers branches three ways (in-memory / read-only file with no
seed+no migrate / writable catalog); the four mutating routes return 403 when
read-only; `/api/config` carries `writable`; the file watcher is skipped for
in-memory sessions. `--catalog`/`--config` are mutually exclusive and neither
combines with an ad-hoc target or `--header`; `--header` requires an ad-hoc
HTTP/SSE server (clear errors otherwise — no silent no-op).
UI: `useServerListWritable` reads `writable` from /api/config; the Servers
screen hides add/edit/clone/remove/reorder/settings and shows a read-only
banner when false. All new props default to writable=true (back-compatible).
Docs: spec gaps G2/G3/G4 updated; launcher README documents the flags.
Verified end-to-end against the built launcher: --config (writable:false,
file byte-stable, mutations 403), ad-hoc --header (in-memory, header lifted),
and --catalog (writable:true, POST persists).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): address review — Mantine <Code> in read-only banner, clarify watcher comment
- Replace raw <code> with Mantine <Code> in the read-only-session Alert
(repo convention: prefer Mantine primitives over raw HTML).
- Add a comment near `watchable` clarifying that a read-only `--config` file
is still watched on purpose (read-only blocks UI writes, not external
observation; `writable` does not gate watching).
Deferred (per reviewer): consolidating useSandboxUrl + useServerListWritable
into one useConfig hook to avoid the duplicate /api/config fetch — optional
follow-up. The redundant ad-hoc header type-guard is kept as defensive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#1323. Adds an `error`-event-driven `lastError` on useInspectorClient and a mid-session failure toast in App.tsx; the transport onerror is scoped to non-handshake (status !== connecting) so handshake failures stay on the connect() rejection path. Follow-up: #1490.
* feat(web): reorderable ServerCards — drag-and-drop + keyboard (#1369)
Let users control the order of servers on the Servers screen instead of
hand-editing mcp.json. Each ServerCard gets a grip handle (in the header,
left of the name) that drives an @dnd-kit sortable; pointer and keyboard
sensors both work, with live-region announcements for screen readers.
- Backend: PUT /api/servers/order rewrites mcp.json in the supplied order
inside the write lock, reusing the atomic-write + watcher-notify path.
Registered before /:id so "order" isn't captured as a server id. Rejects
with 409 when the id set no longer matches disk and 400 on duplicate /
malformed bodies, so a reorder racing an external edit can't drop or
duplicate an entry.
- Hook: useServers.reorderServers(orderedIds) — optimistic local reorder,
PUT, refresh-to-disk-truth on failure.
- UI: SortableServerCard wrapper owns the dnd-kit concerns; ServerListScreen
wraps the grid in DndContext + SortableContext (rect strategy) with
pointer + keyboard sensors and announcements. Reorder math and a11y copy
extracted to reorderIds.ts / serverReorder.ts for direct unit testing.
Renders plain cards (no grip) when no onReorder is wired.
- Wired onServerReorder through InspectorView -> App.tsx.
Tests: pure-helper units, hook (persist / optimistic / 409 revert), 9
backend integration tests (incl. id-set-mismatch rejection and the
route-registration guard), ServerCard/ServerListScreen units, and a
real-browser Storybook keyboard-reorder play (the dnd-kit keyboard sensor
needs layout rects that happy-dom doesn't provide).
Adds @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): notify on reorder failure; skip reorder glue when not reorderable
Address PR #1447 review:
- App.tsx: catch reorderServers rejection (409 from a racing external edit,
or a network error) and show a red toast, matching the error-handling
pattern used by every other mutation — previously the rejection was
discarded with `void` and the drag silently bounced back.
- ServerListScreen: only build handleDragEnd / announcements when
`reorderable`, so the four-closure announcements object isn't allocated
every render when no onReorder is wired.
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(web): wire the listChanged indicator across all screens (#1402)
The per-screen list-changed indicator was hardcoded false in
InspectorView for Tools, Apps, Prompts, and Resources, so it could
never light up. Source it from the managed-state layer instead.
Each ManagedXState now tracks a listChanged flag: set true when its
*/list_changed notification arrives (the existing auto-refresh leaves
it set), exposed via getListChanged()/a listChangedChange event, and
cleared by clearListChanged() or on disconnect. The useManagedX hooks
surface the flag and clear it on a user-initiated refresh (the
indicator's Refresh button = acknowledge). App threads the three flags
to InspectorView, which maps them to the screens — Apps shares the
tools flag since apps are a filtered view of tools.
Tests cover set/clear/disconnect at the state-manager layer, the hook
subscription + refresh-clears behavior, and the InspectorView routing
(including Apps sharing the tools flag and no cross-wiring between
screens).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): close clear-after-await race in managed-list refresh (#1402 review)
The useManagedX refresh() callbacks cleared the listChanged flag AFTER
awaiting the fetch. If a */list_changed arrived mid-fetch, the state
re-set the flag (and auto-refreshed), then the trailing clear wiped that
genuinely-new signal. Clear BEFORE the await instead: the user's refresh
acknowledges only the change in hand, and a mid-fetch change re-sets the
flag and survives. Adds a race regression test on the tools hook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): drop duplicate Refresh on the Apps screen (#1402)
AppControls uniquely had a standalone toolbar Refresh button AND the
ListChangedIndicator on a separate row, so once the indicator could
actually appear (this PR), the Apps screen showed two Refresh buttons.
Match the other three controls (Tools/Prompts/Resources): the indicator
is the single header-right refresh affordance. Updates AppControls and
AppsScreen tests to assert exactly one Refresh (and none when the list
hasn't changed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(core): stop auto-invoking */list on list_changed; user pulls via Refresh (#1402)
With auto-refresh on, the list was already current by the time the
list-changed indicator appeared, making its Refresh button moot. Now the
managed Tools/Prompts/Resources state managers only flag the change on
*/list_changed (they no longer auto-refresh); the list stays as-is until
the user clicks Refresh, which fetches the new list and clears the
indicator. The connect-time initial load is unchanged.
Resource templates (no own indicator, part of the Resources screen) also
stop auto-refreshing; onRefreshResources now refreshes both resources and
templates, since a single resources/list_changed covers both. Tasks are
left auto-refreshing — they have no list-changed indicator and need live
status updates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(core): correct ManagedResourceTemplatesState header comments (#1402 review)
The class no longer subscribes to resourceTemplatesListChanged; update
the stale file/class docstrings that still claimed it syncs on that
notification to reflect connect + statusChange only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): add per-server 'Auto Refresh on List Changed Notifications' option (#1402)
New Options section in ServerSettingsForm with a single checkbox. When
checked, the managed Tools/Prompts/Resources (and resource templates)
state auto-refreshes on */list_changed notifications; when unchecked
(default) the notification only lights the list-changed indicator and the
user pulls via Refresh.
Plumbing: new optional autoRefreshOnListChanged on InspectorServerSettings
and StoredMCPServer, round-tripped through serverList (lifted with a false
default, persisted only when true, added to the exhaustive field-key map);
surfaced from InspectorClient via a new getServerSettings() on the protocol;
the state managers read it live at notification time to decide auto-refresh
vs flag-only. Resource templates re-subscribe to their list_changed but only
auto-refresh when the option is on (they have no indicator of their own).
Tests cover the round-trip (true persists, false omitted), the form
section (render/checked/toggle), and each state manager's on/off branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): persist autoRefreshOnListChanged through validateSettings (#1402)
The PUT /api/servers write path runs settings through validateSettings,
which rebuilds the value from explicitly-named fields and silently drops
unknown keys — so autoRefreshOnListChanged never reached disk (the
serverList round-trip test passed because it calls
inspectorSettingsToStoredFields directly, bypassing this path). Validate
it as an optional boolean and include it in the built value; false is
omitted on disk by inspectorSettingsToStoredFields as before.
Adds integration tests over the real PUT path: true persists, false is
omitted, and a non-boolean is rejected (400).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): default the Server Settings modal to the Options section (#1402)
Open 'Options' instead of 'Custom Headers' when the Server Settings modal
first opens, so the new Auto Refresh toggle is the first thing surfaced.
Updates the modal default-expanded section, the modal test's initial-state
assertions, the three header tests (now expand 'Custom Headers' first since
it's no longer open by default), and the story defaults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: drop accidentally-committed Inspector history dump; gitignore the class
83076441 swept an exported Inspector message-history JSON (manual-testing
artifact at the repo root) into the tree via git add -A. Untrack it and
extend the existing repo-root debug-artifact .gitignore block (alongside
/*.png) to cover /inspector-history-*.json so these can't be re-added.
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(web): show negotiated MCP protocol version on ServerCard (#1324)
Add optional protocolVersion to ConnectionState and render it as a
dimmed "MCP <version>" label at the end of the transport-badge/
description row on ServerCard, shown only when connected. The value
is plumbed through useInspectorClient as part of #1324.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(core,web): surface negotiated protocolVersion end-to-end (#1324)
Plumb the MCP protocol version negotiated during initialize from the
InspectorClient through to the ServerCard so the active connected card
displays "MCP <version>".
The SDK Client doesn't expose protocolVersion; it stamps it onto the
transport via setProtocolVersion. MessageTrackingTransport now captures
it (and forwards to the base transport when present), so it works for
stdio too — whose base transport has no setProtocolVersion.
- core: MessageTrackingTransport captures + exposes protocolVersion;
InspectorClient stores it, exposes getProtocolVersion(), dispatches a
protocolVersionChange event, and clears it on disconnect.
- core: useInspectorClient subscribes and returns protocolVersion.
- web: App reads it from the hook (drops the hard-coded "2025-06-18")
and InspectorView splices it onto the active server's connection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): decouple initializeResult from protocolVersion (#1324 review)
Gate the initializeResult memo on serverInfo only, not protocolVersion.
initializeResult also drives the connected ViewHeader and Connection Info
modal, so requiring the version risked hiding the whole connected surface
in the edge case where it's absent. protocolVersion now flows through as
the optional field it already is everywhere downstream ('' reads as
unknown; the ServerCard label and modal value both tolerate it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): drop redundant connected-check in InspectorView splice (#1324 review)
initializeResult is derived from connectionStatus in App's memo, so a
truthy initializeResult.protocolVersion already implies a live
connection — the extra connectionStatus === 'connected' guard in the
splice was redundant. ServerCard keeps its own status guard (dumb-
component robustness). No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the ported Tasks screen + core task subsystem through App.tsx: sticky Clear Completed, cancel/refresh error toasts, core-side per-task progress correlation, Run-as-task on the Tools screen (gated on server support), per-server Task TTL, and live task-status toasts.
* feat(web): render pending sampling/elicitation requests (#1407)
Server-initiated sampling and elicitation requests never reached the v2
web UI: the InspectorClient queued them and returned a Promise that only
resolved on a user response, but nothing subscribed to the pending-change
events or rendered the (already-built) request components. A tool that
triggered one spun the Execute button forever.
Wire the queues to the UI:
- Add usePendingClientRequests() (core/react) subscribing to
pendingSamplesChange / pendingElicitationsChange and returning the live
arrays. Expose getPendingSamples() / getPendingElicitations() on
InspectorClientProtocol (and the test fake) so hooks can read them.
- Add PendingClientRequestModal, an App-level modal that surfaces one
queued request at a time (sampling-first) via the existing
SamplingRequestPanel / ElicitationFormPanel / ElicitationUrlPanel. It is
non-dismissable so the blocked call only resolves via an explicit action.
- Wire it in App.tsx: responding/rejecting resolves the client's handler
Promise, which unblocks the originating call and clears the spinner.
Also fold in two issues surfaced while verifying:
- SchemaForm rendered an `array` + `items.enum` (multi-select enum) as a
JSON textarea because only `items.anyOf` had a branch; add a MultiSelect
branch with enumNames label support.
- ToolsScreen's result lives in App and outlived the screen's local
selection (reset on tab-switch unmount), leaving a result with no
selected tool. Clear the result on unmount for a clean slate on return.
Prompts/Resources gate their preview on selection, so they are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(elicitation): support explicit decline; honest queue label; review polish
Address @claude review on #1411:
- Add an explicit "Decline" action to ElicitationFormPanel so the spec's
decline (refusal) is distinguishable from cancel (dismissal); the JSDoc
claimed accept/decline/cancel but only accept/cancel were wired. Wire the
modal body to emit { action: "decline" }.
- Replace the misleading "1 of N" queue label (the leading 1 never changed
since the modal always shows the head) with an "N pending" hint, hidden when
nothing else is queued.
- Make the dead useMemo honest: derive the head request inside the memo from
the source arrays so it actually caches (the inline activeElicitation had a
fresh identity every render).
- Document the URL elicitation accept-on-open semantic with a comment pointing
at the follow-up (#1415).
- Add an App-level round-trip test driving pendingSamplesChange end-to-end
(modal opens, respond() fires, empty event closes it).
Follow-ups filed: #1415 (URL two-step completion flow), #1416 (form required-
field submit gate + double-submit guard).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(elicitation): include untouched schema defaults in submitted content
The schema form displays a field's default via resolveValue(), but onChange
only writes a field into `values` once the user edits it. So an elicitation
submitted with default-only fields left untouched dropped them entirely —
v2 sent just the manually-edited fields while v1 sent defaults too (reported
with the everything server: firstLine/integer/number/enum defaults missing).
Add collectSchemaDefaults() to jsonUtils (recurses nested objects, omits
fields with no default) and seed ElicitationFormModalBody's values state with
it via a lazy initializer. The modal body is keyed by request id, so it
re-seeds per request; user edits still override seeded defaults via the
existing onChange merge.
Tests: collectSchemaDefaults unit tests (field types, nested, empty) in
jsonUtils.test; a modal test asserting an untouched-default form still submits
its defaults in content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(forms): seed schema defaults for tool-call and app-launch forms too
Extend the elicitation defaults fix (collectSchemaDefaults) to the other two
SchemaForm consumers that own form state and submit it: ToolsScreen (tool
execute args) and AppsScreen (app launch args). Both reset formValues to {} on
selection, so default-only fields the user never edited were dropped from the
submitted args — same root cause as the elicitation modal.
Seed formValues from the selected tool/app inputSchema defaults on select.
User edits still override via the existing onFormChange merge.
(InlineElicitationRequest has no consumers wired up, so it's left as-is.)
Tests: ToolsScreen sends a tool's default on Execute without editing;
AppsScreen sends an app's default on Open App without editing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(elicitation): gate form submit on required fields; guard double-submit (#1416)
Fold the #1416 hardening into this PR:
- Gate the elicitation form's Submit button on required fields. Promote the
hasMissingRequiredFields helper out of AppDetailPanel into jsonUtils (shared)
and use it in ElicitationFormPanel so Submit is disabled until every required
field has a value.
- Guard against double-submit. Add an optional `busy` prop to the sampling,
elicitation-form, and elicitation-url panels that disables their responding
actions. PendingClientRequestModal's bodies use a new ref-guarded
useRespondOnce() hook: the first action dispatches and locks the panel; later
clicks no-op. This stops a fast double-click from calling respond()/reject()
twice (which throws and produced an unhandled rejection). Copy URL stays
enabled — it doesn't resolve the request.
Tests: hasMissingRequiredFields unit tests; ElicitationFormPanel submit-gate
and busy-lock tests; a modal double-click-resolves-once test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tools): cap detail/result panels at viewport and scroll internally
The Tools screen's detail and result columns sized to their content and bled
past the bottom of the viewport on long output (e.g. the env dump or a long
elicitation result). Cap each column at the screen's available height and let
its panel scroll internally instead.
- ToolsScreen: drop `align: flex-start` and wrap each column in a stretched
`ContentPane` (Flex column, capped by `mah` = viewport − header − xl*2). The
pane's definite height is what lets the card's inner ScrollArea know how far
to shrink — a bare `mah` on the card does not (the card sizes to content, so
the scroll region renders full-height and overflow:hidden just clips it,
unreachable). Mirrors the proven Prompts/Resources preview panes.
- ToolResultPanel: pinned header + inner ScrollArea (flex 0 1 auto, mih 0).
- ToolDetailPanel: pinned title/annotations header, scrolling body
(description + form + progress), pinned Execute footer.
Audited the other screens: Prompts/Resources already use this pattern,
ServerList and the four list panels (Logging/Network/History/Tasks) already cap
via ScrollArea.Autosize. Only AppsScreen's app-input form had the same gap —
fixed it the same way: AppDetailPanel now scrolls internally and the card uses
variant="preview" so the running app's iframe and a long form both stay within
the viewport.
Verified in-browser against the everything server: the get-env result now
stops at the viewport bottom with margin and scrolls within the card
(clientHeight 502 vs scrollHeight 1931), header pinned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(remote): use sync pino destinations in logging tests to fix teardown race
CI flaked with an uncaught `ENOENT: ... /tmp/inspector-log-unknown-level-*/app.log`
that failed an unrelated later test. The remote-logging integration tests create
`pino.destination(logFile)` loggers (async sonic-boom streams) and rmSync the temp
dir in finally/afterEach without closing the stream — a buffered background flush
then fires after the dir is gone and throws an uncaught error, which Vitest counts
as a run failure even though all tests pass.
Make those destinations `sync: true` so the write lands during the request instead
of on a later flush, removing the race. Test-only; unrelated to this PR's feature
work but it was blocking the PR's CI.
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(apps): render running MCP Apps end-to-end (#1370)
Wire the v2 Apps screen so clicking Open App actually runs an MCP App.
Previously App.tsx stubbed the three load-bearing pieces (sandbox URL,
bridge factory, action handlers), so the iframe loaded about:blank and
nothing crossed the bridge.
- Add useSandboxUrl hook (core/react) to read sandboxUrl from GET
/api/config and thread it down as sandboxPath; render an "MCP Apps
unavailable" state instead of a blank iframe when absent.
- Add createAppBridgeFactory: builds a host-side ext-apps AppBridge over
the active client's SDK client, reads the tool's UI resource on the
sandbox handshake and pushes it into the proxy, handles openLinks, and
connects a PostMessageTransport.
- AppRenderer: pass the tool to the bridge factory; buffer tool
input/result and release them once the view signals `initialized`
(input before result, latest-wins).
- Move the AppRenderer handle ref to App.tsx so onOpenApp issues
tools/call with the form values and delivers input + result to the
running app via the bridge; onCloseApp tears the bridge/iframe down.
- Stories: themed sandbox placeholder (visible in light/dark), an
echo-bridge running story, and an unavailable story. New unit tests
for the factory, the buffering, useSandboxUrl, and the empty state.
The listChanged indicator badge wiring is tracked separately in #1402.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): make MCP Apps render in dev — single bridge + result passthrough
Two bugs kept apps (e.g. NightRider's MIDI Pattern Editor) stuck on an empty
shell even though the host fetched the resource and called the tool correctly.
1. StrictMode double-mounted the bridge. The dev-only effect setup→cleanup→
setup built TWO host bridges/transports, double-loaded the sandbox, and
raced the app's ui/initialize handshake (it timed out, so `initialized`
never fired and buffered input/result were never flushed). AppRenderer now
defers bridge disposal to a microtask and, when the effect re-runs
synchronously with identical inputs, cancels the disposal and reuses the
live bridge — so a single bridge survives the double-invoke. The pending
input/result buffer is no longer cleared on a transient cleanup.
2. The SDK rejected the server's result. callTool() validates structuredContent
against the tool's outputSchema and throws on a mismatch — but an MCP App is
a passthrough: the host must forward whatever the server returned to the
running view (which legacy hosts render fine). InspectorClient.callTool gains
a `skipOutputValidation` option that routes through client.request() (no
wrapper validation); onOpenApp uses it.
Verified end-to-end against a real app via Playwright: the editor now renders
its grid + pattern data identically to the v1 inspector.
Tests: AppRenderer StrictMode single-bridge/no-dispose case; a get_temp_extra
test-server fixture (returns an extra structuredContent property) with
integration tests covering both the throwing default path and the
skipOutputValidation passthrough.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): warn (toast) when an app's result violates its outputSchema
The skipOutputValidation passthrough keeps schema-mismatched apps rendering,
but that silently hid a real spec problem from server developers. Now the
host still renders, but surfaces the mismatch.
- InspectorClient.callTool runs the SDK's own Ajv output validation on the
skipOutputValidation path NON-fatally and returns the message as
`outputValidationError` on the ToolCallInvocation (and the
toolCallResultChange event); the result is still delivered.
- App.tsx onOpenApp shows a yellow toast when outputValidationError is set,
explaining strict MCP clients may not render the app.
Tests: integration cases asserting outputValidationError is set for a
schema-violating result (get_temp_extra) and unset for a valid one (get_temp).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): summarize output-schema warning in toast + details modal
The previous toast dumped the full (very long) validation error and
overflowed. Now the toast shows a one-line summary with a "View validation
details" link that opens a modal containing the full error in a read-only,
scrollable textarea.
- New OutputValidationModal group component (+ story + tests).
- App.tsx: toast message is a summary + link; clicking it opens the modal
populated with the tool name and the full outputValidationError.
Verified via Playwright against the NightRider MIDI editor: toast is concise,
link opens the modal with the scrollable details.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(apps): address PR review — theme stability, onError, validation polish
Responds to the automated PR review on #1403.
- Theme toggle no longer rebuilds a running app's bridge. createAppBridgeFactory
reads the resolved theme from the DOM (Mantine's data-mantine-color-scheme) at
bridge-build time instead of capturing isDark in the factory deps, so the
factory identity is stable across theme flips. (#1)
- Wire AppRenderer's onError through AppsScreen -> InspectorView -> App.tsx,
surfacing bridge/runtime failures (no client, late rejection, failed
tools/call) as a red toast instead of a silent blank iframe. (#2)
- Extract validateToolOutput into a pure, unit-tested helper covering the
matching / extra-property / missing-structuredContent / error / bad-schema
cases (the no-structuredContent branch can't be produced via the SDK server,
which rejects it before sending). (#4)
- useSandboxUrl aborts its in-flight fetch on unmount (cancelled flag) to avoid
setState after unmount. (#5)
- Schema-mismatch toast: autoClose:false (details modal is one click away). (note)
- AppRenderer: add a state-machine comment block documenting the bridge
lifecycle refs. (#6)
- inspectorClient: note why the `as CallToolResult` casts are safe. (note)
Tests: AppsScreen onError surface; validateToolOutput unit suite; factory theme
read from DOM. validate + test:storybook green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(apps): clarify theme attribute + useSandboxUrl fetch comments
Re-review follow-ups (non-blocking):
- createAppBridgeFactory: note that Mantine's data-mantine-color-scheme is only
ever light/dark (auto is resolved before paint), so no `auto` branch is needed
and matchMedia only covers an absent attribute.
- useSandboxUrl: correct the doc to say it fetches on mount AND re-fetches on
baseUrl/authToken change (idempotent), not "once on mount".
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(server-info): wire Server Info modal to the connection handshake
Closes#1364
- Add ServerInfoModal wrapping the existing ServerInfoContent, mirroring the
ServerSettingsModal layout.
- Gate the Server Info button on ServerCard to render only when the
connection is "connected".
- Expose `getClientCapabilities()` on InspectorClient (and protocol +
FakeInspectorClient), and surface it through useInspectorClient so the
modal can render the client side of the capability handshake without
poking the SDK Client's private state.
- App.tsx: open the modal from the gated Server Info button, derive the
active server's transport and OAuth details (synchronously, from the
guided-OAuth state snapshot + persisted settings), and reset the open
flag on InspectorClient `disconnect` so the modal does not auto-reopen
on a future reconnect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(server-info): dedupe modal header and render instructions in ContentViewer
- Drop the in-content "Server Information" Title from ServerInfoContent and
rename the modal title to "Server Information" so the modal renders one
heading instead of two stacked ones.
- Swap the instructions Blockquote for ContentViewer (copyable, with the
existing wrapping Code variant) so long instructions wrap and the user
can copy the full payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server-info): rename ServerInfo -> ConnectionInfo
The popup covers server identity AND client capabilities AND OAuth state —
it's a snapshot of the live connection, not just the server. Rename to match
what it actually shows. Also add a "Server Implementation" heading above
the top section (name/version/protocol/transport) to balance the existing
capability headings.
- ServerInfoContent -> ConnectionInfoContent (directory, file, exports)
- ServerInfoModal -> ConnectionInfoModal (directory, file, exports)
- ServerCard button label "Server Info" -> "Connection Info";
prop onServerInfo -> onConnectionInfo
- Modal title -> "Connection Info"
- App.tsx / ServerListScreen / InspectorView wiring updated to match
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(connection-info): cap instructions block height so it scrolls in place
A long server prompt (e.g. an Everything-style instruction document) was
pushing the OAuth section and the modal chrome off-screen — users had to
scroll the whole modal to reach the close button. Wrap the ContentViewer
in ScrollArea.Autosize with mah=280 so the instructions section scrolls
internally and the rest of the modal stays put.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(connection-info): add subtle background to section headings
Add a `section` variant for Title that paints the heading in the existing
`--inspector-surface-subtle` token (same surface token already used for
modal call-out panels). Apply it to each section heading in
ConnectionInfoContent (Server Implementation, Server Capabilities, Client
Capabilities, Server Instructions, OAuth Details) via a SectionHeading
subcomponent constant, so the sections read as distinct cards against the
modal background instead of bleeding into one stream of text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(stories): wrap long import line in ConnectionInfoModal.stories.tsx
Prettier flagged the import in CI. The line exceeded printWidth=80 and
needed to wrap onto multiple lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(agents): require `npm run format` before committing
Add a pre-commit instruction to run the auto-fixing `npm run format` ahead
of `npm run validate`. `validate` runs `format:check` (non-fixing) which
will fail CI on any unformatted file — running the auto-fixer first
eliminates that class of failure entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(favicon): use theme/assets/MCP.svg as the favicon source of truth
Point index.html's icon link at /src/theme/assets/MCP.svg so the same MCP
logo asset already imported by ViewHeader is also the favicon. Delete the
now-unused clients/web/public/favicon.svg. Vite resolves the source path
during build and emits a hashed copy under dist/assets/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(server-card): surface failed-handshake errors with a 5s auto-dismiss
Two related fixes so the user actually sees what went wrong when a
connect attempt fails (e.g. OAuth-backed server, bad URL, missing dev
auth token):
- App.tsx: add `connectInFlightRef` and skip clearing `activeServerId`
in the InspectorClient `disconnect` listener while a connect attempt
is in flight. The transport closes during a failing handshake and
dispatches `disconnect` before the catch block sets `errorMessage`;
without this guard InspectorView would map the card to plain
"disconnected" and the InlineError would never render.
- InlineError: opt-in `autoDismissMs` prop that runs a Mantine `slide-up`
Transition exit animation after the message has been visible for the
given duration. Visibility is derived from a "dismissed message"
state (rather than a mounted boolean + setMounted-in-effect), so the
timer re-arms whenever `error.message` changes without tripping the
react-hooks/set-state-in-effect rule. ServerCard passes
autoDismissMs={5000}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server-card): own InlineError visibility + animate parent-driven removal
Move the 5s auto-dismiss timer and visibility state out of InlineError
and into ServerCard so the slide-up exit animation runs in both cases:
- Timer-driven: after 5s the alert animates out (existing behavior).
- Parent-driven: when `connection.error` is cleared mid-flight (e.g. a
successful reconnect), the alert ALSO slides up instead of vanishing.
To keep the message painted during the parent-driven exit, ServerCard
tracks a `lastError` state that mirrors `connection.error` only on its
truthy edges. The conditional setState during render is the supported
React pattern for "remember the last seen value of a prop"; gated on
message inequality so it can't loop. The `useRef` version of this
pattern failed `react-hooks/refs` (no ref writes/reads during render).
InlineError is now a pure presentational component: `mounted` prop
controls a Mantine `slide-up` Transition. The internal `autoDismissMs`
prop is gone — its single caller has moved to the controlled `mounted`
pattern.
Closes the "error vanishes the moment the parent removes it" half of #1364
(the timer half landed in 6d147e0f).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server-card): surface connection errors as a toast, drop InlineError
Move handshake-failure messaging out of the ServerCard body and onto a
Mantine toast (`notifications.show`). The inline alert had a 5-second
visibility contract entangled with InspectorView's status pipeline, and
the active card was flipping through error → disconnected → error during
a failing handshake; surfacing the error as a toast sidesteps that whole
timing problem and keeps the card focused on the live status indicator.
- App.tsx: toast on the connect-catch path; drop `errorMessage` state +
`connectInFlightRef` (the latter was only there to keep the inline
alert visible past the transport's close event).
- InspectorView: drop the `errorMessage` prop and the
`connectionStatus === "error" && errorMessage` mapping branch.
- ServerCard: drop the InlineError rendering + the displayedError /
errorVisible visibility state machine.
- Delete the InlineError component — no other consumers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(notifications): anchor toasts to the bottom right
Move the Notifications portal from top-right to bottom-right so a
connection-failure toast doesn't overlap the ViewHeader / tabs row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: address PR feedback on the Connection Info modal
1. Hoist `EMPTY_CLIENT_CAPABILITIES = Object.freeze({})` to module scope
in `useInspectorClient` so the fallback returns a stable reference
(avoids invalidating downstream `useMemo`/`useEffect` deps when no
client is attached).
4. Drop the `?? "stdio"` silent fallback on `connectionInfoTransport`
in the JSX — gate the modal render on `activeServer` too. The
"stdio" default still lives at the derivation site (config.type is
schema-optional and `ServerType` is required by the modal prop),
but the gate makes the invariant explicit.
6. Replace the raw `document.querySelector("button.mantine-CloseButton-root")`
in `ConnectionInfoModal.test.tsx` with `screen.getByRole("button",
{ name: /close/i })`. Add `aria-label="Close"` to the
CloseButton so the assertion (and screen readers) have a name to
match on.
7. Switch the OAuth scope splitter from `/\s+/` to the literal `" "`
per OAuth 2.1 §3.3.
Also clarify in comments:
2. Note that `connectionInfoOAuth` uses snapshot-at-open semantics on
purpose — token refreshes mid-session won't re-render the modal.
5. Note that we prefer the AS-advertised `authorization_endpoint`
over `authorizationUrl` because the latter is per-flight noise
(state, code_challenge) rather than a stable identifier.
Finding 3 (modal exit animation short-circuited by the `{initializeResult
&& …}` guard) left as intentional — we want the snap-shut on disconnect
so a stale info dialog can't keep rendering with no live client behind it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(connection-info): tighten the OAuth memo comment per re-review
The reviewer noted that "snapshot-at-open" was imprecise — the memo
re-runs on any of its deps (connectionStatus, inspectorClient,
activeServer), which includes settings edits that re-reference the
active server. Reframe as "snapshot-at-last-derivation" and call out
the natural triggers (server switch, reconnect, settings edit) so a
future reader doesn't conclude the memo is open-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): make settings form re-render on every keystroke (#1361)
The settings modal is fully controlled by App.tsx, but pending edits were
stashed in a useRef — refs don't trigger re-renders, so the input's `value`
prop stayed stale until the 300ms debounce + PUT + refetch round-tripped.
Clicking "Add Header" / "Add Metadata" showed no row, and typing in OAuth
fields had no character echo, until that round-trip completed (~500ms).
The bug landed in #1353 with the debounced-flush pattern. It surfaced after
#1356 because per-keystroke PUTs now also write the keychain, making the
round-trip latency more noticeable.
Lift the draft into `useState`. Every change re-renders the modal
immediately. Keep the 300ms debounce on the PUT side. Initialize the draft
from the server-list entry only when the modal opens to a new target (not
on every `servers` change) so background refreshes don't clobber the user's
in-progress edits. Flush on close to preserve final keystrokes.
Verified live against the dev server: header/metadata rows appear on click,
OAuth Client ID / Client Secret typing echoes without lag, and the values
persist correctly (clientId in mcp.json, clientSecret in the macOS
Keychain).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(servers): extract useSettingsDraft hook + tests; review polish (#1361)
Picks up the three review observations on #1362:
1. Extract the draft state + debounce + flush logic from App.tsx into
`core/react/useSettingsDraft`. Same behavior; thinner App.tsx;
testable in isolation with `renderHook`. Tests pin the regression
#1361 was about (synchronous onChange → draft update) plus the
debounce window, burst-collapse, flush-on-close, error propagation,
and the "background refresh must not clobber in-progress edits"
contract via a `resolveInitial` closure swap.
2. Hoist the empty `InspectorServerSettings` fallback to a module-scope
`EMPTY_SETTINGS` const. Both the hook's init path and the modal's
closed-state fallback share the same object identity; the inline
`useMemo` that previously re-derived it is gone.
3. Add a comment on the `resolveInitial` callback noting that we read
`servers` from this render's closure under the assumption that the
entry point is the "Settings" button on a rendered server card.
Documents the contract for future callers (e.g. a keyboard shortcut
that opens the modal pre-hydration).
Validate clean: 1756 tests pass. New useSettingsDraft.test.tsx adds 13
test cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(servers): stabilize flush identity + add unmount-cleanup test (#1361)
Two non-blocking observations from the third review on #1362:
1. `flush` identity churned on every keystroke because the useCallback
closed over `draft` + `targetId`. Read both through refs instead so
the returned callback identity is stable. The behavior is unchanged
— but the consumer's `onSettingsModalClose` (which wraps `flush`)
no longer re-allocates per keystroke, which means the modal's
`onClose` prop identity also stays stable. A new test pins this.
2. Added a comment in `onChange` calling out that callers switching
`targetId` mid-debounce must `flush()` first to preserve the
pending PUT. The only switch path today (`onSettingsModalClose` in
App.tsx) does that already — the comment is for future refactors.
3. New unmount-cleanup test pins the intentional "drop final
<debounceMs window on unmount" trade-off — flushing on unmount
could fire a PUT against an unmounting component, which is a
worse footgun than losing a few hundred ms of typing on route
change / HMR.
Validate clean: 144 files, 1758 tests pass.
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): hot-reload server list on external mcp.json edits (#1345)
Watch ~/.mcp-inspector/mcp.json from the backend and push a change event
on a new /api/servers/events SSE channel. The web client subscribes via
fetch + ReadableStream (so the existing bearer-auth contract is
preserved — EventSource can't send custom headers) and re-fetches on
each event. The watcher is lazy: it starts on the first SSE subscriber
and stops when the last one disconnects, so tests that never open the
channel don't spin up a real fs watcher. Self-fires from the backend's
own POST/PUT/DELETE are suppressed by capturing the post-write mtime and
comparing it on each watcher event.
Chokidar is used for cross-platform editor-save semantics (temp-file +
rename produces unlink+add sequences that bare fs.watch handles poorly
on macOS/Linux); awaitWriteFinish coalesces those into a single event.
createRemoteApp now returns a close() to release the watcher, which the
standalone server and the vite dev plugin chain into their existing
close paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review on hot-reload (#1357)
- writeMcpAndTrackMtime now stat()s the file before writing; if the
mtime differs from the last value we recorded, an external editor
slipped in between our writes and chokidar would coalesce both into
one event whose mtime matches our own write — broadcast directly so
peer subscribers learn about the external edit (review finding 1, 5).
- Split useServers.refresh into a public refresh() (loading toggle) and
an internal refreshInternal(background) the SSE handler calls; the
list now stays visible during background refreshes instead of
flashing skeleton state on every external mcp.json edit (finding 2).
- Coalesce multiple \n\n frames landing in one decode chunk to a single
refresh call so back-to-back broadcasts don't race two concurrent
GETs whose setState order is unspecified (finding 3).
- Clarify createRemoteApp's close() docblock about in-flight SSE stream
callbacks (finding 7).
- Add integration coverage for PUT/DELETE self-suppression and for the
external-edit-between-backend-writes broadcast (finding 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address pass-2 review on hot-reload (#1357)
- Clarify writeMcpAndTrackMtime comment so a future reader can't
mistake the broadcast for a content-preservation guarantee: the
external edit's content may already have been read-around and
overwritten depending on the interleave with readMcpConfig.
- Drop the no-op GET in the PUT/DELETE self-suppression tests. The
pre-stat branch is short-circuited by `lastWrittenMtimeMs === null`
with or without it; matches the POST test's structure.
- Add explicit no-flicker regression test: tracks every `loading`
value across renders during an SSE-driven refresh and asserts none
flipped back to true. Catches a future regression that reaches for
setLoading(true) in the background path even under React batching.
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): persist per-server settings to mcp.json (#1352)
- Add optional `settings` node alongside the transport config in each
`~/.mcp-inspector/mcp.json` entry. Round-trips through
`mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and the
`/api/servers` POST/PUT routes.
- Drop `MCPServerConfig.headers` from `SseServerConfig` /
`StreamableHttpServerConfig` and remove the headers textarea from
`ServerConfigModal`. Custom headers are now entered only in
`ServerSettingsForm` and reach the wire via `settings.headers`.
- Wire `ServerSettingsModal` into `App.tsx`; saves dispatch through a new
`useServers.updateServerSettings` mutator. `updateServer` carries the
existing settings across config-only saves so they aren't dropped.
- Apply settings on the *first* outbound request after a server loads —
no need to open the settings form first:
- `settings.headers` → SSE / streamable-http wire headers.
- `settings.metadata` → `InspectorClientOptions.defaultMetadata`,
merged into every outgoing `_meta` (call-time keys win).
- `settings.requestTimeout` → `InspectorClientOptions.timeout`.
- `settings.connectionTimeout` → Promise.race wrapper around connect().
- `settings.oauth*` → pre-seeded OAuth client credentials.
- `RemoteConnectRequest` carries `settings` so the remote backend can
source transport headers from the same source as the local path.
- `oauthClientSecret` is persisted in `mcp.json` alongside stdio `env`
values, both protected by the file's `0o600` permission.
- Update `specification/v2_servers_file.md` with the new shape and the
removal of the legacy headers field.
Tests: round-trip in `serverList.test.ts`; persistence on POST/PUT in
`servers-route.test.ts`; first-connect header injection in
`transport.test.ts`; `defaultMetadata` merging in
`inspectorClient.test.ts`; `useServers.updateServerSettings` +
`updateServer` settings-preservation in `useServers.test.tsx`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): rebuild InspectorClient on every connect so settings refresh
The previous onToggleConnection branch reused the existing InspectorClient
on a same-server reconnect, which froze settings at the moment the client
was first constructed. Editing headers, metadata, timeouts, or OAuth
credentials in the settings form was therefore invisible until the user
either switched servers or reloaded the page — surprising right after a
visible save.
Always call setupClientForServer on (re)connect so the latest
target.settings flow through to the transport's headers / requestInit, to
defaultMetadata, and to the OAuth manager.
Also adds remoteClientTransport tests that pin the wire-level contract:
settings round-trip through the /api/mcp/connect body when supplied, and
the field is omitted when no settings are configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(history): stop wiping pendingRequestEntries inside the connect event
State managers (ManagedToolsState, ManagedPromptsState, ManagedResourcesState,
ManagedResourceTemplatesState, ManagedRequestorTasksState) handle the
client's "connect" event by calling `void this.refresh()`. The synchronous
portion of refresh runs through SDK Protocol.request → transport.send →
MessageTrackingTransport.send → trackRequest, all before yielding to the
network — so each list request gets tracked into MessageLogState's
pendingRequestEntries inside the same connect event dispatch.
MessageLogState's own "connect" listener was registered after those state
managers (it's the last one constructed in App.tsx) and cleared
pendingRequestEntries on every connect. Listener order meant the clear ran
*after* the requests were tracked but before their responses arrived,
leaving the history with unmatched "response" entries marked PENDING.
The clear-on-disconnect (via statusChange → disconnected) already handles
session-boundary cleanup, so clear-on-connect was purely defensive and now
actively harmful. Drop the connect listener; keep the disconnect path.
Tests: updated the unit test to assert the new behavior (connect does not
clear); added an integration regression that wires a sibling connect
listener firing listTools and asserts the response folds into the request
entry instead of appearing as an orphan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: drop accidentally tracked debugging screenshot
pending-requests.png was a local debugging artifact attached to a
conversation, not source. Remove it from the tree (the working copy is
kept so the user still has it locally) and rely on the existing untracked
behavior going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(servers): add e2e check that settings.headers reach upstream MCP
Stitches the full v2 pipeline together in one assertion:
browser-style RemoteClientTransport
→ POST /api/mcp/connect (settings in body)
→ backend createTransportNode applies settings.headers to requestInit
→ SDK StreamableHTTPClientTransport
→ upstream MCP test server
Captures the upstream HTTP request via FetchRequestLogState and asserts
both `X-Tenant` and `X-Trace` are present on the outbound POST. Catches
any regression that breaks the wire between the form and the upstream
server in a single integration test, instead of the previous coverage
which exercised each hop in isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ServerSettingsModal): invert ListToggle compact prop so icon matches state
ListToggle's `compact` prop represents the list's *current* compact state:
true means "currently compact" and renders the expand icon to advertise
the action (open up). The settings modal was passing
`compact={allExpanded}` — exactly inverted. When all sections were open
it showed the expand icon (suggesting "open more") and when collapsed it
showed the collapse icon, opposite of every other ListToggle caller in
the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(HistoryListPanel): match ServerListScreen Export button — rename + default variant
Renames "Export JSON" to "Export" and switches from the subtle ToolbarButton
to the same `Button variant="default"` used by ServerListControls so the
two Export affordances look identical across screens. Also moves the
button to the right of the ListToggle (mirroring the order on the server
list screen) and disables it when there are no entries to export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(HistoryListPanel): move Clear button to top toolbar, match Export style
Moves the Clear affordance out of the "History (N)" section header and
up to the panel toolbar alongside Export, where it now uses the same
`Button variant="default"` styling. Order in the toolbar is now:
ListToggle · Clear · Export. Clear is disabled when there are no
unpinned entries to remove (pinned entries are intentionally preserved).
ToolbarButton helper is unused after this change and dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: ignore local debugging screenshots at repo root
Stops `git add -A` from re-staging the `*.png` files Claude Code drops
at the repo root from attached conversation screenshots. The pattern
only matches the top-level (`/*.png`) so legitimate image assets in
subdirectories (e.g. `clients/web/public/`) still get tracked when
explicitly added.
Also untracks the two screenshots that slipped through previously
(initialize-request.png, pending-requests.png).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(LogStreamPanel): match HistoryListPanel button styling
Clear / Export / Copy All on the Logging screen were rendered with the
subtle ToolbarButton helper while History's equivalents use
`Button variant="default"`. Switch all three to the default variant so
the toolbars look identical across screens and mirror the disabling
behavior — each button is now disabled when `entries.length === 0`.
ToolbarButton helper is unused after this change and dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review — debounce save, preserve-on-omit PUT, validate shape, lift connect timeout
Round-up of fixes from https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4526861959:
1. ServerSettingsModal saves were firing on every keystroke (one PUT +
full mcp.json refresh per character). App.tsx now debounces by 300ms
and flushes on modal close so a burst of edits coalesces into one PUT
and the form value can't flicker back mid-typing.
2. `PUT /api/servers/:id` with an omitted `settings` field used to
silently wipe the persisted settings node. The route now treats the
three intents distinctly:
- field omitted → preserve existing on-disk settings
- explicit null → clear the settings node
- settings object → validate + apply
`useServers.updateServer` is simplified to drop the client-side
carry-through hack, and the test that documented the broken behavior
is flipped + augmented with explicit-clear and bad-shape cases.
3. `connectionTimeout` Promise.race is moved from App.tsx into
`InspectorClient.connect()`, reading `this.serverSettings?.connectionTimeout`.
TUI/CLI consumers now get the same behavior; on timeout the client
internally disconnects so the next connect starts clean.
4. Custom Headers hint in `ServerSettingsForm` notes that `Authorization`
is owned by the OAuth flow when OAuth is configured — avoids the
sharp edge where the SDK's authProvider silently overwrites a
user-set Authorization header.
5. New `validateSettings` on the server route rejects malformed shapes
(`settings: []`, `settings.headers: "oops"`, non-numeric timeouts,
etc.) with 400 instead of letting them persist and crash the UI.
7 (nit). Renamed `settings` → `savedSettings` inside `setupClientForServer`
so the variable holding the persisted settings doesn't shadow the
later locally-derived values.
Follow-ups opened separately:
#1356 — Move oauthClientSecret out of mcp.json into the OS keychain (review #6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review pass 2 — connect-timeout test, unmount cleanup, tagged intent, lenient settings read
Pass-2 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4527089705:
(a) New integration test pins the new client-layer connect-timeout
contract: a hanging stub transport with `connectionTimeout: 50`
rejects with the descriptive error and lands the client in "error"
status. Documents what TUI/CLI consumers should expect from the
lifted Promise.race in InspectorClient.connect().
(b) useEffect cleanup clears `pendingSettingsTimerRef` on unmount so a
route change or HMR mid-debounce can't fire one stale PUT against
an unmounted component.
(c) Refactor the PUT-route `settingsIntent` discriminator from
`"preserve" | "clear" | { value }` to a tagged union with `kind`,
and consume it with a `switch` for exhaustiveness — reads cleaner
and makes adding a fourth intent painless.
(d) `normalizeMcpServers` now runs `validateSettings` on the on-disk
settings node and silently strips it if invalid, mirroring the
existing lenient-on-read pattern from `normalizeServerType`. Closes
the read/write asymmetry where a hand-edited malformed `settings`
could survive on disk and get carried through preserve-on-PUT.
New route test pins the strip-on-GET + no-propagate-on-preserve-PUT
behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-3 review — patch-style PUT, toast on close-flush fail
Pass-3 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529276897.
(b) `PUT /api/servers/:id` now treats both `config` and `settings` as
optional patches:
- field omitted → preserve the on-disk value
- explicit body value → apply
- settings: null → clear
`updateServerSettings` stops reading `existing.config` from the
in-memory `servers` snapshot, which closes the closure-capture
hazard where a refresh between debounce-schedule and flush could
silently revert a separate edit. The route is the single source of
truth for transport config now. New tests cover settings-only PUT,
rejected non-object config, and the empty-body no-op patch case.
The previous "rejects a missing config" test is rewritten to
document the new no-op-preserve semantics.
(c) Inline comment near the PUT rebuild loop documents that writing
the full mcpServers map back is what gives the route its desirable
self-healing property for malformed settings on *other* entries —
deliberate side-effect, not an accident of normalization.
(d) `flushPendingSettings` surfaces failures via @mantine/notifications
instead of swallowing them. The modal already closed by the time
the flush PUT resolves, so a silent fail would leave the user
thinking their last edits saved (especially painful for the OAuth
client secret). The notifications wiring was already mounted in
main.tsx.
Pass-3 (a) — settings form being effectively uncontrolled mid-edit — is
acknowledged in the reply: working as intended today; the proper fix is
internal modal state when file-watching (#1345) forces the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-4 review — strip smuggled settings out of config
Pass-4 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529861349.
Main finding: `normalizeServerType` spreads unknown keys, so a body
that nested `settings` inside `config` would smuggle a settings field
onto the stored entry without ever passing through `validateSettings`.
Strip the settings key off the incoming config in `buildStoredEntry`
so `validateSettings` remains the single write path for the settings
node — the "one source of truth" invariant pass 2 set up holds again.
Two new tests pin the strip on both routes:
- POST with `config.settings = { bogus }` lands an entry with no
settings node on disk.
- PUT with `config.settings = { bogus }` + `settings: null` clears
the real settings *and* doesn't re-attach the bogus payload via
the spread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-5 review — absorb late connect rejection, tighten validateSettings, log smuggle warning
Pass-5 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529954490.
Main finding: when the connect-time `Promise.race` lost to the timeout,
the original `this.client.connect(this.transport)` promise was left
pending. After `disconnect()` tore the transport down, real transports
(SSE / streamable-http) would reject that promise later — with no
handler attached, producing a Node `unhandledRejection` (and a vitest
suite warning). Attach a no-op `.catch` right after creating the
connect promise so Node sees the late rejection as handled.
Nit (a): `validateSettings` was casting the raw object through to
`InspectorServerSettings`, which let unknown keys ride along onto disk.
Replaced the cast with an explicit pick-and-build over the known
fields. Unknown stowaways now silently drop on the way through. New
route test pins the behavior.
Nit (b): `buildStoredEntry` now logs a `warn` via the route's pino
logger when an incoming `config` carries a `settings` key. The strip
is still defensive (it isn't a documented contract) but the warning
lets a misconfigured client find out about the wasted field instead
of debugging a silent drop.
Nit (c) — the defensive `?? {}` fallback in `buildStoredEntry` —
acknowledged in the review reply; left in place as belt-and-braces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-6 polish — scope absorber, thread id, drop empty OAuth, guard `in`
Pass-6 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529979135. Reviewer signed off as ready; landing the four marginal cleanups since they're all small.
(a) `connectPromise.catch(() => {})` moves inside the
`if (connectTimeoutMs > 0)` branch. The await-only path propagates
rejection cleanly through the outer try/catch — the absorber is
only needed when the race leaves the original promise unattended.
Code now reads as "absorber is tied to the race," which is the
real invariant.
(b) Thread `id` (POST id, PUT newId) through `buildStoredEntry` and
include it in the smuggle warning's pino bindings. Two clients
smuggling simultaneously are distinguishable in the log instead
of indistinguishable.
(c) `validateSettings` coerces empty-string OAuth fields to absent.
The form emits `""` when the user clears an input, and an empty
`oauthClientId` on disk would later be misread as "OAuth configured."
New route test pins it.
(d) `buildStoredEntry` adds a null/object guard before `"settings" in
configObj`. The route layer pre-checks this on POST and PUT, but
the helper's `unknown` parameter is a weak contract; the guard
makes it safe to call from anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): reflect settings persistence + patch-PUT contract in v2_servers_file
The doc was authored when `mcp.json` only stored the server list. Bring it
forward to match what shipped in #1352 / PR #1353:
- Summary calls out that the file also stores per-server settings and
links into the Per-server settings section.
- On-disk format note explains that `settings` is an optional Inspector-
specific extension that other MCP tools ignore.
- New top paragraph on the Per-server settings section explains the
basic-config / settings UI split rationale (simpler form for the
common case, advanced settings tucked behind a second dialog).
- UI bullet is rewritten to match the patch-style PUT semantics that
emerged across the review cycle: settings-only and config-only
PUTs preserve their counterpart inside the write lock, so neither
modal can silently clobber the other half. The earlier "must
re-send settings on PUT" hack is gone.
- New bullets capture the save cadence (300ms debounce + flush-on-close
+ failure toast), the patch-PUT contract (omit-preserve / null-clear /
rename), and the write-path / read-path validator gates.
- Secret storage bullet links to #1356 (OS-keychain follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): show a third entry with the optional settings node
The On-disk format example demonstrated only basic stdio entries. Add a
streamable-http entry with a populated `settings` node so readers can
see the shape inline rather than having to scroll to the Per-server
settings section. Inline comment in the block cross-links there for the
full contract and notes that other MCP tools ignore the extension key.
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): persist server list to ~/.mcp-inspector/mcp.json (#1343)
Replaces the hardcoded SEED_SERVERS in App.tsx with a file-backed list at
~/.mcp-inspector/mcp.json. Full CRUD round-trips through new /api/servers
routes; first launch writes the two existing seeds if the file is absent.
Backend (core):
- store-io: getDefaultMcpConfigPath() — sibling of getDefaultStorageDir()
- core/mcp/serverList.ts: pure converters MCPConfig <-> ServerEntry[],
DEFAULT_SEED_CONFIG, and normalizeServerType (moved from node/config.ts
to keep serverList.ts Node-free)
- core/mcp/remote/node/server.ts: GET/POST/PUT/:id/DELETE/:id /api/servers
routes, plus RemoteServerOptions.mcpConfigPath defaulting to
getDefaultMcpConfigPath(). Ids validated with validateStoreId.
Frontend:
- core/react/useServers.ts: { servers, loading, error, refresh, addServer,
updateServer, removeServer } over the new endpoints
- App.tsx: drops SEED_SERVERS, wires useServers, disconnects on remove of
the active server, follows id on rename of the active server
- ServerConfigModal: Add/Edit/Clone form (stdio: command/args/env/cwd;
sse/streamable-http: url/headers as one-per-line text)
- ServerRemoveConfirmModal: confirm-on-remove with id + transport summary
Tests (52 new):
- unit: getDefaultMcpConfigPath env-var permutations, serverList round-trip,
hook flows against real Hono app.fetch (no TCP), modal forms + validation
- integration: /api/servers routes against a real createRemoteApp + tmp file
- stories with play functions for both new modals
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR #1349 review (correctness + nits)
Bugs:
- normalizeServerType: validate against the allowed set and default
unknown / non-string types to "stdio" (matches the missing-type case).
Prevents a hand-edited {"type":"websocket"} from leaking past `as
ServerType` and surprising downstream narrowing.
- onConfirmRemove: when removing the active server, also destroy() the 9
state managers and null them + the client out so they GC immediately
instead of waiting for the next server switch.
- /api/servers mutating routes: serialize POST/PUT/DELETE through an
in-process write-lock so concurrent read-modify-write cycles can't lose
updates. New integration test fires 25 parallel POSTs and asserts every
entry lands.
- readMcpConfig comment: update to match actual behavior (valid-JSON file
with no `mcpServers` → empty; invalid JSON → 500 from the outer try, so
corruption surfaces rather than silently emptying the list). New
integration tests pin the 500 path on both GET and POST.
Nits:
- Add --inspector-surface-subtle token, use it in ServerRemoveConfirmModal
instead of the raw Mantine var.
- Rename useServers buildHeaders boolean param to includeJsonBody.
- Add a comment on the env-value vs header-value trim asymmetry (env
values preserve trailing whitespace; header values follow RFC 7230).
Tests: 1630 unit + 404 integration + 306 storybook all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ServerConfigModal): null currentTarget in functional setState updater
React nulls SyntheticEvent.currentTarget after the synchronous handler
returns. Reading e.currentTarget.value inside `setForm((f) => ...)`
worked under happy-dom (which keeps currentTarget around) but threw
"Cannot read properties of null (reading 'value')" on the first
keystroke in a real browser. Capture the value before the functional
updater closes over it.
All seven onChange handlers in ServerConfigModal had the bug. Extended
the AddEmpty storybook play to type into Server ID and Command — the
real-Chromium runner now exercises this path so a future regression
fails the test suite instead of the user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(remote-server): surface stderr + transport_error when subprocess crashes during startup
A stdio subprocess that spawns successfully but exits before the browser
opens the SSE events stream used to vanish into a bare "Session not
found" 404. The fix has three parts:
1. RemoteSession.markTransportDead always pushes the transport_error
event (via pushEvent's queue) rather than dropping it when no
consumer is attached. The queued event drains when the consumer
eventually arrives.
2. /api/mcp/connect's transport.onclose handler no longer deletes the
session immediately when no consumer is attached. Instead it holds
the session (with queued stderr + transport_error) for a 30s grace
window so the events endpoint can drain it. A timeout sweeps if no
consumer ever connects.
3. /api/mcp/events detects isTransportDead after attaching the consumer
(which just drained the queue), yields once to flush, then closes
the stream and deletes the session.
End result: the user sees the actual error (e.g. "Cannot find module"
from stderr + the transport-closed reason) instead of a confusing 404.
Tests:
- New unit tests (clients/web/src/test/integration/mcp/remote/remote-session.test.ts)
cover the queueing behavior, live delivery, and clearEventConsumer
signaling under all transport-state permutations.
- New integration test (connect-crash.test.ts) spawns a node subprocess
that writes to stderr and exits, then verifies the events stream
delivers both the stderr line and the transport_error event before
closing — the exact scenario from PR #1349.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(state): gate listRequestorTasks on the server's `tasks` capability
ManagedRequestorTasksState was calling tasks/list on every connect.
Servers that don't advertise the `tasks` capability (the common case)
reply with -32601 "Method not found", which then surfaces in the
inspector's console for every connect.
Skip the call when the server doesn't declare `tasks` — empty tasks
list is the right semantics. Existing tests opt into capabilities so
the live-path coverage is preserved; two new tests pin the gated
behavior (direct refresh + connect-event-triggered refresh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(App): clear activeServerId on disconnect so other server cards re-enable
ServerCard dims any card whose id differs from `activeServer` (and marks
it inert), so cards stay non-interactive after a disconnect until the
user reconnects elsewhere. activeServerId was set on connect but never
reset on disconnect, leaving the whole screen locked to the just-
disconnected card.
Subscribe to InspectorClient's `disconnect` event and clear
activeServerId there. Covers all three disconnect paths uniformly:
- user toggles the connected card off
- user clicks the header Disconnect button
- mid-session transport failure (subprocess exit / network drop)
Event-based instead of effect-based on connectionStatus to avoid the
first-render-clobbers-new-id trap (status starts as "disconnected" for
a freshly-created client before connect() runs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Expose live resource subscriptions via a hook (#1325)
Add ResourceSubscriptionsState that mirrors InspectorClient's subscribed
URIs as InspectorResourceSubscription[], resolving each URI against the
managed resources list (so the subscription tile shows server-supplied
name/title) and stamping lastUpdated on notifications/resources/updated.
Pair it with useResourceSubscriptions and wire App.tsx so the Resources
screen reflects subscribe/unsubscribe actions in real time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address review notes on resourceSubscriptionsState
- Soften the onResourceUpdated comment: the client's dispatch is already
guarded by subscribedResources.has(uri), so the re-check is true
defense-in-depth rather than guarding a known hazard.
- Use this.getSubscriptions() in the statusChange handler so every emit
goes through the defensive-copy path.
- Document the deliberate fallback to a synthetic Resource when a
previously-listed resource is removed from the managed list while
the user is still subscribed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Reflect subscribed state in preview + compact subscribed tile labels
- Wire `isSubscribed` on the ReadResourceState passed to ResourcesScreen
by deriving it from the live subscriptions list in App.tsx. The
ResourcePreviewPanel's SubscribeButton already flips its label to
"Unsubscribe" when subscribed; without this derivation isSubscribed
was always false and the button looked stuck on "Subscribe".
- In ResourceSubscribedItem, display only the last URI path segment
(truncated with ellipsis if it still overflows) and surface the full
URI via a hover tooltip. Keeps the Subscriptions pleat readable when
resources have long path-style URIs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Auto-read resource on sidebar click
Clicking a resource in the URIs accordion now triggers onReadResource
in addition to setting the selection, so the preview panel jumps
straight to the pending → ok render path. Removes the unreachable
"Click to read this resource" placeholder, since the loader / preview /
error states always cover the rendered output for a selected resource.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Render markdown resources via react-markdown + fix preview overflow
- Add react-markdown / remark-gfm dependencies.
- ContentViewer accepts an optional mimeType prop; when it matches
text/markdown (or text/x-markdown), text blocks render via
react-markdown wrapped in a `.markdown-content` container. Non-markdown
text now uses the existing Code "wrapping" variant so long lines stay
inside the panel.
- ResourcePreviewPanel infers `text/markdown` from a `.md` / `.markdown`
URI suffix when the server didn't supply a mimeType, and threads the
effective mime through to ContentViewer per content item.
- ResourcesScreen flex children get miw=0 so a long unwrappable line in
the resource body can no longer push the preview past the viewport's
right edge.
- App.css gains a `.markdown-content` ruleset constraining nested pre,
table, code, and img elements to the container's width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Contain resource preview: sticky header/footer, scrollable body
Restructure ResourcePreviewPanel into a fixed-height column with the
resource title + URI pinned to the top and the timestamp / annotations /
subscribe-refresh actions pinned to the bottom. The content viewer area
in the middle now owns its own ScrollArea, so a long markdown body
scrolls within the panel instead of pushing the subscribe button below
the viewport.
In ResourcesScreen, the selectedResource branch (and the template
branch's right pane) now hosts the panel inside a PreviewPane Flex
column with the screen's max-height, and renderReadState wraps in a
FillDetailCard sized to fill that column. The legacy outer
ScrollArea.Autosize is removed for these panes since scrolling is
internal now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Auto-hide template after read; size preview to content with cap
- After the user clicks Read Resource on the template form,
handleReadResource now clears selectedTemplateUri so the screen
swaps from the template form to the resource preview. Previously
both panels stayed mounted side-by-side.
- The preview no longer hard-fills the viewport. The Card uses a new
"preview" theme variant (overflow: hidden) and is content-sized,
capped at SCROLL_MAX_HEIGHT. ResourcePreviewPanel's flex column
marks the header / meta / footer rows as `flex: 0 0 auto` and the
ContentScroll as `flex: 0 1 auto` with `mih: 0`, so:
- short content → card hugs it, footer sits right under the body
- long content → card caps at viewport, ContentScroll shrinks
and scrolls internally, footer stays pinned at the cap
- ScrollArea / Group imports in ResourcesScreen pruned to match the
simpler single-pane layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Cap template form panel at 40% width
A bare text input + Read Resource button stretched across the whole
main content area looks awkward on wide displays. Apply maw=40% to
the PreviewPane in the template branch so the form keeps a reasonable
form-field width regardless of viewport.
The preview branch is unaffected — resource bodies still get the full
main area for content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Live completion/complete suggestions on resource template inputs
Wires the server's `completions` capability through to the resource
template form so each variable's input becomes an Autocomplete that
fires `completion/complete` (debounced, 300ms) on every keystroke and
surfaces the returned values as a dropdown — mirroring v1's behavior.
- core/mcp/inspectorClientProtocol.ts: surface getCompletions on the
protocol so non-runtime callers (state managers, hooks, tests) can
depend on it. FakeInspectorClient gains a vi.fn-backed stub.
- ResourceTemplatePanel: accepts onCompleteArgument + completionsSupported.
When both are present it renders Mantine Autocomplete instead of
TextInput, debounces keystrokes via per-arg timers, aborts in-flight
requests on the next keystroke, and disables client-side filtering
(the server already filtered for the typed prefix).
- ResourcesScreen / InspectorView: thread the props through; the
screen-level callback re-injects the active template's URI as the
`ref: "ref/resource"` so the panel-level callback stays ref-free.
- App.tsx: wires onCompleteArgument to inspectorClient.getCompletions
and derives completionsSupported from `capabilities?.completions`.
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(core): port v1.5 hook layer (useInspectorClient + useManaged*/usePaged* + log hooks)
Wholesale port of v1.5's React hook layer and the EventTarget-based state machinery the hooks subscribe to (#1243). Hooks decouple from the concrete InspectorClient via a new `InspectorClientProtocol` interface so this layer can land before the runtime class is ported.
Added under `core/`:
- `core/react/` — useInspectorClient + 5 useManaged* + 5 usePaged* + 3 log hooks
- `core/mcp/state/` — 5 managed*State + 5 paged*State + messageLogState + stderrLogState + fetchRequestLogState
- `core/mcp/inspectorClientProtocol.ts`, `inspectorClientEventTarget.ts`, `typedEventTarget.ts`, `samplingCreateMessage.ts`, `sessionStorage.ts`
- v2-only wrapper types in `core/mcp/types.ts`: InspectorUrlElicitRequest, InspectorPendingRequest, InspectorRequestHistoryItem
Tests live under `clients/web/src/test/core/` (28 new test files, 264 new tests). Test fake: `core/mcp/__tests__/fakeInspectorClient.ts`.
Build wiring:
- `vite.config.ts`: dedupe react/react-dom so core/ files resolve them via clients/web/node_modules.
- `tsconfig.app.json` / `tsconfig.test.json`: paths map react/react-dom/vitest to clients/web/node_modules so `tsc -b` resolves bare imports from core/ source files (which sit above the package's node_modules). Exclude core/__tests__ from app config; include in test config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: address PR #1300 review findings
Bugs:
- fetchRequestLogState: add .catch() on sessionStorage.loadSession so a corrupt
or unreadable session doesn't surface as an unhandled rejection (matches the
saveSession swallow pattern).
- messageLogState: sync pendingRequestEntries when clearMessages() evicts a
pending request entry, so a later response with the same id falls through
to the append branch instead of mutating an unreachable map entry. Add tests
for both predicate and no-predicate paths.
Cleanups:
- Extract mergeTaskIntoList helper to core/mcp/state/mergeTaskIntoList.ts so
managed/paged RequestorTasks state classes share one definition.
- Add a one-line JSDoc note on each PagedTools/Prompts/Resources/Templates
state explaining why they deliberately do not subscribe to *ListChanged
(cursor invalidation mid-pagination).
- Document on useInspectorClient that appRendererClient is read lazily on
every render and is not subscribed (changes once at connect time).
Carry-overs from v1.5 deliberately left for the follow-up InspectorClient
runtime port:
- Refresh/status-change race in Managed*State.refresh() (status check is only
at entry; a disconnect during await can re-clear before refresh resumes).
- nextCursor exposure asymmetry across usePaged* (only usePagedRequestorTasks
surfaces it via state field; others return it from loadPage() only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>