Commit graph inspector/clients/web/src/test/integration/mcp/inspectorClient.test.ts
Author SHA1 Message Date
cliffhall
4bad110942 test(web): eliminate timeout flakiness in the web test suite (closes #1596)
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.

Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.

Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
  ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
  past the 300ms completion debounce (await setTimeout(400)) — which race
  the debounce timer under load — with awaited conditions (findBy/waitFor)
  on the real rendered outcome, plus delay:null. Negative assertions now
  lean on the component's synchronous state/timer teardown instead of a
  timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
  piped out-of-band from the tool's JSON-RPC response, so reading the log
  synchronously after callTool raced the stderr chunk. Wrapped the assertion
  in vi.waitFor so it polls until the line lands.

Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 21:11:11 -04:00
Bob Dickinson
13dafe457b
Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)
* refactor(web): update OAuth E2E tests to use new authentication method

- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.

Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.

* Guided-auth removal, OAuth smoke testing, and TUI/CLI support

* Added CLI OAuth runner flags tests

* Added clarifying comment on port 0 binding

* README notes on TUI oauth callback port, prettier fixes for CLI tests

* CIMD validation uses inline like EMA IdP

* Fixed TUI test failures from merge

* test: bring 5 OAuth files over the 90 per-file coverage gate

The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:

- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
  throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
  provably-dead guards (empty host, out-of-range port, String(err)
  fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
  with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
  the stale-onChange-after-close prev===null guard, and the
  timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
  kinds, the expired IdP session, and the configured-scope/empty-scope
  branches of formatScopes.

npm run coverage now passes (exit 0) across web, cli, tui, launcher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:35:51 -04:00
Bob Dickinson
5de4e911e3
feat(web,core): enterprise-managed authorization (EMA) (#1509) (#1512)
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).
2026-06-24 22:15:12 -04:00
Cliff Hall
5ee347571f
feat(web,core): persist & edit STDIO cwd/env in Server Settings (#1515) (#1518)
* feat(web,core): edit STDIO cwd/env in Server Settings (#1515)

Persist and edit a STDIO server's working directory and environment
variables through the Server Settings modal.

- Add `env` (pair-array, mirrors headers) and `cwd?` to
  `InspectorServerSettings`.
- `storedFieldsToInspectorSettings` lifts the stdio config's `env`/`cwd`
  into settings so the modal can show them; they stay on `config` for the
  transport. Add `envRecordToPairs` / `envPairsToRecord` helpers.
- `env`/`cwd` remain config-owned on disk (the standard mcp.json location):
  the `/api/servers` PUT route writes an edited settings.env/cwd back onto
  the stored stdio config on a settings-only patch (supporting clear), and a
  config-providing PUT keeps config authoritative. `validateSettings`
  accepts `env`/`cwd`.
- Server Settings modal: Working Directory field in Options and a dedicated
  collapsible Environment Variables section — both STDIO-only.
- Registry server.json import sets no `cwd` (the schema has no
  working-directory concept); documented in serverJson.ts.

Tests: serverList converter round-trip, route write-through (set/clear,
config-precedence, non-stdio no-leak, validation), import preservation
(client-config cwd, registry cwd skip), and form/modal component coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): preserve config env/cwd when a settings PUT omits them (#1515 review)

Address Claude's review of #1518.

- `/api/servers` PUT now distinguishes an *absent* `env`/`cwd` (preserve the
  stored value) from an explicit empty list / blank string (clear). Previously
  `validateSettings` coerced a missing `env` to `[]`, so an env-unaware caller
  patching only a timeout would silently wipe the server's env/cwd. The
  integrated web client always resends the full GET-rehydrated env, so it was
  unaffected — this protects the raw HTTP contract.
- `validateSettings` reports `envProvided`/`cwdProvided`; the write-through only
  touches `config.env`/`cwd` for fields the caller actually sent.
- On the omit path, rehydrate the untouched env's keychain values onto the
  rebuilt entry so the secret reconcile re-persists them rather than sweeping
  the blanked-on-disk keys out of the keychain.
- Minor: guard the stdio cast in `mcpConfigToServerEntries` on the transport
  type instead of casting unconditionally.
- Test: settings-only PUT omitting env/cwd preserves config keys + cwd + the
  keychain secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:15:43 -04:00
Cliff Hall
dc31c28102
fix(core): make "error" the canonical terminal status on a mid-session crash (#1490) (#1493)
* fix(core): make "error" the canonical terminal status on a mid-session crash (#1490)

On a real mid-session transport crash many SDK transports fire BOTH
`onclose` and `onerror` in a transport-dependent order. The `onclose`
guard (`if status !== "disconnected"`) let a trailing `onclose`
downgrade a crash's `"error"` status back to `"disconnected"`, so the
persistent terminal status differed by ordering:

- onerror-then-onclose → "disconnected"
- onclose-then-onerror → "error"

`onclose` now treats `"error"` as terminal (only `"connected"`/
`"connecting"` → `"disconnected"`), so `"error"` is the canonical resting
status in both orderings. It still emits the `disconnect` event exactly
once so session-teardown consumers fire identically either way.

Because the state managers gated list/message/subscription/task teardown
on `getStatus() === "disconnected"`, holding the status at `"error"`
would have left them stale in the onerror-first ordering — swapping one
ordering-dependence for another. Introduce `isTerminalStatus()`
(disconnected || error) and key every teardown guard off it so they
clear identically regardless of crash ordering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): single disconnect event when explicitly disconnecting from "error" (#1490 re-review)

Addresses the non-blocking observation from the PR review: explicitly
calling disconnect() from a held-"error" status could fire the
`disconnect` event twice. close() can trigger the transport's onclose
synchronously; onclose (holding status at "error") fired `disconnect`,
then disconnect()'s own guard saw status still != "disconnected" and
fired it again — a timing-dependence on whether the SDK closes sync.

Add a `disconnecting` ownership flag: onclose defers its status set +
`disconnect` event to disconnect() while it is set, so the
explicit-disconnect path emits exactly one `disconnect` regardless of
sync/async close. Crash-path onclose (flag unset) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): deterministically pin the sync-close disconnect double-fire (#1490 re-review)

The real-stdio test asserts the single-disconnect invariant but may not
exercise the sync-close branch (the one that double-fired) if the SDK
stdio transport closes asynchronously. Add a test that overrides the SDK
client's close() to invoke the transport's onclose synchronously,
forcing the exact timing the `disconnecting` flag guards. Verified it
asserts 2 (fails) without the flag and 1 (passes) with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:15:29 -04:00
Cliff Hall
b4716a1405
feat(core): surface mid-session transport failures via lastError (#1489)
Closes #1323. Adds an `error`-event-driven `lastError` on useInspectorClient and a mid-session failure toast in App.tsx; the transport onerror is scoped to non-handshake (status !== connecting) so handshake failures stay on the connect() rejection path. Follow-up: #1490.
2026-06-15 08:46:59 -04:00
Cliff Hall
53dc4549be
fix(core): trace dropped fetch body update when entry rotates out (#1390) (#1480)
* fix(core): trace dropped fetch body update when entry rotates out (#1390)

FetchRequestLogState.onFetchRequestBodyUpdate silently dropped a deferred
response-body update when the matching entry had already been evicted by
log rotation (>= maxFetchRequests newer requests appended between the
append and the fire-and-forget body read). The user saw an empty Response
Body with no way to distinguish "body never came back" from "body dropped
after eviction".

Plumb an optional pino logger into FetchRequestLogState (defaulting to the
silent logger) and emit a logger.debug at the early-return site so the drop
is traceable. The web client wires the existing remote logger from
createWebEnvironment into the state manager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): emit dropped-body trace at warn level so it is observable (#1390)

Review feedback: the web remote logger (createRemoteLogger) defaults to
level 'info' with no override at the environmentFactory call site, so a
logger.debug at the drop site is filtered out by both the pino level gate
and transmit.level — the diagnostic never reaches /api/log and the drop
stays invisible in the shipped web app, defeating the issue's goal.

Promote the trace to logger.warn (>= info, so it transmits by default). A
response body lost to log rotation is unexpected data loss, not routine
diagnostic noise, so warn is the appropriate severity. Update the comment
to explain the level choice and switch the test stub from debug to warn,
extracting a small typed makeWarnLogger helper per review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): configurable Network Log Size per server + body-dropped toast (#1390)

Turn the otherwise-invisible "response body dropped after rotation" case into
an actionable signal:

1. Per-server `maxFetchRequests` setting (default 1000, 0 = unlimited),
   editable as "Network Log Size" in the first (Options) accordion section of
   the Server Settings form. Plumbed through the full settings pipeline:
   InspectorServerSettings + StoredMCPServer types, the serverList disk
   converters (read defaults to 1000, write omits the default to keep diffs
   minimal — 0 is a meaningful non-default), and the node server's GET-sanitize
   + write-side validateSettings (non-negative number). Applied live to the
   active connection on settings-modal close via a new
   FetchRequestLogState.setMaxFetchRequests (shrinking trims immediately), and
   at connect time for the next session.

2. FetchRequestLogState now emits a `fetchRequestBodyDropped` event, but only
   for genuine rotation drops (log at capacity with a finite cap) — not for
   benign post-clear stragglers or unlimited logs, which previously also hit
   the warn path. App shows a deduped, persistent toast (stable per-server id,
   no auto-close) explaining what happened and the likely causes, with a link
   that opens this server's settings on the Options section (modal remounts via
   key) so the user can raise the limit if it's just a high-traffic server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): don't treat a cleared Network Log Size field as unlimited (#1390)

Review feedback: clearing the NumberInput emits "", and `parseInt("") || 0`
coerced that to 0 (unlimited). Since the value applies live on modal close, a
user who cleared the field to retype and then closed the modal would silently
switch the log to unlimited. Fall back to the current value on an empty/NaN
parse and reserve 0 for an explicit numeric entry. Adds a cleared-input 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>
2026-06-14 17:35:52 -04:00
Cliff Hall
3220d6239a
fix(web): send a cancellation notification when cancelling an ordinary tool call (#1458) (#1463)
* fix(web): send a cancellation notification when cancelling an ordinary tool call (#1458)

The Tool detail panel's Cancel button was a silent no-op for ordinary
(non-task) tool calls: App.onCancelToolCall found no captured taskId and
did nothing, leaving the button looking actionable while the call ran on.

Wire request cancellation through the MCP cancellation flow. callTool now
tracks the in-flight call with an AbortController whose signal is threaded
into the SDK request options; aborting it makes the SDK send a
notifications/cancelled for that request and reject the pending call.
cancelToolCall() performs the abort. callTool surfaces the cancellation as
a ToolCallCancelledError (without recording a failed call), which App
clears as a clean cancel plus a short confirmation toast.

Task-augmented calls are unchanged: they have a server-side task and are
still cancelled via cancelRequestorTask (#1457).

Closes #1458

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): address review — distinguish disconnect from cancel; tidy callTool params

Review follow-ups on #1463:

- Disconnect no longer surfaces as "Tool call cancelled". The catch in
  callToolWithRetries now throws ToolCallCancelledError only when the abort
  reason matches the deliberate user-cancel reason (TOOL_CALL_CANCELLED_REASON);
  a disconnect aborts the same controller with a different reason and falls
  through to the normal error path, so no misleading toast.
- Note the activeToolCallAbortController slot is shared (last-writer-wins)
  across all callTool callers.
- Fold callToolWithRetries/attemptToolCall's long positional parameter list into
  a single ToolCallRequest object; move the URL-elicitation loop state into
  callToolWithRetries.

Adds an integration test that a disconnect mid-call rejects as an ordinary
error (not ToolCallCancelledError), using a slow send_progress call over HTTP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:23:02 -04:00
Cliff Hall
6bb49e55ba
fix(web): cancel the underlying task when cancelling a task tool call (#1455) (#1457)
* fix(web): cancel the underlying task when cancelling a task tool call (#1455)

The ToolDetailPanel Cancel button fired onCancel, but App never supplied
an onCancelToolCall handler, so clicking Cancel did nothing — a tool
running as a task kept running on the server.

App now captures the in-flight task-augmented call's taskId from the
`toolCallTaskUpdated` event callToolStream dispatches, and the new
onCancelToolCall handler cancels that task via the existing tasks API
(reusing onCancelTask). The id is reset at the start of every call, so an
ordinary (non-task) call's Cancel stays a no-op (no task to cancel, no
request-abort channel yet) — preserving prior behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): show a toast when a task is cancelled

Cancellation goes out as a `taskCancelled` event (not a status
notification), so a running task's open 'Task <status>' toast would
otherwise hang with no confirmation. Subscribe to `taskCancelled` and
replace that toast (or show a fresh one) with a short auto-closing
'Task cancelled' toast, pruning the dead progress entry. Covers both
cancel paths — the Tasks screen and the Tool detail panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): report a cancelled task as cancelled, not failed

When a task is cancelled, the in-flight callToolStream rejects with a
generic -32603 error, whose error path reported the task as "failed" —
so the Tasks screen flashed FAILED until a manual refresh fetched the
server's true "cancelled" state. InspectorClient now records cancelled
task ids (set in cancelRequestorTask before the await, since cancelling
unblocks the stream) and the stream's error path labels the terminal
task "cancelled" with a matching status message. Cleared on disconnect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: address review nits on task-cancel wiring

- Prune the consumed taskId from cancelledTaskIds in the stream error path
  so the set can't grow across a long session (disconnect-clear remains the
  backstop for cancels whose task completed before the cancel landed).
- Clear activeToolCallTaskIdRef before the cancel resolves so a rapid second
  Cancel click doesn't re-cancel the terminating task (which would surface a
  spurious 'Failed to cancel task' toast). Add a 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>
2026-06-13 18:00:48 -04:00
Cliff Hall
721dae08c6
feat: apply auto-refresh setting live + diff-aware list-changed indicator (#1444) (#1445)
* feat: apply auto-refresh setting live + diff-aware list-changed indicator (#1444)

Two follow-ups to #1402.

1. Live setting application — InspectorClient gains setServerSettings()
   (on the protocol + fake); App pushes edited settings onto the live
   client when the Server Settings modal closes for the connected server,
   so toggling auto-refresh-on-list-changed takes effect without a
   reconnect (the managed state reads getServerSettings() at notification
   time).

2. Diff-aware indicator — in flag-only mode, on */list_changed the managed
   Tools/Prompts/Resources state now fetches the new list and compares it
   to the current one, lighting the indicator only when it actually
   differs. The displayed list stays put until the user pulls via Refresh
   (pull-on-demand preserved). Servers like everything re-send identical
   lists on list_changed; this suppresses the false-positive indicator.
   Refactors each manager's refresh into fetch + apply helpers shared with
   the peek path.

Trade-off: item 2 reintroduces a fetch on */list_changed (to compare), but
unlike the pre-#1402 behavior it never replaces the displayed list under
the user — only the indicator reflects a real change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): make the diff-aware peek clear the indicator on revert (#1444 review)

peekForChange now sets the list-changed flag to whether the server list
differs from the displayed one (setListChanged(differs)) instead of only
ever setting it true. So a later notification that reverts the server back
to the displayed list clears the indicator — nothing left to pull. Adds a
clear-on-revert regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): consolidate managed-list state managers onto a shared base (#1444)

The four per-primitive list managers (tools, prompts, resources, resource
templates) were near-identical copies of the same fetch/apply/peek/connect/
disconnect/list_changed machinery, a standing drift risk. Extract a generic
ManagedListState<T, M> base parameterized by a config (change event,
list_changed notification, capability key, page fetcher, and whether the
list drives an indicator). Each manager is now a ~40-line subclass: its
config plus a typed getter alias.

Public contracts are unchanged — same per-manager *Change / listChangedChange
events and get*/refresh/getListChanged/clearListChanged methods — so the
hooks and all existing tests pass untouched. Templates opt out of the
indicator (supportsIndicator: false): a list_changed auto-refreshes only
when the server opts in, else does nothing. Net -245 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(core): coalesce bursts of list_changed peeks (#1444 review)

A rapid burst of */list_changed notifications fired one concurrent
paginated fetch per notification. Guard peekForChange with an in-flight
flag: while a peek is fetching, a new notification queues a single re-run
instead of launching a concurrent fetch, and the running peek loops once
more on completion to capture the latest server state. Collapses a burst
into sequential peeks. Adds a coalescing test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): debounce list_changed bursts into a single fetch (#1444)

The in-flight guard only coalesced fetches that actually overlapped — when
a server (e.g. everything) emits a burst of */list_changed spaced just far
enough apart that each list call returns before the next notification, we
still fired one list call per notification. Add a real debounce in the
ManagedListState base: a notification (re)starts a timer and the
refresh/peek runs once the burst settles, collapsing N notifications into
one fetch. The in-flight coalescing guard stays as a second line of defense
for a post-debounce notification landing during a slow paginated fetch.

The delay is injectable (DEFAULT_LIST_CHANGED_DEBOUNCE_MS = 250; subclasses
take an optional constructor arg) so tests run with 0 for fast, determinist
ic behavior. Timer is cleared on disconnect, unsubscribe, and destroy. Adds
debounce + in-flight regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): extend the in-flight guard to the auto-refresh path (#1444 review)

The overlap guard was only on the peek path; a burst-during-a-slow auto-
refresh could let a slower older fetch clobber a newer list (applyItems is
last-write-wins). Lift the guard from peekForChange up to runListChanged so
it wraps both the auto-refresh and peek actions: while one is fetching, a
new post-debounce notification queues a single re-run. Adds an auto-refresh
coalescing test alongside the peek one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): light the indicator without fetching when auto-refresh is off (#1444)

Per maintainer feedback: with auto-refresh off, a list_changed should not
issue any list call. The diff-aware peek (fetch-to-compare) meant one
tools/list went out per settled burst even with auto-refresh off, which the
maintainer doesn't want. Revert flag-only mode to lighting the indicator
blindly on a (debounced) list_changed — no network — and the user pulls the
new list via Refresh.

Trade-off (accepted): the indicator lights even when the server re-sent an
identical list (the everything-server case), since we no longer fetch to
compare. Keeps the debounce (one indicator light per burst), the
auto-refresh path + its in-flight guard, the live setServerSettings, and the
ManagedListState consolidation. Removes peekForChange and its tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): scrub stale peek docstrings after the blind-indicator revert (#1445 review)

The diff-aware revert removed peekForChange's body but left its docstring,
which orphaned onto destroy() and described fetch-to-compare/clear-on-revert
behavior that no longer exists. Replace it with a one-line destroy() doc, and
update clearListChanged's doc (peek -> blind light).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): finish scrubbing stale peek references after the revert (#1445 review)

Round-7's scrub missed four more peek/fetch-to-compare mentions in
managedListState.ts (supportsIndicator, fetchAll, the debounce-default and
onListChanged comments) plus six 'async peek' comments in the state tests.
Update them all to the blind-indicator semantics. Doc/comment-only; no
behavior change. grep -i peek over core/mcp/state now returns nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:00:28 -04:00
Cliff Hall
9b558afee1
feat(web): display negotiated MCP protocol version on ServerCard (#1324) (#1442)
* 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>
2026-06-08 07:40:02 -04:00
Cliff Hall
49d6be6987
feat(web): add per-server Roots config + client roots capability (#1425) (#1431)
* feat(web): add per-server Roots config + client roots capability (#1425)

Add MCP roots support to the v2 web Inspector as a per-server config
section in the server settings form (no separate Roots tab/screen).

- Persistence: add `roots: Root[]` to `InspectorServerSettings` and
  `StoredMCPServer`; thread through serverList converters (lift on read,
  drop empty-uri rows / blank names on write, omit when empty) and the
  remote server's read-side drop guard + `validateSettings`.
- Capability: `setupClientForServer` always advertises the roots
  capability (`roots: cleanRoots(savedSettings?.roots ?? [])`) so the
  server can issue `roots/list` and receive `roots/list_changed`.
- Live apply: edits to a connected server's roots are pushed via
  `inspectorClient.setRoots(...)` once, on settings-dialog close — diffed
  against the client's current roots — rather than per keystroke, to
  avoid flooding the server with `roots/list_changed` / `roots/list`.
- Form: new Roots accordion section with a required URI and an optional
  name per row (unlike v1, which was URI-only), + Add Root, and remove.

Tests cover the persistence round-trip (serverList + servers-route),
the form/modal section, and reuse the existing InspectorClient
roots/`setRoots` integration coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(roots): address review — dedup guards/cleanRoots, preserve extra fields, test live-apply

Responding to the PR #1431 code review:

- Dedup `isRootArray`: drop the copy inside `validateSettings` and reuse the
  module-scope guard the read path uses, so the two can't drift
  (core/mcp/remote/node/server.ts).
- Dedup the clean-roots normalizer: hoist a single exported `cleanRoots`
  into core/mcp/serverList.ts and reuse it from both
  `inspectorSettingsToStoredFields` and App.tsx (drops the local copy).
- Preserve non-form root fields (e.g. `_meta`): `cleanRoots` now spreads the
  carried-through fields and only normalizes uri/name, and the modal's
  `handleRootChange` spreads the existing root on edit — so a hand-edited
  root's `_meta` survives load + persist + edit instead of being silently
  dropped.
- Test the App live-apply-on-close path: new App.test.tsx cases assert
  setRoots is called once with cleaned roots when changed on the active
  server, and not called when the cleaned roots already match what the
  client advertises. Adds a cleanRoots unit test (blank drop + _meta
  preserve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:08:18 -04:00
Cliff Hall
ed6eda7492
feat(web): wire up MCP task support — Tasks screen, Run-as-task, status toasts (#1422) (#1424)
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.
2026-06-04 08:49:47 -04:00
Cliff Hall
5fb2b503f4
feat(apps): render running MCP Apps end-to-end (#1370) (#1403)
* 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>
2026-06-01 18:01:44 -04:00
Cliff Hall
440b194f64
fix(state): dispatch connect event after capabilities are populated (#1405)
* fix(state): dispatch connect event after capabilities are populated

The list-state managers (tools/prompts/resources/templates) refresh on the
"connect" event and, since #1395, gate their list RPC on getCapabilities().
But connect() dispatched "connect" before fetchServerInfo() populated
this.capabilities, and refresh() runs synchronously up to its first await —
so the capability gate executed during the synchronous dispatch, read
undefined capabilities (reset to undefined on every disconnect), tripped,
and wiped every list to empty on connect. Tools, prompts, resources, and
resource templates all vanished against any server.

Move the "connect" dispatch to after the awaited fetchServerInfo() so
capabilities are in place before any manager refreshes. The unit tests
missed this because their FakeInspectorClient has capabilities populated
from construction; added an integration regression test wiring a real
ManagedToolsState on connect and asserting the tool list is non-empty
(fails without the fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(integration): harden connect-race regression test per review

Address the two non-blocking suggestions from the PR review:
- Replace the 100ms settle sleep with waitForEvent() on the managers'
  change events, so the test is deterministic against slow CI instead of
  racing a fixed timeout.
- Assert a second affected primitive (ManagedResourcesState alongside
  ManagedToolsState). The fix is a single shared line, but covering two
  distinct capabilities guards against a future change gating one
  primitive differently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:10:07 -04:00
Cliff Hall
85a4b4c057
feat(servers): persist per-server settings to mcp.json (#1352) (#1353)
* feat(servers): persist per-server settings to mcp.json (#1352)

- Add optional `settings` node alongside the transport config in each
  `~/.mcp-inspector/mcp.json` entry. Round-trips through
  `mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and the
  `/api/servers` POST/PUT routes.
- Drop `MCPServerConfig.headers` from `SseServerConfig` /
  `StreamableHttpServerConfig` and remove the headers textarea from
  `ServerConfigModal`. Custom headers are now entered only in
  `ServerSettingsForm` and reach the wire via `settings.headers`.
- Wire `ServerSettingsModal` into `App.tsx`; saves dispatch through a new
  `useServers.updateServerSettings` mutator. `updateServer` carries the
  existing settings across config-only saves so they aren't dropped.
- Apply settings on the *first* outbound request after a server loads —
  no need to open the settings form first:
  - `settings.headers` → SSE / streamable-http wire headers.
  - `settings.metadata` → `InspectorClientOptions.defaultMetadata`,
    merged into every outgoing `_meta` (call-time keys win).
  - `settings.requestTimeout` → `InspectorClientOptions.timeout`.
  - `settings.connectionTimeout` → Promise.race wrapper around connect().
  - `settings.oauth*` → pre-seeded OAuth client credentials.
- `RemoteConnectRequest` carries `settings` so the remote backend can
  source transport headers from the same source as the local path.
- `oauthClientSecret` is persisted in `mcp.json` alongside stdio `env`
  values, both protected by the file's `0o600` permission.
- Update `specification/v2_servers_file.md` with the new shape and the
  removal of the legacy headers field.

Tests: round-trip in `serverList.test.ts`; persistence on POST/PUT in
`servers-route.test.ts`; first-connect header injection in
`transport.test.ts`; `defaultMetadata` merging in
`inspectorClient.test.ts`; `useServers.updateServerSettings` +
`updateServer` settings-preservation in `useServers.test.tsx`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): rebuild InspectorClient on every connect so settings refresh

The previous onToggleConnection branch reused the existing InspectorClient
on a same-server reconnect, which froze settings at the moment the client
was first constructed. Editing headers, metadata, timeouts, or OAuth
credentials in the settings form was therefore invisible until the user
either switched servers or reloaded the page — surprising right after a
visible save.

Always call setupClientForServer on (re)connect so the latest
target.settings flow through to the transport's headers / requestInit, to
defaultMetadata, and to the OAuth manager.

Also adds remoteClientTransport tests that pin the wire-level contract:
settings round-trip through the /api/mcp/connect body when supplied, and
the field is omitted when no settings are configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(history): stop wiping pendingRequestEntries inside the connect event

State managers (ManagedToolsState, ManagedPromptsState, ManagedResourcesState,
ManagedResourceTemplatesState, ManagedRequestorTasksState) handle the
client's "connect" event by calling `void this.refresh()`. The synchronous
portion of refresh runs through SDK Protocol.request → transport.send →
MessageTrackingTransport.send → trackRequest, all before yielding to the
network — so each list request gets tracked into MessageLogState's
pendingRequestEntries inside the same connect event dispatch.

MessageLogState's own "connect" listener was registered after those state
managers (it's the last one constructed in App.tsx) and cleared
pendingRequestEntries on every connect. Listener order meant the clear ran
*after* the requests were tracked but before their responses arrived,
leaving the history with unmatched "response" entries marked PENDING.

The clear-on-disconnect (via statusChange → disconnected) already handles
session-boundary cleanup, so clear-on-connect was purely defensive and now
actively harmful. Drop the connect listener; keep the disconnect path.

Tests: updated the unit test to assert the new behavior (connect does not
clear); added an integration regression that wires a sibling connect
listener firing listTools and asserts the response folds into the request
entry instead of appearing as an orphan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop accidentally tracked debugging screenshot

pending-requests.png was a local debugging artifact attached to a
conversation, not source. Remove it from the tree (the working copy is
kept so the user still has it locally) and rely on the existing untracked
behavior going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(servers): add e2e check that settings.headers reach upstream MCP

Stitches the full v2 pipeline together in one assertion:

  browser-style RemoteClientTransport
    → POST /api/mcp/connect (settings in body)
    → backend createTransportNode applies settings.headers to requestInit
    → SDK StreamableHTTPClientTransport
    → upstream MCP test server

Captures the upstream HTTP request via FetchRequestLogState and asserts
both `X-Tenant` and `X-Trace` are present on the outbound POST. Catches
any regression that breaks the wire between the form and the upstream
server in a single integration test, instead of the previous coverage
which exercised each hop in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ServerSettingsModal): invert ListToggle compact prop so icon matches state

ListToggle's `compact` prop represents the list's *current* compact state:
true means "currently compact" and renders the expand icon to advertise
the action (open up). The settings modal was passing
`compact={allExpanded}` — exactly inverted. When all sections were open
it showed the expand icon (suggesting "open more") and when collapsed it
showed the collapse icon, opposite of every other ListToggle caller in
the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(HistoryListPanel): match ServerListScreen Export button — rename + default variant

Renames "Export JSON" to "Export" and switches from the subtle ToolbarButton
to the same `Button variant="default"` used by ServerListControls so the
two Export affordances look identical across screens. Also moves the
button to the right of the ListToggle (mirroring the order on the server
list screen) and disables it when there are no entries to export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(HistoryListPanel): move Clear button to top toolbar, match Export style

Moves the Clear affordance out of the "History (N)" section header and
up to the panel toolbar alongside Export, where it now uses the same
`Button variant="default"` styling. Order in the toolbar is now:
ListToggle · Clear · Export. Clear is disabled when there are no
unpinned entries to remove (pinned entries are intentionally preserved).

ToolbarButton helper is unused after this change and dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: ignore local debugging screenshots at repo root

Stops `git add -A` from re-staging the `*.png` files Claude Code drops
at the repo root from attached conversation screenshots. The pattern
only matches the top-level (`/*.png`) so legitimate image assets in
subdirectories (e.g. `clients/web/public/`) still get tracked when
explicitly added.

Also untracks the two screenshots that slipped through previously
(initialize-request.png, pending-requests.png).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(LogStreamPanel): match HistoryListPanel button styling

Clear / Export / Copy All on the Logging screen were rendered with the
subtle ToolbarButton helper while History's equivalents use
`Button variant="default"`. Switch all three to the default variant so
the toolbars look identical across screens and mirror the disabling
behavior — each button is now disabled when `entries.length === 0`.

ToolbarButton helper is unused after this change and dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): address PR review — debounce save, preserve-on-omit PUT, validate shape, lift connect timeout

Round-up of fixes from https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4526861959:

1. ServerSettingsModal saves were firing on every keystroke (one PUT +
   full mcp.json refresh per character). App.tsx now debounces by 300ms
   and flushes on modal close so a burst of edits coalesces into one PUT
   and the form value can't flicker back mid-typing.

2. `PUT /api/servers/:id` with an omitted `settings` field used to
   silently wipe the persisted settings node. The route now treats the
   three intents distinctly:
     - field omitted  → preserve existing on-disk settings
     - explicit null  → clear the settings node
     - settings object → validate + apply
   `useServers.updateServer` is simplified to drop the client-side
   carry-through hack, and the test that documented the broken behavior
   is flipped + augmented with explicit-clear and bad-shape cases.

3. `connectionTimeout` Promise.race is moved from App.tsx into
   `InspectorClient.connect()`, reading `this.serverSettings?.connectionTimeout`.
   TUI/CLI consumers now get the same behavior; on timeout the client
   internally disconnects so the next connect starts clean.

4. Custom Headers hint in `ServerSettingsForm` notes that `Authorization`
   is owned by the OAuth flow when OAuth is configured — avoids the
   sharp edge where the SDK's authProvider silently overwrites a
   user-set Authorization header.

5. New `validateSettings` on the server route rejects malformed shapes
   (`settings: []`, `settings.headers: "oops"`, non-numeric timeouts,
   etc.) with 400 instead of letting them persist and crash the UI.

7 (nit). Renamed `settings` → `savedSettings` inside `setupClientForServer`
   so the variable holding the persisted settings doesn't shadow the
   later locally-derived values.

Follow-ups opened separately:
  #1356 — Move oauthClientSecret out of mcp.json into the OS keychain (review #6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): address PR review pass 2 — connect-timeout test, unmount cleanup, tagged intent, lenient settings read

Pass-2 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4527089705:

(a) New integration test pins the new client-layer connect-timeout
    contract: a hanging stub transport with `connectionTimeout: 50`
    rejects with the descriptive error and lands the client in "error"
    status. Documents what TUI/CLI consumers should expect from the
    lifted Promise.race in InspectorClient.connect().

(b) useEffect cleanup clears `pendingSettingsTimerRef` on unmount so a
    route change or HMR mid-debounce can't fire one stale PUT against
    an unmounted component.

(c) Refactor the PUT-route `settingsIntent` discriminator from
    `"preserve" | "clear" | { value }` to a tagged union with `kind`,
    and consume it with a `switch` for exhaustiveness — reads cleaner
    and makes adding a fourth intent painless.

(d) `normalizeMcpServers` now runs `validateSettings` on the on-disk
    settings node and silently strips it if invalid, mirroring the
    existing lenient-on-read pattern from `normalizeServerType`. Closes
    the read/write asymmetry where a hand-edited malformed `settings`
    could survive on disk and get carried through preserve-on-PUT.
    New route test pins the strip-on-GET + no-propagate-on-preserve-PUT
    behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): pass-3 review — patch-style PUT, toast on close-flush fail

Pass-3 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529276897.

(b) `PUT /api/servers/:id` now treats both `config` and `settings` as
    optional patches:
      - field omitted → preserve the on-disk value
      - explicit body value → apply
      - settings: null → clear
    `updateServerSettings` stops reading `existing.config` from the
    in-memory `servers` snapshot, which closes the closure-capture
    hazard where a refresh between debounce-schedule and flush could
    silently revert a separate edit. The route is the single source of
    truth for transport config now. New tests cover settings-only PUT,
    rejected non-object config, and the empty-body no-op patch case.
    The previous "rejects a missing config" test is rewritten to
    document the new no-op-preserve semantics.

(c) Inline comment near the PUT rebuild loop documents that writing
    the full mcpServers map back is what gives the route its desirable
    self-healing property for malformed settings on *other* entries —
    deliberate side-effect, not an accident of normalization.

(d) `flushPendingSettings` surfaces failures via @mantine/notifications
    instead of swallowing them. The modal already closed by the time
    the flush PUT resolves, so a silent fail would leave the user
    thinking their last edits saved (especially painful for the OAuth
    client secret). The notifications wiring was already mounted in
    main.tsx.

Pass-3 (a) — settings form being effectively uncontrolled mid-edit — is
acknowledged in the reply: working as intended today; the proper fix is
internal modal state when file-watching (#1345) forces the issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): pass-4 review — strip smuggled settings out of config

Pass-4 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529861349.

Main finding: `normalizeServerType` spreads unknown keys, so a body
that nested `settings` inside `config` would smuggle a settings field
onto the stored entry without ever passing through `validateSettings`.
Strip the settings key off the incoming config in `buildStoredEntry`
so `validateSettings` remains the single write path for the settings
node — the "one source of truth" invariant pass 2 set up holds again.

Two new tests pin the strip on both routes:
  - POST with `config.settings = { bogus }` lands an entry with no
    settings node on disk.
  - PUT with `config.settings = { bogus }` + `settings: null` clears
    the real settings *and* doesn't re-attach the bogus payload via
    the spread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): pass-5 review — absorb late connect rejection, tighten validateSettings, log smuggle warning

Pass-5 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529954490.

Main finding: when the connect-time `Promise.race` lost to the timeout,
the original `this.client.connect(this.transport)` promise was left
pending. After `disconnect()` tore the transport down, real transports
(SSE / streamable-http) would reject that promise later — with no
handler attached, producing a Node `unhandledRejection` (and a vitest
suite warning). Attach a no-op `.catch` right after creating the
connect promise so Node sees the late rejection as handled.

Nit (a): `validateSettings` was casting the raw object through to
`InspectorServerSettings`, which let unknown keys ride along onto disk.
Replaced the cast with an explicit pick-and-build over the known
fields. Unknown stowaways now silently drop on the way through. New
route test pins the behavior.

Nit (b): `buildStoredEntry` now logs a `warn` via the route's pino
logger when an incoming `config` carries a `settings` key. The strip
is still defensive (it isn't a documented contract) but the warning
lets a misconfigured client find out about the wasted field instead
of debugging a silent drop.

Nit (c) — the defensive `?? {}` fallback in `buildStoredEntry` —
acknowledged in the review reply; left in place as belt-and-braces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(servers): pass-6 polish — scope absorber, thread id, drop empty OAuth, guard `in`

Pass-6 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529979135. Reviewer signed off as ready; landing the four marginal cleanups since they're all small.

(a) `connectPromise.catch(() => {})` moves inside the
    `if (connectTimeoutMs > 0)` branch. The await-only path propagates
    rejection cleanly through the outer try/catch — the absorber is
    only needed when the race leaves the original promise unattended.
    Code now reads as "absorber is tied to the race," which is the
    real invariant.

(b) Thread `id` (POST id, PUT newId) through `buildStoredEntry` and
    include it in the smuggle warning's pino bindings. Two clients
    smuggling simultaneously are distinguishable in the log instead
    of indistinguishable.

(c) `validateSettings` coerces empty-string OAuth fields to absent.
    The form emits `""` when the user clears an input, and an empty
    `oauthClientId` on disk would later be misread as "OAuth configured."
    New route test pins it.

(d) `buildStoredEntry` adds a null/object guard before `"settings" in
    configObj`. The route layer pre-checks this on POST and PUT, but
    the helper's `unknown` parameter is a weak contract; the guard
    makes it safe to call from anywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): reflect settings persistence + patch-PUT contract in v2_servers_file

The doc was authored when `mcp.json` only stored the server list. Bring it
forward to match what shipped in #1352 / PR #1353:

- Summary calls out that the file also stores per-server settings and
  links into the Per-server settings section.
- On-disk format note explains that `settings` is an optional Inspector-
  specific extension that other MCP tools ignore.
- New top paragraph on the Per-server settings section explains the
  basic-config / settings UI split rationale (simpler form for the
  common case, advanced settings tucked behind a second dialog).
- UI bullet is rewritten to match the patch-style PUT semantics that
  emerged across the review cycle: settings-only and config-only
  PUTs preserve their counterpart inside the write lock, so neither
  modal can silently clobber the other half. The earlier "must
  re-send settings on PUT" hack is gone.
- New bullets capture the save cadence (300ms debounce + flush-on-close
  + failure toast), the patch-PUT contract (omit-preserve / null-clear /
  rename), and the write-path / read-path validator gates.
- Secret storage bullet links to #1356 (OS-keychain follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): show a third entry with the optional settings node

The On-disk format example demonstrated only basic stdio entries. Add a
streamable-http entry with a populated `settings` node so readers can
see the shape inline rather than having to scroll to the Per-server
settings section. Inline comment in the block cross-links there for the
full contract and notes that other MCP tools ignore the extension key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:23:26 -04:00
Cliff Hall
8bb389c8e3
test: switch integration project to folder-based manifest (#1314) (#1318)
* test: switch integration project to folder-based manifest (#1314)

Replace the `integrationTests` enumeration in vite.config.ts with a
folder glob (`src/test/integration/**/*.test.ts`). Files under that
folder run in the integration vitest project (node env, 30s timeouts);
everything else runs in the unit project (happy-dom).

This removes the silent misclassification trap from #1307: a new
integration-style test no longer falls into the unit project (and
happy-dom) just because someone forgot to add it to the array.

The 13 ported v1.5 tests + their helpers move under src/test/integration/
in a strict mirror of core/ source layout (mcp/, auth/, auth/node/,
storage/). Renames use git mv so history is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: address PR review on #1318 (glob extensions + AGENTS doc)

- vite.config.ts: widen integrationGlob from `*.test.ts` to
  `*.test.{ts,tsx}` so the integration folder fully owns its contents.
  Without this, a `.test.tsx` placed under src/test/integration/ would
  match the unit project's `{ts,tsx}` include, fail the unit `.ts`-only
  exclude, and silently land under happy-dom — the exact misclassification
  trap this PR set out to close.
- AGENTS.md: update the line describing where integration tests live;
  it still pointed at the old `src/test/core/` location and the removed
  `integrationTests` enumeration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:54:49 -04:00
Renamed from clients/web/src/test/core/inspectorClient.test.ts (Previous history)