Review follow-ups on the RemoteOAuthStorage web migration:
- useEmaIdpLoginState.logout(): add a .catch to the now-async
clearEmaIdpSession call so a storage failure no longer surfaces as an
unhandled rejection; leave loginState unchanged on failure (the session
was not actually cleared). Add a test asserting no unhandled rejection.
- createOAuthStore: attach a no-op catch to the eager persistLoadPromise
so a hydration read that fails before any load() awaits it does not
become an unhandled rejection; real awaiters still observe the rejection
via the shared reference.
- storage-remote.test: replace `as unknown as typeof fetch` double-casts
with the typed vi.fn<typeof fetch>() pattern the same PR established.
- remoteOAuthStorage: correct the doc comment to describe the cache-of-one
accurately instead of implying a keyed map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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
Address the TypeScript-safety review items on the mid-session auth work:
- oauthManager: replace the unguarded `this.oauthConfig.storage!` assertions on
the EMA re-mint/persist paths with a `requireStorage()` helper that throws a
clear error; in `getOAuthState` reuse the already-narrowed local `storage`.
- cli: `parseRunnerOAuthCallbackUrl` always returns a config, so make
`callMethod`'s `callbackUrlConfig` (and the always-passed `cliAuthOverrides`)
required instead of asserting them non-null at three call sites.
- test-servers/resolve-config: drop the `as unknown as T` double cast; collect
into a base-shaped array and do a single honest downcast at the return.
No behavior change; validate + coverage green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
`reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.
No behavior change; validate + integration green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
The mid-session auth work passed `validate`/CI but failed the local-only
`npm run test:coverage` per-file gate (≥90% lines/statements/functions/
branches) on 12 files. Add targeted unit/integration tests so every changed
file clears the gate; annotate provably-dead defensive branches with justified
`/* v8 ignore */` comments rather than relaxing the gate.
Files brought to ≥90 on all four dimensions:
- core/auth: mcpAuth, oauthUx, utils, challenge, node/runner-interactive-oauth
- core/mcp: oauthManager, inspectorClient, node/authChallengeFetch,
remote/remoteClientTransport, remote/node/{remote-session,server}
- clients/web/src/utils: oauthResume, browserTabVisibility
`npm run test:coverage` now passes (3575 tests, 0 threshold failures);
`npm run validate` green across all four clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
Clear the loopback callback timeout in runner-interactive-oauth finally
so already_authorized and error paths do not hang the CLI/TUI. Check EMA
step-up satisfaction against minted token scope before persisting the
requested union, matching the standard OAuth path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a specification/v2_auth.md landing page summarizing the four auth specs
(EMA / XAA, Hardening, Mid-session, Smoke testing) with a section submenu,
mirroring the V2 UX index pattern.
Add a V2 Auth link (after V2 UX) and a V2 New Spec Impact link to every spec
page's top menu so the top-level nav is uniform, and restructure the four
v2_auth_*.md pages onto the shared two-tier menu (top menu + auth submenu).
Docs only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
* 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>
* 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>
* feat(web): add View Source toggle to resource preview (closes#1552)
Markdown, CSV, and HTML resources render in a way that hides the
underlying text (react-markdown, the CSV table, the sandboxed HTML
iframe). Add a "View Source" button to the resource preview footer, left
of Refresh and styled to match it, that swaps the rendered preview for
the raw resource text. In source mode the button reads "View Rendered".
- Only shown for markdown/csv/html (other kinds already show their text,
or have no meaningful text source).
- Source mode forces ContentViewer through its plain-text renderer; the
displayed MIME label keeps the resource's real type.
- The toggle resets to the rendered view when the previewed resource
changes (React's adjust-state-during-render pattern — no effect).
Tests cover the toggle round-trip, per-kind visibility, and the reset;
a Storybook play function exercises rendered → source → rendered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): address View Source review nits (#1553)
- Per-item source gating: in source mode only source-toggleable items
switch to plain text, so a mixed multi-part resource doesn't force an
image/PDF blob through the text decoder.
- Share a FooterButton constant between Refresh and the View Source
toggle so the two stay visually identical (per the subcomponent
guidance — both had the same two inline props).
- Add aria-pressed to the toggle so assistive tech gets explicit on/off
state beyond the label flip.
Tests added for the aria-pressed state and the mixed multi-part case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): expand ContentViewer to render PDF, CSV, HTML, XML, JSON, CSS (closes#1329)
Turn ContentViewer into a per-MIME dispatch surface for resource content.
It now accepts raw TextResourceContents | BlobResourceContents (in addition
to the existing ContentBlock path), so ResourcePreviewPanel.toContentBlock
collapses to a passthrough and blob-typed resources render natively instead
of falling back to opaque "[Binary content …]" text.
New renderers (clients/web/src/components/elements/):
- CodeHighlight: lazy react-syntax-highlighter prism-light element. Runtime,
tomorrow theme, and each grammar dynamic-import on first use (one chunk per
grammar). Module-level registeredLanguages / failedLoads / loadingPromises
caches; plain Code block until a grammar resolves.
- PdfFrame: iframe over a Blob URL (#view=FitH), revoked on unmount.
- CsvTable: papaparse -> Mantine Table (first 100 rows), plain-Code fallback.
- HtmlFrame: sandbox="" iframe over a Blob URL with an injected CSP meta
(no script-src, falls through to default-src 'none').
- contentViewerUtils: getMimeKind, base64 decode helpers, formatXml,
wrapHtmlWithCsp, SAFE_HREF markdown-anchor allowlist (unsafe href -> span).
ResourcePreviewPanel: extend the URI-suffix MIME inference table
(.csv/.json/.xml/.html/.htm/.css/.pdf alongside .md).
Deps: papaparse + react-syntax-highlighter (+ @types). tsconfig paths stubs
redirect the deep RSH imports for typecheck (the @types ambient declarations
are shadowed by the real .js under bundler resolution) — same pattern the
repo uses for react/pino.
Tests cover PDF blob-URL creation + cleanup, CSV table render + fallback,
HTML sandbox attributes + CSP injection, JSON/XML/CSS highlighted output,
the lazy-load/alias/failed-load/shared-runtime paths, and the URI-suffix
MIME-inference paths. validate, test:coverage (per-file gate), test:storybook,
and integration all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): harden ContentViewer blob/CSV rendering per review (#1551)
Address review feedback on the ContentViewer MIME-dispatch PR:
- useObjectUrl: fix a dev-only StrictMode bug where the committed object
URL was revoked but never recreated (setup→cleanup→setup with no
re-render), blanking PDF/HTML previews. Revocation is now deferred to a
microtask and guarded by a "live URL" ref so a StrictMode re-setup
cancels it, while a real unmount / blob change still releases the URL.
Avoids set-state-in-effect; mirrors AppRenderer's deferred-disposal.
- atob: guard base64 decoding with tryDecodeBase64ToUtf8 /
tryDecodeBase64ToBytes so a malformed blob from an external server
degrades to the binary-content notice instead of throwing during render
and taking down the whole preview panel.
- CsvTable: surface the 100-row display cap with a
"Showing first N of M rows" caption (no silent truncation).
- Extract BinaryNotice into its own module so PdfFrame can reuse it
without an import cycle.
Tests added for the StrictMode path, malformed-base64 fallbacks (PDF +
textual blob), the safe-decode helpers, and the truncation caption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #1545. The close-block only fired for an invalid issuer, so an
EMA config enabled with a valid issuer but a blank client ID was still silently
dropped on close by the persist gate — the same no-feedback bug, different
field.
- validateClientSettings gains a requireComplete mode that also flags the
required IdP fields (issuer, clientId) when blank, with distinct per-field
messages. Inline (as-you-type) validation is unchanged — required-but-blank
errors only surface on reveal, so the form never nags about not-yet-filled
fields.
- canPersistClientSettingsDraft is redefined in terms of that mode, so the
persist gate and the close-time field errors share one source of truth.
- ClientSettingsModal.handleClose now blocks + reveals on ANY require-complete
error (invalid issuer OR blank required field) and keeps the modal open. The
form prop revealIssuerError becomes revealErrors and surfaces the client-ID
error too. Escape hatch: disable enterprise IdP (an empty issuer no longer
closes, since that's itself incomplete).
Tests cover the blank-clientId close path, fix-each-field, and disable-EMA
escape, plus requireComplete validation.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): update OAuth E2E tests to use new authentication method
- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.
Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.
* Guided-auth removal, OAuth smoke testing, and TUI/CLI support
* Added CLI OAuth runner flags tests
* Added clarifying comment on port 0 binding
* README notes on TUI oauth callback port, prettier fixes for CLI tests
* CIMD validation uses inline like EMA IdP
* Fixed TUI test failures from merge
* test: bring 5 OAuth files over the 90 per-file coverage gate
The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:
- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
provably-dead guards (empty host, out-of-range port, String(err)
fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
the stale-onChange-after-close prev===null guard, and the
timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
kinds, the expired IdP session, and the configured-scope/empty-scope
branches of formatScopes.
npm run coverage now passes (exit 0) across web, cli, tui, launcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): defer IdP issuer URL error to onBlur in Client Settings (#1544)
The Enterprise-Managed Authorization Issuer field validated its URL inline on
every render, so the 'Must be an http(s) URL' error nagged while the user was
still mid-typing (e.g. before 'https:/…' is complete).
Gate the issuer error display on an issuerTouched flag set onBlur; once
touched it tracks the value live, so the error clears as soon as a valid URL
is entered. The persist gate (canPersistClientSettingsDraft → validateClientSettings)
is unchanged and still validates independently, so an invalid issuer is never
written to the backend regardless of touched state.
Closes#1544
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): reveal issuer error and block close on invalid issuer (#1544)
Extend the on-blur deferral so an invalid issuer can't be silently dropped on
modal close (the auto-save flush would otherwise discard it with no feedback).
ClientSettingsModal now intercepts its close (X / Esc / overlay): if the issuer
is invalid it reveals the field error (via the form's revealIssuerError prop,
overriding on-blur gating) and keeps the modal open instead of closing. The
user can fix the URL or clear the field — an empty issuer is valid to leave.
The block lives in the modal (unit-tested) rather than App, so App.tsx stays a
thin flush+close that only runs when the close is allowed; the persist gate is
unchanged. Reveal state resets via the modal's open/close remount key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): require a real host in isAbsoluteHttpUrl, not just an http(s) scheme (#1544)
The issuer validation used URL.canParse + an http(s) protocol check, which the
WHATWG parser accepts for bare/degenerate values that aren't actually usable
URLs — e.g. https://foo, https://example, https://. , https://.. all passed.
Tighten the shared rule (used by both the Zod config schema and the form's
inline validation): after parsing and the http(s) scheme check, require the
host to actually look like a host — localhost, an IPv6 literal, or a dotted
domain/IPv4 with at least two non-empty labels. Single-label, bare-dot, and
empty-label hosts are now rejected. Add core tests for the accepted
(dotted/IPv4/IPv6/localhost) and newly-rejected cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): cover useSelectableList hook + leaf Ink components (#1501)
Add ink-testing-library and renderer-based tests for useSelectableList,
SelectableItem, and Tabs. Widen the vitest include glob to .test.tsx and
lift these files from the interim React-surface coverage exclusion. The
blanket src/components/**/*.tsx exclude is expanded into an explicit
per-file list so remaining components can be lifted incrementally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): add ink-scroll-view passthrough mock for renderer tests (#1501)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): add ink-form passthrough mock for modal renderer tests (#1501)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): cover all Ink tab + modal components via renderer tests (#1501)
Add ink-testing-library tests for the 12 Ink components (InfoTab, AuthTab,
ResourcesTab, PromptsTab, ToolsTab, NotificationsTab, HistoryTab,
RequestsTab, and the Tool/Resource/Prompt test modals + DetailsModal),
mounting them through the ink-scroll-view / ink-form passthrough doubles
and driving keypresses via stdin. Modal components render position:absolute
(empty frame under the test renderer) so their tests assert on behavior —
injected InspectorClient fakes and onClose — while the passthrough
ScrollView still mounts the inner JSX for coverage.
Lift these 12 components from the interim React-surface exclusion in
vitest.config.ts; only App.tsx remains excluded. All files clear the
uniform 90/90/90/90 per-file gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): add App.tsx renderer-test harness + foundation tests (#1501)
Mock the full @inspector/core surface (InspectorClient, the 7 managed-state
classes, the 8 react hooks, transport + auth) behind a controllable harness,
plus the ink-scroll-view/ink-form passthroughs, so App renders under
ink-testing-library and its keyboard state machine can be driven via stdin.
Concurrent ink mounts share raw-mode stdin, so each render is unmounted in
afterEach. Foundation covers server-list nav, tab accelerators, connect/
disconnect, OAuth-tab gating, and connected-status rendering (~50% baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): cover App.tsx via mocked-core renderer tests; lift final exclusion (#1501)
Extend App.test.tsx to 67 tests driving the keyboard state machine, tab
content, modals, OAuth flows, and connect/disconnect across the controllable
@inspector/core mock. App.tsx now clears the 90/90/90/90 per-file gate
(94.6/90.6/94.5/96.6).
Remove the last React-surface exclusion (src/App.tsx) from
clients/tui/vitest.config.ts. The whole clients/tui/src tree is now under
the uniform per-file coverage gate with no React-surface exclusions,
completing the interim-scope lift from #1484.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): raise stdout max-listeners in App test to silence warning (#1501)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): raise App.tsx branch coverage >90 and de-flake coverage runs (#1501)
Add targeted App.tsx tests for the OAuth handlers' non-Error throw arms
(quick onCallback, guided start/advance, run-to-completion) and a re-entrant
quick-auth guard, lifting App.tsx branch coverage 90.6% -> 91.48% for a
safer margin above the 90 gate.
Harden the App renderer tests against the heavier v8 coverage instrumentation,
where React+ink's multi-macrotask render scheduling races fixed ticks:
- press() now settles two ticks per key so focus/tab changes flush before the
next key is routed (ink re-subscribes useInput on re-render);
- expectFrame()/waitUntil() poll for frame content and async flow state
(callbackServer opts) instead of asserting after a single fixed tick;
- ResourcesTab's content-clear test polls for fetched content likewise.
Plain 'npm test' (the CI path) was already stable; this makes the local
coverage gate stable too (13/13 clean runs vs ~2/8 flaky before).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): de-flake component renderer tests under slow CI (#1501)
CI flaked on AuthTab's 'renders quick auth results' under the plain test run:
its oauthFlowState is populated by a post-mount useEffect reading the
inspectorClient, and a single-macrotask tick can race that effect -> setState
-> re-render chain on a slow/loaded CI runner.
Make each component test file's tick() flush several macrotask cycles so such
chains settle deterministically before assertions (no test relies on precise
tick timing — no fake timers / debounce). App.test.tsx already polls via
expectFrame/waitUntil and passed CI, so it's unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tui): address review — shared setupFile for max-listeners, consume-once ink-form global (#1501)
Review follow-ups on #1538 (both non-blocking cleanups):
- Hoist process.std{out,in}.setMaxListeners into a shared vitest setupFile
(__tests__/setup.ts) wired via setupFiles, so every renderer test file
inherits it and the MaxListenersExceededWarning is silenced across the
whole suite — not just App.test.tsx. Remove the per-file call.
- Make the ink-form double consume (clear) __INK_FORM_SUBMIT_VALUE__ on
submit, eliminating the latent cross-test coupling on the shared global
regardless of how the modal test files are organized.
No production code changed; suite + coverage gate stay green (App.tsx
94.8/91.5/94.5/96.6), and the warning no longer prints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): remove mantine-border-animate, use green highlight border (#1535)
The @gfazioli/mantine-border-animate dependency emitted a persistent
React "forwardRef render functions accept exactly two parameters"
warning on every page load. Remove the dependency entirely and replace
the freshly-added card's animated border with a green border.
- Drop @gfazioli/mantine-border-animate from clients/web (package + lock)
and remove its styles.css import from main.tsx.
- Add a `highlighted` variant to the Card theme that draws a 2px green
border, driven by a new --inspector-highlight-border token in App.css.
- ServerCard now selects the variant (disabled > highlighted) instead of
wrapping in BorderAnimate; the BorderAnimate flex-wrapper `w="100%"`
workaround is no longer needed.
- Update ServerCard/ServerListScreen tests to assert the highlighted
variant instead of the removed BorderAnimate DOM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): drop redundant Card props on ServerCard (#1535)
ThemeCard.defaultProps already sets withBorder and padding="lg", so the
explicit props on ServerCard's <Card> were redundant. Addresses review
feedback on PR #1536.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(web): note highlighted Card variant relies on inherited withBorder (#1535)
Addresses a maintainability flag from the PR #1536 re-review: the
highlighted variant only sets borderColor/borderWidth and depends on the
withBorder:true default, so make that coupling explicit in a comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Enterprise-Managed Authorization "Issuer" field accepted arbitrary
non-URL text and only failed at save time, surfacing a raw Zod issue array
in "Failed to save client settings" toasts.
- Inline field-level validation: the Issuer input shows a friendly error
("Must be an http(s) URL, like https://idp.example.com").
- canPersistClientSettingsDraft gates on issuer validity (deferring to
validateClientSettings) so an invalid value is never sent to the backend.
- Extract isAbsoluteHttpUrl in core/client/config-parse so the Zod schema
and the form share one http(s)-only URL rule.
- Harden the fallback save-error toast via formatClientConfigLoadError.
Closes#1533
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* fix(web): keep server cards a fixed height (#1528)
Two causes made server cards ragged-height:
1. The command/URL box wrapped long content onto multiple lines, growing
the card. Add a `nowrap` Code variant (single line, ellipsis on
overflow) and a `wrap` prop on ContentViewer (default true); ServerCard
passes `wrap={false}`. The full value stays available via the copy
button.
2. At ~1000-1500px the grid packed cards narrow enough that a connected
card's action row (Clone/Edit/Remove + Connection Info/Settings) wrapped
and stacked. Switch the SimpleGrid to container queries with thresholds
that keep every card >= ~500px wide at each column count, so it drops to
fewer/wider columns instead of wrapping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): address review — correct threshold comment, single-line note, title tooltip
- Correct the column-threshold comment in ServerListScreen: the values
(1040/1560) use N gaps, not N−1; restate the formula accurately so the
numbers aren't "fixed" to match a wrong formula later.
- Note in the ContentViewer `wrap` JSDoc that `false` is for single-line
values only (nowrap collapses embedded newlines).
- Add a native `title` tooltip to the non-wrapping Code box so a clipped
command/URL is readable on hover, not only via copy. Covered by tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Header navigation tabs now appear only when the connected server
advertises the corresponding server capability (from the MCP
`initialize` result `capabilities`), instead of being shown
unconditionally or proxied through list content.
Previously the Tools and Logs tabs were always shown when connected,
regardless of whether the server advertised `tools` / `logging`. The
content-gated tabs (Prompts/Resources/Tasks) only appeared correct
because an unsupported capability yields an empty list — but a server
advertising a capability with a currently-empty list would have its
tab wrongly hidden.
InspectorView.availableTabs now gates each server-capability tab on
the matching field of `initializeResult.capabilities` (already
threaded from App via the handshake):
Tools -> capabilities.tools
Logs -> capabilities.logging
Prompts -> capabilities.prompts
Resources -> capabilities.resources
Tasks -> capabilities.tasks
Apps -> capabilities.tools AND >=1 app tool (Apps is a filtered
view of tools, so it keeps its content check)
Client-side sections are never gated on server capabilities: Servers
and History are always available, and Network stays transport-gated
(hidden for stdio). Tabs recompute live when reconnecting to a server
with a different capability set.
Tests: reworked the content-gating component tests into
capability-gating tests (including the Tools-hidden regression, Logs
present/absent, capability-present-but-empty-list, History-always, and
a reconnect-recomputes test). Connected story/fixtures advertise a
full capability set so the Connected story still shows every tab.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): re-enable other server cards when a connection errors (#1521)
When a server's connection settled into the terminal `error` state, the
other server cards stayed dimmed/`inert` (Connect toggle + actions
disabled), so the user couldn't interact with any other server.
`ServerCard` dims any card whose id differs from `activeServer`, and App
clears `activeServerId` only on the InspectorClient `disconnect` event.
A terminal `error` (a connect-time handshake failure, or a mid-session
transport `onerror` that fires without an `onclose`) sets status to
`error` but never dispatches `disconnect`, so `activeServerId` lingers
and every other card stays inert. We can't clear `activeServerId` on
error because the errored card's status, the connected header, and the
connection-info modal are all keyed off it.
Fix: dim the other cards only while the active session is actually live
(connecting or connected). InspectorView now passes the active id to the
ServerListScreen as the dim source only when `connectionStatus` is
`connecting`/`connected`; once it reaches a terminal state the others
re-enable. The errored card still shows its real status via the merged
`servers` list, which keys off the real `activeServer`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): reuse isTerminalStatus for card-dim gate (#1521 review)
Replace the hand-rolled `sessionLive` check with the existing
`isTerminalStatus` predicate (the #1490 teardown convention) so the
dim gate stays aligned with the canonical "session is over" branching
and can't desync from a future status addition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
* feat(web,core): edit STDIO cwd/env in Server Settings (#1515)
Persist and edit a STDIO server's working directory and environment
variables through the Server Settings modal.
- Add `env` (pair-array, mirrors headers) and `cwd?` to
`InspectorServerSettings`.
- `storedFieldsToInspectorSettings` lifts the stdio config's `env`/`cwd`
into settings so the modal can show them; they stay on `config` for the
transport. Add `envRecordToPairs` / `envPairsToRecord` helpers.
- `env`/`cwd` remain config-owned on disk (the standard mcp.json location):
the `/api/servers` PUT route writes an edited settings.env/cwd back onto
the stored stdio config on a settings-only patch (supporting clear), and a
config-providing PUT keeps config authoritative. `validateSettings`
accepts `env`/`cwd`.
- Server Settings modal: Working Directory field in Options and a dedicated
collapsible Environment Variables section — both STDIO-only.
- Registry server.json import sets no `cwd` (the schema has no
working-directory concept); documented in serverJson.ts.
Tests: serverList converter round-trip, route write-through (set/clear,
config-precedence, non-stdio no-leak, validation), import preservation
(client-config cwd, registry cwd skip), and form/modal component coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): preserve config env/cwd when a settings PUT omits them (#1515 review)
Address Claude's review of #1518.
- `/api/servers` PUT now distinguishes an *absent* `env`/`cwd` (preserve the
stored value) from an explicit empty list / blank string (clear). Previously
`validateSettings` coerced a missing `env` to `[]`, so an env-unaware caller
patching only a timeout would silently wipe the server's env/cwd. The
integrated web client always resends the full GET-rehydrated env, so it was
unaffected — this protects the raw HTTP contract.
- `validateSettings` reports `envProvided`/`cwdProvided`; the write-through only
touches `config.env`/`cwd` for fields the caller actually sent.
- On the omit path, rehydrate the untouched env's keychain values onto the
rebuilt entry so the secret reconcile re-persists them rather than sweeping
the blanked-on-disk keys out of the keychain.
- Minor: guard the stdio cast in `mcpConfigToServerEntries` on the transport
type instead of casting unconditionally.
- Test: settings-only PUT omitting env/cwd preserves config keys + cwd + the
keychain secret.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(state): wait for connect refresh before asserting list RPC gate (#1396)
Replace fixed double-microtask flushes in Managed*State connect-path tests with *Change event waits so the capability gate is asserted after refresh completes.
* test(state): fail fast with a readable message if a connect refresh never dispatches its change event (#1396)
The hardened connect-path gate tests now depend on the gated (capability-less /
empty-list) refresh still emitting its *Change event. A future optimization that
skipped that redundant dispatch would make these awaits hang to the vitest
per-test timeout with no clue why.
Extract the per-file waitFor*Change helpers into a shared, generically-typed
waitForChangeEvent() that races the listener against a short timeout and rejects
naming the event that never fired — turning a future hang into a fast, readable
failure. This also removes the duplicated listener/Promise boilerplate across
the five state test files. No production code changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(state): cover void-detail event resolution in waitForChangeEvent
Add a case asserting the helper still resolves for a signal-only (void-detail)
event — CustomEvent normalizes an absent detail to null, so the listener fires
and resolves rather than hanging. Closes the coverage gap noted in review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(core): correct TypedEventGeneric void-detail comment to null
CustomEvent normalizes an absent detail to null, so listeners receive null
(not undefined) for void events — as the new waitForChangeEvent void case
establishes. Comment-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* 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>
* 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>
* feat(cli): lift config-file headers/timeouts/OAuth into serverSettings (#1482)
The CLI resolved file-sourced servers via resolveServerConfigs(), which
returns bare MCPServerConfig and dropped disk-level headers, timeouts, and
OAuth — CLI connections from a --catalog/--config file silently ignored
them. The TUI already lifted these via mcpConfigToServerEntries().
Extract the TUI's catalog/config -> per-server-settings resolution into a
shared core module (core/mcp/node/servers.ts: loadServerEntries +
selectServerEntry) and route both CLI and TUI through it, so the two share
one file->settings path. --header still overrides the file's headers for
the run while preserving the file's timeouts/OAuth.
- core: add loadServerEntries/selectServerEntry/headersToServerSettings
- tui: tui-servers.ts re-exports the shared core helpers (no logic dup)
- cli: resolve via loadServerEntries + selectServerEntry; drop the
CLI-only conflict check and bare-config path
- tests: core servers.test.ts (settings lifting, header merge/override,
stdio overrides, seed/error/conflict, selection); CLI integration tests
asserting a config header is sent over HTTP and --header overrides it
- docs: CLI README note on file settings being applied
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(core): address #1500 review — dedup overrides, drop dead wrapper, clearer error, spec refresh
- Remove the now-unused resolveLaunchServerConfigs() wrapper (both CLI and
TUI route through loadServerEntries now); migrate its default-catalog
launch tests to compose withDefaultCatalogPath + resolveServerConfigs.
- Deduplicate applyStdioOverrides against config.ts applyOverrides: export
the latter (now trims cwd) and reuse it in servers.ts.
- selectServerEntry's unknown-name error is source-agnostic ("not found"
not "not found in config file") since entries may be a single ad-hoc
default, where "config file" would mislead.
- Refresh specification/ docs (v2_cli_tui_launcher.md,
v2_catalog_launch_config.md): CLI/TUI resolution now via
loadServerEntries/selectServerEntry; mark gap G1 resolved (#1482);
withDefaultConfigPath → withDefaultCatalogPath.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cli): extend smoke:cli to cover #1482 lift→serverSettings
Adds end-to-end smoke coverage for the config-file headers/timeouts/OAuth
lift introduced in #1482:
- unknown --server now errors source-agnostically ("not found")
- multi-server catalog selection via --server (and the omit-it error)
- --header merge does not break the stdio connect path
- HTTP transport: a config-file `headers` object is lifted onto the wire,
and a CLI --header overrides it (verified against an in-process test
server's recorded requests via a non-blocking spawn)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* 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>
* feat(web): catalog vs read-only session launch model + apply --header (#1481, #1483)
Per specification/v2_catalog_launch_config.md, `mcp-inspector --web` now selects
the backend's server-list source and a session-wide writability, instead of
seeding an inert `initialMcpConfig` the UI never read:
- `--catalog <path>` (and `MCP_CATALOG_PATH`) → active WRITABLE catalog
(seed/CRUD/watch), like the default catalog.
- `--config <path>` → READ-ONLY session file: served in the UI but never
written, seeded, or plaintext-secret-migrated — safe to point at a foreign
config (e.g. claude_desktop_config.json).
- ad-hoc `--server-url`/command (+ `--header`) → one server held IN MEMORY
(no file written); `--header` is lifted onto the entry so the seeded
connection actually uses it (closes the #1483 warn-only no-op).
- nothing → default catalog (writable), unchanged.
Backend (`createRemoteApp`): new `writable` + `initialServers` options. GET
/api/servers branches three ways (in-memory / read-only file with no
seed+no migrate / writable catalog); the four mutating routes return 403 when
read-only; `/api/config` carries `writable`; the file watcher is skipped for
in-memory sessions. `--catalog`/`--config` are mutually exclusive and neither
combines with an ad-hoc target or `--header`; `--header` requires an ad-hoc
HTTP/SSE server (clear errors otherwise — no silent no-op).
UI: `useServerListWritable` reads `writable` from /api/config; the Servers
screen hides add/edit/clone/remove/reorder/settings and shows a read-only
banner when false. All new props default to writable=true (back-compatible).
Docs: spec gaps G2/G3/G4 updated; launcher README documents the flags.
Verified end-to-end against the built launcher: --config (writable:false,
file byte-stable, mutations 403), ad-hoc --header (in-memory, header lifted),
and --catalog (writable:true, POST persists).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): address review — Mantine <Code> in read-only banner, clarify watcher comment
- Replace raw <code> with Mantine <Code> in the read-only-session Alert
(repo convention: prefer Mantine primitives over raw HTML).
- Add a comment near `watchable` clarifying that a read-only `--config` file
is still watched on purpose (read-only blocks UI writes, not external
observation; `writable` does not gate watching).
Deferred (per reviewer): consolidating useSandboxUrl + useServerListWritable
into one useConfig hook to avoid the duplicate /api/config fetch — optional
follow-up. The redundant ad-hoc header type-guard is kept as defensive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
* fix(core): make "error" the canonical terminal status on a mid-session crash (#1490)
On a real mid-session transport crash many SDK transports fire BOTH
`onclose` and `onerror` in a transport-dependent order. The `onclose`
guard (`if status !== "disconnected"`) let a trailing `onclose`
downgrade a crash's `"error"` status back to `"disconnected"`, so the
persistent terminal status differed by ordering:
- onerror-then-onclose → "disconnected"
- onclose-then-onerror → "error"
`onclose` now treats `"error"` as terminal (only `"connected"`/
`"connecting"` → `"disconnected"`), so `"error"` is the canonical resting
status in both orderings. It still emits the `disconnect` event exactly
once so session-teardown consumers fire identically either way.
Because the state managers gated list/message/subscription/task teardown
on `getStatus() === "disconnected"`, holding the status at `"error"`
would have left them stale in the onerror-first ordering — swapping one
ordering-dependence for another. Introduce `isTerminalStatus()`
(disconnected || error) and key every teardown guard off it so they
clear identically regardless of crash ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): single disconnect event when explicitly disconnecting from "error" (#1490 re-review)
Addresses the non-blocking observation from the PR review: explicitly
calling disconnect() from a held-"error" status could fire the
`disconnect` event twice. close() can trigger the transport's onclose
synchronously; onclose (holding status at "error") fired `disconnect`,
then disconnect()'s own guard saw status still != "disconnected" and
fired it again — a timing-dependence on whether the SDK closes sync.
Add a `disconnecting` ownership flag: onclose defers its status set +
`disconnect` event to disconnect() while it is set, so the
explicit-disconnect path emits exactly one `disconnect` regardless of
sync/async close. Crash-path onclose (flag unset) is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(core): deterministically pin the sync-close disconnect double-fire (#1490 re-review)
The real-stdio test asserts the single-disconnect invariant but may not
exercise the sync-close branch (the one that double-fired) if the SDK
stdio transport closes asynchronously. Add a test that overrides the SDK
client's close() to invoke the transport's onclose synchronously,
forcing the exact timing the `disconnecting` flag guards. Verified it
asserts 2 (fails) without the flag and 1 (passes) with it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#1323. Adds an `error`-event-driven `lastError` on useInspectorClient and a mid-session failure toast in App.tsx; the transport onerror is scoped to non-handshake (status !== connecting) so handshake failures stay on the connect() rejection path. Follow-up: #1490.
* fix(web): section-header hover outline + clear buttons out of tab order (#1487)
Two button-behavior adjustments:
1. History section headers (Pinned / History) now use the filter-toggle
style — a thin outline on hover instead of a background fill — by pointing
the `sectionHeader` Group variant at the shared `.filter-toggle` class. The
open section keeps its filled background (passed per-instance via `bg`).
2. Every input clear button added in #1448 is removed from the keyboard tab
order via `tabIndex={-1}` (CloseButton clear buttons app-wide, plus the
Autocomplete clear button through `theme/Autocomplete.ts` clearButtonProps),
so tabbing through a form moves to the next field, not onto the clear button.
The buttons stay mouse-clickable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): apply filter-toggle hover outline to Resources accordion headers (#1487)
The Resources screen `disclosure` accordion controls now share the same
outline-on-hover treatment as the filter toggle buttons and History section
headers: a thin border on hover instead of a background fill, plus a filled
background when the section is open (keyed off `aria-expanded`). The `.filter-
toggle` class is added to the disclosure control slot; the default-variant
accordion (Server Settings) keeps its background-fill hover via a `:not(.filter-
toggle)` guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): extract ClearButton element, replace 34 inline clear buttons
Addresses the code-review suggestion on PR #1488: the same five-line
`CloseButton` clear-affordance block (aria-label="Clear" + tabIndex={-1} +
onClick) was hand-copied across 17 files. Extract a single
`elements/ClearButton` (Mantine `CloseButton.withProps` with the aria-label and
tabIndex baked in) and replace all 34 call sites, so the clear-button contract
lives in one place. A global CloseButton theme default would be wrong here — it
would also catch tabbable modal-close X buttons and the Select clear buttons —
so a dedicated element is the right altitude.
Net -151 lines. Adds element unit tests + a Storybook play test covering the
aria-label / tabIndex / clickable contract for all sites at once.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* fix(core): trace dropped fetch body update when entry rotates out (#1390)
FetchRequestLogState.onFetchRequestBodyUpdate silently dropped a deferred
response-body update when the matching entry had already been evicted by
log rotation (>= maxFetchRequests newer requests appended between the
append and the fire-and-forget body read). The user saw an empty Response
Body with no way to distinguish "body never came back" from "body dropped
after eviction".
Plumb an optional pino logger into FetchRequestLogState (defaulting to the
silent logger) and emit a logger.debug at the early-return site so the drop
is traceable. The web client wires the existing remote logger from
createWebEnvironment into the state manager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): emit dropped-body trace at warn level so it is observable (#1390)
Review feedback: the web remote logger (createRemoteLogger) defaults to
level 'info' with no override at the environmentFactory call site, so a
logger.debug at the drop site is filtered out by both the pino level gate
and transmit.level — the diagnostic never reaches /api/log and the drop
stays invisible in the shipped web app, defeating the issue's goal.
Promote the trace to logger.warn (>= info, so it transmits by default). A
response body lost to log rotation is unexpected data loss, not routine
diagnostic noise, so warn is the appropriate severity. Update the comment
to explain the level choice and switch the test stub from debug to warn,
extracting a small typed makeWarnLogger helper per review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): configurable Network Log Size per server + body-dropped toast (#1390)
Turn the otherwise-invisible "response body dropped after rotation" case into
an actionable signal:
1. Per-server `maxFetchRequests` setting (default 1000, 0 = unlimited),
editable as "Network Log Size" in the first (Options) accordion section of
the Server Settings form. Plumbed through the full settings pipeline:
InspectorServerSettings + StoredMCPServer types, the serverList disk
converters (read defaults to 1000, write omits the default to keep diffs
minimal — 0 is a meaningful non-default), and the node server's GET-sanitize
+ write-side validateSettings (non-negative number). Applied live to the
active connection on settings-modal close via a new
FetchRequestLogState.setMaxFetchRequests (shrinking trims immediately), and
at connect time for the next session.
2. FetchRequestLogState now emits a `fetchRequestBodyDropped` event, but only
for genuine rotation drops (log at capacity with a finite cap) — not for
benign post-clear stragglers or unlimited logs, which previously also hit
the warn path. App shows a deduped, persistent toast (stable per-server id,
no auto-close) explaining what happened and the likely causes, with a link
that opens this server's settings on the Options section (modal remounts via
key) so the user can raise the limit if it's just a high-traffic server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): don't treat a cleared Network Log Size field as unlimited (#1390)
Review feedback: clearing the NumberInput emits "", and `parseInt("") || 0`
coerced that to 0 (unlimited). Since the value applies live on modal close, a
user who cleared the field to retype and then closed the modal would silently
switch the log to unlimited. Fall back to the current value on an empty/NaN
parse and reserve 0 for an explicit numeric entry. Adds a cleared-input test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>