| Filename | Latest commit message | Latest commit date |
|---|---|---|
`smoke:tui` spawned the launcher with `stdio: ["ignore", …]`, waited for
"MCP Servers" in the output, printed OK, and SIGTERMed. It never looked at
the child again.
The child was already dead. Ink mounts `useInput`, `useInput` needs raw
mode, and raw mode is a property of the file descriptor — with stdin on
/dev/null the TUI painted one frame and exited 1 about 40ms later with
"Raw mode is not supported on the current process.stdin". Measured here:
first frame at 396ms, exit 1 at 433ms. The smoke won that race and
reported success, so what it asserted was *first paint*, not survival —
contradicting its own header ("boots and renders without crashing").
This is not environment-specific: `stdio: ["ignore", …]` makes the child's
stdin /dev/null whether or not the developer's terminal is a TTY, so this
smoke has never verified a running TUI on any machine. And the race cut
both ways — an `exit` processed first failed the run, which reads as flake
rather than as the standing defect.
Two pieces:
- `scripts/lib/pty.mjs` gives the child a real terminal via `script(1)`,
no dependency. The three flavors are not interchangeable (BSD takes
argv; util-linux takes one shell string plus `-e`; busybox that string
without `-e`), so the invocation is built and unit-tested rather than
written inline. A platform with no `script(1)` skips loudly instead of
running without one, which would be a guaranteed failure rather than a
weaker check.
- `scripts/lib/render-smoke.mjs` is the fix: the child must still be
running `surviveMs` (default 2s) after first paint. Without that, a
future harness change reintroduces the same false green and nothing
notices — which is why it lives in a module `test:scripts` drives
against a stub that paints the marker and immediately exits. Today's
harness passes that stub; this one fails it, with a diagnostic that
distinguishes "died after painting" from "died before painting".
The smoke additionally fails if the raw-mode error appears in a passing
run, so a future Ink that degrades to a warning instead of throwing can't
ship a TUI with no keyboard input past this gate.
Verified end to end: with a `script` shim that allocates no pty, the smoke
now fails with "it painted one frame, it did not run"; with the real one
the TUI is still running 2s after its first frame and never logs the
raw-mode error.
The `process.env.CI` skip stays, but its comment is corrected: the PTY
removes the technical blocker it used to cite, so the smoke is local-only
by decision now, not by capability. Whether it joins GitHub CI is out of
scope here (#2146 is keeping the other local-only smokes out on purpose).
Signed-off-by: Cliff Hall <cliff@futurescale.com>
Signed-off-by: cliffhall <cliff@futurescale.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>under a pseudoterminal, asserts the Ink app renders its first frame within a timeout, then waits and asserts it is still running before shutting it down (a shallow boot/render/survival check, not full interaction).The second half is the assertion (#2147). Spawned with its stdin on
/dev/null, Ink cannot enter raw mode foruseInput, so the TUI painted one frame and exited 1 about 40ms later — and this smoke, which settled OK on the first frame, won that race and reported success on every machine. It is local-only (self-skips underCI), which is precisely where a false green goes unnoticed.
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.