inspector/clients/cli
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Cliff Hall a029e42a19
cli: Wave 3 — programmatic review path (rollup of #1573–#1575) (#1655)
* feat(cli): programmatic ergonomics — initialize, tool-args-json, connect-timeout, env-var fixes

Adds the #1573 programmatic-ergonomics surface to the CLI, re-implemented
informed by PR #1510 and adapted to the v2/main OAuth-wired cli.ts:

- `--method initialize` — connect-only probe emitting
  `{serverInfo, protocolVersion, capabilities, instructions}`.
- `--tool-args-json '{"k":"v"}'` — tool args passed verbatim as JSON (no
  key=value coercion); mutually exclusive with `--tool-arg`, with clear
  errors for combined use, malformed JSON, and non-object values.
- `--connect-timeout <ms>` — default 15000 for ad-hoc `--server-url`/target
  runs, file-level timeout otherwise; `0` disables. Backed by the exported
  `withConnectTimeout()` helper.
- MCP_CATALOG_PATH is ignored when an ad-hoc target is given, so a shell that
  exports it can still run one-off ad-hoc invocations.
- MCP_STORAGE_DIR is honored by core `getStateFilePath` (`<dir>/oauth.json`),
  below the per-file MCP_INSPECTOR_OAUTH_STATE_PATH override.

Tests: new programmatic-ergonomics.test.ts (CLI, in-process runCli + direct
withConnectTimeout unit tests) and getStateFilePath resolution cases in the
web storage integration suite. Per-file coverage gate stays ≥90.
README: documents the new flags and env-var semantics.

Closes #1573

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

* feat(cli): --app-info probe + --format json envelope

Adds the #1574 app-probing and machine-readable output surface, re-implemented
informed by PR #1510 and adapted to the v2/main OAuth-wired cli.ts.

- `--method tools/call --tool-name <t> --app-info` — prints one JSON line
  describing the tool's app posture (hasApp, resourceUri, csp, permissions,
  domain, …) WITHOUT invoking the tool; exit 0 when it has an app, 2 (no_app)
  when it doesn't. A missing tool exits 5 (tool_not_found) — distinct from the
  no-app exit so a typo isn't mistaken for "no app".
- `--method tools/list --app-info` — one NDJSON line per tool over a single
  connection (`| jq -c 'select(.hasApp)'`).
- `--format json` — a single stdout envelope (`{result}` plus `{appInfo}` as a
  sibling key for App tools), no banner, so any method pipes into jq.
- A tools/call returning isError:true now exits 5 (tool_is_error) after
  printing its payload, so && chains stop on a failed call.

Implementation: `collectAppInfo()` (core `extractAppInfo` + a tolerated
resource read that reports `resourceError`), an NDJSON loop in tools/list, and
a single `emitResult()` that owns format + exit-code handling. `runMethod` now
returns a `MethodOutcome` discriminated union so the NDJSON path can signal
"already emitted".

Tests: app-info.test.ts (exit-0/2/5 + NDJSON + no-invoke), format-json.test.ts
(envelope shapes + bad --format), emit-result.test.ts (direct unit coverage of
the isError→exit-5 and resourceError branches via exported emitResult/
collectAppInfo). Updated the tools.test "nonexistent tool" case to the new
exit-5 tool_not_found contract. Per-file coverage ≥90 on all four dimensions
(cli.ts 100% stmts/funcs/lines). README documents the flags, NDJSON shape, and
exit semantics.

Closes #1574

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

* feat(cli): stored-auth suite — --use-stored-auth, --wait-for-auth, --list-stored-auth, --print-handoff

Adds the #1575 web→CLI OAuth handoff surface, re-implemented informed by PR
#1510 and adapted to v2/main.

- `--use-stored-auth` — read the shared oauth.json and inject
  `Authorization: Bearer <token>` for --server-url; exit 3 (no_stored_token),
  listing stored keys, when none matches.
- `--wait-for-auth <sec>` — poll oauth.json (500ms) until a token for
  --server-url appears, then proceed; timeout → exit 3 (auth_wait_timeout).
- `--list-stored-auth` — print `{oauthStatePath, storedServerUrls}` and exit
  (short-circuit, no connect).
- `--print-handoff` — emit `{serverUrl, deepLink, portForwardCmd,
  oauthStatePath, apiToken, note}` for --server-url and exit.

Adaptations vs. the reference:
- State-path resolution reuses core `getStateFilePath()` (the
  MCP_INSPECTOR_OAUTH_STATE_PATH → <MCP_STORAGE_DIR>/oauth.json → default
  precedence added in #1573) instead of a CLI-local resolver.
- The state file is read through the shared isomorphic
  `parseOAuthPersistBlob`, so both the plain `{servers,idpSessions}` layout the
  web backend writes and the legacy `{state,version}` envelope are accepted.
- `parseArgs` now returns a ParseResult union so the short-circuit modes return
  without a server connection.

Blind injection: the token is passed as-is; a stale one surfaces as HTTP 401 →
exit 3. Documented in the README along with all four flags, the state-path
precedence, and a follow-up note that --print-handoff's deepLink shape is
interim pending #1576's canonical handoff format.

Tests: stored-auth.test.ts drives injection, --header coexistence, missing
--server-url, no-token exit-3, URL-normalisation matching, MCP_STORAGE_DIR
resolution, listing, handoff (+note), wait-for-auth polling/timeout, and
normalizeServerUrl units — all against a real in-process HTTP test server.
Per-file coverage ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines).

Closes #1575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

* docs(core): scope MCP_STORAGE_DIR comment in getStateFilePath to the OAuth backend

Address review of PR #1651: the doc comment claimed MCP_STORAGE_DIR was
honoured "across the app, so setting it once points every persist backend at
the same directory", but the branch only relocates oauth.json. Narrow the
comment to state it is OAuth-backend-only (client.json/mcp.json still resolve
from getDefaultStorageDir → HOME) and record why the resolution lives here
rather than in getDefaultStorageDir (the default-path constant is evaluated at
module load, so a runtime-set MCP_STORAGE_DIR — as the CLI stored-auth /
--wait-for-auth flows set it — must be read at call time). Comment-only; no
behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

* review(cli): per-tool-tolerant app-info listing + drop dead helper (PR #1653)

Address review of PR #1653:

- collectAppInfo now never throws: a malformed `_meta.ui.resourceUri`
  (extractAppInfo throws) folds into `{hasApp:false, resourceError}` just like a
  resources/read failure. This makes `tools/list --app-info` per-tool tolerant —
  one bad tool no longer aborts the whole NDJSON listing with a generic exit-1.
  Guarding inside collectAppInfo (vs. a loop-local try/catch) keeps the branch
  unit-testable; added an emit-result.test.ts case for the malformed-URI path.
- Documented that `tools/list --app-info` always emits NDJSON regardless of
  `--format` (the per-tool list shape is fixed; --format json only reshapes the
  single-result paths) — in both the code comment and the README.
- Removed the now-unused `expectJsonError` helper (its only caller moved to the
  exit-5 tool_not_found contract).

Coverage stays ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

* review(cli): stored-auth follow-ups — TODO(#1576), wait-for-auth guard test, docs (PR #1655)

Address review of PR #1655:

- Added an explicit TODO(#1576) on buildHandoff's deepLink: `transport:"http"`
  is hardcoded even for SSE, and `autoConnect=<token>` is not one of the three
  token sources the web app reads (per CLAUDE.md) — both to be reconciled with
  #1576's canonical handoff format.
- Added a test for `--wait-for-auth` without `--server-url`, covering the
  previously-untested `--wait-for-auth` arm of the shared missing-server-url
  guard's message ternary.
- Documented that `--list-stored-auth` / `--print-handoff` are mutually
  exclusive short-circuit modes (list takes precedence) and that a stored
  refresh_token is not yet used (noted as a follow-up in code + README).

No behavior change; coverage stays ≥90 on all four dimensions (cli.ts 100%
stmts/funcs/lines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:14:23 -04:00
..
__tests__ cli: Wave 3 — programmatic review path (rollup of #1573–#1575) (#1655) 2026-07-11 22:14:23 -04:00
src cli: Wave 3 — programmatic review path (rollup of #1573–#1575) (#1655) 2026-07-11 22:14:23 -04:00
eslint.config.js refactor: self-validating clients, fast validate + separate coverage/smoke (#1504) (#1505) 2026-06-21 11:41:33 -04:00
package-lock.json Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1 2026-07-09 09:41:08 -04:00
package.json refactor(version): single-source the version from the root package.json 2026-07-10 17:31:21 -04:00
README.md cli: Wave 3 — programmatic review path (rollup of #1573–#1575) (#1655) 2026-07-11 22:14:23 -04:00
tsconfig.json feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452) 2026-06-14 20:27:49 -04:00
tsup.config.ts build(publish): fix v2 single-package bundle and add pack:verify 2026-07-10 12:36:26 -04:00
vitest.config.ts test: raise per-file coverage gate to 90 across all clients (closes #1271) (#1532) 2026-06-27 15:26:03 -04:00

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.

Running the CLI

You can run the CLI client directly via npx:

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

The CLI mode supports operations across tools, resources, and prompts, returning structured JSON output.

Examples

Basic usage

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

With a configuration file

npx @modelcontextprotocol/inspector --cli --config path/to/config.json --server myserver

List available tools

npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

Call a specific tool

npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg key=value --tool-arg another=value2

Call a tool with JSON arguments

npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg 'options={"format": "json", "max_tokens": 100}'

List available resources

npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list

List available prompts

npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list

Remote Servers

You can also connect to remote MCP servers using HTTP or SSE transports.

Connect to a remote MCP server (default is SSE)

npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com

Connect with Streamable HTTP transport

npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list

Pass custom headers

npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key"

When a server is loaded from a --catalog/--config file, its per-server settings (headers, connection/request timeouts, and OAuth) are applied to the connection — the same resolution the TUI uses. A --header flag overrides the file's headers for that run while leaving the file's timeouts and OAuth in place.

Environment-variable semantics. MCP_CATALOG_PATH is honored only when no ad-hoc target is given (positional command, --server-url, or --transport) — so a shell that exports it can still run one-off ad-hoc invocations without hitting the catalog/ad-hoc conflict. MCP_STORAGE_DIR sets the storage directory used by the OAuth persist backend (<MCP_STORAGE_DIR>/oauth.json); the per-file MCP_INSPECTOR_OAUTH_STATE_PATH override still takes precedence over it.

HTTP proxy support

Connections to remote HTTP/SSE servers honor the conventional proxy environment variables: HTTPS_PROXY / HTTP_PROXY (and their lowercase forms) select the proxy, and NO_PROXY exempts hosts. This applies to the Node transport shared by the CLI and the web backend — no inspector-specific flag is needed. When a proxy variable is set, outbound requests are routed through undici's EnvHttpProxyAgent.

Proxy routing is powered by the undici package (^8.5.0, which requires Node >= 22.19.0 — the inspector's supported floor). It is imported lazily only when a proxy variable is set, so runs without a proxy configured pay no cost.

Options

MCP server (which server to connect to)

Options that specify the MCP server (catalog/config file, ad-hoc command/URL, env vars, headers) are shared by the Web, CLI, and TUI and are documented in MCP server configuration: --catalog (writable catalog, seeded if missing; default ~/.mcp-inspector/mcp.json or MCP_CATALOG_PATH), --config (read-only session, errors if absent), --server, -e, --cwd, --header, --transport, --server-url, and the positional [target...]. --catalog and --config are mutually exclusive, and neither combines with an ad-hoc target.

CLI-specific (what to invoke)

Option Description
--method <method> MCP method to invoke. Supports initialize (connect-only probe → {serverInfo, protocolVersion, capabilities, instructions}), tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, logging/setLevel.
--tool-name <name> Tool name (for tools/call).
--tool-arg <key=value> Tool argument; repeat for multiple. Use key='{"json":true}' for JSON. Values are coerced (JSON-parsed, so count=1 becomes a number).
--tool-args-json <json> Tool arguments as a single JSON object (e.g. '{"zip":"10001"}'). Passed verbatim — no key=value coercion, so "012" stays a string. Mutually exclusive with --tool-arg.
--uri <uri> Resource URI (for resources/read).
--prompt-name <name> Prompt name (for prompts/get).
--prompt-args <key=value> Prompt arguments; repeat for multiple.
--log-level <level> Logging level for logging/setLevel (e.g. debug, info).
--metadata <key=value> General metadata (key=value); applied to all methods.
--tool-metadata <key=value> Tool-specific metadata for tools/call.
--connect-timeout <ms> Connection timeout in ms. Defaults to 15000 for ad-hoc --server-url/target runs (so a black-holed host fails fast) and to the file-level timeout for --catalog/--config runs. 0 disables the timeout.
--app-info Probe a tool's MCP App UI metadata without invoking it. With --method tools/call --tool-name <name>: prints one JSON line (hasApp, resourceUri, csp, permissions, domain, …) and exits 0 if the tool has an app or 2 (no_app) if not. With --method tools/list: emits NDJSON — one app-info line per tool over a single connection.
--format <text|json> Output format. text (default) pretty-prints the result. json emits a single JSON object on stdout ({ "result": … }, plus { "appInfo": … } as a sibling key for App tools) with no banners, so the whole output pipes cleanly into jq.

App probing (--app-info) and machine-readable output (--format json)

--app-info inspects a tool's MCP App UI posture without calling the tool, so a pipeline can decide whether to open a browser before touching one:

# Probe one tool. Exits 0 (has app) or 2 (no_app). One JSON line on stdout.
mcp-inspector --cli <server> --method tools/call --tool-name my_tool --app-info
# → {"hasApp":true,"toolName":"my_tool","resourceUri":"ui://…","csp":{…},"permissions":{…},"prefersBorder":true,"resourceMimeType":"text/html"}

# Probe every tool at once — NDJSON, one line per tool, single connection.
mcp-inspector --cli <server> --method tools/list --app-info | jq -c 'select(.hasApp)'

Exit semantics: a tool that has an app exits 0; one with no app exits 2 (no_app); a missing tool exits 5 (tool_not_found) — distinct so a typo isn't mistaken for "no app". A probe failure (an unreadable UI resource, or a malformed _meta.ui.resourceUri) is tolerated and reported in a resourceError field rather than aborting — so in tools/list --app-info one bad tool never kills the rest of the listing.

--format json wraps any method's output in a single stdout envelope with no banners, so App tools and plain tools both pipe cleanly into jq:

mcp-inspector --cli <server> --method tools/call --tool-name my_app_tool --format json
# → {"result":{…tool result…},"appInfo":{"hasApp":true,"resourceUri":"ui://…",…}}

tools/list --app-info always emits NDJSON (one raw app-info object per line) regardless of --format — the per-tool list shape is fixed. --format json only reshapes the single-result paths (tools/call, tools/list without --app-info, etc.) into the {result[, appInfo]} envelope.

A tools/call that returns isError:true still prints its payload but exits 5 (tool_is_error) so && chains don't proceed on a failed call.

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:

  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).

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.

Shared with TUI (config only, not interactive login):

  • Per-server OAuth fields from mcp.json (static client, EMA resource credentials, scopes)
  • Install-level settings from ~/.mcp-inspector/storage/client.json (or --client-config / MCP_CLIENT_CONFIG_PATH) — EMA IdP, CIMD
  • CLI flags --client-id, --client-secret, --client-metadata-url override client.json when set
  • Keychain-backed secrets in mcp.json are rehydrated on catalog load (same as TUI)

OAuth callback URL

Surface Default callback
Web http://localhost:6274/oauth/callback
TUI http://127.0.0.1:6276/oauth/callback (interactive — callback server)
CLI http://127.0.0.1:6276/oauth/callback (interactive — same callback server as TUI)

Register http://127.0.0.1:6276/oauth/callback on static or enterprise IdPs that require pre-registered redirect URIs before using the TUI or CLI. Override with --callback-url or MCP_OAUTH_CALLBACK_URL. Only one process should bind the default port at a time.

Flags

Option Env Description
--client-config <path> MCP_CLIENT_CONFIG_PATH Install-level client config (default: ~/.mcp-inspector/storage/client.json).
--client-id <id> — OAuth client ID (static client); overrides client.json.
--client-secret <secret> — OAuth client secret; overrides client.json.
--client-metadata-url <url> — CIMD metadata URL; overrides client.json.
--callback-url <url> MCP_OAUTH_CALLBACK_URL Redirect URI sent to the authorization server (default: http://127.0.0.1:6276/oauth/callback).

Example — list tools on an OAuth-protected server using stored tokens and CIMD from the command line:

npx @modelcontextprotocol/inspector --cli --catalog mcp.json --server my-http-server \
  --client-metadata-url https://example.com/.well-known/oauth/client-metadata.json \
  --method tools/list

See EMA / enterprise-managed auth and OAuth smoke testing (§3 Stytch/CIMD; §5 mid-session manual validation — CLI C1–C2).

Stored-auth (web → CLI handoff)

For the common case where OAuth was already completed in the web inspector on the same machine, the CLI can reuse the resulting token instead of running its own interactive flow. It reads the shared OAuth state file (the oauth.json the web backend writes) directly from disk and injects Authorization: Bearer <token> for --server-url.

Option Description
--use-stored-auth Read the stored access token for --server-url and inject it as Authorization: Bearer. Exits 3 (no_stored_token) — listing the stored keys — when no token matches. Requires --server-url.
--wait-for-auth <sec> Poll the OAuth state file (500 ms interval) until a token for --server-url appears, then proceed as if --use-stored-auth were set. Times out at <sec> with exit 3 (auth_wait_timeout). Use after handing off to a human to complete OAuth in a browser.
--list-stored-auth Print { oauthStatePath, storedServerUrls } (the server keys that currently have a token) and exit. No server connection is made.
--print-handoff Print a JSON handoff block (deepLink, portForwardCmd, oauthStatePath, apiToken) for --server-url and exit — everything a script/remote VM needs to drive the browser-side OAuth dance. Requires --server-url.

State-file resolution follows MCP_INSPECTOR_OAUTH_STATE_PATH → <MCP_STORAGE_DIR>/oauth.json → ~/.mcp-inspector/storage/oauth.json — the same precedence the rest of the Inspector uses, so the CLI and web backend agree on the file. Server keys are canonicalised with new URL().href (the scheme the web store writes), so a trailing-slash or case mismatch between the URL a human opened and the one the agent passed still resolves.

Blind injection. The token is injected as-is; the CLI does not validate or refresh it (a stored refresh_token is not yet used — a natural follow-up). A stale/expired access token surfaces as an HTTP 401 on the first request → exit 3 (auth_required). Re-complete the flow in the web inspector (or use --wait-for-auth) and retry.

Short-circuit modes. --list-stored-auth and --print-handoff each print their output and exit without connecting to a server; they ignore the method/target flags. They are mutually exclusive — if both are passed, --list-stored-auth takes precedence.

The deepLink shape emitted by --print-handoff is interim, pending the web deep-link auto-connect work (#1576); it will be reconciled with that issue's canonical handoff format once it lands.

# On a remote VM: print what a human needs to complete OAuth in their browser.
mcp-inspector --cli --server-url https://api.example/mcp --print-handoff

# Then block until the token lands and run the call with it.
mcp-inspector --cli --transport http --server-url https://api.example/mcp \
  --wait-for-auth 120 --method tools/list

Exit codes & error envelopes

Every non-zero exit maps to a stable failure class, so a programmatic caller (CI, a script, an agent) can branch on why the CLI failed without scraping prose from stderr:

Code Meaning
0 Success.
1 Usage / unexpected error (the catch-all).
2 No MCP App found on the tool (--app-info probe).
3 Server requires authentication (401/403, WWW-Authenticate, OAuth).
4 Server unreachable (DNS, connection refused, timeout, fetch failed).
5 Tool error (tools/call returned isError:true, or the tool was not found).

On any non-zero exit the CLI also writes a single JSON line to stderr — the ErrorEnvelope:

{ "error": { "code": "auth_required", "message": "Unauthorized", "status": 401, "url": "https://api.example/mcp" } }

The code is a stable identifier for the failure class; message is the human-readable error; cause, status, and url are included when known. Because it is one line, a caller can parse it with 2>&1 | tail -1 | jq .error.

Why use the CLI?

While the Web Client provides a rich visual interface, the CLI is designed for:

  • Automation: Ideal for CI/CD pipelines and batch processing.
  • AI Coding Assistants: Provides a direct, machine-readable interface (JSON) for tools like Cursor or Claude to verify changes immediately.
  • Log Analysis: Easier integration with command-line utilities (like jq) to process and analyze MCP server output.

Development

Like the other clients, the CLI self-validates from its own folder:

npm run validate       # format:check && lint && test:coverage
npm test               # build test-servers + binary, then run all tests
npm run test:coverage  # build + tests under the per-file 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.

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 for details.