Commit graph inspector/AGENTS.md
Author SHA1 Message Date
Cliff Hall
373f346e5f
chore: close quality-gate coverage gaps (#1689) (#1766)
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
Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history.
2026-07-24 17:20:12 -04:00
Cliff Hall
2439ef9c3c
web: disable Mantine transitions in tests to kill post-teardown timer flake (#1760) (#1761)
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
* web: disable Mantine transitions in tests to kill post-teardown timer flake (#1760)

A Mantine `Transition`/`Modal` open/close `setTimeout` can fire after happy-dom
tears down `window` at the end of a run, throwing an uncaught
`ReferenceError: window is not defined` that fails the whole `coverage` job even
when every assertion passed (seen on PR #1759's CI, originating in
ServerRemoveConfirmModal.test.tsx).

Set `env="test"` on the shared `renderWithMantine` MantineProvider so Mantine
renders transitions synchronously (no timer). This removes the leak class for
every test that renders through the shared wrapper (Modals, Menus, Popovers, …).
Also apply it to ViewHeader's local forced-dark provider.

The handful of tests that assert mid-flight transition state (ViewHeader's
`data-anim="out"` crossfade checks, #1450) genuinely need real transitions, so
add an opt-in `renderWithMantineTransitions` wrapper (env="default") and route
those three tests through it — each already drives the transition to completion
(waitFor/findBy) so it doesn't reintroduce the leak. Tests that assert only the
entered state stay on the default (leak-free) wrapper.

Full `npm run ci` green; unit run reports zero unhandled errors.

Closes #1760

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

* docs: note the renderWithMantine env=test invariant in AGENTS.md (#1760)

Per review feedback: document that tests must render through
`renderWithMantine` (env="test", transition-free) rather than a hand-rolled
`MantineProvider`, and reserve `renderWithMantineTransitions` for asserting
mid-flight animation state (driving the transition to completion). Keeps future
tests from quietly reintroducing the post-teardown timer leak class.

Refs #1760

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-24 10:58:23 -04:00
Cliff Hall
e70a11360c
Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735)
* Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632)

Card 9/10 of the SDK-v2 + New Spec stack. Adds SEP-2243 `x-mcp-header`
tooling to the Tools tab and adjusts result rendering for SDK v2's
unknown-tool behavior, and bumps the SDK to 2.0.0-beta.5.

- **`core/json/xMcpHeader.ts`**: a faithful port of the SDK's (non-public)
  `scanXMcpHeaderDeclarations` — validates every SEP-2243 constraint
  (primitive types incl. `number`, RFC 9110 token, case-insensitive
  uniqueness, statically-reachable `properties`-only chain) and derives the
  mirrored `Mcp-Param-{Name}` params for a kept tool.

- **Excluded tools**: the SDK silently drops tools with invalid annotations
  from `tools/list`. InspectorClient now re-lists the RAW (unfiltered) list
  and scans it to recompute the excluded set with reasons — gated to the
  modern non-stdio era (matching the SDK's gate), recomputed on every
  aggregate refresh, surfaced via a new `excludedToolsChange` event +
  `getExcludedTools()` and `useInspectorClient().excludedTools`. The Tools
  sidebar lists them struck-through under an "Excluded (SEP-2243)" divider
  with the reason on hover.

- **Mirrored args**: ToolDetailPanel shows a "Mirrored request headers"
  section listing each arg → `Mcp-Param-{Name}` (with the browser-omits note).

- **-32602 rendering**: SDK v2 rejects an unknown-tool call with `-32602`
  instead of an `isError` result, which previously rendered nothing. New
  ToolCallErrorPanel renders a thrown tool-call error, with a targeted
  "Unknown Tool" hint for `-32602`.

Adds unit + Storybook + integration coverage (a real modern server with an
invalid-header tool fixture) for every path. `npm run ci`-equivalent checks
green.

Closes #1632

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

* test-servers: x-mcp-header modern showcase config + preset (#1632)

Register the `invalid_header_tool` fixture as a preset and add
`xmcpheader-modern-http.json` — a modern HTTP showcase serving echo, a
valid-header `get_weather`, and the invalid-header tool — so the Tools-tab
x-mcp-header tooling (mirrored headers, excluded-tools surface, -32602
rendering) can be exercised manually. Documented in the README alongside the
other per-SEP showcase configs. Smoke-tested end to end in the web client.

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

* docs: add #1632 proof screenshots to pr-screenshots

End-to-end web-client verification of the x-mcp-header Tools tooling against
the modern showcase server: excluded-tools surface + reason, mirrored-headers
section, and the -32602 unknown-tool error panel.

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

* Respond to review: disambiguate -32602, doc/search nits (#1632)

Addresses the review on PR #1735:

1. -32602 "Unknown Tool" mislabel: `-32602` is generic Invalid params, so a
   known tool called with bad arguments hit the same code and rendered under
   the definitive "Unknown Tool" heading. Replace `isUnknownToolError` with
   `classifyToolCallError(code, message)` → "unknown-tool" only when the
   message names it (not found / unknown tool / …), else "invalid-params"
   (new "Invalid Parameters" heading + schema hint), else "generic".

2. Double pagination walk: expand the `listAllTools` comment to document that
   the excluded-tools recompute is a deliberately un-cached second raw walk
   (doubles list round-trips on modern non-stdio, runs even on a cache-served
   aggregate) — accepted for a debugging tool, no-op on legacy/stdio.

3. Doc nit: `scanXMcpHeaderDeclarations` no longer claims to visit "$ref
   targets" — reworded to note `$ref` is never followed (refs resolve into the
   unreachable `$defs` body).

4. Search nit: the excluded-list filter now matches name AND title, matching
   the main tools filter.

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

* Tighten unknown-tool matcher to be tool-scoped (#1632 re-review nit)

The re-review noted the -32602 disambiguation was symmetric: broad markers
("does not exist", "unrecognized") could inversely mislabel a genuine
invalid-params message (e.g. "property 'region' does not exist") as "Unknown
Tool". Scope the not-found/does-not-exist family to require the word "tool" in
the same clause (matching the SDK's "Tool <name> not found"); keep "unknown
tool" / "no such tool" as standalone unambiguous markers. Adds a
tool-less-"does not exist" test asserting it classifies as invalid-params.

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

* Add trigger_invalid_params showcase tool + Invalid Parameters screenshot (#1632)

Adds a `trigger_invalid_params` spec-error trigger to the modern showcase
server: the injector answers its `tools/call` with a real `-32602 Invalid
params` JSON-RPC error whose message does NOT name an unknown tool. This lets
the "Invalid Parameters" branch of the tool-call error panel (added in the
review response) be exercised live, distinct from the unknown-tool `-32602`.

- `SPEC_ERROR_TRIGGERS` gains the `trigger_invalid_params` entry (HTTP 200,
  in-band -32602) + preset registration.
- Enable `injectSpecErrors` on `xmcpheader-modern-http.json` and add the tool.
- README + pr-screenshots README document it; adds the live "Invalid
  Parameters" proof screenshot.

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-22 11:42:54 -04:00
Cliff Hall
2259b1a3e1
Tasks: io.modelcontextprotocol/tasks extension on modern servers (era fork) (#1731)
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
* Tasks: modern io.modelcontextprotocol/tasks extension (era fork core)

Core + test-server implementation of the SEP-2663 tasks extension for modern
(2026-07-28) connections, forked from the legacy 2025-11-25 tasks path.

SDK v2 removed all tasks support AND era-gates the tasks/* spec methods out of
the modern era on both the client (outbound) and server (inbound) sides, and its
codec rejects a resultType:"task" result outright. So the Inspector drives the
extension itself: task creation rides tools/call (a modern method) with the
resultType:"task" frame rewritten at the transport to a CallToolResult carrying
the handle in _meta; tasks/get/update/cancel go over a raw-wire request channel
that bypasses the era gate (full modern envelope, string ids, consumed by the
transport). input_required tasks reuse the existing pending-request modal via
fulfilInputRequests, answered with tasks/update. No tasks/list, no tasks/result.

Tab + run-as-task gate on the negotiated extension; the managed task store's
refresh re-polls known handles on modern (no server list). Test server serves
the extension via an Express interceptor (SDK can't) + a shared poll-driven
runtime. Full integration coverage of both eras.

Refs #1631

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

* Tasks modern era: tests + coverage (raw-wire, schemas, era integration)

Adds unit coverage for modernTaskSchemas and the raw-wire request channel
(transport-null guard, send failure, timeout, error response, teardown), plus
end-to-end era integration tests (create/poll/input-required/update/cancel,
synchronous passthrough, unknown-id error, modern store re-poll). Fixes the App
and constructor-capabilities tests for the always-on tasks extension.

Refs #1631

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

* Tasks modern era: docs + share test servers per era to de-flake coverage

Document the tasks era-fork test configs in README and the new core/test-server
files in AGENTS.md. Share one modern + one legacy test server across the era
integration block (started lazily, stopped in afterAll) — starting a fresh
Express server per test made connection negotiation flaky under the heavy
concurrent coverage run.

Refs #1631

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

* Restore pr-screenshots deleted inadvertently

The logging/subscriptions PR screenshots + README were staged-deleted in the
working tree before this branch; restore them from v2/main so this PR doesn't
remove other PRs' artifacts.

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

* Tasks modern era: offer Run-as-task for any tool + PR screenshots

On modern (SEP-2663) task creation is server-directed — the client declares the
extension once and any tool may return a task — so the Tools screen now offers
'Run as task' for every tool on a modern connection (threaded via a modernTasks
prop through InspectorView → ToolsScreen → ToolDetailPanel), rather than gating
on the legacy per-tool taskSupport. Verified end-to-end in the web UI (remote
proxy): create → poll tasks/get → input_required elicitation → tasks/update →
inline result, plus the unchanged legacy tasks/list/result flow. Adds proof
screenshots.

Refs #1631

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

* Tasks modern era: address two follow-ups (task-input note + ordinary-path auto-poll)

1. The pending-request note now distinguishes a modern task round from an MRTR
   round: a new 'task-input-required' PendingRequestOrigin threads through
   fulfilInputRequests → the pending-request modal → MrtrOriginNote, which shows
   'submitted via a tasks/update request (SEP-2663), not a retry' instead of the
   MRTR retry copy.

2. The ordinary (non-run-as-task) modern callTool path now auto-polls an
   unsolicited task handle: attemptToolCall detects the rewritten handle in the
   result _meta and drives it to termination (get → input_required/update →
   inline result), dispatching task events so the Tasks tab + toasts track it and
   the tool result resolves to the task's final result. The poll loop is shared
   with the streaming path via extracted helpers (submitModernTaskInput /
   modernTaskTerminalOutcome / modernTaskPollInterval).

Refs #1631

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

* Tasks modern era: address review (negotiated envelope version, input-round cap, clarifying comment) + fresh smoke screenshots

Review of #1731:
1. withModernTaskEnvelope now stamps the NEGOTIATED protocol version
   (getProtocolVersion() ?? MODERN_PROTOCOL_VERSION) so the envelope matches the
   MCP-Protocol-Version header instead of a hardcoded constant.
2. A never-completing input_required task is now bounded by MRTR_MAX_ROUNDS in the
   shared submitModernTaskInput (threaded through both poll loops), so a
   non-conformant server can't re-prompt the user forever. New modern_loop_task
   test-server tool + integration test cover the cap.
3. ToolDetailPanel: comment clarifying that per-tool taskSupport:"forbidden" is
   deliberately ignored on modern (task creation is server-directed under SEP-2663).

(The abort-during-poll observation is addressed in the PR reply: the cancel path
uses tasks/cancel and disconnect rejects the in-flight raw request, so the poll
stops; matches legacy.)

Fresh web-UI smoke screenshots (modern connected, task completed, input_required
with the corrected tasks/update note, input completed).

Refs #1631

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

* Tasks modern era: fix uncancelable input-required loop + sticky cancel

Two bug fixes reported during manual testing of the tasks era-fork (#1631):

1. A modern task paused at `input_required` (e.g. `modern_loop_task`, which
   never advances) could not be cancelled: the poll loop blocked on the
   pending-request modal, so `cancelRequestorTask` sent `tasks/cancel` but the
   modal kept re-prompting. Now each modern poll loop registers a per-task
   AbortController (`registerTaskInputAbort`), `cancelRequestorTask` aborts it
   to reject the pending elicitation and unblock the poll, and the embedded
   input requests are tagged with their owning task id
   (`RELATED_TASK_META_KEY`) so the pending-request UI's Cancel action cancels
   the TASK rather than sending a "cancel" answer the server would re-prompt on.

2. On the legacy server, cancelling a task briefly showed CANCELLED then
   flipped to COMPLETED when the in-flight tool call resolved. Cancellation is
   a deliberate terminal decision, so `ManagedRequestorTasksState` now remembers
   cancelled task ids and ignores any later non-cancelled status update.

Also: move the compact task id onto its own line in the embedded monitoring
sidebar so the status badge / Cancel Task control don't wrap; DRY the duplicated
task poll-interval logic into a shared `taskPollInterval` helper.

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

* Refresh tasks PR screenshots + capture both bug-fix scenarios

Recapture the five tasks flow screenshots against the current build and add
two demonstrating the post-review fixes: modern_loop_task cancelled from the
pending-request modal (uncancelable-loop fix) and a legacy task staying
CANCELLED after the in-flight call resolves (sticky-cancel fix).

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

* Address re-review: sticky cancel on legacy refresh + App cancel-routing test

Re-review of the cancelable-loop / sticky-cancel fix raised two non-blocking
hardening items:

1. The legacy tasks/list refresh() rebuilt the list from the server, honoring
   only dismissedTaskIds — so a cooperative-cancel server that re-lists a
   user-cancelled task as completed could un-stick the cancel on a manual
   Refresh. The refresh path now pins a cancelledTaskIds task to "cancelled"
   (keeping it visible-as-cancelled, matching the live-merge handlers) rather
   than dropping it. Covered by a new state test.

2. Added an App.test.tsx case locking in the user-facing half of the fix: Cancel
   on a task-input-required elicitation calls cancelRequestorTask (not respond).

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-21 19:17:04 -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
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
cliffhall
59b2ebacb4 update AGENTS.md with note on board management 2026-07-11 00:15:24 -04:00
cliffhall
0326e32eef Merge branch 'v2/main' into 1619-ci-coverage-parity
# Conflicts:
#	AGENTS.md
2026-07-09 11:22:48 -04:00
cliffhall
edc83c49a8 fix: make npm run ci a true superset of GitHub CI (coverage gate)
The local `npm run ci` was not at parity with `.github/workflows/main.yml`.
CI runs `validate → coverage → smoke → test:storybook`, but the root `ci`
script ran `validate → ci:integration → smoke → ci:storybook`, omitting
`npm run coverage` — the per-file ≥90 (lines/statements/functions/branches)
gate that CI enforces. So a push could pass every documented local pre-push
command and still fail CI on the coverage gate (this exact gap broke PR #1601
on a HistoryEntry.tsx function-coverage regression).

Two changes:

1. Root `package.json` `ci` script now chains `npm run coverage` in place of
   `ci:integration`. Because web's `coverage:web` (`test:coverage`) already
   runs the integration project under instrumentation, `coverage` supersedes
   the old integration-only step and makes `ci` a genuine superset of GitHub
   CI. The `ci:integration` / `ci:storybook` script definitions are left in
   place for any other references.

2. AGENTS.md: rename the pre-push section to "Mandatory pre-push gate", make
   running the coverage gate a hard non-optional step (remove the "or at
   minimum `npm run validate`" loophole and the soft "optionally also run
   coverage" phrasing), and correct the parity claim so `npm run ci` is
   described as `validate → coverage → smoke → storybook`.

Closes #1619

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
2026-07-09 10:45:52 -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
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
cliffhall
143e0d3f71 test+docs: add inspectorTabs test; document mid-session auth modules
- Add a dedicated `inspectorTabs.test.ts` (was relying only on incidental
  coverage from App.test.tsx) — covers the tab-id constants and both branches
  of the `isInspectorTabId` type guard; 100% per-file.
- AGENTS.md: extend the `core/auth/` structure-tree entry to describe the new
  mid-session recovery modules (challenge parsing, scope union, shared OAuth UX
  copy, force-reauthorization) and the node loopback callback flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 13:20:29 -04:00
Bob Dickinson
0635d09e83 Fixed CI failure (test fixture) and added local CI script 2026-07-02 17:07:25 -07:00
cliffhall
859911d5ad revert(rollup): remove CONTRIBUTORS docs (#1537/#1517) from Wave-1 rollup — tracked separately, not Wave 1 2026-07-02 13:03:41 -04:00
cliffhall
61ca22003f ci: enforce the ≥90% per-file coverage gate in CI (closes #1550)
Add a CI step that runs `npm run coverage` so a PR that drops any file
below 90% on lines/statements/functions/branches fails the job. Remove
the now-redundant standalone `Run web integration tests` step, since
web's `test:coverage` already runs `--project=unit --project=integration
--coverage`, and update AGENTS.md to reflect that CI now enforces the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-02 11:59:32 -04:00
cliffhall
3df4f0e7e2 Merge remote-tracking branch 'origin/v2/main' into merge-1537 2026-07-01 20:57:34 -04:00
Cliff Hall
066676a241
docs(AGENTS): board tracks issues only, never PRs (#1582)
* docs(AGENTS): board tracks issues only, never PRs

Reconcile a contradiction in the board workflow: line 109 said every board
item is a real issue, but the "created" callout and the add-to-board bullet
said "issue or PR." Make it consistent — only issues go on board #28; a PR
still gets the v2 label but is tracked through its linked issue's card via
Closes #N.

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

* docs(AGENTS): use <issue-url> placeholder in board one-liner recipe

Address PR review nit: make the one-liner's placeholder <issue-url> to
match the two-step recipe, reinforcing that only issues go on the board.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:52:01 -04:00
Cliff Hall
d5d80d3b80
docs(AGENTS): document board membership + status workflow; drop closed V1.5 board (#1555)
* docs(AGENTS): document board membership + status workflow; drop closed V1.5 board (closes #1554)

The Issue-driven Work Style section described board interaction in prose
but only gave concrete gh commands for the version label, so agents
labeled issues/PRs without adding them to board #28 or setting a Status.

- Add explicit gh recipes for `project item-add` + `project item-edit`,
  including the V2 board's project/field/Status-option IDs.
- Correct Status names to the real board values (In progress / In review,
  plus Backlog / Building / Done).
- State that a v2 item isn't "created" until labeled AND on the board
  with a Status set.
- Remove the closed V1.5 board (project 39) and its base-branch/label
  references.

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

* docs(AGENTS): require real issues (no drafts) and assign issues to their creator

Per maintainer guidance: every board item must be an actual GitHub issue,
not a draft card, and each issue must be assigned to the user it is created
for. Replaces the draft-items-vs-issues bullet.

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

* docs(AGENTS): clarify PR-to-issue linking limits on non-default branch

Closing keywords (Closes #N) only auto-link and auto-close for PRs that
target the repo's default branch. v2 PRs target v2/main, so the keyword is
only a cross-reference there. Document that:
- the PR body's first line must still be `Closes #<ISSUE_NUMBER>`,
- there is no gh flag to manually link (no `gh pr edit --add-issue`),
- on merge, the issue must be closed manually and its board item moved to
  Done, since auto-close won't fire on v2/main.

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

* docs(AGENTS): drop V1.5 board mention; annotate v1 board scope

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

* docs(AGENTS): drop stale "v1.5-ported" qualifier from integration-test notes

v1.5/main is fully merged into v2/main, so the integration suite is just
the v2 integration suite — the provenance tag no longer distinguishes
anything.

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

* docs(AGENTS): mark v1.5/main as a frozen reference branch (do not delete)

v1.5/main is merged and takes no new work, but the branch is kept as a
reference point (e.g. to track down a regression introduced by the merge
into v2/main). Note that it should not be deleted.

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

* docs(AGENTS): add MCP Apps Extension status; refresh regenerated board option IDs

Adding the new status option regenerated every Status option ID on board
#28, so update the whole option-ID table (not just the new row) plus the
inline references to the Done and In progress option IDs.

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

* docs(AGENTS): use gh --jq in board one-liner; tie building statuses to workflow

Address PR review: drop the python3 pipe in favor of gh's built-in --jq
flag (no python3-on-PATH assumption), and add a one-line pointer mapping
the Building / MCP Apps Extension statuses to the workflow.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:11:08 -04:00
cliffhall
73d7439320 docs: add CONTRIBUTORS.md issues-only policy (#1517)
Document the contribution policy: we accept issues, not pull requests.
Maintainers handle design and implementation through a prompt-driven
workflow. Contributors who have already built a change locally should
share the prompt they used rather than a diff.

Cross-reference the policy from AGENTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:47: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
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
5efe9ddc7b
refactor: self-validating clients, fast validate + separate coverage/smoke (#1504) (#1505)
* refactor(launcher): self-contained validate; split smokes into top-level step (#1504)

Previously `validate:launcher` built cli/tui/web and ran the cross-client
smokes, so the launcher's "validation" depended on every other client
being built — and the launcher had no lint/format/coverage of its own.

- clients/launcher: add a web-style `validate` (format:check && lint &&
  build && test:coverage) with a Node-only eslint flat config, prettier,
  and a per-file coverage gate (parse-launcher-argv.ts covered;
  src/index.ts excluded as binary bootstrap). 100% on the gated file.
- Root `validate:launcher` now just delegates: `cd clients/launcher &&
  npm run validate` — no dependency on the other clients.
- New top-level `smoke` step (smoke:launcher + smoke:cli + smoke:tui +
  smoke:web) with NO build commands; `validate` runs it last. Each
  client's own validation builds its bundle first, so nothing builds
  twice (cli/tui via test:coverage, launcher via its validate;
  smoke:web builds dist on demand).
- clients/tui test:coverage now builds the TUI bundle (mirroring cli) so
  the build-free smoke has it available.
- CI: replace the standalone cli/tui/launcher build step with a
  "Validate launcher" step. Docs updated (AGENTS.md, launcher README).

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

* refactor(cli,tui,root): every client self-validates; smoke separate from validate

Extends the launcher self-validate work to the CLI and TUI, and makes the
top-level `validate` nothing but the per-client validations — smokes are
now a separate top-level target.

- clients/cli, clients/tui: add a web-style `validate` with a Node flat
  eslint config + prettier. CLI lint/format pass as-is. TUI registers
  eslint-plugin-react-hooks for the classic rules only (rules-of-hooks +
  exhaustive-deps, which the source already references); the stricter
  react-hooks@7 rules are not enforced on the interim component surface
  (#1501).
- Avoid double builds:
  - CLI's e2e spawns the built binary, so its `test:coverage` builds first;
    `validate` folds that in (`format:check && lint && test:coverage`, no
    separate `build`).
  - TUI tests don't spawn a bundle, so `validate` keeps an explicit
    `build` (for the smoke) and `test:coverage` is pure.
- Root: `validate` = validate:web && validate:cli && validate:tui &&
  validate:launcher (each delegates to the client's own validate). `smoke`
  is no longer part of `validate` — it's a separate top-level target run
  after a build/validate, with no build commands of its own.
- CI: per-client "Validate X" steps + a single "Run cross-client smokes"
  (npm run smoke). Docs updated (AGENTS.md, cli/tui READMEs).

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

* chore(root): drop redundant test aggregator scripts

Now that every client self-validates and the top-level `validate` chains
the per-client validations, the root `test` / `test:web` / `test:cli` /
`test:tui` / `test:coverage` aggregators are redundant — run
`npm run validate` (all) or `cd clients/<name> && npm run validate` (one).
`test:storybook` stays (it's the one check not part of validate). CI was
already using each client's own scripts, so it's unaffected. Docs updated.

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

* test(web): silence happy-dom infra noise in unit runs

Passing unit runs printed dozens of alarming-but-expected lines that made
green runs look broken — all from happy-dom performing real I/O with no
server behind it:
- `<iframe src="/sandbox.html">` (MCP Apps sandbox) made happy-dom fetch
  and later unload that URL, emitting ~40 `DOMException [NetworkError/
  AbortError]` + `AsyncTaskManager destroyed` lines. Disable child-frame
  navigation in the unit project (the tests only assert on the iframe
  element/attributes, not its document), removing the noise with no
  coverage loss.
- App-level renders read `GET /api/config` via useSandboxUrl /
  useServerListWritable, 404-ing ~76x. Add a benign default `fetch` (empty
  200) in the unit setup; tests that exercise fetch still override it.

Negative tests that assert on errors already suppress their own console
output (e.g. jsonUtils/schemaUtils); CLI, TUI, and the web integration
suite were already clean. Unit suite is now noise-free; 2424 web tests
pass and the coverage gate is unchanged.

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

* refactor(scripts): make validate fast (test), split coverage into its own target

Each client's `validate` now runs `test` instead of `test:coverage`, so
the everyday pre-push check skips v8 instrumentation (and, for web, the
slower integration project). The per-file coverage gate moves to a
dedicated top-level `coverage` script:

  coverage = coverage:web && coverage:cli && coverage:tui && coverage:launcher
  coverage:<name> = cd clients/<name> && npm run test:coverage

`validate` still builds each client's bundle (cli via its test `pretest`,
the others via an explicit `build`), so `npm run validate && npm run
smoke` works unchanged. CI now runs `validate` (fast checks + builds),
then `coverage` (the gate), then `smoke` — collapsing the old per-client
steps. Docs updated.

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

* ci: drop the coverage gate from CI; keep web integration as its own step

CI no longer runs `npm run coverage` — the per-file coverage gate is now a
local/manual check (`npm run coverage` before pushing). To avoid silently
dropping the web integration suite (it only ran via test:coverage, and
`validate` runs the unit project only), CI gains a standalone
`clients/web` `test:integration` step that exercises the real
stdio/HTTP/OAuth/storage paths without coverage instrumentation.

CI is now: validate (fast) → web integration → smoke → storybook. Docs
updated.

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

* refactor(smoke): give smoke:launcher its own script like the others

smoke:launcher was the odd one out — an inline chain of three
`node clients/launcher/build/index.js ... --help` commands while smoke:cli
/ smoke:tui / smoke:web each run a `scripts/smoke-*.mjs`. Extract it to
`scripts/smoke-launcher.mjs`, which runs the same `--help` / `--cli --help`
/ `--tui --help` dispatch checks but now also asserts each prints that
mode's usage banner (proving the launcher resolved the right client build)
and verifies the required builds exist up front with a clear error. Prints
a `smoke:launcher OK — ...` summary in the same style as the rest. Docs
updated.

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

* chore: reorder package.json scripts for readability

Move frequently-run scripts toward the top and rarely-run-by-hand ones
(pretest hooks, lint/format internals, etc.) toward the end, consistently
across the root and client manifests. Pure reordering — no script names or
values changed, and no other manifest content is touched.

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

* docs: drop references to removed root inspector / test:storybook scripts

The reordering dropped the root `inspector` and `test:storybook`
aggregators (intentional — `inspector` is redundant with `web`, and
storybook is run from clients/web in CI). Update AGENTS.md so it no longer
points at `npm run inspector`, and run `test:storybook` from `clients/web/`.

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

* chore: address PR #1505 review (format glob gap, fetch-baseline comment)

- TUI format/format:check now also cover the client-root source files
  (index.ts, tui.tsx, dev.ts) that live outside src/ — `eslint .` already
  lints them, so they were linted but not format-checked. (cli/launcher
  have no source outside src/; their root .ts are single-quoted configs
  intentionally left out of prettier, matching web.)
- Reword the setup.ts default-fetch comment: it overstated auto-restore
  (the unit project sets no restoreMocks/unstubGlobals), and now also
  flags that the permissive 200 default means a test asserting on a fetch
  FAILURE must install its own rejecting stub.

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

* chore: remove redundant root install:clients script

`install:clients` ran the same `node scripts/install-clients.mjs` as
`postinstall`, which fires automatically on `npm install` — so the manual
alias was redundant. Drop it and the doc references (AGENTS.md, spec).

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

* fix(ci): skip smoke:tui in CI (Ink needs a real TTY)

This PR made CI run the full `npm run smoke`, which surfaced that
smoke:tui fails in headless CI: the Ink TUI throws "Raw mode is not
supported" on mount (no TTY) and exits before rendering its first frame.
The old CI never ran smoke:tui, so this was latent.

smoke-tui.mjs now self-skips when `process.env.CI` is set — the boot/
render check is inherently local-only (a TUI needs a terminal). The TUI is
still built and unit-tested in CI; `npm run smoke` stays the single
command everywhere (full locally, tui-skipped in CI). Verified
`CI=true npm run smoke` is green (launcher/cli OK, tui SKIPPED, web OK).
Docs + CI comment updated.

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-21 11:41:33 -04:00
Cliff Hall
388ea5a7bd
test(cli,tui): add in-process source coverage gates (#1484) (#1502)
* test(cli,tui): add in-process source coverage gates (#1484)

CLI and TUI source had no coverage gate: CLI tests spawned the built
binary out-of-process (coverage never saw clients/cli/src) and the TUI
had only two smoke tests. Regressions there were invisible to CI.

CLI:
- Rewrite __tests__/helpers/cli-runner.ts to invoke runCli() in-process
  (capture process.std*.write, map thrown errors to an exit code),
  preserving the CliResult shape so all existing tests run unchanged and
  clients/cli/src is now measured under v8 instrumentation.
- cli.ts: program.exitOverride() so a commander usage error throws
  (catchable in-process) instead of tearing down the worker; --help /
  --version still exit cleanly. Drop the dead, never-imported main().
- Add a thin out-of-process e2e.test.ts that spawns the built binary for
  the shebang / process.exit paths the in-process runner can't reach.
- Add @vitest/coverage-v8, a per-file gate (90/85/80/50) excluding only
  src/index.ts (binary bootstrap), and pool: 'forks' for capture
  isolation. cli.ts lands at ~98% lines / 100% functions.

TUI:
- Add @vitest/coverage-v8 and an interim per-file gate over the
  non-React logic (logger.ts, components/tabsConfig.ts, utils/*); the
  Ink components, App.tsx, and hooks/ are excluded with a documented
  follow-up (#1501). Add tests covering the included modules (100%).

Wiring + docs:
- CI runs each client's test:coverage; root test:cli / test:tui (used by
  validate) and root test:coverage now enforce the gates.
- gitignore clients/**/coverage; update AGENTS.md and client READMEs.

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

* docs(cli): clarify in-process runner footguns from PR #1502 review

Review notes on the in-process CLI test runner (no behavior change):
- Warn in runCli's docblock that --help/--version must NOT go through the
  in-process runner: exitOverride only re-throws non-zero exits, so exitCode
  0 falls through to process.exit(0) and would kill the vitest worker. Those
  belong in the out-of-process e2e.test.ts layer.
- Document that a timeout orphans the uncancellable invokeCli promise (its
  stdio subprocess stays alive; later writes hit the restored stdout) — a
  known, acceptable trade-off on the hang-only failure path.
- Fix a stale reference to the removed main() (errors now route through
  index.ts).

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

* docs(cli): fix remaining stale main() reference in vitest exclusion comment

PR #1502 re-review caught a second stale `main()` mention (the first was
fixed in 52c23e41). src/index.ts inlines the bootstrap as
runCli(process.argv).then(...).catch(handleError) — there is no main().
Comment-only; no behavior change.

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

* chore: compose root test:coverage from per-client scripts

Replace the brittle inline cd-chain with a composition of the existing
test:cli / test:tui scripts plus a new matching test:web. Behavior is
unchanged (web → cli → tui coverage gates) but there's a single source of
truth for each client's coverage invocation.

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 15:05:58 -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
36b48e61a2
feat(web): build dist on demand for prod --web + end-to-end CI smoke (#1486) (#1497)
* feat(web): build dist on demand for prod --web + end-to-end CI smoke (#1486)

Prod `mcp-inspector --web` serves static assets from clients/web/dist,
which only exists after a build. In a fresh checkout it was absent and the
prod path served a broken page (ENOENT on index.html); the launcher smoke
test only covered --help, so this path was unverified in CI.

- ensure-web-build.ts: build dist on demand (vite build via build:client)
  when missing, or exit with an actionable error if the build can't run.
- run-web.ts: invoke ensureWebBuild before startHonoServer in the prod path
  (--dev runs Vite directly and skips it).
- scripts/smoke-web.mjs + smoke:web: start --web against built dist and
  assert GET / returns 200 with the injected __INSPECTOR_API_TOKEN__.
  Wired into validate:launcher and a new CI step.
- Docs: launcher README + AGENTS.md.

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

* fix(web): surface spawn error in build-on-demand; clarify smoke comment

Address PR #1497 review:
- runViteBuild now logs result.error.message (e.g. ENOENT when npm isn't on
  PATH) before returning a null status, so the spawn-failure cause surfaces
  instead of only the generic actionable error. The build seam gains a `log`
  param; added a test covering the ENOENT path.
- Reword scripts/smoke-web.mjs header so it no longer implies the readiness
  wait covers a cold build-on-demand vite build (it's sized for a pre-built
  dist, which validate/CI guarantee).

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 18:34:52 -04:00
Cliff Hall
12b8eb11fd
feat: root validate chains CLI/TUI/launcher build + test (#1485) (#1495)
Root `npm run validate` previously delegated only to `clients/web`
(`format:check` + `lint` + `build` + `test:coverage`), so a contributor
running it before pushing could miss CLI/TUI/launcher breakage that CI
would later catch. There was no single local command mirroring CI.

Extend root `validate` to mirror the CI job end to end:
- `validate:web` — web format:check + lint + build + test:coverage
- `test:cli` / `test:tui` — CLI and TUI suites
- `validate:launcher` — build CLI + TUI + launcher, then the launcher
  `--help` smoke (`smoke:launcher`: --help, --cli --help, --tui --help)

The new sub-scripts can be run individually when iterating on one
client. Storybook stays out of `validate` (Playwright-dependent, slow),
consistent with the existing convention. Documented in AGENTS.md.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:48:11 -04:00
Cliff Hall
a1f40a4222
fix(web): remove config-load warning filter, fix root cause (#1491) (#1494)
The `UNRESOLVED_IMPORT` warnings that `quiet-config-warnings.mjs` suppressed
no longer occur, so the filter (and the build-time `onwarn` suppressions for
the same three deps) are removed.

Root cause: Vite's config bundler (Rolldown) statically walks the node-only
dev backend (`core/mcp/remote/node/server.ts`) when it loads `vite.config.ts`,
reaching `chokidar`, `atomically`, and `@napi-rs/keyring`. `core/` has no
node_modules of its own, so at config-bundle time those deps resolve from
`core/`'s parent chain — the repo-root `node_modules`. Before #1452 the deps
lived only under `clients/web/node_modules` (a sibling, off the resolution
chain), so Rolldown emitted `UNRESOLVED_IMPORT`. #1452 declared them in the
repo-root `package.json` (for the CLI/TUI/launcher, which consume core/'s node
code), installing them at the repo root and making them resolvable. Verified
by hiding the root copy: the warning returns; restore it and the dev/build
startup is clean.

A dynamic import of the backend does not help — Rolldown follows dynamic
imports during config bundling too. The fix is the deps being resolvable, so
the suppression is just removed and the invariant is documented in
`vite.config.ts`.

- Remove `clients/web/server/quiet-config-warnings.mjs` + `.d.mts` and its test
- Restore the `dev` script to plain `vite` (drop the `--import` hook)
- Remove the now-dead `build.rollupOptions.onwarn` UNRESOLVED suppressions
  (never fire: the Hono plugin is `apply: 'serve'`, out of the build graph)
- Document why config-bundle resolution succeeds in `vite.config.ts`
- Update AGENTS.md project tree

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:37:06 -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
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
8be1680494
chore(web): silence benign config-load dep warnings in npm run dev (#1408)
* chore(web): silence benign config-load dep warnings in `npm run dev`

`vite dev` printed three UNRESOLVED_IMPORT warnings at startup (chokidar,
atomically, @napi-rs/keyring). They originate from Vite's config bundler
(Rolldown): the Hono plugin statically imports the node-only dev backend
(core/mcp/remote/node/server.ts), so loading vite.config.ts bundles it and
Rolldown walks its node-only deps. Those deps are correctly externalized — the
browser never loads them and Node resolves them at runtime — so the warnings are
pure noise.

They can't be filtered the usual ways: the static import is required (the
monorepo has no node_modules under core/, so the aliases must resolve the deps
at bundle time — ssrLoadModule / `--configLoader runner` both fail to resolve
them), and the warnings are written directly to the process streams during
config load, bypassing customLogger, logLevel, and rollupOptions.onwarn.

Fix: a tiny `--import` hook (server/quiet-config-warnings.mjs) that drops only
those specific benign warning writes, wired into the `dev` script. Scoped to
`npm run dev` — build, tests, and prod are untouched.

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

* chore(web): harden dev warning filter per review

- Tighten BENIGN_PAIRS to repo-relative path fragments (not basenames) so an
  unrelated future UNRESOLVED_IMPORT warning mentioning the same dep + a
  same-named file isn't silently dropped. Verified Rolldown prints the full
  repo-relative path. (review #1)
- Add a note that the matcher tracks Rolldown's message format and degrades to a
  safe no-op (warnings return) if that format changes. (review #2)
- Export isBenignWarning/BENIGN_PAIRS, guard the stream patching behind
  !VITEST, add a .d.mts so the pure helper is importable from TS, and add a unit
  test locking the signature pairs and the over-match guards. (review #3)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:48:12 -04:00
Cliff Hall
881ba4b5e8
feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378) (#1382)
* feat(auth): inject MCP_INSPECTOR_API_TOKEN into served index.html (#1378)

Reloading the web client at the bare URL (no `?MCP_INSPECTOR_API_TOKEN=…`
query string) with empty sessionStorage made every `/api/*` request 401 —
the browser had no way to recover the backend's auth token.

Embed the token into `index.html` on every page load so the browser no
longer depends on the query string surviving navigation:

- New shared helper `clients/web/server/inject-auth-token.ts` embeds
  `<script>window.__INSPECTOR_API_TOKEN__ = "…"</script>` (escaped against
  `</script>` injection; no-op when auth is dangerously omitted).
- Dev: the Vite plugin injects via `transformIndexHtml`.
- Prod: the Hono server injects on the `/` route.
- `App.tsx` `getAuthToken()` now reads the injected global first, then the
  query string, then sessionStorage (both fallbacks preserved).
- Shared global name lives in `INSPECTOR_API_TOKEN_GLOBAL`
  (`core/mcp/remote/constants.ts`).

Tests: helper unit coverage + an integration test exercising the real
prod server's `/` → `/api/*` flow (injected token authenticates; missing
token 401s). AGENTS.md documents the token-recovery order.

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

* fix(auth): inject token into prod SPA fallback + prime sessionStorage + no-store (#1378)

Addresses code-review feedback on the token-injection PR:

- Prod `server.ts` SPA deep-link fallback (e.g. `/oauth/callback`) previously
  served the raw index.html off disk via serveStatic, bypassing injection — so
  a bookmark/reload at a non-`/` route with empty sessionStorage would 401.
  Route the SPA fallback through the same `serveIndexHtml` (inject) helper;
  real static assets (paths with a dot) still serve verbatim. Dev already
  injected on every HTML serve via Vite `transformIndexHtml`.
- `getAuthToken()` now persists the injected `window.__INSPECTOR_API_TOKEN__`
  to sessionStorage (not just the URL-param branch), priming the backstop for
  any later navigation that loses the global.
- Injected HTML responses now send `Cache-Control: no-store`, so a page
  carrying a token isn't cached and served stale after a restart regenerates
  the token.

Integration tests added: SPA fallback (`/oauth/callback`) carries the token,
`Cache-Control: no-store` on injected HTML, real assets served verbatim, and
unknown `/api` routes 404 rather than falling through to the HTML shell.

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-05-30 20:10:27 -04:00
Cliff Hall
e3b0b6a908
feat(connection-info): wire Connection Info modal to the connection handshake (#1377)
* feat(server-info): wire Server Info modal to the connection handshake

Closes #1364

- Add ServerInfoModal wrapping the existing ServerInfoContent, mirroring the
  ServerSettingsModal layout.
- Gate the Server Info button on ServerCard to render only when the
  connection is "connected".
- Expose `getClientCapabilities()` on InspectorClient (and protocol +
  FakeInspectorClient), and surface it through useInspectorClient so the
  modal can render the client side of the capability handshake without
  poking the SDK Client's private state.
- App.tsx: open the modal from the gated Server Info button, derive the
  active server's transport and OAuth details (synchronously, from the
  guided-OAuth state snapshot + persisted settings), and reset the open
  flag on InspectorClient `disconnect` so the modal does not auto-reopen
  on a future reconnect.

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

* fix(server-info): dedupe modal header and render instructions in ContentViewer

- Drop the in-content "Server Information" Title from ServerInfoContent and
  rename the modal title to "Server Information" so the modal renders one
  heading instead of two stacked ones.
- Swap the instructions Blockquote for ContentViewer (copyable, with the
  existing wrapping Code variant) so long instructions wrap and the user
  can copy the full payload.

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

* refactor(server-info): rename ServerInfo -> ConnectionInfo

The popup covers server identity AND client capabilities AND OAuth state —
it's a snapshot of the live connection, not just the server. Rename to match
what it actually shows. Also add a "Server Implementation" heading above
the top section (name/version/protocol/transport) to balance the existing
capability headings.

- ServerInfoContent -> ConnectionInfoContent (directory, file, exports)
- ServerInfoModal -> ConnectionInfoModal (directory, file, exports)
- ServerCard button label "Server Info" -> "Connection Info";
  prop onServerInfo -> onConnectionInfo
- Modal title -> "Connection Info"
- App.tsx / ServerListScreen / InspectorView wiring updated to match

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

* fix(connection-info): cap instructions block height so it scrolls in place

A long server prompt (e.g. an Everything-style instruction document) was
pushing the OAuth section and the modal chrome off-screen — users had to
scroll the whole modal to reach the close button. Wrap the ContentViewer
in ScrollArea.Autosize with mah=280 so the instructions section scrolls
internally and the rest of the modal stays put.

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

* feat(connection-info): add subtle background to section headings

Add a `section` variant for Title that paints the heading in the existing
`--inspector-surface-subtle` token (same surface token already used for
modal call-out panels). Apply it to each section heading in
ConnectionInfoContent (Server Implementation, Server Capabilities, Client
Capabilities, Server Instructions, OAuth Details) via a SectionHeading
subcomponent constant, so the sections read as distinct cards against the
modal background instead of bleeding into one stream of text.

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

* fix(stories): wrap long import line in ConnectionInfoModal.stories.tsx

Prettier flagged the import in CI. The line exceeded printWidth=80 and
needed to wrap onto multiple lines.

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

* docs(agents): require `npm run format` before committing

Add a pre-commit instruction to run the auto-fixing `npm run format` ahead
of `npm run validate`. `validate` runs `format:check` (non-fixing) which
will fail CI on any unformatted file — running the auto-fixer first
eliminates that class of failure entirely.

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

* chore(favicon): use theme/assets/MCP.svg as the favicon source of truth

Point index.html's icon link at /src/theme/assets/MCP.svg so the same MCP
logo asset already imported by ViewHeader is also the favicon. Delete the
now-unused clients/web/public/favicon.svg. Vite resolves the source path
during build and emits a hashed copy under dist/assets/.

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

* fix(server-card): surface failed-handshake errors with a 5s auto-dismiss

Two related fixes so the user actually sees what went wrong when a
connect attempt fails (e.g. OAuth-backed server, bad URL, missing dev
auth token):

- App.tsx: add `connectInFlightRef` and skip clearing `activeServerId`
  in the InspectorClient `disconnect` listener while a connect attempt
  is in flight. The transport closes during a failing handshake and
  dispatches `disconnect` before the catch block sets `errorMessage`;
  without this guard InspectorView would map the card to plain
  "disconnected" and the InlineError would never render.

- InlineError: opt-in `autoDismissMs` prop that runs a Mantine `slide-up`
  Transition exit animation after the message has been visible for the
  given duration. Visibility is derived from a "dismissed message"
  state (rather than a mounted boolean + setMounted-in-effect), so the
  timer re-arms whenever `error.message` changes without tripping the
  react-hooks/set-state-in-effect rule. ServerCard passes
  autoDismissMs={5000}.

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

* refactor(server-card): own InlineError visibility + animate parent-driven removal

Move the 5s auto-dismiss timer and visibility state out of InlineError
and into ServerCard so the slide-up exit animation runs in both cases:

- Timer-driven: after 5s the alert animates out (existing behavior).
- Parent-driven: when `connection.error` is cleared mid-flight (e.g. a
  successful reconnect), the alert ALSO slides up instead of vanishing.

To keep the message painted during the parent-driven exit, ServerCard
tracks a `lastError` state that mirrors `connection.error` only on its
truthy edges. The conditional setState during render is the supported
React pattern for "remember the last seen value of a prop"; gated on
message inequality so it can't loop. The `useRef` version of this
pattern failed `react-hooks/refs` (no ref writes/reads during render).

InlineError is now a pure presentational component: `mounted` prop
controls a Mantine `slide-up` Transition. The internal `autoDismissMs`
prop is gone — its single caller has moved to the controlled `mounted`
pattern.

Closes the "error vanishes the moment the parent removes it" half of #1364
(the timer half landed in 6d147e0f).

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

* refactor(server-card): surface connection errors as a toast, drop InlineError

Move handshake-failure messaging out of the ServerCard body and onto a
Mantine toast (`notifications.show`). The inline alert had a 5-second
visibility contract entangled with InspectorView's status pipeline, and
the active card was flipping through error → disconnected → error during
a failing handshake; surfacing the error as a toast sidesteps that whole
timing problem and keeps the card focused on the live status indicator.

- App.tsx: toast on the connect-catch path; drop `errorMessage` state +
  `connectInFlightRef` (the latter was only there to keep the inline
  alert visible past the transport's close event).
- InspectorView: drop the `errorMessage` prop and the
  `connectionStatus === "error" && errorMessage` mapping branch.
- ServerCard: drop the InlineError rendering + the displayedError /
  errorVisible visibility state machine.
- Delete the InlineError component — no other consumers.

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

* chore(notifications): anchor toasts to the bottom right

Move the Notifications portal from top-right to bottom-right so a
connection-failure toast doesn't overlap the ViewHeader / tabs row.

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

* review: address PR feedback on the Connection Info modal

1. Hoist `EMPTY_CLIENT_CAPABILITIES = Object.freeze({})` to module scope
   in `useInspectorClient` so the fallback returns a stable reference
   (avoids invalidating downstream `useMemo`/`useEffect` deps when no
   client is attached).
4. Drop the `?? "stdio"` silent fallback on `connectionInfoTransport`
   in the JSX — gate the modal render on `activeServer` too. The
   "stdio" default still lives at the derivation site (config.type is
   schema-optional and `ServerType` is required by the modal prop),
   but the gate makes the invariant explicit.
6. Replace the raw `document.querySelector("button.mantine-CloseButton-root")`
   in `ConnectionInfoModal.test.tsx` with `screen.getByRole("button",
   { name: /close/i })`. Add `aria-label="Close"` to the
   CloseButton so the assertion (and screen readers) have a name to
   match on.
7. Switch the OAuth scope splitter from `/\s+/` to the literal `" "`
   per OAuth 2.1 §3.3.

Also clarify in comments:
2. Note that `connectionInfoOAuth` uses snapshot-at-open semantics on
   purpose — token refreshes mid-session won't re-render the modal.
5. Note that we prefer the AS-advertised `authorization_endpoint`
   over `authorizationUrl` because the latter is per-flight noise
   (state, code_challenge) rather than a stable identifier.

Finding 3 (modal exit animation short-circuited by the `{initializeResult
&& …}` guard) left as intentional — we want the snap-shut on disconnect
so a stale info dialog can't keep rendering with no live client behind it.

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

* docs(connection-info): tighten the OAuth memo comment per re-review

The reviewer noted that "snapshot-at-open" was imprecise — the memo
re-runs on any of its deps (connectionStatus, inspectorClient,
activeServer), which includes settings edits that re-reference the
active server. Reframe as "snapshot-at-last-derivation" and call out
the natural triggers (server switch, reconnect, settings edit) so a
future reader doesn't conclude the memo is open-only.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 16:42:37 -04:00
Cliff Hall
37a4d99575
Port v1.5 web dev backend (vite-hono-plugin + remote-server bootstrap) (#1321)
* feat(web): port v1.5 dev backend (vite-hono-plugin + sandbox + remote-server bootstrap)

Ports the v1.5 in-process dev backend into v2 so `npm run dev` serves the
Hono remote-server (`/api/*`) alongside the Vite-served browser app. Without
this, the browser-side `createRemoteTransport` had nothing to talk to during
local development.

Adds under `clients/web/server/`:
- vite-hono-plugin.ts (Hono middleware on the Vite dev server; gated to
  `apply: 'serve'` and skipped when `process.env.VITEST` is set so it stays
  inert in vitest/storybook runs)
- sandbox-controller.ts (standalone HTTP server for the MCP Apps sandbox)
- web-server-config.ts (env parsing + initial-config payload + banner;
  `autoOpen` defaults off under VITEST so test runs don't shell out to `open`)
- server.ts (standalone Hono prod server for the future v2 launcher, #1246)
- start-vite-dev-server.ts (in-process Vite starter for the future launcher)
- vite-base-config.ts (shared optimizeDeps exclusions)

Wires `honoMiddlewarePlugin(buildWebServerConfigFromEnv())` into
`vite.config.ts` with `apply: 'serve'`. Adds the `open` dependency and a
DOM lib + `@inspector/core/*` path alias to `tsconfig.node.json` so the
dev-backend files type-check alongside vite.config.ts.

Coverage: web-server-config.ts (100% lines), sandbox-controller.ts (91% lines),
and vite-base-config.ts (100%) all clear the per-file gate. The Vite plugin,
prod server, and dev-server starter are excluded from coverage as runtime
glue exercised by `npm run dev` end-to-end (callers covered separately;
their logic lives in core/mcp/remote/node, which integration tests already
exercise).

Closes #1320.
Unblocks #1244 (Wire App.tsx) — its test plan can now run end-to-end.

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

* fix(web/server): address PR review feedback on the dev-backend port

- sandbox-controller: resolve start() with empty values on listen error
  (e.g. EADDRINUSE on a fixed MCP_SANDBOX_PORT) instead of leaving the
  promise pending forever. Without this, the Vite plugin's
  `await sandboxController.start()` could hang the entire dev backend on a
  port collision. Banner now silently omits the sandbox line; getUrl()
  keeps returning null. Adds a regression test exercising the EADDRINUSE
  path against a real socket.
- vite-hono-plugin: listen for `error` and `close` on the incoming request
  stream (in addition to `end`) so an aborted POST/PUT (browser navigates
  mid-upload) doesn't leak the middleware indefinitely.
- vite-hono-plugin: add a `.catch()` to the initial `pump()` kickoff so a
  sync throw before the first `await reader.read()` can't surface as an
  unhandled rejection.
- sandbox_proxy.html: drop the stale CSP comment — v1.5's sandbox HTTP
  server never set CSP either; this PR doesn't regress anything. Replace
  with the actual isolation model (iframe sandbox attribute + origin
  allowlist) so future readers don't assume a CSP header is enforced.
- Hoist `WebServerHandle` to a shared `server/types.ts` so the future
  launcher port (#1246) doesn't need to pick one of two duplicate
  declarations.
- Extract `resolveAutoOpen()` helper out of the triple-nested ternary in
  `buildWebServerConfigFromEnv` for legibility, with the three-way logic
  documented in one place.
- Add an inline comment explaining why
  `webServerConfigToInitialPayload` falls back to streamable-http for
  unknown discriminators (forward-compat with future SDK transports)
  rather than throwing.

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

---------

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:54:49 -04:00
Cliff Hall
88b55b093b
test: run v1.5-ported integration tests under node-env vitest project (#1307) (#1312)
* feat(test): run v1.5-ported integration tests under node-env vitest (#1307)

Adds an `integration` vitest project alongside `unit` and `storybook` so
the 13 v1.5-ported tests that need real HTTP/stdio servers (or fail under
happy-dom's SDK mock resolution) run again under node env with 30s
timeouts. Restores test-servers/ as a buildable package so the stdio
subprocess spawns a real .js file. Drops the v1.5-source `coverage.exclude`
globs and fills coverage gaps across the v1.5-ported core/ modules; two
large files (inspectorClient.ts, remote/node/server.ts) keep narrow
per-file excludes tracked in #1310.

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

* test(core): address PR #1312 review — TS6-safe tsconfig, real transport_error path

- Drop `baseUrl` / `paths` / `typeRoots` / `declaration*` from
  test-servers/tsconfig.json. They were only needed for type
  resolution, and we already pass `--noCheck` from `test-servers:build`.
  Under TS 6+ the deprecated `baseUrl` exited with code 2, which
  silently skipped vitest in the `&&` chains of test:integration /
  test:coverage / validate.
- Rework the "send() throws after closed" test to drive `closed` via
  an actual transport_error SSE event instead of mutating private
  state through a cast — matches the real path the branch guards.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:15:17 -04:00
Cliff Hall
0ead1deea6
feat(core): port v1.5 InspectorClient + transports + auth subsystem (#1302) (#1306)
* wip(core): bulk-copy v1.5 InspectorClient runtime files (#1302)

Verbatim ports from origin/v1.5/main into v2/main core/:

- logging/ (2 files)
- storage/ + storage/adapters/ (4 files)
- auth/ + auth/browser/ + auth/node/ + auth/remote/ (16 files)
- mcp/config.ts + mcp/node/ (4 files, stdio transport)
- mcp/remote/ + mcp/remote/node/ (12 files, HTTP/SSE transport)
- mcp/inspectorClient.ts (~2169 LOC), messageTrackingTransport,
  fetchTracking, oauthManager, index, state/index

Total: ~7,800 LOC of source files, copied unmodified via
`git show origin/v1.5/main:<path> > <dest>`.

Also:
- Add deps: pino, hono, @hono/node-server, atomically, zustand, @types/node
- tsconfig.app.json: extend paths to resolve new node-only deps from
  core/, add "node" to types, disable erasableSyntaxOnly (v1.5 code
  uses parameter properties — TODO to re-enable in follow-up)

Compiles down to 47 type errors, all in three categories:
- 24 TS2305: v2 dropped v1.5 type exports (InspectorClientOptions,
  CreateTransport, InspectorClientEnvironment, AppRendererClient,
  SamplingCreateMessage class, ElicitationCreateMessage class,
  jsonUtils helpers, OAuth events). Per user direction, will re-add
  these to v2's diverged files (types.ts, samplingCreateMessage.ts,
  etc.) so the ported code stays verbatim.
- 15 TS7006: implicit-any params in v1.5 code (v1.5 tsconfig was
  looser than v2's noImplicitAny). Need explicit annotations.
- 8 other TS errors: ../package.json import, Transport|null narrowing,
  hono Context generic mismatch, one unused parameter.

Tests not yet ported (19 files, ~11,170 LOC).

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

* wip(core): integrate ported v1.5 InspectorClient + port tests (#1302)

Source integration (typecheck + build now pass):
- Re-add v1.5 types/classes dropped during v2 evolution:
  - types.ts: CreateTransport*, AppRendererClient, InspectorClient{Environment,Options}
  - samplingCreateMessage.ts / elicitationCreateMessage.ts: classes + keep v2's
    InspectorPending{Sampling,Elicitation} interface
  - inspectorClientEventTarget.ts: oauth* event keys
  - json/jsonUtils.ts: convertParameterValue / convertToolParameters /
    convertPromptArguments + ParameterSchema
- inspectorClient.ts: replace `../package.json` import (v2 has no
  core/package.json) with a hardcoded identity constant
- remote/node/server.ts: cast hono Context for streamSSE (hono's
  generic typing tightened since v1.5)
- oauth-callback-server.ts: drop unused hostname field
- tsconfig: enable @types/node, add paths for new bare-module deps
  (pino, zustand, hono, atomically) in app + test configs
- vite.config.ts: mirror those module aliases for the unit project
  (which runs from repoRoot)

Tests ported (19 files, ~11K LOC) under clients/web/src/test/core/:
- All core/__tests__/auth/* (7 files)
- inspectorClient.test.ts + 4 OAuth variants
- mcp/oauthManager.test.ts, jsonUtils.test.ts
- transport, remote-transport, remote-server-config, storage-adapters
- helpers/oauth-client-fixtures.ts
Bulk-rewrote `from "../mcp/…"` → `from "@inspector/core/mcp/…"` etc.

Current state:
- typecheck: 0 errors
- build: passes (vite v8, 788 modules)
- unit tests: 1014 / 1034 pass (937 pre-existing + 77 new core tests)
- 20 failures in 13 files; failure modes:
  - 13 file-load failures needing the v1.5 `test-servers/` package
    (@modelcontextprotocol/inspector-test-server) which isn't in v2 yet
    or real fs/network not available in happy-dom
  - 7 assertion failures in discovery.test.ts + state-machine.test.ts
    (SDK 1.29 vs 1.25 API drift — mocks don't match new shape)

Coverage gate not yet checked against new files.

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

* wip(core): port test-servers, more types, partial test integration (#1302)

- Port v1.5 test-servers/ package (12 files, ~5K LOC) under
  repo-root/test-servers/, wire @modelcontextprotocol/inspector-test-server
  alias in both vite.config.ts (regex form) and tsconfig.test.json.
- Add deps: express ^5, yaml ^2, @types/express ^5.
- Re-export InspectorClientOptions/Environment/Transport* from inspectorClient.ts
  so v1.5 tests that import them from that module path keep resolving.
- Switch InspectorClientEventMap to use SamplingCreateMessage /
  ElicitationCreateMessage classes (matches v1.5 event payload contracts)
  while keeping the InspectorPending{Sampling,Elicitation} interfaces in
  the *.ts modules for v2 consumers that still use them.
- Fix verbatim-module-syntax issue in test-servers and drop unused stdio
  config field.
- Adjust inspectorClient.test.ts task types to TaskWithOptionalCreatedAt
  (SDK 1.29 tightened Task shape vs v1.5's 1.25).
- Drop spurious maxMessages/maxFetchRequests/maxStderrLogEvents from
  remote-transport.test.ts (never on InspectorClientOptions).

State:
- typecheck: 0 errors
- build: passes
- unit tests: 1099 / 1270 pass
- 171 failures in 10 test files, almost all integration tests that need
  real HTTP/fs/network (happy-dom can't host them). Next: add a node-env
  vitest project to run these properly.

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

* feat(core): port v1.5 InspectorClient + transports + auth subsystem (#1302)

Bulk-port of the v1.5 core runtime (~7,800 LOC source + ~16K LOC tests +
~5K LOC test-servers/) into v2. Files copied verbatim from origin/v1.5/main
via `git show >`; only minimal adapters added on the v2 side.

Subsystems landed
-----------------
- core/logging/, core/storage/, core/auth/ (browser + node + remote)
- core/mcp/ runtime: inspectorClient.ts (2169 LOC), oauthManager,
  messageTrackingTransport, fetchTracking, config, index
- core/mcp/node/ (stdio transport)
- core/mcp/remote/ + remote/node/ (HTTP/SSE transport + Hono server)
- test-servers/ package (composable MCP test servers, fixtures, harness)

v2 adapter layer
----------------
- types.ts: append CreateTransport*, AppRendererClient,
  InspectorClient{Environment,Options} (v2 had pruned these — v1.5 needs them)
- samplingCreateMessage / elicitationCreateMessage: keep v1.5 classes AND
  v2's Inspector Pending* interfaces in the same files
- inspectorClientEventTarget: add oauth* event keys; switch pending
  sampling/elicitation payloads to the classes (matches v1.5 contracts)
- json/jsonUtils: append convertParameterValue/convertToolParameters/
  convertPromptArguments + ParameterSchema
- inspectorClient.ts: re-export Options/Environment/Transport* so tests
  that import these from `inspectorClient.js` keep resolving; replace
  `../package.json` import with a hardcoded identity (v2 has no core/package.json)
- remote/node/server.ts: cast hono Context for streamSSE (hono generic
  tightened since v1.5)
- oauth-callback-server: drop unused hostname field
- tsconfig.app.json / tsconfig.test.json: add @types/node + paths for
  pino/zustand/hono/@hono/node-server/atomically/express/yaml, disable
  erasableSyntaxOnly (parameter-property syntax from v1.5)
- vite.config.ts: regex-form module aliases (so the package's exports
  field handles subpaths like `hono/streaming`, `zustand/middleware`),
  alias for @modelcontextprotocol/inspector-test-server → test-servers/src

Tests
-----
- 19 v1.5 test files copied under clients/web/src/test/core/,
  imports rewired to @inspector/core/* and @modelcontextprotocol/inspector-test-server
- 1004 unit tests pass; 298 storybook tests pass; coverage gate passes

Known follow-ups (out-of-scope here, tracked in TODOs)
------------------------------------------------------
- 13 integration test files are excluded from the unit project (spawn real
  HTTP/stdio servers; run real OAuth flows; mock SDK auth → happy-dom CORS).
  Needs a node-env vitest project. Files listed under `test.projects[0].test.exclude`.
- Source files exercised only by those integration tests are excluded from
  the coverage gate pending the node-env project. Listed under
  `test.coverage.exclude` with comment.
- Manual smoke tests against real stdio / HTTP / OAuth server per the AC
  on #1302 still need verification locally.

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

* docs/fix: address #1306 review (AGENTS tree, #1307 links, @types/node, .substr)

- AGENTS.md: expand the project-structure tree to include all v1.5-ported
  subsystems (core/auth/{browser,node,remote}, core/storage, core/logging,
  core/mcp/{node,remote/{,node}}, core/mcp/state) plus the new top-level
  test-servers/ directory (with a note about its @modelcontextprotocol/
  inspector-test-server alias).
- vite.config.ts: reference #1307 from both the test exclude block and the
  coverage exclude block so the next person editing either sees the
  marching order for shrinking the lists.
- package.json: restore @types/node to ^24 (an earlier `npm install
  --save-dev @types/node@^22` accidentally pinned it lower than the repo
  was using; nothing here required ^22).
- core/mcp/fetchTracking.ts: replace deprecated .substr(2, 9) with
  .slice(2, 11) to match the convention in core/mcp/inspectorClient.ts.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:36:17 -04:00
Cliff Hall
a4215e57b9
ci(storybook): wire test:storybook into CI (#1251) (#1297)
* ci(storybook): wire test:storybook into CI (#1251)

- Add test:storybook:watch to clients/web for interactive iteration; the
  one-shot script already existed.
- Surface test:storybook at the repo root, delegating to the web client.
- Add a separate CI step that installs the Playwright Chromium binary
  and runs the storybook play-function suite after validate. Kept out of
  validate because the suite needs a real browser and is ~10s slower
  than the unit tests.
- Document the new pre-push step in AGENTS.md.

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

* ci(storybook): cache Playwright browsers + tighten AGENTS wording

Cache the Chromium binary keyed on clients/web/package-lock.json so the
~150MB download is a no-op on CI cache hits. Also clarify that CI runs
test:storybook after the unit/lint/build checks (not literal `validate`,
which is a local-only script).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:58:15 -04:00
Cliff Hall
75f74d64c1
test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245) (#1269)
* test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245)

Establishes the unit-test runtime AGENTS.md mandates: a vitest "unit"
project alongside the existing storybook project, RTL + jest-dom +
happy-dom, a Mantine-aware render helper, and a 90%-line per-file
coverage gate enforced by CI. Adds 66 test files covering all 62
components under elements/groups/screens/views, plus core/mcp and
clients/web/src/utils. Total: 577 tests, 98.7% line coverage globally.

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

* test(coverage): address PR #1269 review feedback

- AGENTS.md: correct coverage gate description (lines ≥ 90, statements ≥ 85,
  functions ≥ 80, branches ≥ 50) with rationale for the relaxed branch
  threshold
- InspectorView.test.tsx: document what the Math.random() mock values
  (0.1 / 0.99) control vs. STUB_SUCCESS_RATE in InspectorView.tsx
- vite.config.ts / tsconfig.test.json: drop unused `globals: true` and the
  `vitest/globals` type now that every test file imports describe/it/expect/vi
  explicitly; manual `afterEach(cleanup)` in setup.ts remains required because
  testing-library auto-cleanup relies on globalThis.afterEach

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:15:32 -04:00
Cliff Hall
5a2d24bac7
docs(agents): require version label on new issues and PRs (#1257)
* docs(agents): require version label on new issues and PRs (#1256)

Recent v2 PRs (#1247, #1249, #1250, #1252, #1255) all landed without a
v2 label, making version-filtered queries unreliable. Codify the
expectation directly in the issue-driven workflow: each branch / board
gets a fixed label, and the label is set at create time via the
gh create flag rather than backfilled.

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

* Also fix stale "PR against main" line

The PR-target line still said "Open a PR against main", which is
correct only for v1 work. Mirror the version-label mapping above so
the PR base matches the board.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:59:26 -04:00
cliffhall
3afb9803fb Update AGENTS.md with issue driven workstyle and more context about the three main branches and the associated projects
Update package.json with a validate script
2026-04-27 12:14:06 -04:00
cliffhall
dd46acba87 Refactor renamed the component groups to something less obtuse:
Elements -> Groups -> Screens -> Views
2026-03-28 12:58:47 -04:00
cliffhall
69b17add75 Update AGENTS.md 2026-03-24 14:14:14 -04:00
cliffhall
bd4606a4b2 add guidance on theme files vs atom components to AGENTS.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:16:17 -04:00
cliffhall
019e10429d * Added AGENTS.md 2026-03-17 10:43:56 -04:00
cliffhall
39ef184b53 feat: Add Mantine theme, 12 atom components, and 25 molecule components with Storybook stories
- Mantine theme with component extensions (Badge, Card, Paper, Button, etc.)
- CSS custom properties for design tokens (--inspector-* vars)
- Dark/light mode support with auto detection
- 12 atoms: StatusIndicator, AnnotationBadge, CapabilityItem, LogEntry,
  ProgressDisplay, ContentViewer, ListChangedIndicator, CopyButton,
  ConnectionToggle, TransportBadge, InlineError, MessageBubble
- 25 molecules: ServerCard, AddServerMenu, SchemaForm, ToolListItem,
  ToolDetailPanel, ResultPanel, ResourceListItem, ResourcePreviewPanel,
  ResourceTemplateInput, PromptArgumentsForm, PromptMessagesDisplay,
  LogControls, TaskCard, HistoryEntry, ServerInfoContent, ServerSettingsForm,
  RootsTable, SamplingRequestPanel, ElicitationFormPanel, ElicitationUrlPanel,
  InlineSamplingRequest, InlineElicitationRequest, PendingClientRequests,
  ImportServerJsonPanel, ExperimentalFeaturesPanel
- Exclude storybook-static from version control
- Exclude .stories.tsx from app tsconfig (Storybook-only modules)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:23:56 -04:00
cliffhall
442bb68532 Remove all from v2/main 2025-11-28 13:49:31 -05:00
olaservo
cbb6a0a53c Add version consideration and context for Inspector V2 development in documentation 2025-11-08 19:27:05 -07:00
olaservo
694b202251 Move CLAUDE.md content to AGENTS.md 2025-10-08 07:38:10 -07:00