Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inspector/
│ │ │ # vite-base-config.ts (shared optimizeDeps exclusions)
│ │ └── static/ # sandbox_proxy.html (served by sandbox-controller for MCP Apps tab)
│ ├── cli/ # CLI client (tsup bundle, @inspector/core alias)
│ │ ├── src/cli.ts # One-shot entry (`mcp-inspector --cli`)
│ │ ├── src/session/ # Session front-end (`mcpi` → mcp-bin.ts)
│ │ ├── src/daemon/ # Implicit Unix-socket session daemon
│ │ └── src/handlers/ # Shared runMethod + method types (one-shot + daemon)
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
│ ├── launcher/ # Shared launcher (relative imports into sibling build/ outputs)
├── core/ # Shared core code (no package.json — consumed via the `@inspector/core` vite alias)
Expand Down Expand Up @@ -188,7 +192,7 @@ gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-i
- **`validate` is fast: it runs `test`, not `test:coverage`.** The coverage gate (slower — adds v8 instrumentation, and for web the integration project) is a **separate** top-level `npm run coverage` (and per-client `coverage:web` / `coverage:cli` / `coverage:tui` / `coverage:launcher`, each delegating to that client's `test:coverage`). Run `npm run coverage` when you want to reproduce the gate locally before pushing. **CI runs `coverage`** on every push (#1550): the per-file ≥90 gate is CI-enforced, so a PR that drops any file below 90 on lines/statements/functions/branches fails the job. CI runs `validate` (fast) for format/lint/build/unit tests, then `coverage` for the instrumented gate. Because web's `test:coverage` already runs the integration project, CI has no separate `test:integration` step — the integration paths are exercised inside the coverage gate.
- Each client's `test:coverage` enforces a **uniform per-file gate of ≥ 90 on all four dimensions** — lines, statements, functions, and branches — across `clients/web`, `clients/cli`, `clients/tui`, and `clients/launcher` (CI enforces this gate). This is the result of a codebase-wide audit: the branch floor was first lifted 50 → 70 for web (#1271), then the whole gate raised to 90 with real tests added for every outlier. Genuinely-unreachable branches are **not** waved through by lowering the gate — they are annotated at the source with a justified `/* v8 ignore … -- <reason> */` comment. Acceptable reasons are happy-dom-inherent paths (Mantine portal mount points, `useMediaQuery` fallbacks, `typeof window` SSR guards), React StrictMode effect-replay blocks, and provably-dead defensive guards (e.g. a `?? fallback` for a value the types guarantee non-null, or a `Select.onChange` receiving a value outside the allowed list). New code must clear 90 on every dimension; reach for a justified `v8 ignore` only when a branch is genuinely impossible to exercise.
- The **same per-file gate** is enforced for the CLI and TUI (#1484), not just web:
- **CLI** (`clients/cli`): tests run **in-process** by importing `runCli()` (see `__tests__/helpers/cli-runner.ts`) so `clients/cli/src` is measured under v8 instrumentation. A thin out-of-process layer (`__tests__/e2e.test.ts` + `scripts/smoke-cli.mjs`) still spawns the built binary for the shebang/`process.exit` paths; `src/index.ts` (binary bootstrap) is the only coverage exclusion. `commander` uses `.exitOverride()` so a parse error throws instead of tearing down the test worker.
- **CLI** (`clients/cli`): tests run **in-process** by importing `runCli()` / `runMcp()` (see `__tests__/helpers/cli-runner.ts`, `mcp-runner.ts`) so `clients/cli/src` is measured under v8 instrumentation. A thin out-of-process layer (`__tests__/e2e.test.ts` + `scripts/smoke-cli.mjs`) still spawns the built binary for the shebang/`process.exit` paths. Coverage exclusions (bootstraps + hard-to-stabilize accept/stream races): `src/index.ts`, `src/mcp-bin.ts`, `src/daemon/run.ts`, `src/daemon/ipc-glue.ts`, `src/daemon/stream-client.ts` — see `clients/cli/vitest.config.ts` and [specification/v2_cli_v2.md](specification/v2_cli_v2.md) §8.2 for follow-ups. `commander` uses `.exitOverride()` so a parse error throws instead of tearing down the test worker.
- **TUI** (`clients/tui`): the gate covers the **non-React logic** only — `logger.ts`, `components/tabsConfig.ts`, and `utils/*` (server resolution lives in `core/` and is measured by the web suite). The Ink components, `App.tsx`, and `hooks/` are an **interim exclusion** in `clients/tui/vitest.config.ts` pending the renderer-based follow-up (#1501). When adding new **non-React** logic under `clients/tui/src`, it falls under the gate automatically — add tests for it.
- Run `npm run test:integration` (also from `clients/web/`) for the InspectorClient + transport + auth integration suite. It runs under a separate `integration` vitest project in node env (no happy-dom) with 30s timeouts. The script builds `test-servers/` first via `tsc -p ../../test-servers --noCheck` so the stdio MCP test server can be spawned as a real subprocess. CI does not run `test:integration` as its own step — the integration project is covered by the CI `coverage` gate, whose web `test:coverage` runs `--project=unit --project=integration --coverage`.
- Test files live alongside the source as `<Name>.test.tsx` (or `.test.ts` for non-React modules). Integration tests live under `clients/web/src/test/integration/`, mirroring the `core/` source layout (`mcp/`, `mcp/node/`, `mcp/remote/`, `auth/`, `auth/node/`, `storage/`). Any test file under that folder is automatically picked up by the `integration` vitest project (node env, 30s timeouts) via the folder glob in `vite.config.ts` — placement is the manifest, there is no enumeration to keep in sync. Tests outside the folder run in the `unit` project (happy-dom). When adding a new test for, e.g., `core/mcp/remote/foo.ts`, put it at `src/test/integration/mcp/remote/foo.test.ts`.
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ A developer tool for inspecting [Model Context Protocol](https://modelcontextpro
- **CLI** — a scriptable command-line client for automation, CI, and fast agent feedback loops.
- **TUI** — an interactive terminal UI built with [Ink](https://github.com/vadimdemedes/ink).

All three run through one global `mcp-inspector` binary:
All three run through one global `mcp-inspector` binary. The package also ships a session-oriented `mcpi` binary (connect once, many commands):

```bash
npx @modelcontextprotocol/inspector # web UI (default)
npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --cli # one-shot CLI
npx @modelcontextprotocol/inspector --tui # TUI
npx --package=@modelcontextprotocol/inspector mcpi servers/list # session CLI

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a toe-stubber (different command line for one client). Can the launcher not be the entry point and hand off to the mcpi?

```

> **Repo status.** This is the **v2** line of the Inspector (branch `v2/main`). The `main` branch is the legacy v1 implementation (bug fixes only). v2 will eventually replace `main`. See [`AGENTS.md`](./AGENTS.md) for branch/board conventions.
Expand All @@ -24,7 +25,7 @@ v2 is **not** an npm workspace. Each client under `clients/*` keeps its own `pac
inspector/
├── clients/
│ ├── web/ # Web client (Vite + React + Mantine). src/ = browser app; server/ = Node dev/prod backend
│ ├── cli/ # CLI client (tsup bundle, @inspector/core alias)
│ ├── cli/ # CLI client — one-shot (`cli.ts`) + session (`session/`, `daemon/`, `handlers/`)
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
│ └── launcher/ # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/ # Shared code consumed via the `@inspector/core` alias (no package.json)
Expand Down
83 changes: 66 additions & 17 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,62 @@
# MCP Inspector CLI Client

The CLI mode enables programmatic interaction with MCP servers from the command line. It is ideal for scripting, automation, continuous integration, and establishing an efficient feedback loop with AI coding assistants.
The CLI package provides two entrypoints:

## Running the CLI
- **One-shot** — `mcp-inspector --cli` (frozen contract): connect → one `--method` → disconnect.
- **Session** — `mcpi` (MCP Inspector session CLI): connect once, then run many commands against a named session.

You can run the CLI client directly via `npx`:
## Session CLI (`mcpi`)

```bash
# From a built tree / published package:
mcpi servers/list --config path/to/mcp.json # marks entries with a live session (@name / MRU)
mcpi servers/show test-stdio --config path/to/mcp.json
mcpi connect test-stdio --config path/to/mcp.json
mcpi connect my-http --config path/to/mcp.json --relogin # ignore stored OAuth; login only if auth required
mcpi auth/list
mcpi auth/clear https://example.com/mcp
mcpi auth/clear --all --yes
mcpi tools/list
mcpi tools/call echo message:=hi
mcpi tools/call echo '{"message":"hi"}' # or --tool-arg / --tool-args-json
mcpi @test-stdio resources/list # explicit session via @name
mcpi initialize --session test-stdio
mcpi logging/setLevel info
mcpi logging/tail # long-lived; Ctrl-C to stop
mcpi sessions/list
mcpi disconnect --session test-stdio
mcpi daemon status
mcpi daemon stop

# Optional: private daemon for this shell only (ssh-agent style)
eval "$(mcpi private)"
mcpi connect test-stdio --config path/to/mcp.json
mcpi tools/list
```

Slash methods match one-shot `--method` names (`tools/*`, `resources/*`, `prompts/*`, `logging/*`, `tasks/*`, `roots/*`, `initialize`). Streams (`logging/tail`, `resources/subscribe`) stay attached until Ctrl-C and honour `--format` (human lines or pretty JSON per event). Auth uses the shared `oauth.json` store. On connect, the daemon reuses stored tokens when it can; if auth is still required (or stored credentials cannot complete a silent refresh), `mcpi` runs interactive OAuth then retries (pass `--stored-auth-only` to refuse interactive login — also available on one-shot). Use `connect --relogin` (or one-shot `--relogin`) to ignore stored OAuth for that connect: it clears any `oauth.json` entry for the server URL, then connects; interactive login runs only if the server still requires auth (new tokens are saved). For stdio (or any target with no URL-keyed store entry) the flag is a no-op — there is nothing to clear and OAuth does not apply. Manage the store with `auth/list` and `auth/clear <url>` / `auth/clear --all` (`--yes` required for `--all` when non-interactive). On a TTY, the CLI prints a clickable authorization URL (OSC 8) and opens the default browser; non-TTY / CI only prints the plain URL. On the session CLI, authentication is handled at **connect** time only; mid-session re-login / step-up during a later command is available on one-shot `--cli`, not on `mcpi`.

Sessions are kept by a background process under `~/.mcp-inspector/` by default (shared across terminals). Set `MCP_STORAGE_DIR` to isolate parallel runs (daemon + OAuth store). For a daemon that only this shell can use, run `eval "$(mcpi private)"` once — later `mcpi` commands in that shell inherit the private binding.

**Output (session only — different from one-shot):**

| Flag | Behaviour |
| ---- | --------- |
| `--format text` (default) | Human-readable views (tool lists, call results, sessions, …). On a TTY, uses ANSI styling (color, bold/dim, OSC 8 links). |
| `--format json` | Pretty-printed JSON of the payload (no `{ "result" }` envelope; never ANSI) |
| `--plain` | Disable ANSI styling in human text (also honours `NO_COLOR`) |

Put global flags before the subcommand: `mcpi --format json tools/list`, `mcpi --plain tools/list`. Isolate parallel runs with `MCP_STORAGE_DIR` (scopes the daemon socket + OAuth store). One-shot `--cli` keeps its own format contract (`text` = pretty JSON, `json` = `{ result }` envelope).

## One-shot CLI (`mcp-inspector --cli`)

You can run the one-shot CLI via `npx`:

```bash
npx @modelcontextprotocol/inspector --cli node build/index.js
```

The CLI mode supports operations across tools, resources, and prompts, returning structured JSON output.
Supports tools, resources, and prompts (plus `--method servers/list` / `servers/show` for catalog entries without connecting).

### Examples

Expand Down Expand Up @@ -140,13 +186,17 @@ A `tools/call` that returns `isError:true` still prints its payload but exits `5

### CLI-specific (OAuth for HTTP servers)

The CLI runs the same loopback callback server as the TUI (`http://127.0.0.1:6276/oauth/callback` by default). On connect **401** or mid-session interactive auth (re-login / step-up), it:
The CLI runs the same loopback callback server as the TUI (`http://127.0.0.1:6276/oauth/callback` by default).

**One-shot (`mcp-inspector --cli`):** on connect **401** or mid-session interactive auth (re-login / step-up), it:

1. Starts the callback listener on `--callback-url` (or `MCP_OAUTH_CALLBACK_URL`)
2. Prints the authorization URL to the console (`ConsoleNavigation`)
3. Waits for the browser redirect, exchanges the code, and retries connect or the failed RPC

**Step-up (standard OAuth):** when an RPC needs extra scopes, the CLI prompts on stderr: `Proceed with step-up authorization? [y/N]`. **y** continues; **N** exits with an error. EMA step-up re-mints silently (no prompt).
**Step-up (standard OAuth, one-shot only):** when an RPC needs extra scopes, the CLI prompts on stderr: `Proceed with step-up authorization? [y/N]`. **y** continues; **N** exits with an error. EMA step-up re-mints silently (no prompt).

**Session (`mcpi`):** if connect requires auth, interactive OAuth runs and connect is retried. Mid-session step-up during a later command (for example `tools/call`) is not available on the session CLI — use one-shot `--cli` when you need that, or disconnect and connect again after refreshing tokens.

**Shared OAuth storage:** the CLI **reuses** tokens from `~/.mcp-inspector/storage/oauth.json` when they already exist (same file as other Inspector clients). That is passive file sharing, not launching another app.

Expand Down Expand Up @@ -256,18 +306,17 @@ While the Web Client provides a rich visual interface, the CLI is designed for:
Like the other clients, the CLI self-validates from its own folder:

```bash
npm run validate # format:check && lint && test:coverage
npm run validate # format:check && lint && test (fast; no coverage gate)
npm test # build test-servers + binary, then run all tests
npm run test:coverage # build + tests under the per-file coverage gate
npm run test:coverage # build + tests under the per-file ≥90 coverage gate
```

The CLI's `test:coverage` **builds the binary first** (its out-of-process
`e2e.test.ts` spawns it, so it must run against a fresh build). `validate`
therefore folds the build into `test:coverage` rather than repeating it — it is
`format:check && lint && test:coverage`, with no separate `build` step (the
other clients, whose tests don't spawn their bundle, keep an explicit `build`).
The repo-root `validate:cli` just delegates here.
The CLI's `test` / `test:coverage` **build the binary first** (out-of-process
`e2e.test.ts` spawns it). `validate` is `format:check && lint && test` with no
separate `build` step (`pretest` builds). Repo-root `validate:cli` delegates
here; the coverage gate is `npm run coverage` / `coverage:cli` (also in
`npm run ci`), matching AGENTS.md.

Tests run the CLI **in-process** (importing `runCli()`) so `src/` is measured
under coverage, with a thin out-of-process spawn layer for the real binary. See
[`__tests__/README.md`](./__tests__/README.md) for details.
Tests run the CLI **in-process** (importing `runCli()` / `runMcp()`) so `src/`
is measured under coverage, with a thin out-of-process spawn layer for the real
binary. See [`__tests__/README.md`](./__tests__/README.md) for details.
9 changes: 6 additions & 3 deletions clients/cli/__tests__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ root provides a further end-to-end check of the binary.)
- `cliOAuth.test.ts` - Unit tests for `cliOAuth.ts` (step-up confirm, helper wiring, retry)
- `oauth-interactive.test.ts` - **Integration** smoke parity for CLI interactive OAuth: connect-time callback server + step-up **y/N** against composable `TestServerHttp` (auto-completes authorize URL programmatically; not a subprocess binary e2e)
- `e2e.test.ts` - Out-of-process spawn of the built binary (exit codes + boot; no OAuth)
- `mcp-session.test.ts` / `mcp-coverage.test.ts` / `mcp-auth-coverage.test.ts` - Session CLI (`runMcp`) against the daemon
- `daemon-*.test.ts` / `daemon-private.test.ts` / `dispatch.test.ts` / `format-session.test.ts` / `parse-tool-args.test.ts` / `style.test.ts` - Session daemon (incl. `mcpi private` IPC token), dispatch, and formatting

## Helpers

The `helpers/` directory contains shared utilities:

- `cli-runner.ts` - Invokes `runCli()` in-process and captures stdout/stderr + exit code
- `mcp-runner.ts` - Invokes `runMcp()` in-process for the session CLI
- `assertions.ts` - Custom assertion helpers for CLI output validation
- `fixtures.ts` - Test config/catalog file generators and temporary directory management

Expand All @@ -67,7 +70,7 @@ and the bundled stdio server is launched via `getTestMcpServerCommand()`.
a file run sequentially
- Config files use `crypto.randomUUID()` for uniqueness
- HTTP/SSE servers use dynamic port allocation to avoid conflicts
- Coverage is enforced per-file (lines ≥ 90, statements ≥ 85, functions ≥ 80,
branches ≥ 50). `src/index.ts` (the binary bootstrap) is excluded because it
only runs in the spawned binary, which coverage can't instrument
- Coverage is enforced per-file at ≥90 on lines, statements, functions, and
branches. Exclusions: `src/index.ts`, `src/mcp-bin.ts`, `src/daemon/run.ts`,
`src/daemon/ipc-glue.ts`, `src/daemon/stream-client.ts` (see `vitest.config.ts`)
- All tests use the built-in MCP test servers — no external/registry dependencies
Loading
Loading