A request timeout used to surface as a bare `Request timed out`. Every
fact needed to explain it was already in-process: what the client was
still waiting on, when it last heard back, and the state of the
standalone GET notification stream. The SDK's timeout now rejects with
all three, and the same snapshot is shown live in Connection Info and
on the Network entry for the stream.
- core/mcp/connectionDiagnostics.ts: the snapshot type, its one-line
description, and `annotateRequestTimeout`, which rebuilds the SDK's
per-request timeout with the state at that moment.
- InspectorClient decorates the SDK Client's `request` (the one funnel
every verb goes through) so the annotation covers the whole surface;
tracks outstanding requests with their send time, the last response,
and the most recent long-lived transport stream; exposes
`getConnectionDiagnostics()` and `connectionDiagnosticsChange`.
- fetchTracking watches a long-lived stream through a tee'd clone,
counting SSE events and reporting the close, via a new
`updateStream` callback wired through the node transport, the remote
session (`fetch_stream_update`), and the browser transport.
- FetchRequestLogState patches `entry.stream`; the Network entry's
placeholder reports the event count and open/closed state.
- Connection Info gains a Connection Activity section, ticking while
open (`useTickingClock`).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APKSYHGxXBUpnR7ErFiLJn
Signed-off-by: cliffhall <cliff@futurescale.com>
* test(cli,tui): typecheck the __tests__ dirs (#1791)
The cli/tui `typecheck` scripts only checked `src` (the src-only tsconfig
excludes `**/*.test.*`), leaving the `__tests__` dirs — which do the most
`as` casting — with no tsc pass. Add a `tsconfig.test.json` per client that
includes `__tests__` with the test-exclusions dropped and the test-only path
aliases (`@modelcontextprotocol/inspector-test-server`, the `@inspector/core/*`
deep paths, express/vitest) resolving what vitest resolves, then fold
`tsc -p tsconfig.test.json` into each `typecheck`.
Fixes the 76 errors it surfaced:
- cli: a wrong `@modelcontextprotocol/inspector-core/...` module specifier
(should be `@inspector/core/...`; only a type import so vitest never caught
it), `r.headers` optional-access guards, and partial OAuth mocks replaced
with typed `makeFakeCliOAuthClient()` / `makeFakeServerSettings()` factories
instead of `as` casts. Adds `@types/express` (devDep) so the transitively
aliased test-server source typechecks, mirroring clients/web.
- tui: rest-param signatures on the App OAuth spies (fixes the spread-arg and
narrow-return errors), a widened callback param (`iss?`), a `makeFakeServer-
Settings()` factory for the tuiOAuth tests, required OAuth-metadata/client
fields in AuthTab, and a loosened `entry()` message param for the
deliberately-malformed wire messages HistoryTab's defensive branches render.
Gate coverage only; no runtime behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test(cli,tui): address review — real spy signatures, config typecheck, nits
Responds to the @claude review of #1799:
- App.test.tsx: type the OAuth spies against the real InspectorClient method
signatures (`vi.fn<InspectorClient["…"]>()`) and forward `Parameters<…>`
tuples from the FakeClient wrappers, so `toHaveBeenCalledWith(...)` on the
six OAuth-lifecycle spies stays argument-checked under the new gate rather
than accepting anything via `unknown[]`. `getOAuthState` regains its wide
`OAuthConnectionState | undefined` return.
- Typecheck the client config files too: add `vitest.config.ts` / `tsup.config.ts`
(and tui `dev.ts`) to each src `tsconfig.json` include — they were the one
first-party surface still getting no tsc pass.
- Trim the redundant `src/**/*` from each `tsconfig.test.json` include (tsc
pulls in the src the tests import; the src-only config already checks it).
- Rename the tui settings helper to `oauth-test-fakes.ts` to match cli's, and
soften both factories' docstrings to stop naming a loader they don't mirror
field-for-field.
- Fix a stale `@modelcontextprotocol/inspector-core/...` specifier in a
createRemoteTransport JSDoc example (the same wrong package name the gate
caught in the cli tests, surviving in a comment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: address round-2 review — correct claims, tighten types, typecheck launcher
Responds to the round-2 @claude review of #1799:
- Correct the toHaveBeenCalledWith claim: vitest doesn't type-check
`toHaveBeenCalledWith(...)` arguments against the mock's signature, so the
App.test.tsx comment no longer claims it does — the typed spies buy checked
implementations/return payloads (the stale-literal fix), not arg-checking.
- authenticate spy now uses the real `InspectorClient["authenticate"]`
signature (returns a `URL`), so it no longer diverges from production; the
one mockResolvedValue is updated to a URL.
- makeFakeCliOAuthClient members are typed against the real CliOAuthClient
signatures (not bare vi.fn()), so a wrong-signature override is rejected.
- HistoryTab: revert the `entry()` `message: unknown` widening (it un-checked
the ~15 well-formed fixtures) and instead scope a named `MALFORMED` cast to
the two intentionally-malformed messages.
- Typecheck the launcher `__tests__` too (the same gate hole in the one client
the PR hadn't touched): new clients/launcher/tsconfig.test.json + a typecheck
script folded into validate. Completes the "every client's __tests__ is
typechecked" invariant.
- Doc the new test-typecheck + config-file typecheck in AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: address round-3 nits — launcher config typecheck, callTool typed, docs
Responds to the round-3 @claude review of #1799 (all non-blocking):
- launcher: add `vitest.config.ts` to tsconfig.test.json's include (it was in
neither project — the build config's rootDir: ./src rejects it). Now
typechecked.
- App.test.tsx: type `callTool` against `InspectorClient["callTool"]` (+ the
Parameters<…> wrapper) so its `mockResolvedValue` payloads are checked, and
give `clearOAuthTokens` an `async () => {}` impl so the mock actually returns
the `Promise<void>` its type promises.
- AGENTS.md: correct the per-client alias-set description (cli's is widest,
tui's is core+react only, launcher's has no paths) and note launcher's config
file lives in its test project, not the src project.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: address round-4 — durable typecheck-coverage guard + launcher/doc nits
Responds to the round-4 @claude review of #1799 (all non-blocking):
- Add `verify:typecheck-coverage` (scripts/verify-typecheck-coverage.mjs), the
typecheck-coverage analog of verify:format-coverage and the durable guard for
the invariant this PR establishes: for each Node client it runs the tsconfig
projects its `typecheck` names with `tsc --listFiles`, unions them, and fails
on any tracked .ts/.tsx that lands in no project. This closes the class the
review kept surfacing by hand (launcher/__tests__ in round 3,
launcher/vitest.config.ts in round 4) — a new top-level file can no longer go
untypechecked silently. Wired into `npm run ci` and the GitHub workflow.
- launcher: reorder `validate` to run `typecheck` before `build` (matches tui
and the documented order), and make `typecheck` self-contained — it now runs
both `tsconfig.json` and `tsconfig.test.json`, so running it standalone means
the same thing it does in cli/tui.
- makeFakeCliOAuthClient: note on the comment that the typing covers the
defaults, not `overrides` (a bare vi.fn() override is Mock<(...args)=>any>).
- Docs: README table + AGENTS.md updated for the new guard and the corrected
launcher typecheck/validate ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: address round-5 — wire-check the guard, --listFilesOnly, into validate
Responds to the round-5 @claude review of #1799:
- Finding 1 (the pre-merge one): the guard now ports verify:format-coverage's
reachability checks — it asserts each client's `typecheck` is reachable from
its own `validate` and that the root chain runs each client's `validate`, so
it can't stay green while measuring a typecheck nothing invokes. Verified:
stripping `npm run typecheck` from launcher's validate now fails the guard.
- Nit 3: switch `tsc --listFiles` → `tsc --listFilesOnly` (identical file list,
~4× faster: whole guard 23s → ~4s). Now cheap enough for the inner loop, so
it moves into `validate` (right after verify:format-coverage) instead of a
standalone ci step — matching its sibling. Removed the separate ci-chain entry
and GitHub workflow step (validate covers it).
- Nit 4: `npx --no-install tsc` so a missing local tsc fails fast instead of
reaching for the registry (matches verify-build-gate.mjs).
- Nit 5: corrected projectFiles' JSDoc to describe the filter that's actually
there (drops repo-external + node_modules paths; core/ + test-servers/ stay
but are harmless).
- Finding 2: fixed the 2-space misindent on the new root package.json line
(prettier --write). Docs (README/AGENTS) updated for the validate placement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): address round-6 guard nits — dedupe wiring, robust diagnostics
Responds to the round-6 @claude review of #1799 (all guard-ergonomics nits):
- Nit 2: extract the shared reachability logic (`reachableScripts`,
`rootRunsClientValidate`) into scripts/lib/npm-scripts.mjs and consume it from
BOTH verify-format-coverage.mjs and verify-typecheck-coverage.mjs, so the
"is this gate actually run?" wiring can't drift between them (same rationale
as scripts/lib/prod-web-server.mjs).
- Nit 1: typecheckProjects now harvests `-p`/`--project` from every script
reachable from `typecheck` (not the single string), so a delegating
`typecheck` (`npm run typecheck:src && …`) is handled instead of failing with
a false "names no -p project" diagnosis. Also accepts `--project`.
- Nit 3: on a tsc config error, echo the `error TS…` diagnostic (scanning both
streams — tsc prints config errors to stdout) so a broken tsconfig is
self-explaining rather than surfacing only as a misleading "file in no
project" report.
- Nit 4: refreshed the CI workflow's stale "Validate" step name/comment (it now
runs the coverage guards + validate:core + per-client typecheck).
- Nit 5: reworded the `.d.ts` carve-out comment to read as pre-emptive (there
are none under cli/tui/launcher today).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-7 — guard .mts/.cts too; drop dead export
Responds to the round-7 @claude review of #1799:
- Finding 1: verify-typecheck-coverage now requires a tsc pass for tracked
`.mts`/`.cts` (not just `.ts`/`.tsx`), matching verify-format-coverage's
extension set — `.mts` is already the shared-config idiom here, so a client
`vitest.config.ts` → `.mts` rename can no longer drop out of both the tsconfig
include and the guard's required set. No-op on today's tree (no client
`.mts`/`.cts`); confirmed a tracked `.mts` with a type error is now caught.
Ambient `.d.{ts,mts,cts}` stay excluded.
- Nit 2: drop the unused `export` on `rootReachedCommands` (internal to the lib;
nothing imports it, and scripts/ isn't eslint-gated to catch dead exports).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: round-8 — single casts in HistoryTab, uniform wrappers, doc scope
Responds to the round-8 @claude review of #1799:
- Finding 1: reduce the `MALFORMED` helper and `entry()`'s return cast from
`as unknown as` to a single `as` (both compile clean — the malformed shapes
are comparable to the JSONRPCMessage union), and delete the false "only
option" sentence. Fixes the AGENTS.md avoid-double-casts violation.
- Nit 3: write all seven FakeClient OAuth wrappers as
`(...a: Parameters<InspectorClient["…"]>)` (the three zero-arg ones were
hand-written `()` forwarders), so the block comment's "forward the same
Parameters<…> tuple" is literally true and a future added parameter can't be
silently dropped.
- Nit 2: update the five places that still described the guard as `.ts`/`.tsx`
only to `.ts`/`.tsx`/`.mts`/`.cts` (script header + trackedSourceFiles JSDoc,
AGENTS.md ×2, README.md).
Nits 4 (js config files out of the typecheck guard by construction) and the
footnoted callTool/ancestor-tsc items left as agreed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: round-9 — drop entry()'s harmful cast; guards vouch for each other
Responds to the round-9 @claude review of #1799:
- Finding 1: remove `entry()`'s `as MessageEntry` return cast in
HistoryTab.test.tsx. It was unnecessary (the spread of Partial<MessageEntry>
over complete defaults is directly assignable) and harmful — it silenced type
errors in the helper's OWN default fields (a `direction`/`method` typo now
surfaces TS2820/TS2353). MALFORMED's single cast stays (load-bearing). This
leaves MALFORMED as the file's only cast, matching what the comments describe.
- Nit 2: add `rootReachesScript` to scripts/lib/npm-scripts.mjs and have each
coverage guard assert its SIBLING is still wired into the root `validate`. A
guard can't detect being unrun itself, but the two now vouch for each other,
so dropping either from `validate` is caught by the other (only deleting both
slips through). Verified: removing verify:typecheck-coverage from validate
now fails verify:format-coverage, and vice-versa.
Nits 3 (git-index visibility) and 4 (pre-existing as-never/as-unknown casts in
tests — out of scope, none added by this PR) left as agreed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: round-10 — reject non-checking typecheck passes; pre/post hooks; docs
Responds to the round-10 @claude review of #1799:
- Finding 1: verify-typecheck-coverage now rejects a `-p <project>` whose own
command carries `--noCheck` or `--listFilesOnly` — such a pass lists files
without type-checking them, which previously satisfied both the wiring and
file-coverage halves while checking nothing. (Splits each script on
`&&`/`||`/`;` so a flag on one command can't taint another.) Verified a
`typecheck` using `tsc … --noCheck` now fails with a clear message; `--noCheck`
is a live idiom here (cli's test-servers:build), so this is a real copy-paste
hazard.
- Nit 2: reachableScripts (shared lib) now also follows npm's implicit
`pre<name>`/`post<name>` lifecycle hooks, so moving a gate into e.g.
`prevalidate` is recognized instead of producing a false "not reachable"
failure (cli already uses a `pretest` hook). Verified.
- Nit 3: AGENTS.md "Test placement" now states the Node clients (cli/tui/
launcher) keep all tests in `__tests__/`, enforced by verify:typecheck-coverage
(a co-located src test lands in no tsconfig project).
- Nit 4: noted on the factory comments that `Partial<…>` overrides admit an
explicit `undefined` for a required field (exactOptionalPropertyTypes off).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* fix: round-11 — launcher build excludes tests; guard catches all noCheck vectors
Responds to the round-11 @claude review of #1799:
- Finding 1 (shipping bug): clients/launcher/tsconfig.json now excludes
`**/*.test.{ts,tsx}` (matching cli/tui). launcher's `build` is an EMITTING
tsc over src/**, and clients/launcher/build is in the root `files` allowlist,
so a co-located `src/**/*.test.ts` would have been compiled into the published
tarball. Excluding it also makes such a test land in no tsconfig project, so
verify:typecheck-coverage catches it — making the AGENTS.md placement rule
literally true for all three clients.
- Finding 2: verify-typecheck-coverage now catches both remaining ways a
typecheck could check nothing: (a) case-insensitive `--noCheck`/`--nocheck`/
`--listFilesOnly` on the command, and (b) `noCheck` set in the tsconfig itself
(detected via `tsc --showConfig`, which surfaces it from the config or its
extends chain). Verified both fail the guard.
- Nits 3 & 4: restructured into an explicit gate-integrity phase (wiring +
neutered + config-noCheck + no-projects) that reports under its own accurate
header and exits BEFORE the file-coverage pass — so an inert gate no longer
prints file-remediation advice, nor buries the real cause under an
"in no tsconfig project" line for every file that gate covered.
The one CIMD OAuth E2E integration test that flaked in the coverage run passes
32/32 in isolation and on re-run; unrelated to these script/config changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* fix: round-12 — exclude .mts/.cts tests from build; drop false integrity line
Responds to the round-12 @claude review of #1799:
- Finding 1 (tarball, again): the round-11 test exclude only covered
`.ts`/`.tsx`, so a co-located `src/*.test.mts` still compiled into launcher's
shipped `build/`. All three clients now exclude `**/*.test.*` (one glob,
covering .ts/.tsx/.mts/.cts) — which is also exactly what AGENTS.md already
claims. Verified a `src/probe.test.mts` is no longer emitted and now fails the
guard.
- Finding 2: the gate-integrity phase no longer prints a false "names no
`-p <project>`" line when config-`noCheck` disables projects — the condition
now keys off `projects.length` (harvested), not `checking.length` (post-
filter), so a client whose projects were all config-disabled shows only the
accurate per-project lines.
- Nit 3: recorded the boundary on the phase comment — it does not detect
shell-level failure suppression (`… || true`, `; exit 0`).
- Nit 5: the file-coverage remediation now leads with "for a co-located test,
move it to `__tests__/`" (adding it to the src `include` would reintroduce the
build emit) before the general add-to-include advice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-13 — implicit-tsconfig + derived clients; diagnostics
Responds to the round-13 @claude review of #1799:
- Finding 1: verify-typecheck-coverage now understands a `tsc` command with no
`-p`/`-b` project flag — it resolves the implicit `./tsconfig.json` (tsc's own
default), so the idiomatic `tsc --noEmit` form counts as coverage instead of
producing false "in no tsconfig project" failures. Also harvests `-b`/`--build`
project paths. Verified `tsc --noEmit && tsc --noEmit -p tsconfig.test.json`
now reports OK.
- Finding 2: CLIENTS is no longer hardcoded — it's derived from the `cd
clients/<x> && npm run validate` entries in the root `validate` chain (minus an
explicit web exclusion), so a new Node client is auto-required, matching the
repo-wide reach of its sibling verify-format-coverage. Added a
guard-would-check-nothing bail if the derivation yields an empty set. Verified
a new `clients/newclient` is picked up and its uncovered file flagged.
- Nit 3: the phase-2 remediation now leads with the general add-to-`include`
advice and only appends the "move it to __tests__/" note when a failing path
is actually a test file (was misdirecting plain files).
- Nit 4: AGENTS.md no longer says the test config includes "only" __tests__
(launcher's also includes its vitest.config.ts).
(Two different web integration tests each flaked once under parallel load during
the coverage runs — both pass in isolation and on a clean re-run; timing-
sensitive and unrelated to these script/doc-only changes.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-14 — enumerate clients from disk (fail-closed)
Responds to the round-14 @claude review of #1799:
- Finding 1: replace the root-chain `CLIENTS` derivation (round 13) with a
disk enumeration — every `clients/<name>` whose package.json has a `typecheck`
script. The chain derivation was fail-open: a root-chain edit that kept
running a client but didn't match the strict regex silently dropped it from
the required set (112→53 files, still green). Disk is fail-closed — a client
can't be silently dropped, a new client with a typecheck is auto-required, and
web (no typecheck script; builds via `tsc -b`) is excluded on its own, so the
explicit exclusion is gone. The existing `rootRunsClientValidate` wiring check
still verifies the root chain runs each — now LOUD when it doesn't. Verified:
`cd ./clients/tui && …` now hard-fails the integrity phase instead of silently
shrinking coverage; `cd clients/tui && npm run build && npm run validate` stays
gated. Removed the now-unused `clientsRunByRootValidate` from the shared lib.
- Nits 2 & 3: documented the two unreachable-today limitations on
typecheckProjects — a solution-style `tsc -b` (empty `files` + `references`)
would under-report, and the implicit-tsconfig fallback assumes no file operands.
- Nit 4: refreshed the file header to mention `-b`/`--build` + the implicit
`./tsconfig.json` (only the JSDoc had been updated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-15 — enroll-or-exempt enumeration; normalize `cd ./`
Responds to the round-15 @claude review of #1799:
- Finding 1: the disk enumeration keyed on the presence of a script named
`typecheck`, so renaming it silently dropped the client (112→53, still green)
— contradicting the docstring. Now every `clients/*` with a readable
package.json must EITHER declare a `typecheck` (→ enrolled) OR be in an
explicit EXEMPT map (`clients/web`, reason: `tsc -b`); anything else is a hard
gate-integrity failure. This is fail-closed on the rename axis too, and web's
skip is a stated decision rather than an accident of which scripts it declares
— so web gaining a `typecheck` no longer auto-enrolls it into the solution-`-b`
false failures. Verified: renaming tui's `typecheck` now hard-fails; a `tsc -b`
on web stays exempt.
- Nit 3: `rootRunsClientValidate` normalizes a leading `./`, so
`cd ./clients/tui && npm run validate` (which genuinely runs it) is no longer a
false "no longer runs" alarm.
- Nit 2: README/AGENTS/script-header prose no longer names a fixed
cli/tui/launcher scope — they describe the disk-discovered "every clients/*
with a typecheck script (web exempt)" rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-16 — anchor wiring match; close enumeration axes
Responds to the round-16 @claude review of #1799:
- Finding 1 (only false-PASS): rootRunsClientValidate matched the client dir as
a bare substring, so a prefix-sibling (`clients/tui` vs `clients/tui-next`)
satisfied the wiring check for the shorter name — dropping a client from the
root chain went green if a sibling was present. Now an anchored regex (escaped
dir + optional leading `./` + a trailing boundary). Verified the sibling case
now hard-fails.
- Nit 2: a `clients/*` dir holding tracked TS but no readable package.json is
now a gate-integrity failure (was treated as "not a client"), so the
"fail-closed on every axis" claim holds. Verified.
- Nit 3: readdirSync is wrapped, so a missing `clients/` dir yields the intended
"found no clients/* dir" message rather than a raw ENOENT stack.
- Nit 4: EXEMPT's reason is now surfaced in the success line ("clients/web
exempt: typechecks via tsc -b …"), so the exemption is visible at runtime, not
only in source.
(Also moved `isRequiredSource` above nodeClients — nit 2's new tracked-TS check
calls trackedSourceFiles at module-init, which referenced it before init.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-17 — share quote-aware tokenizer between guards
Responds to the round-17 @claude review of #1799:
- Finding 1: the two coverage guards parsed shell strings differently —
verify-format-coverage had a quote-aware tokenize(); verify-typecheck-coverage
used a raw regex. So a quoted `-p "tsconfig.test.json"` (a form tsc accepts)
made the typecheck guard report every file that project covers as uncovered,
and a quoted `cd "clients/tui"` in the root chain tripped a false wiring
failure. Moved tokenize() (now single+double quote) into the shared lib,
imported it into both guards, rewrote typecheckProjects to walk tokens
(`-p`/`--project`/`-b`/`--build` → next token), and strip quotes before the
`cd` match in rootRunsClientValidate. Verified both quoted forms now pass.
- Nit 2: a stale EXEMPT key (a dir that no longer exists) was asserted in the
success line unchecked. nodeClients now fails if an EXEMPT key isn't a real
`clients/*` directory. Verified.
- Nit 3 (build/dist filter belt-and-braces) left as a recorded boundary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-18 — match tsc by basename; strip quotes on both halves
Responds to the round-18 @claude review of #1799:
- Finding 1 (regression from round 17): `tokens.includes("tsc")` didn't match a
path-invoked binary (`node_modules/.bin/tsc`), which the previous `\btsc\b`
regex did — so such a `typecheck` was falsely reported as naming no project.
Now matches on the token basename (`isTsc`), keeping round-17's quoting fix
while restoring the tolerance. Verified a `node_modules/.bin/tsc` typecheck
reports OK.
- Nit 2: `rootRunsClientValidate` stripped quotes for the `cd` match but tested
`npm run validate` against the unstripped string, so `npm run "validate"`
false-failed. Both halves now test the stripped string.
- Nits 3 & 4: documented on typecheckProjects — the contrived `--noCheck false`
(checking on) is still treated as disabling, and the `&&`/`||`/`;` split runs
before tokenizing (a quoted operator inside an arg would split mid-token,
unreachable for project paths).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test: round-19 — typecheck test-servers' unimported bin; --prefix wiring form
Responds to the round-19 @claude review of #1799:
- Finding 1: `test-servers/src/server-composable.ts` (a `#!/usr/bin/env node`
bin entry nothing imports) got no tsc pass — the barrel alias covers the other
13 test-servers/src files transitively, but not this one, and its own build
runs `tsc --noCheck`, so its errors were emitted into `test-servers/build`
unchecked. Added it explicitly to `clients/cli/tsconfig.test.json`'s include
(zero source changes; verified a bad line in it now fails cli's typecheck).
Softened the AGENTS.md clause that implied the whole test-server source is
transitively covered.
- Nit 2: `rootRunsClientValidate` now also recognizes the
`npm --prefix <dir> run validate` invocation form (was `cd`-only).
- Nit 3 (stderr spy drops the completion-callback overload): left as-is —
latent, not live (no exercised path awaits a stderr drain), and adding typed
overload params would reintroduce the write-overload complexity the round-1
vi.spyOn refactor removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-20 — anchor validate tail; guard shared-source class
Responds to the round-20 @claude review of #1799:
- Finding 1 (false PASS): the wiring matcher's tail `/\brun validate\b/` also
matched `run validate:fast` (`:` is a word boundary) — a DIFFERENT script that
may skip typecheck. So the root chain could run a client's `validate:fast`
while the guard kept measuring its `validate` (unrun), green. Anchored the
tail to a boundary (`/\brun validate(?=$|[\s&;])/`). Verified `validate:fast`
now hard-fails.
- Finding 2: durably close the test-servers/src class instead of point-fixing.
Added a SHARED_ROOTS check — every tracked TS under `test-servers/src` + the
root `vitest.shared.mts` (the `format:check:shared`/`lint:shared` surface) must
land in the GLOBAL union of client projects. So a NEW unimported bin entry is
caught (not just server-composable.ts, named in round 19). Verified a fresh
`server-orphan-bin.ts` now fails. Zero source changes — the union already
covers 15/15.
- Nit 4: the integrity message and rootRunsClientValidate docstring now mention
the `npm --prefix` form too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-21 — deny-by-default non-client coverage
Responds to the round-21 @claude review of #1799 (findings 1 & 2, one fix):
- Replaced the `SHARED_ROOTS` allowlist with a deny-by-default non-client set:
all tracked TS minus `clients/*` (per-client pass) minus a reasoned `core/`
exemption (covered by web's `tsc -b`). This closes BOTH round-21 findings at
once:
- Finding 1 (stale allowlist fail-open): a rename of `test-servers/src` no
longer silently drops it — its files reappear in the required "other" set
and hard-fail (verified: `git mv test-servers/src test-servers/source` now
fails instead of going 127→113 green). The subtractive `core/` exemption is
fail-closed on staleness for the same reason.
- Finding 2 (allowlist vs deny-by-default): a new top-level TS location is now
auto-required (verified `tools/probe.ts` is flagged), matching the repo-wide
reach of the sibling verify-format-coverage.
- Nit 3: added the shared-source clause to the README table row (AGENTS got it
last round).
Header comment updated to describe the non-client coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-22 — narrow core/ exemption to what web's tsc -b covers
Responds to the round-22 @claude review of #1799:
- Finding 1: the `core/` exemption was whole-tree, but web's `tsc -b` only
covers `core/**/*.ts` MINUS co-located `*.test.ts(x)` (its app project's
include). So a `core/*.tsx`/`*.mts`/`*.cts` or a co-located `core` test got no
tsc pass from anything. Narrowed the exemption to exactly that shape
(`isExemptCoreFile`): a core `.ts` that isn't a co-located test. So a core
`.tsx`/`.mts`/co-located test is now required in some project. Zero-fix today
(0 such files); verified `core/mcp/probe-widget.tsx` and a co-located core
`.test.ts` are now flagged.
- Nit 2: `EXEMPT_ROOTS` is now a Map (dir → reason) and its reason is surfaced
in the success line alongside the client exemption.
- Nit 3: the non-client miss message is now the general "in no client's tsconfig
project"; the `test-servers` remediation clause is conditional on an actual
`test-servers/` path (tracked via `nonClientMisses`, not string-sniffing the
composed message).
- Nit 4: success line says "(N clients + non-client)".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-23 — core exemption mirrors both web projects
Responds to the round-23 @claude review of #1799:
- Finding 1: round 22's `isExemptCoreFile` mirrored only web's app project, but
web's `tsc -b` also builds `tsconfig.test.json`, which includes
`core/**/__tests__/**/*.{ts,tsx}`. So a `core/**/__tests__/*.test.ts` (a live
layout — `core/mcp/__tests__/fakeInspectorClient.ts` already sits there) was
flagged though web checks it. Now mirrors the UNION: under `__tests__/` →
exempt `.ts`/`.tsx`; otherwise `.ts` minus co-located `*.test.ts(x)`. Verified
a core `__tests__` test now passes while a core `.tsx` outside `__tests__`
stays required.
- Finding 2: `EXEMPT_ROOTS` was a Map whose keys nothing read (round 22 replaced
its filter with hardcoded `isExemptCoreFile`) — a phantom registry. Collapsed
to a single `CORE_EXEMPT_REASON` const used in the OK line.
- Nit 3: README/AGENTS scope descriptions updated (core is no longer wholesale
excluded; the non-client set is deny-by-default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-24 — enroll clients/web via tsconfig references
Responds to the round-24 @claude review of #1799:
- Finding 1: `clients/web` was whole-tree EXEMPT, but web's `tsc -b` only reaches
what its four referenced projects include — a file at web's package root
(e.g. `playwright.config.ts`) got no tsc pass and passed EVERY gate, fully
silently. This was the last whole-tree exemption wider than the gate it
defers to. Now web is ENROLLED: a client with no `typecheck` script but a
`tsconfig.json` `references` array is measured through those reference
projects (`tsc -p <ref> --listFilesOnly`), and is wired iff its `validate`
runs `tsc -b` (`validateRunsTscBuild`). EXEMPT is now the empty escape hatch.
Verified: a web-root `playwright.config.ts` with a type error is now flagged,
and dropping `tsc -b` from web's build hard-fails the integrity phase.
Zero-fix on the tree — the union of web's 4 projects covers all 625 tracked
web files.
- Nit 2: assert `core/` exists (the OK line unconditionally states its
exemption) — parity with the EXEMPT stale-key check.
- Nit 3: a `core/` miss now gets core-specific remediation (widen web's
`tsconfig.app.json` include, not a cli/tui project).
- Nit 4: README/AGENTS scope descriptions corrected — web is enrolled (not
exempt), and the exact core-exempt shape (app `*.ts` minus co-located tests +
test `__tests__/**/*.{ts,tsx}`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-25 — drop redundant core exemption; guard tsc -b --noCheck
Responds to the round-25 @claude review of #1799:
- Finding 1: enrolling web (round 24) made the hand-maintained `core/` exemption
both redundant and fail-open — web's measured `tsc -b` projects already cover
core, and the mirror could go stale (narrowing web's include let an unimported
core error pass). Deleted `isExemptCoreFile`, `CORE_EXEMPT_REASON`, the core
presence assertion, and the exemptNote core clause. `core/` is now required
like everything else and covered by measurement (127→894 files; a `core/*.tsx`
web doesn't reach is now caught by measurement, not a hand-model). Kept the
core-specific remediation clause.
- Finding 2: the reference (`tsc -b`) path skipped the non-inertness check — a
`tsc -b --noCheck` build gated nothing while the guard stayed green. Turned
`validateRunsTscBuild` into `tscBuildStatus` (ok/neutered/none); a neutered
`tsc -b` now hard-fails, matching the typecheck-script path. Factored the
disabling-flag predicate (`isDisablingFlag`) so both paths share it.
- Nit 3: `clientTsconfigReferences` now strips block comments too (every other
tsconfig in the repo uses `/* */`).
- Nit 5: README/AGENTS wiring clause covers web's `tsc -b` (not just the
`typecheck` script), and core is described as measured, not exempt.
Nit 4 resolves itself: EXEMPT is empty and the exemptNote/ternary now collapse
cleanly. Nit 6 (tsc -b bound to a specific tsconfig) left as recorded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-26 — expand solution configs wherever harvested
Responds to the round-26 @claude review of #1799:
- Finding 1: the reference (`tsc -b`) path was selected only by "no typecheck
script", so adding `typecheck: "tsc -b"` to web (the natural uniformity move)
flipped it to the typecheck-script path, where the harvested solution config
lists nothing → 665 false failures. Fixed at the measurement layer:
projectFiles now expands a solution config (`{"files":[], "references":[…]}`
— lists nothing under --listFilesOnly) to its references and measures those
recursively, so a `tsc -b` project is measured no matter which path harvests
it. Generalized the references reader (tsconfigReferences(path)). Verified web
with `typecheck: "tsc -b"` now passes. Dropped the stale "web … is out of
scope" JSDoc clause.
- Nit 2: a `core/**/__tests__/**` miss now points at `tsconfig.test.json` (its
actual owner), not `tsconfig.app.json`.
- Nit 4: AGENTS.md's one-line summary now mentions the non-client (`core/`,
`test-servers/src`) coverage, matching the detailed bullet.
Nit 3 (EXEMPT empty, dormant stale-key check) left as the intended escape hatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-27 — non-inertness follows references on both paths
Responds to the round-27 @claude review of #1799:
- Finding 1: round 26 made COVERAGE follow a solution config's references, but
the non-inertness (`noCheck`) check on the typecheck-script path still only
inspected the harvested solution (empty compilerOptions) — so a `noCheck` in
a REFERENCED project was invisible there (caught only on the reference path).
Factored a shared `resolveLeafProjects` (a solution → its real leaf projects,
cached rawProjectFiles) used by BOTH coverage (`projectFiles`) and a shared
`checkingLeaves` disable-check, so a `noCheck` in a referenced project is now
caught on either path. Verified: `typecheck: "tsc -b"` + `noCheck` in
tsconfig.app.json now hard-fails (was green). Caching also cut the guard from
~20s to ~11s.
- Nit 2: directory-form references (`{ "path": "./packages/a" }`) now resolve to
`<dir>/tsconfig.json`, so a nested-solution dir reference is expanded.
- Nit 3: reworded the empty-listFiles comment (it's "solution config OR errored
config", both of which the reference expansion handles).
- Nit 4: script header notes a `tsc -b` solution is reduced to its references on
either enrollment path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-28 — dedup shared leaf in checkingLeaves
Responds to the round-28 @claude review of #1799 (which found no new gate hole):
- Finding 1: `checkingLeaves` dedup'd the `checking` array but ran
projectDisablesChecking + integrity.push on every visit, so a leaf reachable
from two harvested projects produced a duplicate `noCheck` line (and a
redundant `tsc --showConfig`). Now tracks visited leaves in a Set and skips an
already-seen leaf entirely. Verified a duplicate harvested project yields one
line, not two.
- Nit 3 (record-completeness): noted in AGENTS that the one tier the guard
structurally can't see — a per-file `// @ts-nocheck` — is owned by
`@typescript-eslint/ban-ts-comment` across lint:core/lint:shared/each client.
Nit 2 (the ~11s perf claim isn't reproducible on a CI-class runner) and nit 4
(hybrid-solution / composite boundaries, both tsc-rejected → unreachable) left
as recorded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test(scripts): unit-test the typecheck-coverage guard's parsers
Responds to the round-29 @claude review of #1799 (no new gate hole; the finding
is that the guard itself has no tests):
- Made the guard's pure helpers importable: exported `typecheckProjects`,
`tscBuildStatus`, `tsconfigReferences`, `clientTsconfigReferences`,
`resolveLeafProjects`, `isTsc`, `isDisablingFlag`, `isRequiredSource`, and
moved the module-init + phase execution behind an exported `main()` run only
when the file is executed directly (importing it for tests no longer runs the
guard). Behavior when run directly is unchanged (894 files).
- Added table-driven tests (node's built-in `node --test`; the root has no
vitest harness by design) for the shared lib (`tokenize`, `reachableScripts`
incl. pre/post hooks, `rootRunsClientValidate` across cd/--prefix/quoted/
prefix-sibling/`validate:fast`, `rootReachesScript`) and the guard's parsers
(`isRequiredSource`, `isTsc`, `isDisablingFlag`, `typecheckProjects`,
`tscBuildStatus`) — one case per rule a review round found (r7/r10/r13/r15/
r16/r17/r18/r19/r20/r25). 12 tests. Wired `test:scripts` into `validate`.
- Nit 2: a `scripts/*.ts` miss now gets a scripts-specific remediation (keep it
`.mjs`, or give `scripts/` its own tsconfig project).
- Documented the guard's test suite in AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* test(scripts): make test:scripts self-guarding; fill parser test gaps
Responds to the round-30 @claude review of #1799 (mutation testing killed 5/5;
findings are about the new test gate + suite completeness):
- Finding 1: `test:scripts` lacked the two properties the guard enforces for
every tsc pass. verify-typecheck-coverage now asserts, in phase 1, that
`test:scripts` is reachable from the root `validate` (via the already-imported
rootReachesScript) AND that its tracked `scripts/**/*.{test,spec}.*` file set
is non-empty and every file is matched by the command's glob (a new exported
`globToRegExp`). So dropping it from validate, or renaming a test to
`*.spec.mjs`/`*.test.mts` (which node --test silently skips), now hard-fails.
Verified both.
- Nit 2: extracted a pure `parseTsconfigReferences(raw)` from tsconfigReferences
and table-tested the JSONC tolerance (block/line comments, trailing comma, no
references, malformed, no path) — the r17/r26 rules had no cover.
- Nit 3: added `-b`/`--build`/bare-`tsc -b` and explicit `--project` cases to
typecheckProjects's test so its title matches its table.
- Nit 4: rewrote the rootRunsClientValidate test to put `cmd` in a script
reached from validate, and added the reachability cases (an orphan cd-validate
must NOT count; validate:tui indirection must) — the r5 property.
- Nit 5: added test:scripts to AGENTS's validate ordering + the README table.
15 guard tests now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
* chore(scripts): round-31 — glob harvest follows delegation; brace/? globs
Round-31 review findings, both false failures in the new `test:scripts`
self-guard:
1. The glob half read `rootPkg.scripts["test:scripts"]` literally while the
wiring half two lines above used `reachableScripts`, so a delegating
`test:scripts` (`npm run test:scripts:lib && npm run test:scripts:guard`)
— which genuinely runs all 15 tests — reported every tracked test file as
"not matched by the glob", advising a rename the files already satisfy.
Now harvested across every script reachable FROM `test:scripts`, the same
fix round 6 applied to the `typecheck` harvest; `pretest:scripts` counts
for free.
2. `globToRegExp` escaped `{`/`}` as literals and left `?` as a regex
quantifier, both divergent from `node --test`'s glob. `*.{test,spec}.mjs`
is the natural widening here (the guard's own file scan already treats a
`.spec.` file as a test) and false-failed; `?` silently meant "optional
previous char" and threw outright when segment-leading. Adds `?` → `[^/]`
and a brace-alternation pass, with an unbalanced brace falling back to a
literal.
Nit 3: `resolveLeafProjects`'s two path-resolution rules (the r26
directory-form reference, and refs resolving against the referring config's
dir) extracted as `projectConfigFile`/`refToProject` so they are one-row
tables like the other parsers, rather than untestable behind a `tsc` spawn.
Nit 4: AGENTS.md + README document that the guard now enforces `test:scripts`
on three axes (wired, non-empty, glob-covered).
Verified: probes C and D exit 0; probe B (rename to `*.spec.mjs`) still
caught; `test:scripts` 18/18; `verify:typecheck-coverage` OK 894 files;
`verify:format-coverage` OK 919; `npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-32 — measure globs with node's own matcher
Round-32 review; both findings point at deleting code.
1. `globToRegExp` hand-modeled `node --test`'s glob and was still one form
short — character classes (`*[.]test.mjs`, `*.test.[mc]js`) hit the escape
set and became literals, so the guard false-failed on files node genuinely
runs. Replaced the whole translator (and its brace balance pre-scan) with
node's `path.matchesGlob` — 22.5.0+, and the repo floor is >=22.19.0. Same
*measure, don't model* move round 25 made when it deleted `isExemptCoreFile`
in favor of measuring web's projects; being exact for every form node
accepts is the point, since node's matcher is the thing being modeled.
2. The round-31 glob harvest was inline in `main()`, so mutation-reverting it
to the literal single-script read left the suite green — the commit's
headline fix was its one untested change. Extracted as `testScriptGlobs`,
with rows for the direct form, the delegating form (both child globs, not
`npm`/`run`/`<name>`), a `pretest:scripts` hook, and an unreachable script
contributing nothing.
Nit 3: `resolveLeafProjects`' JSDoc moved back onto it (the extraction left it
stranded above `projectConfigFile`, which had two stacked doc blocks).
Nit 4a: a `test:scripts` naming no path/glob harvested nothing, and `.some()`
over an empty list blamed every test file with unfollowable advice. Now one
explicit message naming the real problem.
Verified: probes for the direct, delegating, brace, `[.]` and `[mc]` forms all
exit 0 (the last two were false failures before); probe B (rename to
`*.spec.mjs`) still caught; bare `node --test` reports the new single message;
`test:scripts` 18/18; `verify:typecheck-coverage` OK 894; `npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-33 — scope the glob harvest to `node --test` segments
1. False pass. `testScriptGlobs` kept every non-flag token from every reachable
script, so a glob belonging to a NEIGHBOURING command was attributed to the
test runner — and since `.some()` needs only one glob to match, a broader
one suppressed a real miss rather than adding an inert pattern. A
`pretest:scripts` of `prettier --check "scripts/**/*.mjs"` (reachable via
the pre-hook round 31 added) was enough: the probe-B rename to `*.spec.mjs`
then passed while `node --test` silently ran 12 of 18 tests. Now scoped per
command segment to the ones invoking `--test`, exactly as
`typecheckProjects` gates on `tokens.some(isTsc)`. The JSDoc paragraph
claiming a surplus token can't suppress a miss is corrected — it was false.
2. The gate-integrity block is extracted as `testScriptProblems(scripts,
testFiles)`, pure and outside `main()`. The nit-4a branch added last round
was the third consecutive commit whose newest branch sat outside the test
seam and mutation-survived; extracting the block ends the pattern rather
than pinning one more condition from the outside.
Verified. The finding-1 probe (prettier pre-hook + `*.spec.mjs` rename) now
exits 1 where it exited 0; the hook alone still exits 0. Mutation testing over
the whole helper — empty-harvest branch to `if (false)`, segment gate removed,
wiring axis removed, non-empty axis removed, segment split removed,
`reachableScripts` to `Object.keys`, `matchesTestGlob` to equality — kills
7/7, including the branch that survived the last three rounds.
`test:scripts` 21/21 · `verify:typecheck-coverage` OK 894 ·
`verify:format-coverage` OK 919 · `npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-34 — harvest only positional args; normalize `./`
Both findings are the axis round 33 fixed, one level in.
1. False pass. The segment gate stopped a neighbouring COMMAND's glob, but
within a kept segment the filter was still "any non-flag token", so a
glob-valued flag's VALUE was harvested as a positional arg.
`--test-coverage-include "scripts/**/*.mjs"` is broader than the test glob
and matches a renamed `*.spec.mjs`, so it vouched for the very file the
runner skips — and it isn't hypothetical: `scripts/` sits outside the ≥90
coverage gate, so that flag is the obvious next thing added here. Tokens
preceded by a value-taking `--test-*` flag are now dropped.
2. False failure. `./scripts/**/*.test.mjs` — which `node --test` accepts, and
the common npm-script idiom — matched nothing, because `git ls-files` emits
no `./`. Both files were then blamed for a rename that never happened, the
unfollowable-remediation shape fixed for the bare-runner case last round. A
leading `./` is normalized off, as `rootRunsClientValidate` already does.
Nit 4: the gate-integrity footer's typecheck-wiring advice is now conditioned
on at least one issue being about the typecheck wiring — a `test:scripts`
failure carries its own per-line remediation, and appending `--noCheck`/`tsc
-b` advice under a renamed-test-file message sends the reader after the wrong
thing.
Verified. The coverage-include probe with the rename now exits 1 where it
exited 0, and exits 0 without the rename; the `./` probe exits 0 where it
exited 1; a `test:scripts`-only failure no longer prints the typecheck footer.
Mutation sweep 9/9 killed, including both new rules. `test:scripts` 22/22 ·
`verify:typecheck-coverage` OK 894 · `verify:format-coverage` OK 919 ·
`npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-35 — fourth axis: flags that narrow the run
1. False pass on a new axis. The three axes ask whether the tests are run,
whether there are any, and whether the runner is told about each — none asks
what it then DOES with them. `--test-shard 1/2` keeps every glob intact and
executes 6 of 22 tests, exiting 0, with the guard green; `--test-only` runs
2, as does a non-matching `--test-name-pattern`. This is `--noCheck` for the
test runner, one domain over. Adds `NARROWING_TEST_FLAGS` and a fourth axis
in `testScriptProblems`. Three of those flags were already in
`VALUE_TAKING_TEST_FLAGS` — the guard knew them well enough to drop their
values but not that they shrink the run. Both `--flag value` and
`--flag=value` report as the bare flag name.
2. The footer conditioning added last round was itself inside `main()` — the
pattern the same commit's comment documented, recurring in the commit that
documented it. Extracted as `integrityAdvice(problems, nonTypecheck)`.
Doing that fixes nit 3 in the same move: the old condition keyed off
`testScriptIssues` specifically, so it covered one of three non-typecheck
categories. A sibling-vouch failure ("the root validate no longer runs
verify:format-coverage") and client-enrollment problems still got
`--noCheck`/`tsc -b` advice about a typecheck pass they have nothing to do
with. All three sets are now excluded.
Verified. `--test-shard 1/2`, `--test-only` and `--test-name-pattern zzz` each
now exit 1 naming the flag, while node runs 6/2/2 of 22. A sibling-vouch
failure no longer prints the typecheck footer. Mutation sweep 12/12 killed,
including all three new rules and the branch this commit replaces.
`test:scripts` 24/24 · `verify:typecheck-coverage` OK 894 ·
`verify:format-coverage` OK 919 · `npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-36 — discover test files by content, not name
1. False pass on the axis underneath the other four. The required test-file set
was built with the same name pattern the `test:scripts` glob uses, so a
rename escaping BOTH was invisible: the file left `testFiles` entirely, so
nothing asked whether the runner was told about it, and the survivor
satisfied the non-empty axis. A dot→hyphen slip
(`npm-scripts.test.mjs` → `npm-scripts-test.mjs`) dropped 6 of 24 tests with
every gate green — and the file that stops running is the one pinning the
r16 prefix-sibling and r20 tail anchors, the series' two other false passes.
Round 30 fixed "renamed to a form the glob misses" by broadening the name
pattern; a fifth name pattern only moves the boundary again. `node --test`
decides what a test is by whether it registers tests, so `isScriptsTestFile`
reads that instead — the same measure-don't-model move as r25 and r32. It
also covers what no name pattern can: a NEW test file added under a name the
glob misses, never run and never noticed.
Nit 2: `enrollmentProblems` no longer suppresses the footer. "declares no
`typecheck` script …" IS about the typecheck pass and the footer's first clause
is its canonical fix; excluding it lost the advice for the r15 rename scenario.
The criterion is "is this about the typecheck pass at all", not "does it carry
per-line advice" — several typecheck problems carry their own and still want
the footer. JSDoc corrected accordingly.
Nit 3: the complementary-shard boundary (`1/2` + `2/2` across two reachable
scripts genuinely runs everything, and is flagged anyway) recorded on the
helper, alongside the guard's other documented boundaries.
Nit 4: the three `--test-coverage-{lines,branches,functions}` thresholds added
to VALUE_TAKING_TEST_FLAGS. Their values are numeric and so inert rather than
suppressing, but model completeness is cheaper than re-deriving that later.
Verified. The hyphen rename now exits 1 naming the file; the `.spec.mjs` rename
is still caught; an enrollment failure prints the footer again; the guard is
unchanged at 894 files OK (exactly the 2 tracked test files reference
`node:test`, none of the other 12 `scripts/` files do). Mutation sweep 14/14.
`test:scripts` 25/25 · `verify:format-coverage` OK 919 · `npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-37 — register-not-mention; guard the discovery read
Both findings were introduced by round 36's content predicate.
1. False failure. The predicate matched a bare `node:test` mention, so a shared
helper importing the runner's `mock` API — registering no tests — was
enrolled as a required test file and told to rename itself to `*.test.mjs`,
which would only make node run an empty file. `scripts/lib/` is already the
repo's shared-helper location, so this is the plausible next file there.
Tightened to node's actual criterion: an import/require OF `node:test` AND a
test registration.
2. Regression. `readFileSync` was passed as an argument, so it ran for every
tracked `scripts/` file before the predicate's extension gate could reject
it — a file in the index but not the worktree (`rm` without `git rm`) took
the guard down with a raw ENOENT stack, aborting phase 1 before any real
finding printed. Discovery did no reads at all before round 36. Extension is
now gated first and the read wrapped: an unreadable tracked file warns and
is skipped, the round-6 precedent where a failed `tsc` echoes its diagnostic
instead of dying.
Verified: the mock-helper probe exits 0; the tracked-but-deleted probe exits 0
with a one-line skip warning instead of a stack; the dot→hyphen rename is still
caught. Mutation sweep over the predicate 3/3 (import form, registration,
extension). `test:scripts` 25/25 · `verify:typecheck-coverage` OK 894 ·
`verify:format-coverage` OK 919 · `npm run ci` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
* chore(scripts): round-37 nit 3 — one definition of the JS-extension check
Finding 2's fix hoisted the extension gate into `main()` (it must run before
the read), leaving a second copy of the regex inside the predicate that reads
as protection but can never fire there. Extracted as `isJsFile` and used in
both positions, so the two can't drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vgJ6ZYj9VLhfSjgua48zw
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(network): add Network screen surfacing backend fetch log (#1355)
Routes `FetchRequestLogState` to a new Network tab so users can inspect
HTTP-layer request/response headers, status, duration, and category
(auth vs transport) of every fetch the backend makes for an active
session. Adds `NetworkControls` + `NetworkStreamPanel` + `NetworkEntry`
groups, a `monoBreak` Text theme variant for long header values, and
wires onClearNetwork / onExportNetwork (JSON download) from `App.tsx`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): include request / response bodies in search filter
Headers were already covered but bodies were not, so searches for
strings that live inside the JSON-RPC payload (e.g. "jsonrpc",
"initialize", a tool name) silently returned no matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): always render Response Body section, note streaming
Streaming responses (text/event-stream, application/x-ndjson, POST to
/mcp) are intentionally not body-captured by the fetch tracker, so the
section was silently omitted. Now whenever a response was received we
render the section, with either the body, a "(empty)" note, or a
"Streaming response — body not captured" note keyed off content-type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): capture response bodies via async body-update events
Two related fixes:
1. The fetch tracker was marking every streamable-HTTP POST /mcp call as
"not captured" because the response content-type is text/event-stream.
In practice those streams are bounded — the server sends the
JSON-RPC reply (sometimes preceded by progress events) and closes —
so we can clone the body and read it. Only GET + SSE / ndjson is the
unbounded long-lived channel that has to stay uncaptured.
2. Reading the body inline blocked the transport from processing
progress notifications until the entire stream finished, breaking the
`resetTimeoutOnProgress` integration test. The body is now read in
the background and dispatched via a new `fetchRequestBodyUpdate`
event that the FetchRequestLogState patches into the matching entry
by id. Plumbing wires through node/transport, the remote SSE channel
(RemoteSession, RemoteClientTransport), and the remote event types.
Also hides the Network tab when the active server is stdio (no HTTP
traffic to surface) and bumps the inline body preview limit to 100 KB
so typical tools/list responses render rather than tripping the "too
large" notice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(network): show orange SSE badge for long-lived stream entries
Adds a small "SSE" badge to the right side of the NetworkEntry header
(left of the HTTP status badge) when the response is a long-lived
streaming channel (GET + text/event-stream or application/x-ndjson) —
the only case where the body genuinely isn't captured. Makes the
"not captured" placeholder less surprising at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): address PR review findings
- Stop capturing OAuth response bodies (drop updateResponseBody from the
effectiveAuthFetch tracker). Restores pre-PR behavior for auth-category
entries so live access_token / refresh_token values can't leak into the
Network tab body preview during a screen-share. Headers + status are
still tracked.
- Extract `isLongLivedStreamResponse(method, contentType)` to
core/mcp/fetchTracking so the rule lives in one place; consumed by the
tracker and the NetworkEntry UI placeholder.
- Document the intent of NetworkEntry's `useEffect` (the list-level
toggle is authoritative; per-entry overrides are intentionally
discarded on each list toggle).
- Rename MAX_INLINE_BODY_BYTES → MAX_INLINE_BODY_CHARS (`.length` is
UTF-16 code units, not bytes) and update the user-facing notice.
- Switch NetworkStreamPanel's search from joined-string `.includes` to
per-field `.some(.includes)` so a term can't span field boundaries.
Regression test added.
- Add aria-label to ListToggle ("Expand all" / "Collapse all") — both an
a11y improvement and a stable selector that lets the panel test stop
fishing the button out by text-content negation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(network): use ListToggle aria-label in panel + modal tests
Cleanup spotted in PR review follow-up:
- HistoryListPanel + ServerSettingsModal previously fished the ListToggle
out by empty text content because the toggle had no accessible name.
Now that ListToggle exposes "Expand all" / "Collapse all", both tests
can use getByRole + name and the stale comments go away.
- ListToggle's own tests now assert the aria-label contract so a future
refactor can't silently drop it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(network): expose category toggle state via aria-pressed
The category UnstyledButtons in NetworkControls previously communicated
their toggled state only via background color. Adds aria-pressed={active}
so assistive tech can discover the on/off state without visuals, with a
test that asserts the attribute tracks the visibleCategories prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(servers): persist per-server settings to mcp.json (#1352)
- Add optional `settings` node alongside the transport config in each
`~/.mcp-inspector/mcp.json` entry. Round-trips through
`mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and the
`/api/servers` POST/PUT routes.
- Drop `MCPServerConfig.headers` from `SseServerConfig` /
`StreamableHttpServerConfig` and remove the headers textarea from
`ServerConfigModal`. Custom headers are now entered only in
`ServerSettingsForm` and reach the wire via `settings.headers`.
- Wire `ServerSettingsModal` into `App.tsx`; saves dispatch through a new
`useServers.updateServerSettings` mutator. `updateServer` carries the
existing settings across config-only saves so they aren't dropped.
- Apply settings on the *first* outbound request after a server loads —
no need to open the settings form first:
- `settings.headers` → SSE / streamable-http wire headers.
- `settings.metadata` → `InspectorClientOptions.defaultMetadata`,
merged into every outgoing `_meta` (call-time keys win).
- `settings.requestTimeout` → `InspectorClientOptions.timeout`.
- `settings.connectionTimeout` → Promise.race wrapper around connect().
- `settings.oauth*` → pre-seeded OAuth client credentials.
- `RemoteConnectRequest` carries `settings` so the remote backend can
source transport headers from the same source as the local path.
- `oauthClientSecret` is persisted in `mcp.json` alongside stdio `env`
values, both protected by the file's `0o600` permission.
- Update `specification/v2_servers_file.md` with the new shape and the
removal of the legacy headers field.
Tests: round-trip in `serverList.test.ts`; persistence on POST/PUT in
`servers-route.test.ts`; first-connect header injection in
`transport.test.ts`; `defaultMetadata` merging in
`inspectorClient.test.ts`; `useServers.updateServerSettings` +
`updateServer` settings-preservation in `useServers.test.tsx`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): rebuild InspectorClient on every connect so settings refresh
The previous onToggleConnection branch reused the existing InspectorClient
on a same-server reconnect, which froze settings at the moment the client
was first constructed. Editing headers, metadata, timeouts, or OAuth
credentials in the settings form was therefore invisible until the user
either switched servers or reloaded the page — surprising right after a
visible save.
Always call setupClientForServer on (re)connect so the latest
target.settings flow through to the transport's headers / requestInit, to
defaultMetadata, and to the OAuth manager.
Also adds remoteClientTransport tests that pin the wire-level contract:
settings round-trip through the /api/mcp/connect body when supplied, and
the field is omitted when no settings are configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(history): stop wiping pendingRequestEntries inside the connect event
State managers (ManagedToolsState, ManagedPromptsState, ManagedResourcesState,
ManagedResourceTemplatesState, ManagedRequestorTasksState) handle the
client's "connect" event by calling `void this.refresh()`. The synchronous
portion of refresh runs through SDK Protocol.request → transport.send →
MessageTrackingTransport.send → trackRequest, all before yielding to the
network — so each list request gets tracked into MessageLogState's
pendingRequestEntries inside the same connect event dispatch.
MessageLogState's own "connect" listener was registered after those state
managers (it's the last one constructed in App.tsx) and cleared
pendingRequestEntries on every connect. Listener order meant the clear ran
*after* the requests were tracked but before their responses arrived,
leaving the history with unmatched "response" entries marked PENDING.
The clear-on-disconnect (via statusChange → disconnected) already handles
session-boundary cleanup, so clear-on-connect was purely defensive and now
actively harmful. Drop the connect listener; keep the disconnect path.
Tests: updated the unit test to assert the new behavior (connect does not
clear); added an integration regression that wires a sibling connect
listener firing listTools and asserts the response folds into the request
entry instead of appearing as an orphan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: drop accidentally tracked debugging screenshot
pending-requests.png was a local debugging artifact attached to a
conversation, not source. Remove it from the tree (the working copy is
kept so the user still has it locally) and rely on the existing untracked
behavior going forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(servers): add e2e check that settings.headers reach upstream MCP
Stitches the full v2 pipeline together in one assertion:
browser-style RemoteClientTransport
→ POST /api/mcp/connect (settings in body)
→ backend createTransportNode applies settings.headers to requestInit
→ SDK StreamableHTTPClientTransport
→ upstream MCP test server
Captures the upstream HTTP request via FetchRequestLogState and asserts
both `X-Tenant` and `X-Trace` are present on the outbound POST. Catches
any regression that breaks the wire between the form and the upstream
server in a single integration test, instead of the previous coverage
which exercised each hop in isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ServerSettingsModal): invert ListToggle compact prop so icon matches state
ListToggle's `compact` prop represents the list's *current* compact state:
true means "currently compact" and renders the expand icon to advertise
the action (open up). The settings modal was passing
`compact={allExpanded}` — exactly inverted. When all sections were open
it showed the expand icon (suggesting "open more") and when collapsed it
showed the collapse icon, opposite of every other ListToggle caller in
the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(HistoryListPanel): match ServerListScreen Export button — rename + default variant
Renames "Export JSON" to "Export" and switches from the subtle ToolbarButton
to the same `Button variant="default"` used by ServerListControls so the
two Export affordances look identical across screens. Also moves the
button to the right of the ListToggle (mirroring the order on the server
list screen) and disables it when there are no entries to export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(HistoryListPanel): move Clear button to top toolbar, match Export style
Moves the Clear affordance out of the "History (N)" section header and
up to the panel toolbar alongside Export, where it now uses the same
`Button variant="default"` styling. Order in the toolbar is now:
ListToggle · Clear · Export. Clear is disabled when there are no
unpinned entries to remove (pinned entries are intentionally preserved).
ToolbarButton helper is unused after this change and dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: ignore local debugging screenshots at repo root
Stops `git add -A` from re-staging the `*.png` files Claude Code drops
at the repo root from attached conversation screenshots. The pattern
only matches the top-level (`/*.png`) so legitimate image assets in
subdirectories (e.g. `clients/web/public/`) still get tracked when
explicitly added.
Also untracks the two screenshots that slipped through previously
(initialize-request.png, pending-requests.png).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(LogStreamPanel): match HistoryListPanel button styling
Clear / Export / Copy All on the Logging screen were rendered with the
subtle ToolbarButton helper while History's equivalents use
`Button variant="default"`. Switch all three to the default variant so
the toolbars look identical across screens and mirror the disabling
behavior — each button is now disabled when `entries.length === 0`.
ToolbarButton helper is unused after this change and dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review — debounce save, preserve-on-omit PUT, validate shape, lift connect timeout
Round-up of fixes from https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4526861959:
1. ServerSettingsModal saves were firing on every keystroke (one PUT +
full mcp.json refresh per character). App.tsx now debounces by 300ms
and flushes on modal close so a burst of edits coalesces into one PUT
and the form value can't flicker back mid-typing.
2. `PUT /api/servers/:id` with an omitted `settings` field used to
silently wipe the persisted settings node. The route now treats the
three intents distinctly:
- field omitted → preserve existing on-disk settings
- explicit null → clear the settings node
- settings object → validate + apply
`useServers.updateServer` is simplified to drop the client-side
carry-through hack, and the test that documented the broken behavior
is flipped + augmented with explicit-clear and bad-shape cases.
3. `connectionTimeout` Promise.race is moved from App.tsx into
`InspectorClient.connect()`, reading `this.serverSettings?.connectionTimeout`.
TUI/CLI consumers now get the same behavior; on timeout the client
internally disconnects so the next connect starts clean.
4. Custom Headers hint in `ServerSettingsForm` notes that `Authorization`
is owned by the OAuth flow when OAuth is configured — avoids the
sharp edge where the SDK's authProvider silently overwrites a
user-set Authorization header.
5. New `validateSettings` on the server route rejects malformed shapes
(`settings: []`, `settings.headers: "oops"`, non-numeric timeouts,
etc.) with 400 instead of letting them persist and crash the UI.
7 (nit). Renamed `settings` → `savedSettings` inside `setupClientForServer`
so the variable holding the persisted settings doesn't shadow the
later locally-derived values.
Follow-ups opened separately:
#1356 — Move oauthClientSecret out of mcp.json into the OS keychain (review #6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): address PR review pass 2 — connect-timeout test, unmount cleanup, tagged intent, lenient settings read
Pass-2 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4527089705:
(a) New integration test pins the new client-layer connect-timeout
contract: a hanging stub transport with `connectionTimeout: 50`
rejects with the descriptive error and lands the client in "error"
status. Documents what TUI/CLI consumers should expect from the
lifted Promise.race in InspectorClient.connect().
(b) useEffect cleanup clears `pendingSettingsTimerRef` on unmount so a
route change or HMR mid-debounce can't fire one stale PUT against
an unmounted component.
(c) Refactor the PUT-route `settingsIntent` discriminator from
`"preserve" | "clear" | { value }` to a tagged union with `kind`,
and consume it with a `switch` for exhaustiveness — reads cleaner
and makes adding a fourth intent painless.
(d) `normalizeMcpServers` now runs `validateSettings` on the on-disk
settings node and silently strips it if invalid, mirroring the
existing lenient-on-read pattern from `normalizeServerType`. Closes
the read/write asymmetry where a hand-edited malformed `settings`
could survive on disk and get carried through preserve-on-PUT.
New route test pins the strip-on-GET + no-propagate-on-preserve-PUT
behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-3 review — patch-style PUT, toast on close-flush fail
Pass-3 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529276897.
(b) `PUT /api/servers/:id` now treats both `config` and `settings` as
optional patches:
- field omitted → preserve the on-disk value
- explicit body value → apply
- settings: null → clear
`updateServerSettings` stops reading `existing.config` from the
in-memory `servers` snapshot, which closes the closure-capture
hazard where a refresh between debounce-schedule and flush could
silently revert a separate edit. The route is the single source of
truth for transport config now. New tests cover settings-only PUT,
rejected non-object config, and the empty-body no-op patch case.
The previous "rejects a missing config" test is rewritten to
document the new no-op-preserve semantics.
(c) Inline comment near the PUT rebuild loop documents that writing
the full mcpServers map back is what gives the route its desirable
self-healing property for malformed settings on *other* entries —
deliberate side-effect, not an accident of normalization.
(d) `flushPendingSettings` surfaces failures via @mantine/notifications
instead of swallowing them. The modal already closed by the time
the flush PUT resolves, so a silent fail would leave the user
thinking their last edits saved (especially painful for the OAuth
client secret). The notifications wiring was already mounted in
main.tsx.
Pass-3 (a) — settings form being effectively uncontrolled mid-edit — is
acknowledged in the reply: working as intended today; the proper fix is
internal modal state when file-watching (#1345) forces the issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-4 review — strip smuggled settings out of config
Pass-4 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529861349.
Main finding: `normalizeServerType` spreads unknown keys, so a body
that nested `settings` inside `config` would smuggle a settings field
onto the stored entry without ever passing through `validateSettings`.
Strip the settings key off the incoming config in `buildStoredEntry`
so `validateSettings` remains the single write path for the settings
node — the "one source of truth" invariant pass 2 set up holds again.
Two new tests pin the strip on both routes:
- POST with `config.settings = { bogus }` lands an entry with no
settings node on disk.
- PUT with `config.settings = { bogus }` + `settings: null` clears
the real settings *and* doesn't re-attach the bogus payload via
the spread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-5 review — absorb late connect rejection, tighten validateSettings, log smuggle warning
Pass-5 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529954490.
Main finding: when the connect-time `Promise.race` lost to the timeout,
the original `this.client.connect(this.transport)` promise was left
pending. After `disconnect()` tore the transport down, real transports
(SSE / streamable-http) would reject that promise later — with no
handler attached, producing a Node `unhandledRejection` (and a vitest
suite warning). Attach a no-op `.catch` right after creating the
connect promise so Node sees the late rejection as handled.
Nit (a): `validateSettings` was casting the raw object through to
`InspectorServerSettings`, which let unknown keys ride along onto disk.
Replaced the cast with an explicit pick-and-build over the known
fields. Unknown stowaways now silently drop on the way through. New
route test pins the behavior.
Nit (b): `buildStoredEntry` now logs a `warn` via the route's pino
logger when an incoming `config` carries a `settings` key. The strip
is still defensive (it isn't a documented contract) but the warning
lets a misconfigured client find out about the wasted field instead
of debugging a silent drop.
Nit (c) — the defensive `?? {}` fallback in `buildStoredEntry` —
acknowledged in the review reply; left in place as belt-and-braces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(servers): pass-6 polish — scope absorber, thread id, drop empty OAuth, guard `in`
Pass-6 review at https://github.com/modelcontextprotocol/inspector/pull/1353#issuecomment-4529979135. Reviewer signed off as ready; landing the four marginal cleanups since they're all small.
(a) `connectPromise.catch(() => {})` moves inside the
`if (connectTimeoutMs > 0)` branch. The await-only path propagates
rejection cleanly through the outer try/catch — the absorber is
only needed when the race leaves the original promise unattended.
Code now reads as "absorber is tied to the race," which is the
real invariant.
(b) Thread `id` (POST id, PUT newId) through `buildStoredEntry` and
include it in the smuggle warning's pino bindings. Two clients
smuggling simultaneously are distinguishable in the log instead
of indistinguishable.
(c) `validateSettings` coerces empty-string OAuth fields to absent.
The form emits `""` when the user clears an input, and an empty
`oauthClientId` on disk would later be misread as "OAuth configured."
New route test pins it.
(d) `buildStoredEntry` adds a null/object guard before `"settings" in
configObj`. The route layer pre-checks this on POST and PUT, but
the helper's `unknown` parameter is a weak contract; the guard
makes it safe to call from anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): reflect settings persistence + patch-PUT contract in v2_servers_file
The doc was authored when `mcp.json` only stored the server list. Bring it
forward to match what shipped in #1352 / PR #1353:
- Summary calls out that the file also stores per-server settings and
links into the Per-server settings section.
- On-disk format note explains that `settings` is an optional Inspector-
specific extension that other MCP tools ignore.
- New top paragraph on the Per-server settings section explains the
basic-config / settings UI split rationale (simpler form for the
common case, advanced settings tucked behind a second dialog).
- UI bullet is rewritten to match the patch-style PUT semantics that
emerged across the review cycle: settings-only and config-only
PUTs preserve their counterpart inside the write lock, so neither
modal can silently clobber the other half. The earlier "must
re-send settings on PUT" hack is gone.
- New bullets capture the save cadence (300ms debounce + flush-on-close
+ failure toast), the patch-PUT contract (omit-preserve / null-clear /
rename), and the write-path / read-path validator gates.
- Secret storage bullet links to #1356 (OS-keychain follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): show a third entry with the optional settings node
The On-disk format example demonstrated only basic stdio entries. Add a
streamable-http entry with a populated `settings` node so readers can
see the shape inline rather than having to scroll to the Per-server
settings section. Inline comment in the block cross-links there for the
full contract and notes that other MCP tools ignore the extension key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>