inspector/clients/launcher
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Cliff Hall 23e0c39d11
chore(deps): untangle the vitest/Storybook exact-pin knot to clear GHSA-p63j-vcc4-9vmv (#1839) (#1899)
GHSA-p63j-vcc4-9vmv (**critical**) — `@vitest/browser` Browser Mode provider
commands accept a file path from the browser and act on it without checking the
`allowWrite` permission gate. Patched in 4.1.10; `clients/web` was pinned at
4.1.0.

Dev-scope only — `@vitest/*` is absent from the published `files` allowlist, and
Browser Mode here runs this project's own Storybook play functions, never a
third-party page. That is why #1837 deferred it rather than attempting a
toolchain upgrade immediately before an irreversible publish.

## The knot, and what actually unties it

`^4.1.0` already admits 4.1.10, so the declared ranges were never the problem.
The family is welded by *exact* peer pins — `vitest` pins
`@vitest/browser-playwright`, which pins `@vitest/browser` — so nothing moves
alone.

The issue predicted that an explicit coordinated `npm i -D` would fix it. It
does not. With `clients/web/package-lock.json` in place, npm ERESOLVEs even when
every member of the family plus the Storybook packages are named in one command
and `node_modules` is deleted first:

    Found: @storybook/addon-a11y@10.2.19
    Could not resolve dependency: dev @storybook/addon-a11y@"^10.5.5"
    Conflicting peer dependency: storybook@10.5.5

npm resolves against the existing lock tree and will not move the whole
constellation at once. **Regenerating `clients/web/package-lock.json` is the
only thing that unties it** — a clean resolve places all four at 4.1.10 with no
peer complaints at all.

## Versions

    vitest                       4.1.0    -> 4.1.10
    @vitest/browser              4.1.0    -> 4.1.10   (transitive)
    @vitest/browser-playwright   4.1.0    -> 4.1.10
    @vitest/coverage-v8          4.1.0    -> 4.1.10
    storybook + @storybook/*     10.2.19  -> 10.5.5
    eslint-plugin-storybook      10.2.19  -> 10.5.5
    @chromatic-com/storybook     5.0.1    -> 5.2.1

`clients/{cli,tui,launcher}` were already resolving 4.1.9 (no browser pins, so
they floated freely) and were never vulnerable. Their declared `^4.1.0` still
*permitted* 4.1.0 on a fresh resolve, so the floor is raised to `^4.1.10` in all
four manifests — same reasoning as the vite floor in #1841. Pinning only the
lockfile would have left a regression path open.

## The vite 8 peer override

Resolved, as a consequence rather than a workaround.
`@joshwooding/vite-plugin-react-docgen-typescript@0.6.4` declared
`peer vite@"^3 || ^4 || ^5 || ^6 || ^7"` against our vite 8. It is not a direct
dependency — it arrives through `@storybook/react-vite`, which at 10.5.5 depends
on `^0.7.0`, and 0.7.0 adds `^8.0.0` to that range. So taking Storybook 10.5.5
for the peer set drops the override too; nothing is forced.

## Two holds the regeneration forced, each with a tracked issue

Regenerating the lock floats every other in-range dependency to current. Most of
that is welcome — it is what takes `clients/web` from 19 advisories to 1 — but
two floats broke the gate and are unrelated to this security fix:

- **zod 4.3.6 -> 4.4.3** makes `clients/web`'s `tsc -b` die with
  `FATAL ERROR: Ineffective mark-compacts near heap limit` at the ~4GB default.
  Reverting zod alone makes it exit 0, so the attribution is unambiguous.
  Held at `~4.3.6`. See #1896.
- **eslint-plugin-react-hooks 7.0.1 -> 7.1.1** enables `set-state-in-effect`,
  which fails `lint` on 8 pre-existing violations across 7 components. Fixing
  them is a real refactor with interaction-behavior consequences, not a
  suppression. Held at `~7.0.1`. See #1897.

Both `~` pins are deliberate and temporary, and both issues say so — they are
constraints to remove, not preferences.

Storybook 10.5.5 also reports that `setProjectAnnotations` is applied
automatically since 10.3, making `.storybook/vitest.setup.ts` redundant. Left in
place: `./preview` carries the Mantine decorator and the a11y annotations drive
the play-function assertions, and if automatic provisioning missed either, the
462 stories would render unthemed with a11y checks inert and very likely still
pass. That silent-failure risk does not belong in a security bump. See #1898.

## Audit delta

`clients/web`, where the advisory lived:

    before   19 (4 critical, 6 high, 6 moderate, 3 low)
    after     1 (1 low — esbuild-in-tsup, Windows dev server only)

All four critical entries — `vitest`, `@vitest/browser`,
`@vitest/browser-playwright`, `@vitest/coverage-v8` — are gone. The other 14 are
transitive dev-tree advisories (hono, ws, js-yaml, flatted, brace-expansion, ...)
cleared as a side effect of the clean resolve. Elsewhere: cli 6 -> 5,
launcher 3 -> 2, tui 4 -> 4, root unchanged at 9 (its lock is untouched). Those
remainders are pre-existing and unrelated.

## Verification

`npm run ci` green end to end on the upgraded stack — the point of caring here,
since this *is* the test infrastructure:

- coverage gate (>=90 per file, all four dimensions) passes on all four clients;
  no file regressed, so no gate was lowered and no `v8 ignore` added
- 4802 web tests, 304 cli, 282 tui, 5 launcher
- 462 Storybook play functions across 109 files, on vitest 4.1.10 Browser Mode
- verify:build-gate, and all five smokes including `smoke:tui` (real TTY here)

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 19:50:57 -04:00
..
__tests__ refactor: self-validating clients, fast validate + separate coverage/smoke (#1504) (#1505) 2026-06-21 11:41:33 -04:00
scripts feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452) 2026-06-14 20:27:49 -04:00
src feat(web): enforce localhost-only binding; expand loopback origin allow-list (#1796) 2026-07-26 22:15:10 -04:00
eslint.config.js chore: close a batch of quality-gate & tidy gaps (#1785 #1787 #1789 #1790 #1767) (#1792) 2026-07-26 10:43:11 -04:00
package-lock.json chore(deps): untangle the vitest/Storybook exact-pin knot to clear GHSA-p63j-vcc4-9vmv (#1839) (#1899) 2026-08-01 19:50:57 -04:00
package.json chore(deps): untangle the vitest/Storybook exact-pin knot to clear GHSA-p63j-vcc4-9vmv (#1839) (#1899) 2026-08-01 19:50:57 -04:00
README.md docs: restore the missing docs/ targets and split the catalog-seeding wording (#1811) (#1812) 2026-07-27 20:37:14 -04:00
tsconfig.json test(cli,tui): typecheck the __tests__ dirs (#1791) (#1799) 2026-07-27 12:05:35 -04:00
tsconfig.test.json test(cli,tui): typecheck the __tests__ dirs (#1791) (#1799) 2026-07-27 12:05:35 -04:00
vitest.config.ts chore: close a batch of quality-gate & tidy gaps (#1785 #1787 #1789 #1790 #1767) (#1792) 2026-07-26 10:43:11 -04:00

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 --tui immediately 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 (no spawn()).

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) — runs mcp-inspector --cli against the bundled stdio test server via a temp --catalog and asserts tools/list returns the server's tools, plus the --catalog/--config resolution paths (default-catalog seed-on-missing, read-only --config error-without-seed, --catalog/--config conflict).
  • npm run smoke:tui (scripts/smoke-tui.mjs) — launches mcp-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.