A failed stdio launch had no useful diagnostic surface: the message log
(History) is cleared on the error transition and there are no HTTP
requests (Network), so the auto-opened monitoring column was empty. The
process's stderr — which explains the failure (e.g. "No module named
mcp_server_time") — was captured in StderrLogState but never displayed.
- New ConsoleScreen renders the captured stderr stream (search / sort /
clear / export; embeddable in the monitoring column), wired through App
via the existing useStderrLog hook + StderrLogState.
- Console is a pinnable monitor tab like Logs/History/Network, gated to
stdio servers (HTTP has no child process). It lives in the header menu
when the column isn't pinned, and moves into the column when it is.
- The failure column now picks its tabs by which diagnostic actually
captured content (stderr → Console, fetch requests → Network), not by
the active server's transport: a connect failure fires the client
`disconnect` event, which clears `activeServer`, so transport can't be
read back there. The memo re-runs as stderr/fetch entries stream in, so
the diagnostic tab appears as soon as the failing process prints.
Verified live against a failing `python -m mcp_server_time` stdio server:
the column auto-opens to Console showing the real ModuleNotFoundError.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
On a connection failure the user was left without an obvious way to see
why. Open the monitoring column automatically on the transition into the
`error` status (mirroring the connect-success behavior from #1616) so the
diagnostics surface right when they're needed.
- Broaden the auto-open effect to fire on the transition into `error` as
well as `connected`, tracked via a general previous-status ref so it
fires once per transition and never on a mount that starts errored.
- A failed connect never negotiated capabilities, so the failure column
offers History (the local client-side message log) and — for non-stdio
transports — Network (HTTP requests), the entries that explain the
failure. Logs is omitted (it needs the server `logging` capability).
- `effectivePinned` now allows the column while `error`, not only while
`connected`; it still closes on a clean disconnect or narrowing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Auto-open the monitoring column when a connection is established. Gated on the
disconnected → connected transition (a ref-tracked edge), so it fires on an
actual connect rather than on every render while connected or on a mount that
starts already-connected (which would fight a user who closed it). The column
still only appears when wide and a monitor tab is available.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the resize handle disappearing: the animation wrapper is a Mantine Group
(default align: center), and the full-height handle has no intrinsic height, so
it collapsed to 0. Set align: stretch on the wrapper.
- Closing the column now leaves the primary on the screen currently selected in
the header (commits the active tab first), instead of swapping in the column's
screen.
- Keep the primary screen's standard ScreenStage transition; only the column
(handle + column) slides open/closed from the right with a fade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a search box to MonitoringControls (right of the tabs). It's a single
shared search kept across column tab switches, filtering whichever screen is
shown. Embedded panels apply this text but still ignore the sidebar-only
filters (levels/categories/directions/method), which have no control there.
- Close button now uses the "collapse sidebar" icon (TbLayoutSidebarRightCollapse),
the mirror of the pin button's expand icon.
- Closing the column carries its selection to the primary view: the screen the
user was watching becomes the active header tab (rather than snapping back to
the pre-pin selection).
- Animate the column open/close: the handle + column slide in from the right and
fade (reversing on close) via a Mantine Transition.
- HistoryListPanel: drop the redundant "History (N)" header when there are no
pinned requests (lone unpinned section renders headerless).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the ability to "pin" the Logging, History, and Network screens into a
second column on the right of the InspectorView, so a primary screen and a
monitor screen are visible at once (e.g. watch Logs while calling a Tool).
- A "pin as column" button on the Logs/History/Network panel toolbars opens
the column (shown only when wide + connected).
- The column hosts a MonitoringControls tab row (available monitor tabs +
close) over the selected embedded screen. Pinning removes the monitor group
from the header tab bar; closing (x) returns it.
- The divider between the primary area and the column is draggable (pointer +
keyboard); width is clamped and persisted.
- The split collapses to one column below ~1040px (the ServerListScreen
single-card breakpoint) and while disconnected, without discarding the pin
preference — it reopens when wide + connected again.
State (pinned/tab/width) is view-local and persisted via the existing
useLocalStorage pattern, so no new props thread through App and ViewHeader is
unchanged (it just receives a filtered tab list). Screens/panels gain an
`embedded` mode (fills the parent height, drops the 340px filter sidebar) via a
shared EmbeddableScrollArea element.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.
Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.
Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
past the 300ms completion debounce (await setTimeout(400)) — which race
the debounce timer under load — with awaited conditions (findBy/waitFor)
on the real rendered outcome, plus delay:null. Negative assertions now
lean on the component's synchronous state/timer teardown instead of a
timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
piped out-of-band from the tool's JSON-RPC response, so reading the log
synchronously after callTool raced the stderr chunk. Wrapped the assertion
in vi.waitFor so it polls until the line lands.
Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
* test(web): audit branch-coverage outliers, raise per-file gate to 70 (#1271)
The web per-file coverage gate had drifted red: the recently-landed EMA
auth feature (core/auth/ema/*) and ClientSettings components shipped
undertested, and the prior 50% branch floor let business-logic branches
go untested. CI does not run the coverage gate, so this accumulated.
Add tests to bring every failing file above the gate and push branch
coverage well past 70 across the board, then raise the per-file branches
threshold 50 -> 70 in clients/web/vite.config.ts.
- EMA core (emaFlow, resourceContext, jwt, wire): 50-88% -> 86-100% branch
- EMA OIDC/transport (idpOidc, tokenEndpoint, transportProvider): -> 97-100%
- oauthManager / discovery: 73-83% -> 88-92%
- ClientSettingsModal/Form, ConnectionInfoContent: 0-88% -> 96-100%
- useClientSettingsDraft / useEmaIdpLoginState hooks: 20% -> 100%
- TaskStatusBadge / SortableServerCard outliers: 50% -> 100%
Coverable branches reached by mocking useComputedColorScheme (dark theme),
dnd-kit useSortable (isDragging), and fetch/OIDC paths. Remaining sub-100
branches are genuinely-unreachable defensive guards or happy-dom-inherent
(portal / useMediaQuery / typeof window) and documented in test comments.
No source files changed. Global web branch coverage 88.73% -> 90.63%.
AGENTS.md updated; cli/tui/launcher stay at 50 (out of scope).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: raise per-file coverage gate to 90/90/90/90 across all clients
Follows the #1271 web branch-coverage audit (50 -> 70) by raising the
whole per-file gate to >=90 on all four dimensions — lines, statements,
functions, branches — for web, cli, tui, and launcher (core/ is measured
under the web suite). A codebase-wide audit added real tests for every
outlier and brought global coverage up across the board.
Config: all four vitest/vite coverage configs set to 90/90/90/90.
Two pure-type modules (core/mcp/import/types.ts, clients/web/server/
types.ts) added to the web coverage exclude list, matching the existing
type-only exclusions.
Tests: ~50 test files extended/created across core (auth, ema, mcp,
remote, state, storage, react), web components/screens/server, and the
cli/tui clients. Highlights: full EMA OAuth flow + transport coverage,
inspectorClient/remote server backfill, every Mantine control's handler
branches, and the cli unsupported-method / argv-fallback paths.
Genuinely-unreachable branches are annotated at the source with justified
`/* v8 ignore -- <reason> */` comments rather than lowering the gate:
happy-dom-inherent paths (Mantine portal, useMediaQuery, typeof window
SSR guards), React StrictMode effect replay, and provably-dead defensive
guards (?? fallbacks the types make impossible, Select.onChange values
outside the allowed list). No runtime logic changed; the only structural
edits are two behavior-preserving refactors (ResourceTemplatePanel hoists
the field value to a local so the ignore applies in JSX; ToolsScreen
swaps a ternary for an if) and prettier line-wrapping.
Also fixed a flaky real-timer debounce assertion in PromptArgumentsForm
(asserted an exact sibling-typing interleaving that raced the 300ms
debounce; now asserts the load-bearing fact — a non-empty fire-time
sibling capture).
AGENTS.md updated to describe the uniform 90 gate and the v8-ignore
policy. All four client coverage gates, root validate, storybook (370),
and smoke pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(web): make connect-crash SSE reader stop on the terminal event
`readSseEvents` looped until the server closed the stream (`done`). The
server closes the events stream immediately after writing the terminal
`transport_error`, but under the heavy parallel scheduler pressure of a
full `coverage` run (v8 instrumentation across all workers) that close
can lag far enough behind the event to trip the 30s test timeout — a
hang, not a wrong result. This made the test intermittently fail under
`npm run coverage` (it passed in isolation and under CI's lighter
`test:integration`).
Stop reading as soon as the terminal `transport_error` event arrives
(cancelling the reader) instead of waiting only for `done`. That event is
always the last one the session queue emits, so nothing is lost, and the
test no longer depends on stream-close timing. The `done` break is kept
for any stream that closes first.
Verified green across two consecutive full `npm run test:coverage` runs
(3094 tests) where it previously hung, plus isolation (~0.5s).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Header navigation tabs now appear only when the connected server
advertises the corresponding server capability (from the MCP
`initialize` result `capabilities`), instead of being shown
unconditionally or proxied through list content.
Previously the Tools and Logs tabs were always shown when connected,
regardless of whether the server advertised `tools` / `logging`. The
content-gated tabs (Prompts/Resources/Tasks) only appeared correct
because an unsupported capability yields an empty list — but a server
advertising a capability with a currently-empty list would have its
tab wrongly hidden.
InspectorView.availableTabs now gates each server-capability tab on
the matching field of `initializeResult.capabilities` (already
threaded from App via the handshake):
Tools -> capabilities.tools
Logs -> capabilities.logging
Prompts -> capabilities.prompts
Resources -> capabilities.resources
Tasks -> capabilities.tasks
Apps -> capabilities.tools AND >=1 app tool (Apps is a filtered
view of tools, so it keeps its content check)
Client-side sections are never gated on server capabilities: Servers
and History are always available, and Network stays transport-gated
(hidden for stdio). Tabs recompute live when reconnecting to a server
with a different capability set.
Tests: reworked the content-gating component tests into
capability-gating tests (including the Tools-hidden regression, Logs
present/absent, capability-present-but-empty-list, History-always, and
a reconnect-recomputes test). Connected story/fixtures advertise a
full capability set so the Connected story still shows every tab.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): re-enable other server cards when a connection errors (#1521)
When a server's connection settled into the terminal `error` state, the
other server cards stayed dimmed/`inert` (Connect toggle + actions
disabled), so the user couldn't interact with any other server.
`ServerCard` dims any card whose id differs from `activeServer`, and App
clears `activeServerId` only on the InspectorClient `disconnect` event.
A terminal `error` (a connect-time handshake failure, or a mid-session
transport `onerror` that fires without an `onclose`) sets status to
`error` but never dispatches `disconnect`, so `activeServerId` lingers
and every other card stays inert. We can't clear `activeServerId` on
error because the errored card's status, the connected header, and the
connection-info modal are all keyed off it.
Fix: dim the other cards only while the active session is actually live
(connecting or connected). InspectorView now passes the active id to the
ServerListScreen as the dim source only when `connectionStatus` is
`connecting`/`connected`; once it reaches a terminal state the others
re-enable. The errored card still shows its real status via the merged
`servers` list, which keys off the real `activeServer`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): reuse isTerminalStatus for card-dim gate (#1521 review)
Replace the hand-rolled `sessionLive` check with the existing
`isTerminalStatus` predicate (the #1490 teardown convention) so the
dim gate stays aligned with the canonical "session is over" branching
and can't desync from a future status addition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.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>
* fix(web): hide resource subscription UI when server lacks subscribe capability (#1478)
Thread a `subscriptionsSupported` flag derived from
`capabilities?.resources?.subscribe === true` from App through
InspectorView → ResourcesScreen to the leaf components. When false:
- ResourcePreviewPanel hides the Subscribe/Unsubscribe button (Refresh
stays).
- ResourceControls omits the Subscriptions accordion section entirely,
dropping it from `allSections` so the expand/collapse-all toggle and
open-section bookkeeping stay correct even if a stale "subscriptions"
entry persists from a prior subscription-capable session.
The flag defaults to true at the leaf components so existing callers,
tests, and stories keep rendering the subscription controls unless a
server is explicitly marked unsupported.
Adds unit tests and Storybook stories covering the hidden and shown
states across all three components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): give the Servers screen scroll the same gutter/idle-hidden treatment (#1474)
The server grid's ScrollArea.Autosize used the default type="hover" with no
offsetScrollbars, so the scrollbar popped in on hover and overlaid the right
edge of the server cards (occluding their action icons / status badges) — the
same problem #1474 fixed for the History/Network/Logging list panels.
Apply the same `type="scroll"` + `offsetScrollbars` treatment, and reuse the
shared `expectScrollbarGutterIdleHidden` story assertion to regression-protect
the gutter + idle-hidden behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): stop the InspectorView shell from scrolling as a whole
AppShell.Main carried Mantine's default `padding="md"` on top of the
fixed-header offset, but every screen already sizes itself to
`calc(100vh - header)` and supplies its own `xl` padding (with no
allowance for Main's padding). That extra inset pushed screen content
past the viewport, so the whole InspectorView gained a page-level
scrollbar instead of confining overflow to each screen's inner
ScrollArea.
Set the shell `padding` to 0 (Main now contributes only the header
offset, matching what the screens were built for) and clamp the Main
slot to `height: 100dvh` with `overflow: hidden` in the theme, so the
view can never scroll regardless of content — only the inner ScrollArea
regions do.
Adds an InspectorView story play assertion guarding the invariant
(Main clips + is viewport-clamped) and a ManyServers fixture that
confirms it holds under enough content to overflow, with the inner
server-grid ScrollArea absorbing the overflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): address review — align viewport units + drop stale subscriptions intent
Responding to the @claude review on #1479:
- Align all screen height calcs from `100vh` to `100dvh` so they match the
Main slot's `100dvh` clamp. On a mobile viewport where `dvh < vh` a `100vh`
child would have exceeded Main's `100dvh` content box and been clipped by
`overflow: hidden`; the units are now in lockstep.
- Restrict `intendedButEmptySections` in ResourceControls to `allSections` so a
stale "subscriptions" entry persisted from a prior subscription-capable
session is dropped from persisted open-sections instead of being perpetually
re-appended once the section is no longer rendered. Adds a regression test.
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): expandable resource_link with read-on-demand (#1449)
Replace the placeholder `resource_link` rendering in ContentViewer with a
dedicated, Mantine-native `ResourceLink` element that mirrors V1's behavior:
- Shows uri (monospace, link-styled), optional name/description, and a
mimeType badge.
- Expand/collapse affordance (chevron + loading spinner) that is keyboard
accessible (native button: role, aria-expanded, aria-label, Enter/Space).
- On first expand, reads the linked resource on demand via an injected
callback and renders the full read result inline as formatted JSON under a
green "Resource:" label, capped to a max height with scroll for large
results. The fetched result is cached across collapse/expand.
The read handler is threaded as a "dumb component + callback as prop" through
ToolResultPanel → ToolsScreen → InspectorView, with App supplying a handler
that returns the `ReadResourceResult` from `inspectorClient.readResource`.
Also extract the shared `resourceContentsToBlock` helper into
`utils/resourceContents.ts` (previously private to ResourcePreviewPanel).
Tests: unit coverage for ResourceLink (collapsed/expanded/error/cache) and
the ContentViewer wiring, plus Storybook stories with play functions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): scroll large resource_link read results within the card
A plain ScrollArea with only `mah` does not scroll when its parent (the
content-sized card) has no definite height, so a large read result overflowed
the card instead of scrolling. Switch to ScrollArea.Autosize, which grows with
content up to the max height and then scrolls. Adds a LargeResult story
covering the bounded-scroll behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): make ResourceLink a group; extract ResourceLinkInfo element
ResourceLink is stateful (expand/loading/cached result) and fetches data via
a callback — group-like behavior, and the only `elements/` component using
useState. Move it to `groups/` and split out the pure-display metadata as a
new `elements/ResourceLinkInfo` element (uri/name/description/mime + an
optional action slot).
This keeps the layer dependencies clean (no element→group import, which
moving ResourceLink alone would have introduced):
- ContentViewer (element) renders ResourceLinkInfo (element) statically for
`resource_link` blocks and no longer takes an onReadResource prop.
- ToolResultPanel (group) renders the interactive ResourceLink (group) for
`resource_link` blocks, ContentViewer for the rest.
- ResourceLink (group) composes ResourceLinkInfo + ContentViewer.
Also reverts the now-single-consumer resourceContents.ts extraction, restoring
ResourcePreviewPanel's local toContentBlock.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): make ResourceLink read retryable and guard double-fetch
Address PR review: a failed read is no longer cached as terminal — only
a successful result is cached, so collapsing and re-expanding retries the
read after a transient failure. Add a loading guard so toggling while a
read is in flight does not fire a redundant second read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): prettier formatting for ResourceLink test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: remove accidentally-committed build artifacts; gitignore clients/*/build
A stray 'git add -A' swept 8 compiled build files into d25c9382. Remove
them from tracking and add clients/*/build to .gitignore so client build
output is never committed again.
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 Apps tab only when server exposes MCP App tools (#1450)
Gate the "Apps" tab on appTools.length > 0 so it is hidden when the
connected server exposes no MCP App tools. appTools is now computed
before availableTabs, which depends on it — the tab appears/disappears
live as the tool list changes (list-changed refresh, server switch).
The existing activeTab fallback already snaps a user sitting on Apps
back to Servers when the tab disappears. When app tools exist but the
sandbox is unavailable the tab stays visible so the AppsScreen's
"unavailable" message remains reachable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): content-gate the Prompts and Resources tabs too (#1450)
Extend the Apps tab gating to Prompts and Resources: each tab is hidden
when its list is empty and appears reactively when the list gains an
entry (list-changed refresh, server switch). Resources is gated on
resources OR templates, since a server may expose only templates. The
existing activeTab fallback snaps a user on a now-empty tab back to
Servers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): content-gate the Tasks tab too (#1450)
Extend the content gating to the Tasks tab: hidden when no tasks exist,
appears reactively once a task-augmented tool call creates one and
disappears when all are cleared. Consistent with the Apps/Prompts/
Resources gates — the existing activeTab fallback snaps a user on a now-
empty tab back to Servers. The "run as task" affordance still lives on
the Tools screen (gated by server task support), so discovery is
unaffected; advertised capabilities remain visible in Connection Info.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): fade content-gated tabs in to draw attention on appearance (#1450)
Add a brief 325ms fade-in to navigation tab labels via a new `tabLabel`
Text theme variant + `inspector-fade-in` keyframe. SegmentedControl keys
segments by value, so when a content-gated tab (Apps/Prompts/Resources/
Tasks) appears after a list change its label mounts fresh and fades in to
draw the eye, while existing tabs stay put.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): animate tab-bar width when a tab is added or removed (#1450)
Wrap the tab SegmentedControl in a clip element (new `tabBar` Group
variant) whose width is set to the control's measured border-box size and
transitions over 325ms ease-in. The control is sized `max-content` and
the clip `overflow: hidden`, so when a content-gated tab appears or
disappears the bar grows/shrinks smoothly. Width is measured with
useElementSize (ResizeObserver); it falls back to auto width where the
observer is unavailable, so the bar still renders at its natural size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): lengthen tab-label fade-in to 450ms (#1450)
Bump the `tabLabel` fade from 325ms to 450ms so a newly-appearing tab
name fades in more gently. Also formats the previously-unformatted
ViewHeader width-clip test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): drop tab-label fade-in, keep only the 325ms width animation (#1450)
Remove the per-tab fade-in (tabLabel Text variant + inspector-fade-in
keyframe + label wrapping) so the tab bar's 325ms ease-in width change is
the sole animation when a content-gated tab appears or disappears.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): collapse the tab bar to 0 width when it leaves the DOM on disconnect (#1450)
Unify the connected/disconnected ViewHeader into a single tree so the tab
controls can sit in a keep-alive Mantine Transition. On disconnect the
Transition holds the bar mounted for 325ms while the clip's CSS width
transition collapses it measured->0, then unmounts. The connected tab
list/selection is snapshotted (adjust-state-during-render, no refs) so the
bar keeps rendering its tabs as it shrinks. The disconnected header keeps
its logo-left / title-center / theme-right layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): fade the disconnected title in after the tab bar collapses (#1450)
On disconnect, hold the "MCP Inspector" title until the tab bar's
shrink-to-0 exit finishes (tracked via the keep-alive Transition's
onExited), then fade it in over 300ms. Removes the prior overlap where the
title appeared beside the still-collapsing bar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): set both disconnect animations to 300ms (#1450)
Drop the tab-bar collapse from 325ms to 300ms so it matches the title
fade (also 300ms), trimming the overall disconnect sequence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): staggered fade+slide-down crossfade between title and tab bar (#1450)
Replace the tab-bar width/size animation with a staggered crossfade. The
disconnected "MCP Inspector" title and the connected tab bar share one CSS
grid cell, so on connect/disconnect one fades + slides down out while the
other fades + slides down in — the incoming one delayed by half the 300ms
duration so they're staggered and one replaces the other in place.
Both motions travel downward (enter descends from above, exit descends
below) via inspector-fade-slide-in/out keyframes, selected per element with
a data-anim attribute; Mantine Transition provides keep-alive so the exit
plays before unmount. Removes the clip/useElementSize/tabBar-variant
size-animation machinery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): pulse a red glow on a tab that newly appears mid-session (#1450)
When a content-gated tab is added to the bar while connected, pulse a red
text glow around its label in-then-out over 325ms. New tabs are detected by
comparing the shown set to the previous render's (adjust-state-during-
render), so only mid-session additions glow — not the initial set on
connect, nor anything on disconnect. The glow is a CSS keyframe
(inspector-tab-glow) applied via a `tabGlow` Text variant + `data-glow`
attribute; the red comes from a new --inspector-glow-red token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): start the new-tab glow at full intensity, then fade (#1450)
Change the tab glow from an in-then-out pulse to full-on-appear then fade
over 325ms (ease-out), so a newly added tab flashes brightest the instant
it appears.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): keep new-tab label text red through the glow, then revert to theme (#1450)
Animate `color` red across the whole glow keyframe so a newly-appeared
tab's text stays red for the 325ms; with no forwards fill it snaps back to
the label's theme color when the animation ends. The glow text-shadow still
fades over the duration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): revert new-tab text color animation, keep glow only (#1450)
Drop the `color: red` keyframe stops so the label text keeps its theme
color; only the red glow (text-shadow) plays, full intensity on appear then
fading over 325ms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make the new-tab glow ease-in over 325ms (#1450)
Change the tab glow timing function from ease-out to ease-in (duration
already 325ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): animate server name and right-side controls on connect/disconnect (#1450)
Wrap the server name (left) and the status indicator + Disconnect control
(right) in keep-alive Transitions so they fade + slide-down in on connect
and out on disconnect, using the same values as the tab bar. Split the
animation CSS (.header-anim[data-anim]) from the center-stack grid
placement so it's reusable, and broaden the snapshot to retain serverName/
status/latency for rendering during exit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): address review — glow grace window, Select comment, key unification (#1450)
- Arm the new-tab glow only after a GLOW_GRACE_MS window post-connect, so
asynchronously-resolved lists (prompts/resources/tasks) aren't mistaken
for mid-session additions and don't glow on initial load.
- Document why the narrow-viewport Select doesn't glow.
- Route snapshotKey and the glow key through a shared tabsKey()/TAB_SEP
helper (also removes a stray control char from the join separator).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(web): note the slow-server tail of the glow grace window (#1450)
Acknowledge in a comment that GLOW_GRACE_MS keys on time-since-connect, not
on lists having settled, so a server slower than the window glows on its
tail. Per review follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): honor prefers-reduced-motion for the new header animations (#1450)
Add a reduced-motion media query that disables the header fade/slide
(.header-anim) and the new-tab glow (.tab-glow), leaving elements at their
resting state. Per review accessibility suggestion.
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): 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>
* fix(web): don't show a permanent PENDING badge on history notifications (#1439)
`extractStatus` derived the history status badge purely from whether a
`response` was attached, so a fire-and-forget notification (no id, no
response ever) — and any unmatched standalone response — was labelled
"Pending" forever. The pending → OK/Error lifecycle only applies to
request entries (messageLogState attaches the response by JSON-RPC id),
so scope the status to `direction === "request"` and render no
request-style badge for notifications/responses. The method badge already
labels them.
Part of the History-screen work on #1438.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): track outgoing responses so server→client requests resolve in history (#1440)
MessageTrackingTransport.send only tracked outgoing requests, so the
client's response to a server→client request (roots/list, sampling,
elicitation) was never recorded. The inbound request entry never got its
response folded in, leaving it stuck "Pending" with no response body.
Make send symmetric with onmessage: track outgoing responses (by id +
result/error) as well as outgoing requests. The response now correlates
to its request entry, which resolves to OK/Error with the body. Outgoing
notifications remain untracked (separate gap).
Part of the History-screen work on #1438.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): wire up History Pin and Replay (#1438)
Both HistoryEntry actions were rendered but stubbed to todoNoop in App.
- Pin: App now owns a session-scoped Set<string> of pinned entry ids,
toggled by onTogglePinHistory and passed down as pinnedHistoryIds.
HistoryListPanel already sorts pinned entries to the top. The set resets
with the rest of the per-screen state on disconnect/server-switch.
- Replay: onReplayHistory re-issues the entry's original request by method
(tools/call → callTool, prompts/get → getPrompt, resources/read →
readResource). The call flows through InspectorClient → tracked
transport → message log, so the fresh request+response surface as a new
History entry (history-local) without touching the Tools/Prompts/
Resources panels. Unsupported methods / a removed tool surface a toast.
Tests: pin toggle propagation, replay dispatch for each supported method,
unsupported-method and missing-tool toasts.
Closes#1438.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make history Replay support list/discovery requests
Replaying a tools/list entry reported "Replay isn't supported". Extend
replayHistoryRequest beyond the call-style requests to the read/discovery
methods, re-issuing each via its InspectorClient method (preserving the
pagination cursor):
- tools/list → listTools, prompts/list → listPrompts,
resources/list → listResources,
resources/templates/list → listResourceTemplates, ping → ping.
Tests: tools/list replay (cursor preserved) and an unsupported-method
toast (logging/setLevel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): replay tasks/list; reorder + conditionally show History buttons
- Add tasks/list to the replayable set (re-issued via listRequestorTasks),
completing the client-issued */list methods.
- Reverse the HistoryEntry actions to Pin then Replay, and hide Replay for
entries whose method can't be replayed.
- Introduce a single source of truth — REPLAYABLE_HISTORY_METHODS /
isReplayableHistoryMethod in historyUtils — used by HistoryEntry to
show/hide the button and by App's replayHistoryRequest to gate dispatch,
so the two can't drift.
Tests: tasks/list replay, Replay hidden for a non-replayable method, and
Pin-before-Replay ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): add client↔server direction badge to History and Network (#1437)
A dual-state badge at the top-left of each entry shows which way it
traveled: "client → server" (green) for inspector-originated, "client ←
server" (yellow) for server-originated.
- New shared element MessageDirectionBadge (Elements).
- History: MessageEntry gains an `origin` ("client" | "server") set at
tracking time — MessageTrackingTransport tags `send` as client and
`onmessage` as server, threaded through the track callbacks. HistoryEntry
maps origin → badge. (direction stays the request/response/notification
message-type used for response correlation; origin is the flow.)
- Network: fetches are always inspector-originated, so NetworkEntry renders
the badge as outgoing.
Tests + stories for the badge; HistoryEntry direction tests; transport
test asserts the origin argument.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): use purple (not yellow) for the client ← server direction badge
Yellow + green read as caution/ok status; purple vs green reads as
direction. Incoming (server → client) is now violet; outgoing stays green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): make History Pinned/History sections collapsible toggles
The "Pinned Requests (N)" and "History (N)" section headers are now
clickable toggle buttons that expand/collapse their entries — styled like
the LogControls level toggles (UnstyledButton listItem variant with the
active background) and wrapping the entries in a Collapse. Both default
open and collapse independently.
Tests assert the per-section aria-expanded toggling and independence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): replace Expand/Collapse text button with an icon toggle
History, Network, and Task cards now use a shared ExpandToggle ActionIcon
instead of the "Expand"/"Collapse" text button: LuArrowDownToLine when
collapsed (expand), LuArrowUpFromLine when expanded (collapse). The
aria-label stays "Expand"/"Collapse" so the control reads the same.
- New shared element ExpandToggle (+ test + stories).
- HistoryEntry: actions split into a left group (Pin/Replay) with the
toggle pushed right via space-between.
- NetworkEntry / TaskCard: drop their now-unused SubtleButton const.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): pin icon toggle on HistoryEntry, moved to the right
Replace the Pin/Unpin text button with a shared PinToggle ActionIcon
(TiPinOutline when unpinned, TiPin when pinned; aria-label stays
Pin/Unpin). Move it to the right action group, just left of the
expand/collapse toggle — Replay stays on the left.
- New shared element PinToggle (+ test + stories).
- HistoryEntry action row: Replay left; Pin + expand toggle right.
- Drop the now-unused formatPinLabel helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): per-section Clear/Export on the Pinned and History headers
Each section header now shows Clear and Export links on the right, but
only when both sections are present (a single section is already covered
by the panel-level Clear/Export). They act on just that section's entries:
- Clear: removes that section's entries by pin membership (clearing the
Pinned section also drops the now-stale pinned id set).
- Export: downloads just that section's entries
(inspector-history-pinned/-unpinned-...json).
Threaded onClearSection/onExportSection App → InspectorView → HistoryScreen
→ HistoryListPanel; App computes each section from messages + pinned ids.
Extracted a CollapsibleSection subcomponent and a SectionActions links
group. Extended ExportKind with history-pinned/history-unpinned.
Tests cover both-sections visibility gating and per-section dispatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): Filter by Message Direction on the History controls
Add a "Filter by Message Direction" section to HistoryControls, modeled on
LogControls' "Filter by Level": a Select/Deselect All control plus two
listItem toggles — "client → server" and "client ← server" — that show or
hide entries by origin.
- HistoryUiState gains visibleDirections (Record<MessageOrigin, boolean>),
defaulting to both visible; persists with the other per-screen state.
- HistoryScreen wires the toggles (single + all) through onUiChange and
passes the filter to HistoryListPanel, where matchesFilters drops entries
whose direction is off (entries without a recorded origin are never
filtered out).
Tests cover the controls (render, single toggle, select/deselect all), the
HistoryScreen onUiChange wiring, and the list-level direction filtering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): section Clear/Export as subtle buttons, not link anchors
Match the Select/Deselect All control in HistoryControls: the per-section
Clear/Export on the History/Pinned headers now use a subtle Button
(variant subtle, size xs) instead of an Anchor link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): color the direction filter labels to match the badge
The "client → server" / "client ← server" toggles in HistoryControls now
color their labels green / violet, matching the MessageDirectionBadge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): track outgoing client notifications in history too
Review follow-up (#1441): MessageTrackingTransport.send now also tracks
the client's own notifications (initialized, progress,
roots/list_changed) as origin "client", making send fully symmetric with
onmessage. Previously only id-bearing outgoing messages (requests,
responses) were tracked, so client notifications never reached History
while incoming server notifications did.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): Filter by Message Direction on the Network screen
Add the same "Filter by Message Direction" section to NetworkControls as
on HistoryControls. Extracted the shared section into a MessageDirectionFilter
group component (used by both HistoryControls and NetworkControls) so they
stay identical — a Select/Deselect All control plus green/violet
client → server / client ← server toggles.
- NetworkUiState gains visibleDirections (default both visible), persisted
with the rest of the per-screen state; NetworkScreen wires the toggles and
NetworkStreamPanel filters on them.
- Network fetches are always inspector-originated, so all entries are
client → server: the client toggle gates them all and the server toggle
matches nothing (documented in NetworkStreamPanel). The section mirrors
History for parity.
Tests cover the shared filter, both controls' direction sections, the
Network list filtering, and the NetworkScreen wiring; ambiguous "Deselect
All" selectors (now two sections) disambiguated by order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert(web): drop message direction from the Network tab
Network traffic is always client → server — even SSE is a client-initiated
stream tagged outgoing — so a direction badge/filter there is meaningless.
Remove the MessageDirectionBadge from NetworkEntry and the Filter by Message
Direction section (and its NetworkUiState.visibleDirections wiring) from the
Network screen.
History keeps its direction badge + filter; the shared MessageDirectionBadge
and MessageDirectionFilter components stay (still used by History).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): a lone History section isn't a collapsible accordion
When only one of Pinned / History is present, its header is now a plain
title and its entries always show — instead of a `listItem` accordion
toggle that could hide them based on a stale collapsed state from when
both sections were on screen. The accordion (and per-section Clear/Export)
only applies when both sections are present.
Example fixed: collapse History while pins exist, then unpin everything —
History becomes the sole section and shows its entries under a plain
header rather than staying collapsed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): put section Clear/Export on the header pleat itself
The per-section Clear/Export sat to the right of the collapsible header
bar, on the panel background. Make the whole header row the "pleat":
a new Group `sectionHeader` theme variant (rounded + hover, active bg
passed per instance) wraps a flex-1 toggle button (the title) on the left
and the Clear/Export buttons on the right — so the actions live on the
pleat. The toggle is a separate button since the action buttons can't nest
inside it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(web): entry expand toggle uses the same icons as ListToggle
Swap ExpandToggle (History / Network / Task cards) from the LuArrow icons
to RiExpandVerticalLine / RiCollapseVerticalLine — the same expand/collapse
icons the list-level ListToggle uses. Same ActionIcon size, so it matches
the other small buttons on the cards; aria-labels unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): panel Clear keeps pinned entries (history-only)
Review follow-up (#1441): the panel Clear button is gated on the unpinned
count (implying history-only), but onClearHistory wiped everything
(pinned included) and left pinnedHistoryIds stale. Make Clear history-only
— clearMessages((m) => !pinnedHistoryIds.has(m.id)) — so pinning protects
an entry, matching the gating and the per-section model; the pin set stays
valid since its entries survive.
Also drop the stale "and Network" mention from the MessageDirectionFilter
docstring (Network direction filter was reverted).
Test: panel Clear's predicate keeps pinned ids and removes unpinned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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): tool progress toasts + persist tool selection/result across navigation (#1414)
Surface incoming notifications/progress as toasts so the user can watch a
long-running tool's progress without leaving the tool view — the v2 replacement
for v1's always-visible "Server Notifications" shelf. Toasts are keyed by
progress stream and updated (not stacked) per tick so a chatty server updates
one toast rather than flooding the corner. Progress notifications still land in
the History tab via the existing transport-level tracking.
Companion change: lift the selected tool and its form values into App.tsx
alongside the already-lifted toolCallState, make ToolsScreen controlled, and
drop the clear-on-unmount effect. Leaving the Tools tab and returning now
preserves selection, inputs, and result. All three reset together on
disconnect via resetSessionScopedUiState, preserving the #1368/#1394 behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): persist per-screen selection/search/filter across navigation (#1417)
Lift every screen's selection, form, search, and filter state out of the
screen components into session-scoped state owned by App, passed down as
controlled props. The screens unmount on tab switch, so this is what keeps
the user's context in place when they navigate away and back within a live
session. State is cleared only on disconnect (resetSessionScopedUiState) or
an explicit user action — never on plain navigation.
Lifted per screen:
- Tools: sidebar search (selection/form/result were lifted in #1414)
- Prompts: selected prompt, argument values, submitted marker, search
- Resources: selected resource/template URI, originating-template marker,
search, accordion open-sections
- Apps: selected app, form values, search (running/maximized stay local —
they're tied to the live iframe/bridge that is torn down on unmount)
- Tasks: search + status filter
- Logs: filter text + visible-level set
- History: search + method filter
- Network: filter text + visible-category set
The four sidebar *Controls (Tool/Prompt/App/Resource) become controlled for
search; ResourceControls also controls accordion open-sections. Level/category
default maps move to logLevels.ts / fetchCategories.ts so the screen files
keep a single component export.
Pagination already persists (it lives in the App-level Paged*State managers),
so no work was needed there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): persist list scroll position across navigation + reset tests (#1417)
Add useScrollMemory: a module-keyed hook that remembers a Mantine ScrollArea's
offset across unmount/remount (the screens unmount on tab switch). Scroll lives
in a module-scope map rather than threaded App state — it's ephemeral DOM state,
and threading ~16 more props would balloon the surface for no benefit. App calls
clearScrollMemory() in resetSessionScopedUiState so offsets clear on disconnect
alongside the lifted selection/filter state.
Wired into the four main list panels (Logs, History, Network, Tasks) and the
Tools/Prompts/Apps sidebar lists. Pagination already persists via the App-level
Paged*State managers.
Tests: useScrollMemory unit test (save/restore, key isolation, clear), and the
App disconnect test now also exercises representative lifted UI state (tool
search, prompt selection, log filter) clearing on disconnect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): collapse per-screen lifted state into one {screen}Ui object
Replace the ~46 individual selection/search/filter props threaded App →
InspectorView → screen with one `{screen}Ui` object + one `on{Screen}UiChange`
setter per screen (8 pairs). Each screen reads its fields from `ui` and updates
via `onUiChange({ ...ui, field })`; the in-flight result panels
(toolCallState / getPromptState / readResourceState) stay separate since
they're written by App's async action handlers, not the screens.
The `*UiState` interfaces live with their screens; the empty-state defaults are
gathered in screenUiState.ts so the screen files keep a single component export
(react-refresh), mirroring logLevels.ts / fetchCategories.ts.
Trims InspectorView's prop surface substantially and keeps the persistence
behavior identical. Tests/stories collapse their stateful wrappers to a single
ui object; added Tools/Tasks/History interaction tests to keep per-file
coverage above the gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): dismiss visible progress toasts on client teardown (#1414 review)
The progressNotification effect cleanup cleared its id-tracking set but left
on-screen "Tool progress" toasts to auto-close up to 5s later — they could
linger into the next session, and the lingering toast's onClose could delete an
id from the new session's set and trigger a duplicate-id re-show. Hide each
tracked toast in cleanup so both gaps close. Addresses @claude review note #2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): clear the tool result when a different tool is selected
The Tools result panel renders `toolCallState` regardless of which tool is
selected, so selecting a new tool used to leave the previous tool's result
showing beneath it. Route tools UI changes through an App handler that drops
`toolCallState` whenever `selectedToolName` changes; search/form edits keep the
selection (and the result) intact. Tests cover both the clear-on-switch and
keep-on-search paths.
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>
* feat(sort): add SortToggle for Logs, History, and Network (#1371)
Introduces a reusable SortToggle element and wires it into the Logs,
History, and Network screens so users can flip chronological order.
Selection is persisted per-screen to localStorage under the
inspector.sortDirection.* namespace, with invalid stored values falling
back to the newest-first default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(logs): move auto-scroll checkbox left of the sort selector
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(logs): remove auto-scroll checkbox; ListToggle left of SortToggle
- Drops the Logs auto-scroll checkbox and its plumbing through
LoggingScreen / InspectorView.
- Moves the ListToggle to the left of the SortToggle on the History
and Network panel toolbars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(sort): address review on #1372
- Hydrate sort direction synchronously (getInitialValueInEffect: false)
to remove the one-frame newest-first flicker on first paint.
- Drop the redundant `[...matched].sort()` copy across the three panels;
`.filter()` already returns a fresh array.
- Document why serializeSortDirection bypasses Mantine's default JSON
stringify (raw enum value in storage).
- MemoryStorage.getItem: collapse `has` + `!` to `?? null`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(history): default the request list to collapsed for parity with Network
The History list opened expanded while Network opened collapsed; align
History with Network so both panels start in the same compact view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(history,network): persist list compact state to localStorage
Lifts the per-panel `compact` flag out of HistoryListPanel /
NetworkStreamPanel and into InspectorView's localStorage hooks, mirroring
the sort-direction shape:
inspector.listCompact.history
inspector.listCompact.network
Defaults to collapsed (compact=true) on both, with a corrupted-value
deserializer that clamps unknown strings back to the default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(servers,resources): persist ListToggle state to localStorage
Lifts the per-screen compact preference out of ServerListScreen and
ResourceControls into InspectorView's localStorage hooks, matching the
History/Network shape:
inspector.listCompact.servers
inspector.listCompact.resources
Servers and Resources both default to expanded (compact=false). For
ResourceControls the displayed compact is still derived from openSections
(so per-section accordion clicks still work locally), and only explicit
ListToggle clicks update the persisted preference — the handler computes
the next compact value from the click outcome rather than blindly
flipping, so a half-open accordion still records the right post-state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(sort-toggle): switch to icon button matching ListToggle style
Replaces the Mantine Select with a subtle Button (or ActionIcon for the
subtle variant), using TbSortDescending2 for newest-first and
TbSortAscending2 for oldest-first. Clicking the button flips between the
two values.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(sort-toggle): restore Select with sort icon in rightSection
Reverts SortToggle to a Mantine Select with simplified labels ("Newest
First" / "Oldest First") and the corresponding TbSortDescending2 /
TbSortAscending2 icon in the rightSection, replacing the default chevron.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sort-toggle): swap icons so they match label direction
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sort-toggle): use down arrow for newest, up arrow for oldest
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(inspector-view): extract useSortDirection / useListCompact
Collapses seven near-identical useLocalStorage call sites in
InspectorView into two scoped helpers. Each helper carries the shared
namespace, serialize/deserialize adapters, and getInitialValueInEffect:
false; the call sites now read as "this screen has sort persisted to
this scope" rather than re-stating the storage shape every time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sort-toggle): apply prettier formatting
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(export): wire History + Logs export JSON handlers (#1354)
Replaces the `todoNoop` placeholders in App.tsx with real handlers that
serialize the current entries and trigger a browser download via the
existing `downloadJsonFile` helper.
- Adds `buildExportFilename(kind, serverId, now?)` next to the download
helper. Produces sortable, filesystem-safe names like
`inspector-history-alpha-2026-03-17T10-00-42.123Z.json` (URL-encodes
server ids that contain slashes / spaces / colons, swaps `:` for `-`
in the ISO timestamp so the file is safe on Windows).
- Wires `onExportHistory` (full MessageEntry list) and `onExportLogs`
(the LogEntryData list the screen renders).
- Routes the existing Network export through the same filename helper
for consistency.
Unit tests cover the filename builder, including the no-server-id
fallback, special-character encoding, and the default-`now` shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(export): tighten buildExportFilename signature + docs
- Introduce an `ExportKind = "history" | "logs" | "network"` union and
use it as the type of the `kind` parameter. Typos like "hisotry" now
fail at compile time and the supported export kinds are documented
inline.
- Clarify the JSDoc: the segment is omitted when `serverId` is falsy
(undefined OR empty string), matching the existing `serverId ? ... :
undefined` check. New test pins the empty-string branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(logging): remove Copy All button from LogStreamPanel
Export now produces the same JSON the Copy All button used to dump to
the clipboard, so the button is redundant. History and Network panels
have no equivalent — dropping Copy All also brings Logs into parity.
Removes the prop from LogStreamPanel + LoggingScreen + InspectorView +
App.tsx, and updates the matching tests/stories. Tests now assert the
button is *not* in the document so a future re-introduction would have
to update the assertion deliberately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(network): add Network screen surfacing backend fetch log (#1355)
Routes `FetchRequestLogState` to a new Network tab so users can inspect
HTTP-layer request/response headers, status, duration, and category
(auth vs transport) of every fetch the backend makes for an active
session. Adds `NetworkControls` + `NetworkStreamPanel` + `NetworkEntry`
groups, a `monoBreak` Text theme variant for long header values, and
wires onClearNetwork / onExportNetwork (JSON download) from `App.tsx`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): include request / response bodies in search filter
Headers were already covered but bodies were not, so searches for
strings that live inside the JSON-RPC payload (e.g. "jsonrpc",
"initialize", a tool name) silently returned no matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): always render Response Body section, note streaming
Streaming responses (text/event-stream, application/x-ndjson, POST to
/mcp) are intentionally not body-captured by the fetch tracker, so the
section was silently omitted. Now whenever a response was received we
render the section, with either the body, a "(empty)" note, or a
"Streaming response — body not captured" note keyed off content-type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): capture response bodies via async body-update events
Two related fixes:
1. The fetch tracker was marking every streamable-HTTP POST /mcp call as
"not captured" because the response content-type is text/event-stream.
In practice those streams are bounded — the server sends the
JSON-RPC reply (sometimes preceded by progress events) and closes —
so we can clone the body and read it. Only GET + SSE / ndjson is the
unbounded long-lived channel that has to stay uncaptured.
2. Reading the body inline blocked the transport from processing
progress notifications until the entire stream finished, breaking the
`resetTimeoutOnProgress` integration test. The body is now read in
the background and dispatched via a new `fetchRequestBodyUpdate`
event that the FetchRequestLogState patches into the matching entry
by id. Plumbing wires through node/transport, the remote SSE channel
(RemoteSession, RemoteClientTransport), and the remote event types.
Also hides the Network tab when the active server is stdio (no HTTP
traffic to surface) and bumps the inline body preview limit to 100 KB
so typical tools/list responses render rather than tripping the "too
large" notice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(network): show orange SSE badge for long-lived stream entries
Adds a small "SSE" badge to the right side of the NetworkEntry header
(left of the HTTP status badge) when the response is a long-lived
streaming channel (GET + text/event-stream or application/x-ndjson) —
the only case where the body genuinely isn't captured. Makes the
"not captured" placeholder less surprising at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): address PR review findings
- Stop capturing OAuth response bodies (drop updateResponseBody from the
effectiveAuthFetch tracker). Restores pre-PR behavior for auth-category
entries so live access_token / refresh_token values can't leak into the
Network tab body preview during a screen-share. Headers + status are
still tracked.
- Extract `isLongLivedStreamResponse(method, contentType)` to
core/mcp/fetchTracking so the rule lives in one place; consumed by the
tracker and the NetworkEntry UI placeholder.
- Document the intent of NetworkEntry's `useEffect` (the list-level
toggle is authoritative; per-entry overrides are intentionally
discarded on each list toggle).
- Rename MAX_INLINE_BODY_BYTES → MAX_INLINE_BODY_CHARS (`.length` is
UTF-16 code units, not bytes) and update the user-facing notice.
- Switch NetworkStreamPanel's search from joined-string `.includes` to
per-field `.some(.includes)` so a term can't span field boundaries.
Regression test added.
- Add aria-label to ListToggle ("Expand all" / "Collapse all") — both an
a11y improvement and a stable selector that lets the panel test stop
fishing the button out by text-content negation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(network): use ListToggle aria-label in panel + modal tests
Cleanup spotted in PR review follow-up:
- HistoryListPanel + ServerSettingsModal previously fished the ListToggle
out by empty text content because the toggle had no accessible name.
Now that ListToggle exposes "Expand all" / "Collapse all", both tests
can use getByRole + name and the stale comments go away.
- ListToggle's own tests now assert the aria-label contract so a future
refactor can't silently drop it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): expose category toggle state via aria-pressed
The category UnstyledButtons in NetworkControls previously communicated
their toggled state only via background color. Adds aria-pressed={active}
so assistive tech can discover the on/off state without visuals, with a
test that asserts the attribute tracks the visibleCategories prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(servers): export server list as mcp.json download (#1346)
Adds an Export button to the Servers screen, next to "Add Servers".
Clicking it downloads the current server list as a canonical mcp.json
file (2-space indent, same shape serializeStore writes), so the user
can hand it off, sync it to another machine, or symlink into Claude
Desktop / Cursor / Cline.
Wiring:
- ServerListControls: new Export Button (variant=default), disabled
when serverCount === 0 (nothing to download).
- ServerListScreen / InspectorView: thread `onExport` / `onServerExport`
through.
- App.tsx: onServerExport runs serverEntriesToMcpConfig on the in-memory
servers list, JSON.stringify with 2-space indent, and triggers a Blob
download via a temporary anchor (appended to body for Firefox
compatibility, then removed; object URL revoked).
Tests:
- ServerListControls unit tests cover button-count (toggle hidden vs
shown, Export always present), disabled-on-empty, click fires onExport.
- ServerListControls.stories grows play functions for both stories —
WithServers asserts Export is enabled + click fires the spy in real
Chromium, WithoutServers asserts Export is disabled.
- Existing screen + view tests / stories updated for the new prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR #1351 review (extract helpers + exception safety + guard)
Substantive items from the review:
1. Serialization format parity (review #1): added
`serializeMcpConfig(entries)` to core/mcp/serverList.ts — a
browser-safe helper that runs `serverEntriesToMcpConfig` then
stringifies with 2-space indent. Single source of truth for export
formatting; can't drift from serializeStore's on-disk shape.
2. Exception safety on the anchor cleanup (review #2) + a direct test
for the App.tsx download wiring (review #6): extracted
`downloadJsonFile(filename, json)` to clients/web/src/lib/
downloadFile.ts. removeChild + revokeObjectURL run in a `finally`
so a thrown click() doesn't leak the DOM node or the object URL.
Unit-tested under happy-dom with URL.createObjectURL mocked
(happy-dom doesn't ship it).
3. Empty-list guard (review #5): App.tsx onServerExport bails when
`servers.length === 0`. The button's disabled prop already prevents
the UI path, but the handler is now locally correct against any
future programmatic caller (storybook, keyboard shortcut, etc.).
Acknowledged but not changed:
- Tooltip on the disabled Export button (review #3) — Mantine's
disabled-button + Tooltip wart wants a span wrapper + custom events
config. Optional per the reviewer; deferred.
- Repeated exports → `mcp (1).json` browser suffix (review #4) — a
deliberate trade-off since `mcp.json` is the conventional name a user
would expect to symlink into Claude Desktop / Cursor / etc.
Tests: 1647 unit (+6) + 410 integration + 306 storybook = 2363 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* feat(apps): AppsScreen and InspectorView wiring (#1263)
Adds the Apps tab as a top-level screen composed of `AppListItem`,
`AppDetailPanel`, and `AppRenderer`. The screen is presentational —
selection, form values, running, and maximize state are local; data and
side effects come in via props and callbacks.
InspectorView filters `tools` through `isAppTool` (wrapped in a try/catch
so a malformed `_meta.ui.resourceUri` skips one tool instead of dropping
every following App), exposes the Apps tab in `ALL_TABS` after Tools, and
wires Phase-3 stub callbacks plus a stub bridge factory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(apps): extract AppControls and rename Refresh button
Pulls the sidebar list, search input, "List updated" indicator, and
Refresh button out of `AppsScreen` into a dedicated `AppControls`
group, mirroring `ToolControls` and `TaskControls`. The Refresh button
moves from a full-width row beneath the list to the top-right of the
title row (single label "Refresh", matching `TaskControls`).
`AppsScreen` keeps the right-pane orchestration (selection, form,
running, maximize, header actions) and now delegates the entire
sidebar to `AppControls`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): seed InspectorView demo tools with MCP Apps
Adds three apps (Cohort Data, Weather Widget, Ops Dashboard) carrying
real `_meta.ui.resourceUri` to the demo tools list so the Apps tab has
content in the Default story alongside the existing tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(apps): drop AppDetailPanel title row to avoid duplicate name
The parent AppsScreen header already shows the app name, so the panel's
own title/icon row rendered the name twice. Remove the title row (and
its now-unused imports, styled subcomponents, and the four tests that
asserted on title/icon rendering).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(resources): drop annotation badges from ResourceListItem
The list row now renders just the resource title/name. Audience and
priority badges remain visible in the preview/template panels, so the
data is still surfaced — just not duplicated in the picker. Drops the
now-unused AnnotationBadge import, the badge-only stories, and the
badge-rendering test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(apps): address review — share hasInputFields, show header icon
- Extracts the duplicated `Object.keys(inputSchema.properties ?? {}).length > 0`
check from `AppsScreen` (and the equivalent in `AppDetailPanel`'s
`hasFields`) into a shared `hasInputFields` helper in `toolUtils`,
with tests.
- Renders the selected tool's first icon next to the right-pane header
title in `AppsScreen`, restoring the icon visibility that was lost
when `AppDetailPanel`'s title row was removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(apps): finish hasInputFields extraction; document isOpening
- AppDetailPanel now actually calls the shared `hasInputFields` helper
instead of inlining the same `Object.keys(...).length > 0` check.
- AppsScreen comments why `isOpening={false}` is hardcoded: the screen
flips to AppRenderer synchronously, so the loading state is unreachable
here — the prop remains for standalone use and Phase 3 wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245)
Establishes the unit-test runtime AGENTS.md mandates: a vitest "unit"
project alongside the existing storybook project, RTL + jest-dom +
happy-dom, a Mantine-aware render helper, and a 90%-line per-file
coverage gate enforced by CI. Adds 66 test files covering all 62
components under elements/groups/screens/views, plus core/mcp and
clients/web/src/utils. Total: 577 tests, 98.7% line coverage globally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(coverage): address PR #1269 review feedback
- AGENTS.md: correct coverage gate description (lines ≥ 90, statements ≥ 85,
functions ≥ 80, branches ≥ 50) with rationale for the relaxed branch
threshold
- InspectorView.test.tsx: document what the Math.random() mock values
(0.1 / 0.99) control vs. STUB_SUCCESS_RATE in InspectorView.tsx
- vite.config.ts / tsconfig.test.json: drop unused `globals: true` and the
`vitest/globals` type now that every test file imports describe/it/expect/vi
explicitly; manual `afterEach(cleanup)` in setup.ts remains required because
testing-library auto-cleanup relies on globalThis.afterEach
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ServerEntry was previously defined as `Pick<ServerCardProps, ...>` in
ServerListScreen, inverting the natural dependency direction (data shape
derived from a UI prop type). Phase 5's cleanup pass calls this out
specifically.
Move the canonical ServerEntry into core/mcp/types.ts and have
ServerCardProps extend it. ServerListScreen, InspectorView, and their
stories now import ServerEntry from core directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract a 20-tool fixture (filesystem, database, git, http, misc) into
ToolsScreen.fixtures.ts — large enough that the ToolControls list scrolls
and the Tools tab in InspectorView shows realistic surface area.
Wire it into the Default stories of both ToolControls and InspectorView,
mirroring the LoggingScreen.fixtures.ts pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ToolsScreen, PromptsScreen, and ResourcesScreen each tracked the user's
current selection via `selectedXxxName` / `onSelectXxx` props, hoisting
screen-local UI state into whatever rendered them. InspectorView never
threaded those callbacks anywhere, so clicks in the controls panel did
not display the matching detail panel — the detail panel only rendered
if a parent fed selection back in.
Move selection into `useState` inside each screen and drop the
`selectedXxxName` / `onSelectXxx` props. MCP-derived data (`callState`,
`getPromptState`, `readState`) keeps flowing in from `InspectorView` as
props. ResourcesScreen wraps `onReadResource` so a template-expanded URI
becomes the selected resource for preview rendering.
Update each screen's stories to drop the deleted props. Stories that
need a pre-selected item use a `play` function to click it — no
component coupling to Storybook.
Add Phase 4.3 to the interfaces plan describing the rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- InspectorView: cancel any in-flight stub handshake timer when a new
connect request arrives or the user disconnects, via a `useRef`-backed
timer handle. Prevents a stale handshake resolver from overwriting the
next attempt's connection state.
- InspectorView: document that screens unmount on tab switch (matches
prior `{activeTab === X && <Screen />}` behavior; local screen state
resets on every transition). If Phase 3 needs persistence, add
`keepMounted` to the Transition.
- InspectorView: document the `mih: "100%"` height assumption on
ScreenStageContainer — depends on `AppShell.Main` providing a definite
parent height for the stack itself to fill (absolute children render
regardless, but nested ScrollArea screens may need this).
- ViewHeader: comment the `Select w={140}` magic number — sized to fit
the longest current tab label ("Resources").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract `mixedEntries` (50 mixed-level log entries) into
`LoggingScreen.fixtures.ts` so both `LoggingScreen.stories` and
`InspectorView.stories` can consume it. The InspectorView Default story
now shows a populated, scrollable Logs tab.
- InspectorView screen transitions: separate enter (350ms) and exit (250ms)
durations via Mantine Transition's `exitDuration` prop, so screens leave
faster than they enter.
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>
- InspectorView: simulate the connect flow through "connecting" with a
randomized 50-500ms handshake delay and an 85% success rate. On success,
measure observed latency from connect start; on failure, set a plausible
error message. Connection error message flows through to the active
ServerCard's connection state. Real wiring (Phase 3) replaces this with
`useConnection`.
- InspectorView: document that the view owns connection state (input
`serversInput` connection fields are intentionally ignored), and mark
`noop` callbacks as Phase-3 stubs.
- ServerStatusIndicator: add `showLabel?: boolean` escape hatch so consumers
in constrained containers can override the default viewport-based
visibility.
- ServerListScreen.stories: add `WithActiveServer` story exercising the
dimming + `inert` behaviour in isolation.
- ViewHeader.stories: rename "Logging" → "Logs" to match `ALL_TABS` in
InspectorView.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InspectorView replaces the prior pair (ConnectedView/UnconnectedView wrapping
children) with a single state-machine view. Holds activeServer, connectionStatus,
initializeResult, latencyMs, activeTab, and availableTabs internally; renders
the appropriate screen based on activeTab; switches the header between connected
and unconnected modes.
Screen data (servers, tools, prompts, resources, resourceTemplates, subscriptions,
logs, tasks, progressByTaskId, history) flows in via props so stories can populate
the entire view with fixtures. Demo data lives in stories, not in the view.
Stories include one server per transport (stdio, sse, streamable-http) so the
TransportBadge / descriptor variants are all visible.
Reorders ViewHeader.stories tabs to match the new ALL_TABS sequence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>