* feat(servers): export server list as mcp.json download (#1346)
Adds an Export button to the Servers screen, next to "Add Servers".
Clicking it downloads the current server list as a canonical mcp.json
file (2-space indent, same shape serializeStore writes), so the user
can hand it off, sync it to another machine, or symlink into Claude
Desktop / Cursor / Cline.
Wiring:
- ServerListControls: new Export Button (variant=default), disabled
when serverCount === 0 (nothing to download).
- ServerListScreen / InspectorView: thread `onExport` / `onServerExport`
through.
- App.tsx: onServerExport runs serverEntriesToMcpConfig on the in-memory
servers list, JSON.stringify with 2-space indent, and triggers a Blob
download via a temporary anchor (appended to body for Firefox
compatibility, then removed; object URL revoked).
Tests:
- ServerListControls unit tests cover button-count (toggle hidden vs
shown, Export always present), disabled-on-empty, click fires onExport.
- ServerListControls.stories grows play functions for both stories —
WithServers asserts Export is enabled + click fires the spy in real
Chromium, WithoutServers asserts Export is disabled.
- Existing screen + view tests / stories updated for the new prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR #1351 review (extract helpers + exception safety + guard)
Substantive items from the review:
1. Serialization format parity (review #1): added
`serializeMcpConfig(entries)` to core/mcp/serverList.ts — a
browser-safe helper that runs `serverEntriesToMcpConfig` then
stringifies with 2-space indent. Single source of truth for export
formatting; can't drift from serializeStore's on-disk shape.
2. Exception safety on the anchor cleanup (review #2) + a direct test
for the App.tsx download wiring (review #6): extracted
`downloadJsonFile(filename, json)` to clients/web/src/lib/
downloadFile.ts. removeChild + revokeObjectURL run in a `finally`
so a thrown click() doesn't leak the DOM node or the object URL.
Unit-tested under happy-dom with URL.createObjectURL mocked
(happy-dom doesn't ship it).
3. Empty-list guard (review #5): App.tsx onServerExport bails when
`servers.length === 0`. The button's disabled prop already prevents
the UI path, but the handler is now locally correct against any
future programmatic caller (storybook, keyboard shortcut, etc.).
Acknowledged but not changed:
- Tooltip on the disabled Export button (review #3) — Mantine's
disabled-button + Tooltip wart wants a span wrapper + custom events
config. Optional per the reviewer; deferred.
- Repeated exports → `mcp (1).json` browser suffix (review #4) — a
deliberate trade-off since `mcp.json` is the conventional name a user
would expect to symlink into Claude Desktop / Cursor / etc.
Tests: 1647 unit (+6) + 410 integration + 306 storybook = 2363 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(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>
* Port resource preview UX improvements to PromptsScreen (#1328)
Brings the prompt-fetching flow up to parity with the resource preview
work from #1326:
- Cap the argument-form pane at maw=40% so a bare input + button doesn't
stretch across wide displays.
- Auto-fetch no-argument prompts the moment they're picked from the
sidebar (handleSelectPrompt routes them straight to onGetPrompt({})),
and hide the form once the user clicks Get Prompt — the result panel
takes the same fixed-height column the resource preview uses.
- Apply the PreviewCard variant=preview pattern: card sizes to content
but caps at viewport, PromptMessagesDisplay pins its header (flex 0 0
auto) and lets the inner ScrollArea (flex 0 1 auto, mih 0) absorb
overflow.
- Tag getPromptState with the prompt name in App.tsx so the screen can
ignore a stale result whose name no longer matches the selection.
- MessageBubble now routes each content block through ContentViewer
(markdown for text via mimeType="text/markdown", image/audio/resource
via existing ContentViewer branches). Non-renderable block types
(tool_use, tool_result) are filtered out; the role-label header keeps
the bubble visible regardless.
- PromptArgumentsForm gains onCompleteArgument + completionsSupported;
when both are set, each input becomes Mantine Autocomplete with the
same per-arg AbortController + per-arg debounce timer pattern as
ResourceTemplatePanel. PromptsScreen + InspectorView + App.tsx wire
the callback to inspectorClient.getCompletions with a
ref/prompt envelope.
Closes#1328.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add close (X) buttons to ResourcePreviewPanel and PromptMessagesDisplay
A top-left CloseButton dismisses the preview panel. The host screen
decides what to fall back to:
- ResourcesScreen remembers the originating template URI when the user
reads from a template form (originatingTemplateUri); closing the
preview restores the template form. Plain-resource reads (sidebar
selection) just empty the selection and return to the empty state.
Picking a different template / resource from the sidebar clears the
back-trail.
- PromptsScreen flips submittedFor back to undefined when the closed
prompt has arguments — the argument form re-renders with the
user's values preserved so they can edit and re-submit. No-arg
prompts have nothing to fall back to, so the selection is dropped
and the empty state appears.
The X button is hidden when the host omits onClose, so callers that
don't want a dismiss control keep their existing rendering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add close button to pending and error preview states
The OK-state preview already had its X button (inside the panel
component itself). Pending and error states were inline cards in
renderReadState / renderPreview with no way to dismiss them, so a
user who submitted bad input to a resource template or prompt was
stuck staring at the error until they picked a different sidebar
item. Adds a top-left CloseButton row to both states on both screens,
wired to the same handleClosePreview handler — so the template form
or argument form re-appears on close just like it does from the OK
state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fire prompt completions on focus and send every sibling in context
Two fixes to the PromptArgumentsForm autocomplete:
- Focusing an argument input now fires completion/complete immediately
(handleFocus) — the dropdown is populated as soon as the user clicks
in, not only after they start typing. Any in-flight debounce timer
for the same arg is cancelled so a stale keystroke request can't
overwrite the fresh focus response.
- The completion context now includes every declared prompt argument
(with "" for ones the user hasn't typed into yet), minus the one
being completed. Previously the context only carried args the user
had touched, so servers that disambiguate based on co-arguments
couldn't see the full picture on the first keystroke.
Tests cover both: a focus-only path that fires before any keystroke,
and a typing path that asserts the empty sibling is sent through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fire resource-template completions on focus
Mirrors the prompt-side change: focusing a template variable input
now fires completion/complete immediately so the dropdown populates
the moment the user clicks in, rather than waiting for the first
keystroke + 300ms debounce. Any pending debounce timer for the same
variable is cancelled first so a stale keystroke response can't
overwrite the focus response.
The sibling-context coverage was already correct here — `variables`
is seeded with empty strings for every declared template variable at
mount and on template switch, so the context payload always carries
the full variable set minus the one being completed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Style Autocomplete dropdowns to match the rest of the app
Two fixes so the completion dropdowns on prompt-argument and
resource-template inputs look like every other dropdown in the app:
- App.css: the dark-mode dropdown / option-hover rules used to target
.mantine-Select-* only, so the Autocomplete dropdowns shipped with
Mantine's default surface color and a different hover background.
Re-scoped to .mantine-Combobox-* so every Combobox-built input
(Select, Autocomplete, MultiSelect, …) inherits the same styling.
- theme/Autocomplete.ts: new theme module setting `radius: "md"` so
Autocomplete matches the default Select / TextInput corner radius.
Wired through theme/index.ts and theme/theme.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix dropdown styling — Select and Autocomplete need separate selectors
The previous commit re-scoped the dark-mode dropdown rules from
`.mantine-Select-*` to `.mantine-Combobox-*` on the assumption that
both Select and Autocomplete share a Combobox class on the dropdown
root. They don't — Mantine's Combobox factory takes
`__staticSelector` from the parent component, so Select emits
`mantine-Select-dropdown` and Autocomplete emits
`mantine-Autocomplete-dropdown` with no shared `Combobox` class on
that element. The `.mantine-Combobox-*` selector matched nothing,
so the TaskControls filter (and every other Select) lost its gray
surface and reverted to Mantine's default brown-ish dark background.
List both selectors so Select dropdowns keep their styling and
Autocomplete dropdowns pick it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address review feedback: canSubmit, fire-time context, re-click guard
- PromptArgumentsForm: add a canSubmit guard that disables Get Prompt
until every required argument has a value, matching the resource
template form's symmetry. Optional args may stay blank.
- Both forms: capture sibling context inside the debounce callback,
not at schedule time. Hold the latest argumentValues / variables
in a ref + sync via useEffect, and call buildContext() at fire
time. Without this, typing in arg A then arg B within the 300ms
window would ship A's request with B's pre-keystroke value.
- PromptsScreen.handleSelectPrompt: early-return when the user
re-clicks the already-selected prompt — sidebar is for navigation,
✕ is for dismiss. Previously a re-click wiped form values and
re-fired the auto-fetch for no-arg prompts.
- MessageBubble: expand the comment on effectiveMimeForBlock to flag
the unconditional markdown promotion as a known asymmetry with
the resource side (which only promotes when the server signals it),
pending a per-block mimeType in the spec.
- New tests: canSubmit disabled / enabled transitions, fire-time
context capture across siblings, abort-path verification (a stale
in-flight response must not overwrite the fresh one), and the
no-op re-click guard on PromptsScreen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address follow-up review nits: ghost suggestions, dead previewActive branch
- Clear `completions[name]` at the top of handleChange in both
PromptArgumentsForm and ResourceTemplatePanel so the dropdown
doesn't show stale options from the previous prefix during the
300ms debounce + network window.
- Simplify previewActive in PromptsScreen to drop the unreachable
`!getPromptState?.promptName` fallback. App.tsx tags every prompt
state transition with `promptName`, so the fallback was dead code
that obscured the actual runtime invariant.
- Add a focused test for each form that asserts stale dropdown
options vanish the instant a new keystroke arrives.
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(web): wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView
App.tsx mounts InspectorView with live data from the v2 hook layer instead
of the title-page placeholder. InspectorView becomes pure prop-driven; the
demo handshake stub and connection state machinery are removed.
Connection lifecycle:
- App.tsx lazily instantiates `InspectorClient` on the connect edge,
rebuilding it (and its state managers) when the user picks a different
server. State managers (Managed{Tools,Prompts,Resources,ResourceTemplates,
RequestorTasks}State + MessageLog/FetchRequestLog/StderrLog) are
destroyed on switch.
- `useInspectorClient` + the per-primitive `useManaged*` hooks drive the
view's data. `latencyMs` is captured at the connecting → connected edge
via a ref so the intermediate rerenders don't lose the start timestamp.
- `errorMessage` is captured from `client.connect()` rejection; status-
machine-driven errors that don't surface through the promise are a known
follow-up (no `error` event in the v2 InspectorClientEventMap yet).
Action handlers:
- `onCallTool` / `onGetPrompt` / `onReadResource` await the corresponding
InspectorClient method and reflect pending → ok/error in `ToolCallState`
/ `GetPromptState` / `ReadResourceState` panel props. Manually verified
end-to-end against `npx @modelcontextprotocol/server-filesystem` —
the Tools result panel renders the real "Allowed directories: /private/tmp"
output rather than a stub.
- `onSetLogLevel` calls `client.setLoggingLevel(level)` and optimistically
bumps `currentLogLevel` locally (the request has no echo notification).
- `onSubscribeResource` / `onUnsubscribeResource` / `onCancelTask` route to
their respective client methods. Server CRUD (`onServerAdd`/`onServerEdit`
/…) and history pinning are intentionally `todoNoop` for now; the seed
server list is hardcoded per the agreed scope (a `useServers` v2-only
hook is a separate effort).
- `notifications/message` notifications flowing through `MessageLogState`
are filtered into `LogEntryData[]` and passed to the Logs screen, so
`logging/setLevel` and the resulting log stream round-trip.
InspectorView (`InspectorView.tsx`):
- Removed: `STUB_*` constants, `handshakeTimer` ref, `handleToggleConnection`
/ `disconnect` functions, stub `bridgeFactory`/`sandboxPath`, internal
connection state, `logLevel` state.
- Added 38 props covering connection state, panel states, log level, and
action callbacks. The view is pure presentational.
- `availableTabs` is derived from `connectionStatus`; `activeTab` is
clamped to whatever's currently available (avoiding the `set-state-in-effect`
lint and keeping the previously-selected tab on reconnect).
Stories + tests:
- `InspectorView.stories.tsx` adds spy callbacks for every action prop and
new `Connected` / `ConnectionError` stories for the connected/error
narratives.
- `InspectorView.test.tsx` is rewritten around the prop-driven contract
(10 tests covering the empty list, server cards, toggle dispatch,
connected header, error banner, tab snap-back on disconnect, app
filtering, log-level dispatch, autoScroll local toggle).
Dev backend port wiring (`vite.config.ts`):
- Vite's dev server is now pinned to `CLIENT_PORT` (default 6274) with
`strictPort: true`, matching the dev backend's `allowedOrigins`. Without
this, Vite would fall back to 5173 while the Hono origin check still
required 6274 — breaking every browser `/api/*` fetch out of the box.
- Top-level `resolve.alias` now spreads the same bare-module aliases the
vitest projects use. Rolldown can't reach `pino/browser.js` /
`zustand/middleware` from `core/`'s parent (no node_modules there); the
wired App pulls those modules into the browser dep graph for the first
time, so the aliases need to be visible at build time too.
createWebEnvironment helper (`src/lib/environmentFactory.ts`):
- Mirrors v1.5's `lib/adapters/environmentFactory.ts`: builds
`InspectorClientEnvironment` from `createRemoteTransport` +
`createRemoteFetch` + `createRemoteLogger` + `BrowserOAuthStorage` +
`BrowserNavigation`. Returns the assembled environment plus the logger
for direct app-level use.
Closes#1244.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): wire dev-backend auth token into createWebEnvironment + show full stdio command in ServerCard
Two issues surfaced after the #1244 wiring landed locally:
1. **401 on `/api/mcp/connect`** when running `npm run dev` with the
default auth (no `DANGEROUSLY_OMIT_AUTH`). The dev backend generates a
random token and prints `http://localhost:6274?MCP_INSPECTOR_API_TOKEN=…`
in the banner, but App.tsx was calling `createWebEnvironment(undefined, …)`
so the browser never sent `x-mcp-remote-auth`. The Hono middleware
correctly rejected with "Authentication required. Use the
x-mcp-remote-auth header with Bearer token."
Adds a small `getAuthToken()` helper in App.tsx that reads
`MCP_INSPECTOR_API_TOKEN` off `window.location.search`, falls back to
`sessionStorage`, and persists fresh values into sessionStorage so
client-side navigation / OAuth round-trips don't drop the token. The
resolved value is passed to `createWebEnvironment` whenever a new
InspectorClient is built.
2. **Misleading ServerCard display** — the seed server's `config` is
`{ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem",
"/tmp"] }` but the card only rendered `"npx"`, so it looked like the
command was incomplete. `getCommandOrUrl` now joins `command + args`
for stdio configs, so the card shows the same string that gets spawned.
SSE / streamable-http paths unchanged.
Manual end-to-end re-verified against `npm run dev` (no DANGEROUSLY_OMIT_AUTH):
- Banner prints the auth-token URL.
- Browser navigates to that URL → `getAuthToken()` picks the token off the
query string, persists to sessionStorage, threads it through to every
`/api/*` request.
- Server card shows `npx -y @modelcontextprotocol/server-filesystem /tmp`.
- Toggle connect → connection succeeds; ViewHeader shows
`Connected (~1.3s)`; Tools tab populates with the 14 filesystem tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): use dark-9 instead of blue-9 for dark-mode page background
`main.tsx` overrode `--mantine-color-body` to `--mantine-color-blue-9` in
dark mode, while `.storybook/preview.tsx` used `--mantine-color-dark-9`.
The dev app then rendered with a blue background while every story (and
the design intent visible in storybook) used dark grey.
Switch main.tsx to `--mantine-color-dark-9` so the dev app matches the
storybook reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): seed the Servers screen with the "everything" reference server
Adds `@modelcontextprotocol/server-everything` to the hardcoded seed list
alongside the filesystem server. The everything server exposes tools,
prompts, resources, sampling, and completion — covering most of the
surface a developer might want to exercise against the wired-up
InspectorView without configuring anything.
Comment updated to explain the two-seed shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): address #1244 PR review feedback (JsonValue cast, client lifecycle, dead code)
- **#1 — Misleading `Record<string, never>` cast**: Imported `JsonValue`
from `@inspector/core/mcp/index.js` and changed `args as Record<string,
never>` to `args as Record<string, JsonValue>` in `onCallTool`. The new
cast honestly narrows from the screen-level `Record<string, unknown>`
to what `InspectorClient.callTool` actually requires. Added a comment
explaining the boundary.
- **#2 + #3 — Previous InspectorClient not disconnected on server switch
or unmount**: Added a `useEffect` keyed on `inspectorClient` whose
cleanup calls `inspectorClient.disconnect()`. One effect covers both
the swap-on-switch leak (prior session's transport stayed open until
GC) and the unmount-during-connected leak (HMR / tests).
`setupClientForServer`'s existing state-manager `destroy()` calls
handle the listener side; the new effect handles the transport side.
- **#4 — Dead `void` block at bottom of file**: Deleted the trailing
`void FetchRequestLogState; void StderrLogState;` block. The state
managers are already constructed via `new FetchRequestLogState(client)`
/ `new StderrLogState(client)`, so the imports are referenced by the
linter's reckoning. The `void` pair was dead code with a comment that
no longer matched reality.
- **#5 / #7 / #8 — Follow-up TODOs**: Added TODO refs to the new
follow-up issues:
- TODO(#1323): `error` event in InspectorClientEventMap so mid-session
transport failures surface (connect-only catch is incomplete).
- TODO(#1324): negotiated `protocolVersion` through `useInspectorClient`
(currently hard-coded `"2025-06-18"`).
- TODO(#1325): `useResourceSubscriptions` hook so subscribe/unsubscribe
buttons reflect their server-side effect.
Items #6 (useCallback dep churn), #9 (helper unit tests), and #10
(shared no-op bridge factory fixture) are acknowledged in the PR reply
as deferred — no functional impact and out of scope for a "wire the
hook layer" PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- InspectorView: wrap each screen in a Mantine `Transition` (`fade-up`,
350ms). Outgoing screens fade out and slide down 30px; incoming screens
fade in and slide up to meet the ViewHeader. A `pos: "relative"` Stack
container holds the screens so the absolute-positioned `Transition`
wrappers stack in the same area without pushing layout.
- InspectorView: reorder ALL_TABS so Logs sits beside History at the end:
Servers · Tools · Prompts · Resources · Tasks · Logs · History.
- App.tsx: refactor to AGENTS.md guidelines — extract `PageContainer`,
`PageStack`, `ThemeToggle`, `SubtitleText` as `.withProps()`
subcomponents; replace inline unicode glyphs with `MdLightMode` /
`MdDarkMode` icons (matches the ViewHeader pattern); convert the click
handler to a `function` declaration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>