Commit graph inspector/core/mcp
Author SHA1 Message Date
Cliff Hall
b3b04c6939
Thread cacheMode through managed list refresh via SDK cache-aware verbs (#1721) (#1723)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Thread cacheMode through managed list refresh via SDK cache-aware verbs (#1721)

Manual/auto refresh of tools/resources/prompts now forces a cache-bypassing
round trip on modern servers, delivering the raw-request cacheMode control
deferred from #1628.

The InspectorClient list/read path uses the SDK's single-page `client.request`
verbs (for pagination debugging), which never consult the SDK response cache —
so `cacheMode` was inert there. This adds a separate aggregate path that routes
through the SDK's cache-aware high-level verbs, where `cacheMode` is honored:

- InspectorClient: new `listAllTools/Prompts/Resources/ResourceTemplates(
  { cacheMode, metadata })` that call `client.listTools()` etc. (all-page
  aggregate + SEP-2243 x-mcp-header exclusion + cache consult/write). A shared
  `getCacheableRequestOptions` / `aggregateListParams` back all four.
- InspectorClientProtocol: the four `listAll*` methods added.
- ManagedListState: delegates page walking to the SDK verb (config `fetchPage`
  loop → `fetchAll`); `refresh(metadata?, cacheMode?)` threads cacheMode. A
  `list_changed` auto-refresh bypasses the cache (`cacheMode: "refresh"`).
- Managed hooks: user-initiated `refresh()` passes `cacheMode: "refresh"`, so
  the existing per-tab Refresh buttons become genuinely cache-bypassing — no
  App wiring change. Connect-time load stays default (`'use'`).

The single-page `listTools(cursor)` primitive is unchanged; the paged hooks
still surface per-page `nextCursor`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Single-page list pagination: sidebar toggle + server setting (#1721)

Delivers the deferred single-page fetch UI: Tools/Resources/Prompts can be
loaded one page at a time instead of auto-aggregating every page — a defensive
option for servers with very large lists.

- New `singlePageLists` server setting (persisted, server-wide), plumbed through
  ServerConfig/InspectorServerSettings, serverList convert/default, the node +
  remote settings paths, and the Server Settings form ("Fetch Lists One Page at
  a Time").
- New `ListPaginationControls` element (a "Single page" switch + "Load next
  page" button + "N pages loaded" status) rendered in each list sidebar; the
  toggle edits the setting live (optimistic) and persists it.
- The paged state stores (PagedTools/Prompts/ResourcesState) now own loading:
  they auto-load page 1 on connect in single-page mode and track nextCursor +
  page count as observable state; the managed stores skip their all-page walk
  in single-page mode (so nothing is over-fetched) while still lighting the
  list-changed indicator. `usePaginatedList` selects the display source and
  pagination controls per mode — pure, no data-loading in React effects.
- Refresh is mode-aware: reload page 1 in single-page mode, refetch the whole
  aggregate otherwise. Sidebars widened 340→360 so the controls don't crowd
  list entries.
- Test server: `test-servers/configs/pagination-http.json` (12 tools/resources/
  prompts, page size 4 → 3 pages each) exercises the flow end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Add pr-screenshots proving single-page pagination (#1721)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Address @claude review: template load, double-click guard, auth recovery (#1721)

- Fix: resource templates never loaded on connect in single-page mode. The
  connect-time all-page skip now applies only to lists with a paged counterpart
  (new `deferInSinglePage` flag on ManagedListConfig; false for resource
  templates), so templates still aggregate on connect regardless of the setting.
- Guard `PagedTools/Prompts/ResourcesState.loadPage` against a concurrent load
  (fast double-click on "Load next page") so the same page can't be appended
  twice; the in-flight load owns the cursor and the second call is a no-op.
- Thread ambient auth recovery through the single-page refresh, Load-next-page,
  and mode-switch loads (they previously bypassed it) — usePaginatedList's
  onRefresh/onLoadMore now return their promises so App can wrap them.
- Drop the misleading "aggregated list" wording from the paged-store docs.

Tests added for each; npm run coverage green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Review round 2: revert optimistic toggle when persisting the setting fails (#1721)

Non-blocking review follow-up. If updateServerSettings rejects, the optimistic
`singlePageListsOverride` is now cleared and the live client setting rolled back
(the effect only clears the override when the persisted value changes, which
doesn't happen on failure), so the UI and client reflect what's on disk rather
than the failed edit. Added an App test for the revert path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Fix single-page Refresh not clearing list-changed indicator + UI polish (#1721)

Round-3 review fix (blocking): in single-page mode a `list_changed` lights the
list-changed indicator, but the single-page Refresh reloads page 1 of the paged
store and bypassed `useManaged*.refresh()`, so the indicator was never cleared.
Expose `clearListChanged` from useManagedTools/Prompts/Resources and call it in
the single-page branch of onRefreshTools/Prompts/Resources. Tests added.

UI polish (user feedback on the sidebar control):
- Rename the switch label "Single page" → "Paginated".
- Put "Load next page" on the same row as the switch, space-between (switch
  fully left, button fully right).
- Move the "N pages loaded" status to its own line below the row, centered.
- Hide the whole control once the list is known to be a single page
  (single-page mode, page 1 loaded, no nextCursor) — nothing to paginate.

pr-screenshots refreshed to the new layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Rename the singlePageLists setting to paginatedLists for clarity (#1721)

`singlePageLists` read backwards from the "Paginated" switch label (it sounds
like paginated=false). Rename the server setting to `paginatedLists` with the
same value meaning — true = paginated/one-page-at-a-time, false = aggregate-all
— and follow through on the related identifiers for consistency:

- Setting field `paginatedLists` across ServerConfig / InspectorServerSettings,
  serverList convert/default, node + remote settings paths, and the form/modal
  callbacks (onPaginatedListsChange).
- `usePaginatedList` / `ListPaginationControls` prop `singlePage` → `paginated`,
  `onSinglePageChange` → `onPaginatedChange`.
- `ManagedListConfig.deferInSinglePage` → `deferWhenPaginated`.
- App state (paginatedListsOverride, onTogglePaginatedLists, …) and all
  tests/fixtures + mode-referring comments.

"single-page" wording that denotes the SDK's one-page `client.request` verb
(inspectorClient) is intentionally left as-is. Purely a rename — no behavior
change; unit + integration + cli + tui + coverage all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Format App.test.tsx (prettier) after the paginatedLists rename (#1721)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 01:06:31 -04:00
Cliff Hall
d552564966
Network: modern Mcp-* headers, new error codes, cancellation-as-abort (#1628) (#1722)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Network: modern Mcp-* headers, new error codes, cancellation-as-abort (#1628)

Upgrade the Network tab for the modern Streamable HTTP transport
(SEP-2243 / SEP-2575):

- Recognise and highlight the mirrored headers (Mcp-Method, Mcp-Name,
  Mcp-Param-*, MCP-Protocol-Version), decode =?base64?…?= sentinel values
  inline, and cross-check each against the request body so a HeaderMismatch
  is visible before the server rejects it.
- Show the new spec error codes distinctly: -32020 HeaderMismatch,
  -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion
  (with the server-advertised `supported` versions), and modern -32601
  MethodNotFound on HTTP 404 (distinguished from a legacy bare 404 by its
  JSON-RPC body).
- Reflect cancellation as a connection abort ("Cancelled") rather than a
  hard error, per SEP §7.5.
- Confirm and document browser-path header mirroring: the SDK builds the
  Mcp-* headers client-side and the /api/fetch proxy forwards them verbatim
  (new integration test).

New pure util `mcpNetworkHeaders.ts` holds the recognition/decode/classify
logic; new `McpErrorBadge` element renders the distinct error chips. The
optional raw-request controls (cacheMode bypass/refresh, single-page fetch)
are split to #1721 — they are cross-cutting core + feature-tab work beyond
the Network tab.

Also refreshes the AGENTS.md board Status option-ID table (the Building /
MCP Apps Extension columns were removed).

Closes #1628

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network smoke-test showcase + fix redaction clobbering JSON-RPC error codes

Adds a composable "modern-network" showcase server for exercising the #1628
Network-tab work end-to-end, and fixes a real bug the smoke test surfaced.

Bug fix (core): the fetch-tracking body redaction masked ANY field named
`code` (targeting the OAuth authorization code), including the numeric
JSON-RPC `error.code`. That destroyed the field the Network tab classifies
the modern spec errors on, so -32020/-32021/-32022/-32601 never rendered
against real traffic. Redaction now only masks STRING values of a sensitive
field (secrets are always strings; a numeric code is never one).

Test-server showcase (test-servers): `modern-network-http.json` serves a
`get_weather` tool with an `x-mcp-header: "City"` annotation plus four
`trigger_*` tools. A new `transport.modern.injectSpecErrors` flag installs a
middleware on the modern leg that answers the triggers with real HTTP status
+ JSON-RPC error bodies (400/-32020, 400/-32021, 400/-32022 w/ `supported`,
404/-32601), so the Inspector's error rendering can be driven live.

Mirroring finding (verified live, comment corrected in createRemoteFetch.ts):
the SDK builds the STANDARD mirrored headers (Mcp-Method / Mcp-Name /
MCP-Protocol-Version) unconditionally — they reach the server through the
web proxy — but gates `Mcp-Param-*` on a non-browser environment
(`detectProbeEnvironment() !== "browser"`), so the web client never emits
them. An x-mcp-header tool is therefore uncallable from the web client
against a strict modern server (it answers -32020); it works from the Node
CLI/TUI. The earlier "mirroring works through the proxy" note was corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network: move the spec-error badge off the crowded compact top row

In the narrow monitoring sidebar (embedded layout) the McpErrorBadge shared
the top row with the timestamp/method/category/duration/status chips, which
truncated the method and category ("PO…", "TRANSP…") and clipped the error
name. Render the badge separately from `metaBadges` and place it on the
second row, to the right of the URL, so the top row stays readable and the
error name shows in full. The full-width layout keeps it in the top-right
cluster (where there's room).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network: keep the spec error name's casing in McpErrorBadge (no uppercase)

The four spec/SDK error identifiers are PascalCase (`HeaderMismatch`,
`MissingRequiredClientCapability`, `UnsupportedProtocolVersion`,
`MethodNotFound`). Mantine's Badge uppercases label text by default, which
flattened them to a hard-to-read run-together string
("UNSUPPORTEDPROTOCOLVERSION"). Set `tt="none"` so the badge shows the
spec's own casing with word boundaries intact; short badges (POST, status)
keep the default uppercase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network: in the full-width layout, move the spec-error badge to the second row

The full-width entry kept the McpErrorBadge in the top-right cluster, where a
long spec name (e.g. "MissingRequiredClientCapability") crowded the top row
and truncated the method chips and URL. Move it to the start of the second
row (the expand-toggle row) so the top row stays readable; the toggle stays
at the end (`space-between` only when the badge is present). The compact
sidebar already places it on its second row beside the URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Protocol: show Replay as the right-side icon in the wide layout too

The compact (sidebar) ProtocolEntry already renders Replay as an icon next to
the pin/expand icons; the wide layout used a left-aligned "Replay" text
button instead. Make the wide layout match — Replay as the ReplayButton icon
at the start of the right-side action cluster (Replay, Pin, Expand) — so the
two layouts are consistent. Drops the now-unused SubtleButton constant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Move the modern spec-error chip + alert from the Network tab to the Protocol tab

The Network tab is focused on the HTTP transaction; the distinct protocol-error
taxonomy (-32020/-32021/-32022/-32601) belongs with the protocol messages. So:

- ProtocolEntry now renders the McpErrorBadge chip on the top line (right after
  the direction badge, both layouts) plus an explanatory alert in the expanded
  detail, classified by JSON-RPC error code via `classifyProtocolSpecError`.
- NetworkEntry drops the chip + alert; it keeps the HTTP-level features (status
  badge, Mcp-* header highlight/sentinel-decode, header/body mismatch marker,
  cancellation-as-abort).
- The SDK delivers -32020/21/22 (HTTP 400) as JSON-RPC error responses, so they
  reach the Protocol log directly. -32601 (HTTP 404) and other non-enveloped
  errors are *thrown* — the request stays pending. `enrichProtocolEntries`
  (App) correlates the Protocol log to the Network log by JSON-RPC id (parsed
  from the transport fetch's requestBody) and folds the thrown error onto its
  pending request, so it too appears in Protocol. This is a derived view; the
  underlying log is not mutated.

Verified live against the modern-network showcase: both a -32020 error response
and a thrown -32601 render distinctly in the Protocol tab.

Follow-up: a "reveal in Network" link on the Protocol alert (cross-tab
scroll-to + expand of the correlated HTTP entry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Protocol: show the spec-error chip only in the wide layout, after the method

Refine the placement of the spec-error chip added in the previous commit:
- Wide layout: move it from after the direction badge to right after the
  method chip (reads as "tools/call  -32020 HeaderMismatch").
- Compact sidebar row: drop the chip entirely — the two-line row stays
  uncluttered and relies on its ERROR status badge. The explanatory alert
  still appears in the expanded detail in both layouts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Protocol: "reveal in Network" link — jump from a spec error to its HTTP entry

Adds the cross-tab navigation that was the follow-up to relocating the
spec-error rendering. The expanded Protocol alert now shows a "View the HTTP
request in the Network tab" link when the entry has a correlated Network
request; clicking it:

- correlates the Protocol message to its transport fetch by JSON-RPC id
  (`revealableMessageIds` gates the link; `correlateFetchEntry` finds the
  target on click),
- clears whatever filter would hide the target (network filterText +
  category, and the embedded monitorSearch),
- switches to the Network view in whichever mode is active — the pinned
  column (`pinMonitor`) when the sidebar is open, or the full-screen header
  tab (`onActiveTabChange`) when it's closed,
- scrolls the matching NetworkEntry into view and force-expands it.

The reveal is a one-shot: a new `revealId` threads NetworkScreen →
NetworkStreamPanel → NetworkEntry, where the targeted entry scrolls itself
into view (rAF, after `useScrollMemory`'s restore) + expands, then clears the
signal via `onRevealComplete`. Verified live in both the pinned-sidebar and
closed/full-screen layouts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Protocol: only show the "reveal in Network" link when it adds value

Gate the link on a new `httpRelevant` flag: show it only when the error is
either thrown by the SDK (its real HTTP response lives only in the Network
log — the Protocol entry is a synthetic fold) OR tied to the HTTP request
headers. A delivered, protocol-only error gets no link, since the raw HTTP
entry adds nothing over the alert.

- -32020 HeaderMismatch  → link (the mirrored headers are the problem)
- -32601 MethodNotFound  → link (thrown; the 404 response is only in Network)
- -32021 MissingRequiredClientCapability → no link (delivered, protocol-only)
- -32022 UnsupportedProtocolVersion      → no link (supported list is in the alert)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network: cite SEP-2575 for cancellation-as-abort, not the internal doc's §7.5

The cancellation note referenced "(SEP §7.5)", but §7.5 is a section of the
internal v2_new_spec_impact.md doc, not a SEP. The behavior — closing the
Streamable HTTP request stream is the cancellation signal; notifications/cancelled
is stdio-only — is defined in SEP-2575 (stateless-mcp). Cite SEP-2575 in the
user-facing alert and the two related comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network: fix reveal one-shot canceling its own scroll; review doc/comment nits

Address @claude review of #1722:
- Reveal one-shot: move onRevealComplete() inside the rAF (after scrollIntoView)
  so clearing revealId (which flips revealed->false and runs the effect cleanup)
  can no longer race/cancel the pending scroll. Add a flip-back test.
- Comment the intentional double-mount of revealId (pinned monitor + full-screen
  ScreenStage both match; clear is idempotent).
- Doc drift after the Network->Protocol relocation: McpErrorBadge + the
  mcpNetworkHeaders module header now say the spec-error chip lives in Protocol.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Network: clarify redaction array-of-scalars edge + revealableIds superset

Address second @claude review of #1722 (both non-blocking polish notes):
- fetchTracking redaction: note that masking assumes a sensitive value is a
  scalar or object; an array-of-scalars under a sensitive key would slip through
  (no OAuth/token payload has that shape).
- revealableMessageIds: document it's a superset of the linked entries; the
  spec-error/httpRelevant narrowing is applied downstream at render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Protocol: gate modern -32601 framing to a genuine 404, not in-band 200

Address third @claude review of #1722. classifyProtocolSpecError recognised
-32601 from the code alone, so an ordinary in-band 'method not found' returned
on HTTP 200 was dressed up as the modern MethodNotFound taxonomy: the spec
badge, a 404-asserting alert, and a 'reveal in Network' link that jumped to a
plain 200 (claiming diagnostic value that wasn't there).

Gate the modern -32601 treatment on the correlated fetch actually being a 404
(mirroring classifyMcpSpecError). -32020/-32021/-32022 are SEP-reserved and
unaffected. The correlated HTTP status is threaded from InspectorView (new
correlatedFetchStatusById) through ProtocolScreen/ProtocolListPanel to
ProtocolEntry; when unknown, -32601 falls back to code-only recognition (the
thrown-404 path always carries a 404). Badge, alert copy, and link now resolve
together, so a plain in-band -32601 falls through to ordinary error rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Protocol: require a genuine 404 for modern -32601 (drop unknown-status fallback)

Address fourth @claude review of #1722. The prior fix only rejected -32601 when
the status was *known* and not 404; an unknown status fell back to code-only
recognition. But the thrown-404 path always carries a 404 (folded in from a 404
fetch), so the unknown-status branch is only ever reached by an ordinary in-band
-32601 with no correlated 404 — most notably a stdio connection (no HTTP at all)
— which then wrongly got the modern MethodNotFound badge and 404-asserting copy.

Drop the escape hatch: -32601 is the modern taxonomy only on httpStatus === 404.
Nothing intended is lost (the genuine case always has its 404). Update the tests
(code-only/undefined -32601 now expects null; add a stdio no-correlated-fetch
ProtocolEntry case) and mark the MethodNotFound story with correlatedHttpStatus
404.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:32:05 -04:00
Cliff Hall
9b2c188d11
MRTR: drive input_required manually to keep pending-request UX (#1704) (#1719)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* MRTR: drive input_required manually to keep pending-request UX (#1704)

Follow-up to #1627/#1705. Instead of letting the SDK auto-fulfil the modern
(2026-07-28) MRTR retry loop, InspectorClient now drives it itself so the
Inspector keeps its pending-request UX, owns the loop, and shows era-accurate
semantics.

Core (core/mcp/inspectorClient.ts):
- Construct the SDK Client with `inputRequired: { autoFulfill: false }`
  (unconditional; a no-op on legacy, which never returns input_required).
- New `requestWithInputRequired()` driver: issues the request with
  `allowInputRequired` + `withInputRequired(schema)`, and while the result is
  `input_required`, fulfils each embedded request through the SAME pending
  sampling/elicitation plumbing, then retries the original call with
  `inputResponses` + the echoed `requestState` on a fresh id, bounded by
  MRTR_MAX_ROUNDS.
- Route tools/call, prompts/get, and resources/read through the driver.
- Embedded roots/list is auto-answered from configured roots (no pending UX);
  decline/cancel is echoed to the server (not treated as an abort); a tool-call
  abort mid-round settles the pending request and surfaces
  ToolCallCancelledError.
- Extract shared `enqueuePendingSample`/`enqueuePendingElicitation` helpers used
  by both the legacy inbound handlers and the driver; add a per-request `origin`
  ("server-request" | "input-required") to the pending wrappers.

Web UI:
- New `MrtrOriginNote` element shows an "input_required — your answer returns as
  a retry" note on a modern MRTR round; legacy panels are unchanged.
- Thread `origin` App -> PendingClientRequestModal -> the sampling/elicitation
  panels (and the inline panels).

Test servers: new MRTR presets (multi-round, roots, sampling, loop, edge-case
param shaping). Integration tests cover the manual round-trip (asserting the
pending UI pauses and the retry carries inputResponses + requestState on a new
id), multi-round, roots auto-answer, sampling, decline-echoed, maxRounds bound,
abort mid-round, and requestState-only/inputRequests-only param shaping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Review: clarify sampling-reject and between-rounds abort comments (#1704)

Address review nits on PR #1719 (comment-only, no behavior change):
- enqueuePendingSample docstring: sampling has no decline/cancel action; the
  panel either sends a result (resolve) or Rejects (reject → fails the call).
  The shared "declined/cancelled resolves" wording was accurate only for the
  elicitation case.
- callToolWithRetries abort comment: note the between-rounds case — when the
  abort lands while an MRTR round awaits an embedded pending request there is no
  in-flight SDK request, so nothing is sent on the wire; the outcome
  (ToolCallCancelledError, no failed-call record) is identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* test-servers: add mrtr-showcase config exercising every MRTR preset (#1704)

A single modern-era streamable-HTTP server (port 3111) bundling all six MRTR
presets — mrtr_confirm, mrtr_two_step, mrtr_sample, mrtr_roots, mrtr_edge,
mrtr_loop — for manual testing of the manual-driving pending-request UX across
single-round, multi-round, sampling, roots, param-shaping, and maxRounds paths.
Document it in the README alongside modern-mrtr-http.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:15:19 -04:00
Cliff Hall
e026051b47
Era-aware connection model & capability gating (#1626) (#1697)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Era-aware connection model & capability gating (#1626)

Make protocol era (SEP §7.8) a first-class, per-server connection dimension,
orthogonal to the transport type, and drive the SDK Client's versionNegotiation
from it.

Config
- Add `ServerProtocolEra` ("legacy" | "auto" | "modern"), `DEFAULT_PROTOCOL_ERA`
  (legacy, per the SDK's don't-auto-probe guidance), `MODERN_PROTOCOL_VERSION`,
  and `eraToVersionNegotiation()` in core/mcp/types.ts.
- Persist `protocolEra` on `StoredMCPServer` / `InspectorServerSettings`,
  round-tripping through serverList.ts (omitting the default on disk) and the
  `/api/servers` read + write validators.

InspectorClient / state
- Thread `versionNegotiation` through `InspectorClientOptions` (default legacy),
  replacing the hardcoded pin. After connect, read `getProtocolEra()` /
  `getDiscoverResult()` / `getNegotiatedProtocolVersion()`, expose them via
  getters, dispatch `protocolEraChange` / `discoverResultChange`, and clear on
  disconnect. `useInspectorClient` surfaces `protocolEra` / `discoverResult`.

Web UI
- ServerSettingsForm: a Protocol Era selector bound to the per-server setting.
- ConnectionInfoContent: era-aware — Era badge, Session ("Sessionless" on
  modern), and a Discovery section (supported versions + extensions) from the
  `server/discover` result.

CLI/TUI also map the mcp.json `protocolEra` into versionNegotiation so the field
applies uniformly. Version negotiation stays fully client-side; modern connect
works over the existing remote tunnel and disconnect is sessionless (the SDK
skips DELETE when there is no session id). Mcp-* header fidelity is left to the
History/Network card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Address @claude review: era docstrings, era validation, stdio session label, AGENTS.md rule

- Fix stale docstrings/comments in inspectorClient.ts, useInspectorClient.ts,
  inspectorClientEventTarget.ts, and ConnectionInfoContent.tsx: getProtocolEra()
  returns "legacy" (not undefined) on a plain legacy connect; it is undefined
  only when not connected.
- Validate protocolEra at the storedFieldsToInspectorSettings read boundary
  (new shared isProtocolEra guard) so a hand-edited mcp.json read directly by
  the CLI/TUI drops an unknown era instead of passing it to
  eraToVersionNegotiation. Keeps the exhaustive switch intact.
- ConnectionInfoContent: the Session row is HTTP-only — show "N/A (stdio)" for
  stdio instead of the misleading "Session-based".
- AGENTS.md: add a rule against unjustified `as unknown as` double casts, and
  drop the one such cast introduced in the serverList test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Address re-review: dedupe isProtocolEra guard in remote server

Replace the local isProtocolEra in remote/node/server.ts with the shared guard
now exported from serverList.ts (already imported here), so the era literal set
lives in one place and can't drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:39:29 -04:00
Cliff Hall
7fecebed5e
OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1694)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1625)

Re-key the persisted OAuth store to (server, issuer) and add the
SEP-837/2350/2352 authorization-hardening UX. The SDK v2 auth() drives the
SEP-2352 flow off the provider's ctx.issuer; the Inspector's job is to honor it
and round-trip the issuer stamp.

SEP-2352 — issuer-bound credentials
- ServerOAuthState nests issuer-bound credentials under
  byIssuer[issuer] = { clientInformation, clientRegistrationKind, tokens }, with
  an activeIssuer pointer answering the transport's ctx-less per-request bearer
  read.
- Fix a latent bug: OAuthStorageBase re-parsed tokens/client-info through
  OAuthTokensSchema/OAuthClientInformationSchema, which strip the non-RFC
  `issuer` field and silently defeat the SDK's discardIfIssuerMismatch. The
  store re-attaches the stamp from the byIssuer key.
- BaseOAuthClientProvider threads ctx.issuer and implements
  discoveryState()/saveDiscoveryState() (activates the SDK's callback-leg
  AuthorizationServerMismatchError binding check) and invalidateCredentials().
- Lazy, migration-free upgrade: a legacy pre-1625 blob's top-level
  clientInformation/tokens are returned as an unkeyed fallback until the first
  issuer-stamped save promotes them into byIssuer.

SEP-837 — application_type
- DCR declares application_type: "native" (localhost Inspector).
- ClientSettingsForm renders RegistrationRejectedError detail and reflects DCR's
  deprecated-in-favor-of-CIMD (SEP-991) status.

SEP-2350 — step-up scope union
- Per-server onInsufficientScope (reauthorize | throw) setting wired
  ServerSettingsForm -> StoredMCPServer.oauth -> StreamableHTTPClientTransport.
- StepUpAuthModal visualizes the prior-union-challenged scope set, tagging each
  scope `new` vs `already granted`.

mcpAuth simplification
- Delete the hand-rolled authorizeWithoutRefresh (mishandled issuer stamping);
  forward forceReauthorization/skipIssuerMetadataValidation to native SDK auth()
  so both the initial and step-up paths get SEP-2352 stamping.

Vocabulary + docs
- OAuthStep gains cimd_fetch / issuer_comparison / scope_step_up (recorded on a
  step-up redirect); the Network tab labels auth requests by OAuth phase.
- v2_auth_hardening.md gains an as-built status section.

Deferred to the era-aware card: the SdkError(EraNegotiationFailed) ->
UnauthorizedError unwrap and finishAuth(URLSearchParams) form (only fire under
non-legacy negotiation).

Closes #1625

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

* Address @claude review: issuer-stamp source gating + clear/activeIssuer + phase over-match

- SEP-2352: stamp the issuer only when the credential actually came from the
  byIssuer slot (`fromSlot`), not merely when a slot exists. A partially-populated
  slot alongside a surviving legacy field would otherwise stamp a legacy (possibly
  cross-AS) credential with the current issuer, defeating discardIfIssuerMismatch.
- Issuer-scoped clears no longer promote the cleared issuer to `activeIssuer`
  (updateIssuerSlot gains a `setActive` flag; save paths keep the promotion).
- oauthNetworkPhase matches register/token/authorize as the final path segment
  (trailing slash tolerated) so `/token/refresh` or `/api/register/foo` no longer
  over-match.
- Tests for all three.

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-07-16 16:52:53 -04:00
cliffhall
f4fb4b50c9 Forward RFC 9207 iss to the v2 SDK (fixes OAuth + EMA leg 1)
SDK v2 added RFC 9207 §2.4 authorization-response issuer validation
(`validateAuthorizationResponseIssuer` / `IssuerMismatchError`) as a mix-up
attack defense. It is strict: when AS metadata advertises
`authorization_response_iss_parameter_supported: true` and no `iss` reaches
`exchangeAuthorization`/`auth()`, the exchange throws.

v1.29.0 had no `iss` parameter and no RFC 9207 validation at all, so dropping
the callback `iss` was harmless. The migration swapped in the stricter SDK
without feeding it the new input, breaking the authorization-code exchange
against any RFC 9207 authorization server (Okta/xaa.dev advertise it):

    Issuer mismatch in authorization response (RFC 9207):
    expected "https://idp.xaa.dev", received undefined

Found by smoke-testing EMA against live xaa.dev staging; not caught by CI
because the mock IdP never advertised the parameter, so the SDK took its
lenient path (a missing `iss` is only fatal when advertised).

`iss` was dropped in three places, with the plumbing half-built —
`oauthManager`/`inspectorClient` already accepted `iss`, but nothing supplied
or forwarded it:

- `parseOAuthCallbackParams` never extracted `iss` from the callback query
- `mcpAuth` never forwarded `options.iss` to SDK `auth()` — its own comment
  ("forwarded on v2 upgrade; ignored by v1 SDK auth()") predicted this exact
  gap, so standard OAuth was broken identically, not just EMA
- EMA leg 1 (`completeIdpOidcAuthorization`) never passed `iss` to
  `exchangeAuthorization`

Thread `iss` end to end: callback query -> CallbackParams -> resumeAfterOAuth
-> completeOAuthFlow -> {mcpAuth | EMA leg 1} -> SDK. The Node callback server
and runner carry it for CLI/TUI.

Tests: `ema-mock-servers.ts` now advertises
`authorization_response_iss_parameter_supported: true` so the mock is at least
as strict as real IdPs — with it absent the SDK silently takes the lenient path
and a dropped `iss` goes unnoticed. Flipping it made three existing tests
reproduce the failure. Adds regression coverage for forwarding and for
rejecting a mismatched `iss` before any network call.

Verified end to end against live xaa.dev staging: EMA legs 1-3 (IdP SSO ->
ID-JAG mint -> resource token) now complete and the MCP session initializes.

web 3099, cli 212, tui 274 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjMemXZohSR2EndBQYif2b
2026-07-15 20:16:43 -04:00
cliffhall
dbb8c40245 Address review: justify/refine SDK-gap casts (as unknown as)
Per human review on #1688: for each `as unknown as` in the PR, either refactor
or justify with a comment naming the SDK gap (so it can inform SDK-side type
improvements).

- Sampling/elicit receiver-task returns: route through a typed `CreateTaskResult`
  so the object shape IS checked; the remaining cast to CreateMessageResult /
  ElicitResult is the honest SDK gap (the 2-arg setRequestHandler overload types
  the handler return as the spec result only, not the task-augmented variant).
- `_requestHandlers` / `_progressHandlers` private-field casts: comments now name
  the exact missing public API (opt a handler out of result validation; keep a
  progress subscription alive across a resolved request).
- test-server `_requestHandlers` cast: same SDK gap on the server side, noted.
- serverList object->Record casts (pre-existing): documented as plain structural
  widening (no index signature -> TS requires the `unknown` step), not an SDK
  workaround.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:47:49 -04:00
cliffhall
8a7a38f800 Fix MCP App load regression: translate ext-apps v1 setNotificationHandler API to v2
An MCP App (e.g. NightRider's drum-pad) failed to load with
"'[object Object]' is not a spec notification method; pass schemas as the
second argument to setNotificationHandler()". `@modelcontextprotocol/ext-apps`
still peers on SDK v1 and, during the AppBridge connect handshake, subscribes
to tools/resources/prompts list_changed with the v1 schema-first API
`setNotificationHandler(NotificationSchema, handler)`. SDK v2 requires a method
STRING and throws on a schema object — the migration's compat shim covered the
types but not this runtime API difference.

Fix in the app-renderer client Proxy (`getAppRendererClient`): translate a
schema-first `setNotificationHandler` call to v2's method-string form by reading
the method literal off the schema's `shape.method.value`; native string-first
calls pass through, and an unrecognized first arg falls through to the SDK's
clear error rather than being swallowed. Only `setNotificationHandler` needed
this — AppBridge's `request(req, schema, …)` calls work as-is (the v1 SDK
schemas expose `~standard`, so v2 accepts them). Remove when ext-apps#702 ships
a v2 peer.

Added an integration test covering the schema-first translation end-to-end
(handler fires on a real tools/list_changed), plus the string-passthrough and
unrecognized-schema branches. Coverage ≥90 holds; typecheck + App tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:44:20 -04:00
cliffhall
f9f79fff54 Address @claude review: format core/, typed output-validation error, document progress heuristic
- Run Prettier on the 60 core/ files this PR touched — the deterministic
  import rewrite had collapsed multi-line imports and left throws unwrapped;
  core/ is in no format gate so it slipped past CI. Now Prettier-clean.
- Default-path output-schema violation now throws
  ProtocolError(ProtocolErrorCode.InvalidParams, …) instead of a bare Error,
  matching the prior v1 contract (SDK threw a typed McpError with a code) so
  downstream `instanceof`/`.code` branching keeps working.
- Document the "assumes serial construction" invariant on pollTaskToolCall's
  progress-subscription revival (the `_progressHandlers` new-key heuristic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:08:54 -04:00
cliffhall
19a6df7ad3 SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624)
Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.

Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
  transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
  stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
  @modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
  rewrite (schemas -> core, everything else -> client); the codemod misroutes
  client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
  2026-era goes on the wire.

Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
  RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
  use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
  implemented over raw client.request + explicit deprecated schemas;
  experimental.tasks.callToolStream replaced by a local pollTaskToolCall
  async generator (with _progressHandlers revival so task-execution progress
  still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
  validates a sampling/elicit handler result and rejects the legacy { task }
  response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
  entry in the private _requestHandlers map for the task-augmented branch so
  { task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
  for single-page behavior (v2's client.listX() auto-aggregate all pages).

Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
  structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
  (remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
  OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
  local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
  vi.mock intercepts the SUT's import.

Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.

npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).

Closes #1624

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:44:24 -04:00
cliffhall
650e5bd020 feat(web): show the Inspector version in the lower-right corner (#1639)
Display the single-source version (root package.json) in the web UI's
lower-right corner, in grey — matching the CLI and TUI. The browser can't read
the filesystem, so the version flows backend → browser via GET /api/config:

- Add `version?: string` to InitialConfigPayload; the web backend
  (webServerConfigToInitialPayload) reads it once via the shared
  readInspectorVersion() and tags it onto the payload.
- Add a `useInspectorVersion` hook mirroring useSandboxUrl/useServerListWritable.
- Add a presentational `VersionBadge` element rendered fixed lower-right, grey,
  non-interactive; styling lives in a `versionBadge` Text theme variant (no
  inline styles, --inspector-* tokens).
- Wire it into App.tsx.

Verified live: /api/config serves `version: "2.0.0"` and the badge renders
`v2.0.0` fixed at the lower-right in grey. Tests: useInspectorVersion (full
branch coverage), VersionBadge (unit + Storybook), web-server-config payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-10 19:07:31 -04:00
cliffhall
7815548300 refactor(version): single-source the version from the root package.json
The Inspector ships as one package with one version, but four client
package.jsons each carried their own `version` and were read at runtime
(cli.ts, tui App.tsx), and core hardcoded a third, stale value (0.20.0) that
the TUI and web actually reported to servers. Consolidate onto the root
package.json as the single source of truth.

- Add core/node/version.ts: shared, Node-only readInspectorVersion() that walks
  up to the root package.json (always shipped in the tarball). 100% covered by a
  new integration test.
- CLI and TUI read the version via the shared reader instead of their own
  manifest; TUI now also passes a real clientIdentity (was falling through to
  the stale core default).
- Drop the `version` field from all four client package.jsons; the root keeps
  the only version. TUI header name/description become local display constants.
- core default client identity: replace the stale 0.20.0 hardcode with a neutral
  0.0.0 placeholder (real clients pass clientIdentity; web will source its
  version from GET /api/config, see #1639).
- Stop shipping clients/cli/package.json and clients/tui/package.json (no longer
  read at runtime) — supersedes the earlier "ship them" fix; update pack:verify
  and the root README packaging invariants accordingly.
- Add core/node/** to the web coverage include so the new module is gated.
- Add the shared-code architecture diagram to the root README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-10 17:31:21 -04:00
cliffhall
32a3610737 Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts:
#	clients/cli/package-lock.json
#	clients/cli/package.json
#	clients/web/src/App.tsx
#	clients/web/src/lib/environmentFactory.test.ts
#	clients/web/src/lib/environmentFactory.ts
#	clients/web/src/lib/remoteOAuthStorage.test.ts
#	clients/web/src/lib/remoteOAuthStorage.ts
#	clients/web/src/test/core/auth/connection-state.test.ts
#	clients/web/src/test/core/auth/providers.test.ts
#	clients/web/src/test/core/auth/storage-browser.test.ts
#	clients/web/src/test/core/auth/storage-remote.test.ts
#	clients/web/src/test/core/mcp/oauthManager.test.ts
#	clients/web/src/test/integration/auth/node/storage.test.ts
#	clients/web/src/test/integration/storage/adapters.test.ts
#	clients/web/src/utils/clearServerOAuthState.test.ts
#	clients/web/src/utils/clearServerOAuthState.ts
#	core/auth/connection-state.ts
#	core/auth/oauth-storage.ts
#	core/auth/providers.ts
#	core/auth/storage.ts
#	core/auth/store.ts
#	core/storage/adapters/remote-storage.ts
2026-07-09 09:41:08 -04:00
Bob Dickinson
3c65bdb4c6 Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage. 2026-07-04 21:56:45 -07:00
cliffhall
c026d4d84a docs: address review nits on the v2/main merge
- transport.ts: update the SSE-branch comment to say the default path uses
  fetchWithOptionalAuthIntercept (proxy-aware baseFetch + optional auth-challenge
  intercept), not the bare proxy-aware baseFetch.
- clients/cli/README.md: fix the §5 mid-session validation link to use the
  ../../specification/ prefix like the sibling links in the same sentence
  (previously resolved to a nonexistent clients/cli/ path).

Both flagged by the @claude review of PR #1601; comment/doc-only, no runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 15:42:22 -04:00
cliffhall
cfbdfdf2bd Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts:
#	AGENTS.md
#	clients/cli/README.md
#	clients/web/src/App.test.tsx
#	clients/web/src/App.tsx
#	clients/web/src/test/core/auth/utils.test.ts
#	core/mcp/node/transport.ts
#	test-servers/src/composable-test-server.ts
2026-07-03 17:39:07 -04:00
Bob Dickinson
a24091484c fix(auth): await OAuth storage load() to fix remote OAuth races 2026-07-03 14:18:00 -07:00
cliffhall
14404bc405 refactor: remove unsafe non-null assertions / double-cast in auth paths
Address the TypeScript-safety review items on the mid-session auth work:

- oauthManager: replace the unguarded `this.oauthConfig.storage!` assertions on
  the EMA re-mint/persist paths with a `requireStorage()` helper that throws a
  clear error; in `getOAuthState` reuse the already-narrowed local `storage`.
- cli: `parseRunnerOAuthCallbackUrl` always returns a config, so make
  `callMethod`'s `callbackUrlConfig` (and the always-passed `cliAuthOverrides`)
  required instead of asserting them non-null at three call sites.
- test-servers/resolve-config: drop the `as unknown as T` double cast; collect
  into a base-shaped array and do a single honest downcast at the return.

No behavior change; validate + coverage green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 13:16:05 -04:00
cliffhall
f4b6095719 test: close ≥90% per-file coverage gaps for mid-session auth
The mid-session auth work passed `validate`/CI but failed the local-only
`npm run test:coverage` per-file gate (≥90% lines/statements/functions/
branches) on 12 files. Add targeted unit/integration tests so every changed
file clears the gate; annotate provably-dead defensive branches with justified
`/* v8 ignore */` comments rather than relaxing the gate.

Files brought to ≥90 on all four dimensions:
- core/auth: mcpAuth, oauthUx, utils, challenge, node/runner-interactive-oauth
- core/mcp: oauthManager, inspectorClient, node/authChallengeFetch,
  remote/remoteClientTransport, remote/node/{remote-session,server}
- clients/web/src/utils: oauthResume, browserTabVisibility

`npm run test:coverage` now passes (3575 tests, 0 threshold failures);
`npm run validate` green across all four clients.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 12:54:37 -04:00
Bob Dickinson
a066873e37 Fixes for remaining issues 2026-07-02 18:36:44 -07:00
Bob Dickinson
52023ff1c3 fix(auth): clear OAuth callback timer and reorder EMA scope persist
Clear the loopback callback timeout in runner-interactive-oauth finally
so already_authorized and error paths do not hang the CLI/TUI. Check EMA
step-up satisfaction against minted token scope before persisting the
requested union, matching the standard OAuth path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:55:34 -07:00
Bob Dickinson
562bdd00f1 All of the mid-session and step-up auth work 2026-07-02 16:35:31 -07:00
cliffhall
775b98a019 docs(core): clarify redaction scope + add nested request-body test
Address round-1 review feedback on fetchTracking redaction:
- Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader
  understands why two identical "[REDACTED]" constants coexist.
- Document that redactBody deliberately scopes to form-encoded + JSON
  (multipart/binary bodies pass through; OAuth never uses multipart).
- Add a tracker-path test asserting a nested sensitive key in a JSON
  request body is redacted in the recorded copy while the live outbound
  body stays byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 22:34:40 -04:00
cliffhall
524bf2d543 feat(core): redact sensitive body + URL-query fields in fetch log (closes #1593)
Extends the header redaction from #1585. Before a fetch entry reaches any
sink, mask known-sensitive credentials in the request/response bodies and
the URL query string, in addition to headers:

- redactBody(): form-urlencoded + JSON bodies, recursive over nested
  objects/arrays, best-effort (never throws), values-only.
- redactUrlQuery(): masks sensitive query params, preserves path,
  non-sensitive params, and any fragment.
- Sensitive fields: client_secret, code, refresh_token, access_token,
  id_token, code_verifier, client_assertion, assertion, password, token
  (case-insensitive), reusing the [REDACTED] sentinel.

Applied to the request success + error entries and the async response-body
callback. Live outbound requests are byte-identical — only the logged copy
is redacted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 22:20:42 -04:00
cliffhall
4e3cd591a4 merge: v2/1563-node-proxy (#1590) 2026-07-01 22:03:55 -04:00
cliffhall
2d1a99ab37 merge: v2/1561-redact-headers (#1585) 2026-07-01 22:03:51 -04:00
cliffhall
b152f74ccd fix(ci): run CI on Node 22.x and correct undici 8 Node floor (#1563)
undici 8.5.0 requires Node >= 22.19.0 (its declared engines), so CI's
Node 20.x could not load undici's CacheStorage (missing
webidl.util.markAsUncloneable) and the proxy integration tests failed.
Bump the CI setup-node to 22.x to match the repo's Node floor.

Reconcile the declared floor with undici's real requirement: bump root
engines.node to >=22.19.0 and correct the CLI README note (undici 8.5.0
needs >=22.19.0, not >=22.7.5). Also document proxy support in the web
README and clarify that a caller-supplied eventSourceInit.fetch bypasses
the proxy wrapper by design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:17:05 -04:00
cliffhall
ee4b232a1f refactor(core): narrow readUiMeta from unknown; document ui-meta precedence
Address round-1 review nits on apps.ts:
- readUiMeta now accepts `unknown` and narrows internally, removing the
  three `as WithUiMeta` casts at its call sites (readability win).
- Add a comment documenting that the content-block-then-result precedence
  for `_meta.ui` is intentional (posture lives on the content block; no
  shallow merge).
- Add a TODO to switch to the named McpUiToolMeta/McpUiResourceMeta types
  once the upstream extensionless re-export is fixed for NodeNext.

Coverage: apps.ts 100% lines / 97.05% branch / 100% funcs — clears the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:14:16 -04:00
cliffhall
d5be4929e9 feat(core): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY in the Node transport (closes #1563)
Wrap the Node transport's fetch with an undici EnvHttpProxyAgent dispatcher
when a standard proxy env var is set, so the CLI and web backend can reach
remote MCP servers through corporate proxies with zero new flags. undici is
imported lazily (only when a proxy var is present) and added as a dependency
at the repo root and in the CLI client; a missing undici raises an actionable
error. Documents the behavior and dependency in the CLI README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:25:16 -04:00
cliffhall
1a6e32be50 feat(core): add AppInfo + extractAppInfo() to core/mcp/apps.ts (closes #1556)
Add a machine-readable summary of an MCP App's UI metadata: the AppInfo
interface plus extractAppInfo(), which merges a tool's _meta.ui
(resourceUri, visibility) with the linked UI resource's _meta.ui (csp,
permissions, domain, prefersBorder, mimeType). Content-block matching is
exact-URI first, then a lowercase + trailing-slash normalized match, then
a single-block fallback since resources/read returns the requested
resource by definition. Shared plumbing for the programmatic-review path
(CLI --app-info, integration tests) without rendering the app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:20:13 -04:00
cliffhall
381027063d feat(core): redact sensitive headers in the fetch log (closes #1561)
Redact Authorization, Cookie, Set-Cookie, Proxy-Authorization, X-Api-Key,
and x-mcp-remote-auth to [REDACTED] in createFetchTracker before any
request/response entry reaches the in-memory log, pino logger, or
persisted session storage. Comparison is case-insensitive; original key
casing is preserved and the live outbound request is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:19:59 -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
Cliff Hall
c96db0402e
test: raise per-file coverage gate to 90 across all clients (closes #1271) (#1532)
* 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>
2026-06-27 15:26:03 -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
Bob Dickinson
c60a128df3
Test/1396 harden managed state connect tests (#1508)
* test(state): wait for connect refresh before asserting list RPC gate (#1396)

Replace fixed double-microtask flushes in Managed*State connect-path tests with *Change event waits so the capability gate is asserted after refresh completes.

* test(state): fail fast with a readable message if a connect refresh never dispatches its change event (#1396)

The hardened connect-path gate tests now depend on the gated (capability-less /
empty-list) refresh still emitting its *Change event. A future optimization that
skipped that redundant dispatch would make these awaits hang to the vitest
per-test timeout with no clue why.

Extract the per-file waitFor*Change helpers into a shared, generically-typed
waitForChangeEvent() that races the listener against a short timeout and rejects
naming the event that never fired — turning a future hang into a fast, readable
failure. This also removes the duplicated listener/Promise boilerplate across
the five state test files. No production code changes.

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

* test(state): cover void-detail event resolution in waitForChangeEvent

Add a case asserting the helper still resolves for a signal-only (void-detail)
event — CustomEvent normalizes an absent detail to null, so the listener fires
and resolves rather than hanging. Closes the coverage gap noted in review.

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

* docs(core): correct TypedEventGeneric void-detail comment to null

CustomEvent normalizes an absent detail to null, so listeners receive null
(not undefined) for void events — as the new waitForChangeEvent void case
establishes. Comment-only; no behavior change.

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

---------

Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:46:09 -04:00
Cliff Hall
97b1a7fbff
feat(web,core): import client configs and registry server.json (#1348) (#1511)
* 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>
2026-06-23 19:52:08 -04:00
Cliff Hall
e72d4307f3
feat(cli): lift config-file headers/timeouts/OAuth into serverSettings (#1482) (#1500)
* feat(cli): lift config-file headers/timeouts/OAuth into serverSettings (#1482)

The CLI resolved file-sourced servers via resolveServerConfigs(), which
returns bare MCPServerConfig and dropped disk-level headers, timeouts, and
OAuth — CLI connections from a --catalog/--config file silently ignored
them. The TUI already lifted these via mcpConfigToServerEntries().

Extract the TUI's catalog/config -> per-server-settings resolution into a
shared core module (core/mcp/node/servers.ts: loadServerEntries +
selectServerEntry) and route both CLI and TUI through it, so the two share
one file->settings path. --header still overrides the file's headers for
the run while preserving the file's timeouts/OAuth.

- core: add loadServerEntries/selectServerEntry/headersToServerSettings
- tui: tui-servers.ts re-exports the shared core helpers (no logic dup)
- cli: resolve via loadServerEntries + selectServerEntry; drop the
  CLI-only conflict check and bare-config path
- tests: core servers.test.ts (settings lifting, header merge/override,
  stdio overrides, seed/error/conflict, selection); CLI integration tests
  asserting a config header is sent over HTTP and --header overrides it
- docs: CLI README note on file settings being applied

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

* refactor(core): address #1500 review — dedup overrides, drop dead wrapper, clearer error, spec refresh

- Remove the now-unused resolveLaunchServerConfigs() wrapper (both CLI and
  TUI route through loadServerEntries now); migrate its default-catalog
  launch tests to compose withDefaultCatalogPath + resolveServerConfigs.
- Deduplicate applyStdioOverrides against config.ts applyOverrides: export
  the latter (now trims cwd) and reuse it in servers.ts.
- selectServerEntry's unknown-name error is source-agnostic ("not found"
  not "not found in config file") since entries may be a single ad-hoc
  default, where "config file" would mislead.
- Refresh specification/ docs (v2_cli_tui_launcher.md,
  v2_catalog_launch_config.md): CLI/TUI resolution now via
  loadServerEntries/selectServerEntry; mark gap G1 resolved (#1482);
  withDefaultConfigPath → withDefaultCatalogPath.

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

* test(cli): extend smoke:cli to cover #1482 lift→serverSettings

Adds end-to-end smoke coverage for the config-file headers/timeouts/OAuth
lift introduced in #1482:

- unknown --server now errors source-agnostically ("not found")
- multi-server catalog selection via --server (and the omit-it error)
- --header merge does not break the stdio connect path
- HTTP transport: a config-file `headers` object is lifted onto the wire,
  and a CLI --header overrides it (verified against an in-process test
  server's recorded requests via a non-blocking spawn)

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-20 11:33:21 -04:00
Cliff Hall
73b651ff21
feat(cli,tui): normalize --catalog / --config with web; update --help (#1347) (#1499)
* feat(cli,tui): normalize --catalog / --config with web; update --help (#1347)

Bring the CLI and TUI in line with the web catalog vs read-only session
model introduced in #1481/#1483, and close the outstanding --help item
from #1347.

Core (core/mcp/node/config.ts):
- Add `catalogPath` to ServerConfigOptions (writable catalog slot).
- `resolveServerSource()` picks the active source: `--catalog` (writable,
  seed-if-missing) wins over `--config` (read-only, error-if-missing).
- Writable sources seed an empty `{"mcpServers":{}}` on first run (matches
  the web backend) instead of erroring; read-only `--config` still errors.
- `serverSourceConflict()` shared conflict matrix (mirrors run-web):
  --catalog/--config mutually exclusive; neither combines with an ad-hoc
  target.
- `readServerListFile()` public loader for runners (TUI) needing the raw map.
- Rename `withDefaultConfigPath` → `withDefaultCatalogPath`; the no-flag
  default now fills the writable catalog slot, not --config.

CLI / TUI:
- Add `--catalog <path>` (+ MCP_CATALOG_PATH) and re-document `--config` as a
  read-only session in --help.
- Enforce the shared conflict matrix before resolution.

Full writable persistence/CRUD threading remains deferred to #1482 / #1432
(CLI/TUI are read consumers today; the split surfaces as seed- vs
error-on-missing).

Tests: core resolution matrix (source/conflict/seed/read-only), CLI catalog
+ conflict integration tests, TUI loadTuiServers unit tests. READMEs updated.

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

* test(launcher): add smoke:cli + smoke:tui e2e launcher smokes (#1347)

The launcher smokes only checked `--help` for --cli/--tui; nothing
exercised the launcher → CLI/TUI → core path on the built artifact (only
smoke:web did, for web). Add two end-to-end smokes wired into
validate:launcher between smoke:launcher and smoke:web:

- smoke:cli (scripts/smoke-cli.mjs): runs `--cli` against the bundled
  stdio test server via a temp --catalog and asserts tools/list returns
  the tools, plus the #1347 resolution paths — default-catalog
  seed-on-missing, read-only --config error-without-seed, and the
  --catalog/--config conflict.
- smoke:tui (scripts/smoke-tui.mjs): launches `--tui --catalog <temp>`
  and asserts the Ink app renders its first frame ("MCP Servers") within
  a timeout, then SIGTERMs it (shallow boot/render check).

Both build test-servers/build on demand if missing. Docs updated
(AGENTS.md validate description, launcher README).

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

* refactor: address review on #1499 (comment accuracy, CLI/TUI flag parity)

Review feedback from @claude on PR #1499 (all non-blocking):

- Align CLI catalogPath resolution with the TUI: `options.catalog?.trim()
  || process.env.MCP_CATALOG_PATH` so an explicit empty `--catalog ""`
  falls back to MCP_CATALOG_PATH identically in both front-ends (CLI used
  `??`, which only guards undefined/null).
- Clarify the `serverSourceConflict` comment: it mirrors only the
  source-selection portion of web's matrix and deliberately omits web's
  `--header` rejection, since CLI/TUI merge --header into per-server
  settings.
- Note the deliberate --header broadcast semantics in loadTuiServers.
- Share an EMPTY_CATALOG_CONTENT constant between seedEmptyCatalog and
  readServerListContent instead of two separate literals.

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

* refactor(cli): trim --config to match --catalog and the TUI (#1499 review)

Cosmetic uniformity follow-up from the re-review: configPath now uses
`options.config?.trim() || undefined`, matching the catalog slot above it
and the TUI. Behavior is unchanged (resolveServerSource/serverSourceConflict
already trim downstream).

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-19 17:30:26 -04:00
Cliff Hall
91f02fd6db
feat(web): catalog vs read-only session launch model + apply --header (#1481, #1483) (#1496)
* 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>
2026-06-16 14:24:58 -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
Bob Dickinson
2488ca5e38
feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452)
Ports the CLI, TUI, and launcher from v1.5 into v2 behind a single
`mcp-inspector` entrypoint (launcher → web/cli/tui). Includes the
`mcp-inspector --web --dev` launcher fix (Vite alias resolution + prod-server
double-start), the root `postinstall` install cascade with simplified CI, and
spec/AGENTS docs (no-workspaces rationale, Development setup).

Closes #1246.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:27:49 -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
Bob Dickinson
62d442b073 Revert "First pass at pulling the v1.5 CLI, TUI, and Launcher clients into v2."
This reverts commit 467b01edd6.
2026-06-09 21:56:38 -07:00
Bob Dickinson
467b01edd6 First pass at pulling the v1.5 CLI, TUI, and Launcher clients into v2. 2026-06-09 17:38:00 -07:00
Cliff Hall
d5f12b2a82
feat(web): reorderable ServerCards — drag-and-drop + keyboard (#1369) (#1447)
* 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>
2026-06-09 14:15:58 -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