| Filename | Latest commit message | Latest commit date |
|---|---|---|
* chore(deps): migrate to eslint 10 to clear the brace-expansion DoS advisory (#1838) eslint 9 pulled `minimatch@3` → `brace-expansion@1.1.16`, which carries GHSA-mh99-v99m-4gvg (high, DoS via unbounded expansion length). It appeared in all five lockfiles — the root plus each of the four clients, none of which is an npm workspace, so all five had to move together. Deferred from the pre-2.0.0 sweep because it is dev-scope, not attacker- controlled, and a major bump across five packages was the wrong thing to attempt immediately before an irreversible publish. This is that work. ## What moved - `eslint` `^9.39.4/5` → `^10.8.0` and `@eslint/js` → `^10.0.1`, in all five manifests. - `eslint-plugin-react-hooks` `^7.0.1` → `^7.1.1` in web and tui. Required, not cosmetic: 7.0.1's peer range stops at `^9.0.0`, so installing it alongside eslint 10 fails `ERESOLVE`. - Everything else already declared eslint 10 support and stayed put: `typescript-eslint@^8.56.1` (`^8.57 || ^9 || ^10`), `eslint-plugin-react-refresh@^0.5.2` (`^9 || ^10`), `eslint-plugin-storybook@^10.2.19` (`>=8`). ## Config migration None was needed. All five configs were already flat (`eslint.config.js` with `defineConfig`/`globalIgnores` from `eslint/config`), which is the format v10 keeps and the only one it still supports. The root config's `core/` and shared- surface blocks — including the `_`-prefix `argsIgnorePattern` / `varsIgnorePattern` / `caughtErrorsIgnorePattern` — are unchanged and still gate `lint:core` and `lint:shared`. No rule was disabled, downgraded, or scoped away to make the new eslint pass. v10 surfaced 21 real violations across three rules; all 21 are fixed in the code. ### `preserve-caught-error` (new in `eslint:recommended`) — 15 sites A `throw new Error(msg)` inside a `catch` silently drops the original error. Each now passes `{ cause }`, so the underlying failure survives into the chain. Messages are unchanged, so nothing that asserts on them moves. 12 in `core/`, 2 in `clients/cli`, 1 in `test-servers/src`. ### `no-useless-assignment` (new in `eslint:recommended`) — 2 sites `clients/cli/src/cli.ts`'s `optionArgs` and `test-servers/src/test-server-oauth` 's `valid` were initialized and then unconditionally overwritten on every path. Both are now declared without the dead initializer; TypeScript's definite- assignment analysis covers the remaining paths. ### `react-hooks/set-state-in-effect` — 8 sites (web) These were false negatives the plugin fixed in 7.1.0, not a rule we newly opted into: `set-state-in-effect` has been `error` in `flat/recommended` since 7.0.1, and the web config has always extended it. Every one was the same anti-pattern — resetting or re-syncing local state from a prop inside a `useEffect`, which paints the stale value for one frame and then renders again to correct it. All eight now use a new `useValueChange(value, onChange)` hook (`clients/web/src/hooks/useValueChange.ts`), which is React's documented "adjusting state during render": compare against the previous render's value with `Object.is` and call back *during* render, so React discards the in-progress output before it reaches the DOM. - `MrtrConversation`, `ProtocolEntry`, `TaskCard` — mirror `isListExpanded`. - `NetworkEntry` — same mirror, plus the "Reveal in Network" force-open. The reveal's rAF scroll stays an effect (real external-system work); only the `setIsExpanded(true)` moved. `isExpanded` is now seeded from `isListExpanded || revealed`, since a render-time sync fires on change and so cannot cover an entry that mounts already revealed. - `PromptArgumentsForm`, `ResourceTemplatePanel` — reset on template/prompt switch. - `ServerConfigModal` — reset on open. Keyed on `opened ? initial : undefined`, which collapses "opened flipped" and "initial changed while open" into one value; `initial` is already memoized, so it cannot thrash. `useValueChange` has its own test file, and `ServerConfigModal` gains an open→edit→close→reopen test (the close leg was previously untested, which is what left the new guard uncovered). ## Advisory delta `npm audit`, per package, before → after: | Package | before | after | | ------------------- | -------------------------- | -------------------------- | | root | 9 (1 low, 6 mod, 2 high) | 8 (1 low, 6 mod, 1 high) | | `clients/web` | 19 (3 low, 6 mod, 6 high, 4 crit) | 17 (3 low, 6 mod, 4 high, 4 crit) | | `clients/cli` | 6 (2 low, 4 high) | 4 (2 low, 2 high) | | `clients/tui` | 4 (1 low, 3 high) | 2 (1 low, 1 high) | | `clients/launcher` | 3 (3 high) | 1 (1 high) | `brace-expansion` is gone from all five — `eslint@10` resolves `minimatch@10` → `brace-expansion@5.0.9`. Dropping `@eslint/eslintrc` (removed in v10) also took `js-yaml` with it, clearing GHSA-52cp-r559-cp3m (high) from all four clients as a bonus. The remainder are unrelated and tracked separately. `npm run ci` passes end to end — validate, the per-file ≥90 coverage gate, the build gate, all five smokes (including `smoke:tui`, run locally on a real TTY), and 462 Storybook tests. Also documents the effect rule in AGENTS.md and mirrors it into `.github/copilot-instructions.md`, since it is now something a reviewer cites. Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: state useValueChange's referential-stability requirement (#1838) Copilot review on #1900: the hook compares with `Object.is`, so an object or array literal rebuilt in the component body compares unequal every render. Because the callback is what updates state, that is not merely a wasted render — it is an infinite loop. Every current caller passes a primitive (`boolean`, `string`) or an already- memoized value, so nothing changes behaviorally. But the hazard was implicit, and the next caller is the one it would bite. Documented on the hook itself, and one clause added to the AGENTS.md rule with its mirror in `.github/copilot-instructions.md`. No runtime guard: the requirement is the same one a `useEffect` dependency array carries, and here the failure is loud and immediate rather than silent. `npm run ci` passes. Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): stop asserting the --tool-args-json catch binding is an Error (#1838) Copilot review round 2 on #1900: the `--tool-args-json` parse handler formatted its message with `(e as Error).message`. A `catch` binding is `unknown`, so that cast is an assertion rather than a check — a thrown non-Error would render `--tool-args-json is not valid JSON: undefined`, which points the user at nothing. `JSON.parse` does only throw `SyntaxError` today, so this is not a live bug. But the cast is the kind AGENTS.md asks us not to leave unjustified, the same handler was already being touched here to add `{ cause: e }`, and every other site in this PR uses the guarded form. Now it does too. `npm run ci` passes. Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: require useValueChange's onChange to be render-pure (#1838) Copilot review round 3 on #1900: `onChange` is called during render, so it is bound by render-phase purity — and the web client mounts under `StrictMode`, which deliberately double-renders in development. A callback doing anything external would run an unpredictable number of times, and concurrent React can abandon an in-progress render entirely. Every current caller already passes a callback that only calls `setState`, so nothing changes. But "runs during render" is the whole point of the hook and the constraint that follows from it was left implicit. Stated on the hook, with the same clause in the AGENTS.md rule and its `.github/copilot-instructions.md` mirror. All three now point at `NetworkEntry` as the worked example of the split: the reveal's force-open is a state update and lives in `useValueChange`, while its `requestAnimationFrame` scroll stays a `useEffect`. `npm run ci` passes. Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| .. | ||
| __tests__ | ||
| scripts | ||
| src | ||
| eslint.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.test.json | ||
| vitest.config.ts | ||
MCP Inspector Launcher
The launcher is the package that provides the global mcp-inspector binary (e.g. when users run npx @modelcontextprotocol/inspector). It is not a separate user-facing app—it is the single entrypoint that selects and runs one of the clients (web, CLI, or TUI).
Responsibility
- Parse mode from a leading prefix of
--web(default),--cli, or--tuiimmediately after the script name. - Forward all following arguments unchanged (including tokens that look like mode flags).
- Dynamically import that app’s runner from
clients/{web,cli,tui}/build/index.js(relative to the launcher build output) and call it in-process (nospawn()).
All configuration parsing, config-file loading, and server setup are handled by the app runners and by core; the launcher does not interpret config or env vars.
Error reporting. A --cli failure is routed through the CLI's own error sink, so mcp-inspector --cli preserves the CLI exit-code map (1 usage, 2 no-app, 3 auth-required, 4 unreachable, 5 tool-error) and its machine-readable {"error":{…}} stderr envelope — the same as invoking the CLI bin directly. --web / --tui failures print a human-readable Error: <message> and exit 1 (append MCP_DEBUG=1 for the stack).
Web server-list flags (--web)
mcp-inspector --web chooses which server list the UI shows and whether it is
editable (see specification/v2_catalog_launch_config.md):
| Invocation | Server list | Editable in UI? |
|---|---|---|
mcp-inspector --web |
Default catalog ~/.mcp-inspector/mcp.json (seeded with the two sample servers if missing) |
Yes |
mcp-inspector --web --catalog <path> (or MCP_CATALOG_PATH=<path>) |
That file as the active catalog (same seed-if-missing behavior) | Yes |
mcp-inspector --web --config <path> |
That file as a read-only session — shown but never written, seeded, or migrated (safe for a foreign config) | No |
mcp-inspector --web --server-url <url> --transport http --header "Name: Value" (or a positional command) |
One ad-hoc server held in memory, connectable with the given --headers |
No |
Rules: --catalog and --config are mutually exclusive; neither combines with
an ad-hoc target or --header; --header requires an ad-hoc HTTP/SSE server
and is applied to that connection (it is no longer a warn-only no-op).
Seed contents are web-specific. When the web backend creates a missing
writable catalog it seeds DEFAULT_SEED_CONFIG (core/mcp/serverList.ts) — a
filesystem-server-default scoped to /tmp plus the canonical
everything-server-default — so a first launch has something to connect to.
The CLI and TUI seed an empty catalog instead; see the next section. A
read-only --config is never seeded on any surface.
CLI and TUI server-list flags (--cli / --tui)
The CLI and TUI use the same --catalog / --config vocabulary as --web,
resolved by the shared core/mcp/node/config.ts helpers:
| Invocation | Server list source |
|---|---|
mcp-inspector --cli / --tui (no source flag, no ad-hoc target) |
Default writable catalog ~/.mcp-inspector/mcp.json (created/seeded empty if missing) |
--catalog <path> (or MCP_CATALOG_PATH=<path>) |
That file as a writable catalog — seeded empty if missing |
--config <path> |
That file as a read-only session — served as-is, never written or seeded; errors if absent |
positional command / --server-url <url> |
One ad-hoc server |
Note the seed contrast with --web above: the CLI and TUI write an empty
{ "mcpServers": {} } (seedEmptyCatalog in core/mcp/node/config.ts), not
the web client's two sample servers — they are non-interactive or list-driven,
so sample entries would be noise rather than a starting point.
Rules (shared serverSourceConflict): --catalog and --config are mutually
exclusive, and neither combines with an ad-hoc command/URL target. The CLI/TUI
do not perform catalog CRUD yet — they are read consumers — so the
writable/read-only split currently surfaces only as seed-if-missing
(--catalog/default) vs error-if-missing (--config). Full writable
persistence is tracked in #1482 / #1432.
Production web build (--web, no --dev)
Prod --web serves static assets from clients/web/dist/, which only exists
after a build. In the published package dist/ always ships. In a fresh dev
checkout it is absent, so the runner builds it on demand the first time you
launch (vite build via npm run build:client, run in clients/web) instead
of serving a broken page (#1486). If that build can't run — e.g. dev
dependencies are missing — the launcher exits with an actionable error telling
you to run npm run build (from clients/web) or relaunch with --dev to use
the Vite dev server. --dev never needs dist/; it runs Vite directly.
CI and npm run validate (via the top-level smoke step) exercise this prod
path end-to-end with npm run smoke:web (scripts/smoke-web.mjs): it starts
mcp-inspector --web against the built dist/ and asserts GET / returns the
SPA (HTTP 200) with the injected __INSPECTOR_API_TOKEN__.
CLI and TUI smokes
The top-level smoke step also runs end-to-end smokes for the other two modes
through the built launcher artifact (beyond the --help checks in
smoke:launcher):
npm run smoke:cli(scripts/smoke-cli.mjs) — runsmcp-inspector --cliagainst the bundled stdio test server via a temp--catalogand assertstools/listreturns the server's tools, plus the--catalog/--configresolution paths (default-catalog seed-on-missing, read-only--configerror-without-seed,--catalog/--configconflict).npm run smoke:tui(scripts/smoke-tui.mjs) — launchesmcp-inspector --tui --catalog <temp>and asserts the Ink app renders its first frame within a timeout, then shuts it down (a shallow boot/render check, not full interaction).
Both build test-servers/build on demand if it is missing.
Development
Like the web client, the launcher self-validates from its own folder:
npm run validate # format:check && lint && build && test:coverage
This has no dependency on the other clients being built — it only checks the
launcher's own source. eslint.config.js is a Node-only flat config (the web
client's React/Storybook plugins stripped out), and the per-file coverage gate
covers parse-launcher-argv.ts (the pure arg-parsing logic); src/index.ts is
excluded as binary bootstrap and is instead exercised by the smokes above. The
repo-root validate:launcher simply delegates here (cd clients/launcher && npm run validate).
Publishing
The launcher provides the mcp-inspector bin for the single @modelcontextprotocol/inspector tarball. Packaging is a whole-repo concern — how the one-package/single-version tarball is assembled, the "files" allowlist invariants (no source maps, why clients/web/build needs .npmignore, why the cli/tui package.jsons ship), and the npm run pack:verify publish smoke — is documented in the root README.
Architecture
For how the launcher fits with the shared config processor and app runners, see the Launcher and config consolidation document in the repo root docs/ folder.