| Filename | Latest commit message | Latest commit date |
|---|---|---|
* chore: close a batch of quality-gate & tidy gaps (#1785 #1787 #1789 #1790 #1767) Bundles five small, related tidy/gate items into one PR: - #1785: delete the dead src/types/navigation.ts InspectorTab (0 importers; duplicated/drifted from utils/inspectorTabs InspectorTabId). src/types/ now holds only ambient .d.ts stubs. - #1787: add src/hooks/** to the web coverage `include` (it was silently outside the ≥90 gate) and add tests for the two untested hooks, useServerJsonImport and useImportClientConfig, bringing every hook to ≥90 on lines/statements/functions/branches. - #1789: widen clients/{cli,tui,launcher} format/format:check to their top-level configs; add root format:shared / format:check:shared covering test-servers/src/**, vitest.shared.mts, and the repo-root eslint.config.js. - #1790: pin prettier to an exact version (3.8.4, dropping the ^3.8.1 caret) in all five package.json files so the format gate's verdict can't shift with an in-range patch bump; lockfiles synced. - #1767: its root-file format+lint gap is closed here (format:shared + a new lint:shared via a second files block in the root eslint.config.js; web/server/ was already gated in #1788). Its larger cli/tui __tests__ typecheck piece (76 type errors) is split out to #1791. Prettier-normalized the newly-covered files (pure formatting). No runtime behavior change. `npm run ci` green. Closes #1785 Closes #1787 Closes #1789 Closes #1790 Closes #1767 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * chore: address PR #1792 review findings - #1789 gap: widen clients/web format to include .storybook and clients/ launcher format to include scripts/; reformat the 3 .storybook files (were non-conforming under the pinned prettier 3.8.4). Audited all 885 tracked source files — now fully prettier-clean. - Export VALIDATE_DEBOUNCE_MS / COLLAPSE_DELAY_MS / HIGHLIGHT_DURATION_MS from useServerJsonImport and use them in the test instead of hardcoded 300/1000/250 (so a tuned constant can't silently desync the test). - Assert the symmetric renameErrors["alpha"] alongside ["beta"] so a one-directional collision-check regression fails. - Comment the blank-rename test as exercising a UI-unreachable defensive branch (canImport gates submit). - Un-stale the two specification/v2_ux_interfaces_plan.md references to the now-removed navigation.ts InspectorTab. npm run ci green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs: sync AGENTS.md format-surface description with the widened globs (#1792 review r2) 1ce3b64 added .storybook to clients/web's format glob and scripts/ to clients/launcher's, but AGENTS.md:222 still described the pre-fix scope. Bring it in line and note the non-recursive top-level glob is why nested dirs are named explicitly. Docs only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * chore: handle PR #1792 round-3 follow-ups in-place (format-coverage guard, uniform globs, theme gate) Rather than defer these to tickets, fold them into this PR: - Obs 2: widen every client + root format glob to a uniform *.{ts,tsx,mts,cts,js,jsx,mjs,cjs} so a future .mts/.cjs/.tsx top-level file can't silently slip the gate. - Obs 1: add scripts/verify-format-coverage.mjs (wired into `validate`) — it parses the format:check globs out of every package.json, enumerates all tracked source files, and fails listing any not covered by a glob. The durable, standing version of #1789's one-shot audit; negative-tested. - Obs 3: bring src/theme/** under the coverage gate (21/23 files were already ≥90 transitively; add Paper.test.tsx for the one laggard, Paper.ts). src/App.tsx (~4.5k lines, ~42% branches) is converted from a silent omission into an explicit, documented exclusion (comment on the coverage `include` + AGENTS.md) — gating it is a dedicated effort, not a whitelist tweak. npm run ci green (the new guard reports 885/885 tracked source files gated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * chore: address PR #1792 round-4 findings (guard hardening, docs, exclusion list) - Harden verify-format-coverage: only harvest format:check globs from scripts transitively reachable from `validate` (via npm run refs), so an unwired/renamed format:check no longer counts as coverage. Negative-tested — renaming clients/web format:check to an unreferenced name now fails the guard (627 files flagged) instead of passing. - Name src/main.tsx and src/index.ts (browser + bin bootstraps) alongside src/App.tsx in the coverage-include comment and AGENTS.md, so the documented exclusion list is complete rather than claiming App.tsx is the only one. - Doc sync: document verify:format-coverage (AGENTS.md + README script table + scripts/ layout line), fix the `validate` ordering (verify:format- coverage now runs before validate:core) in AGENTS.md and README, and update the quoted globs to the uniform *.{ts,tsx,mts,cts,js,jsx,mjs,cjs} / test-servers *.{ts,tsx,mts,cts}. npm run ci green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * chore: address PR #1792 round-5 findings (airtight guard + doc/config sync) - verify-format-coverage: assert the root `validate` chain invokes each client's `validate` (cd <dir> && npm run validate), closing the last reachability edge — a client dropped from the root chain now fails the guard even though its own globs still exist. Negative-tested. - eslint.config.js: widen the shared `files` block to test-servers/src/**/*.{ts,tsx,mts,cts} to match the widened lint:shared glob, so a future .mts/.cts there is lint-gated, not just prettier-gated. - AGENTS.md: scope the "uniform extension set" claim to client globs (core stays {ts,tsx}, shared {ts,tsx,mts,cts}), with verify:format-coverage as the backstop — removing the self-contradiction with the line below. npm run ci green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| .. | ||
| __tests__ | ||
| src | ||
| eslint.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
| vitest.config.ts | ||
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-infoalways emits NDJSON (one raw app-info object per line) regardless of--format— the per-tool list shape is fixed.--format jsononly reshapes the single-result paths (tools/call,tools/listwithout--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:
- Starts the callback listener on
--callback-url(orMCP_OAUTH_CALLBACK_URL) - Prints the authorization URL to the console (
ConsoleNavigation) - 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-urloverrideclient.jsonwhen set - Keychain-backed secrets in
mcp.jsonare 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 auth for --server-url and inject Authorization: Bearer. When a refresh_token is stored, the CLI runs the OAuth refresh grant first and injects the fresh access token (persisting the rotation); otherwise it injects the stored access token. Exits 3 (no_stored_token) — listing the stored keys — when nothing matches. Requires --server-url. |
--wait-for-auth <sec> |
Poll the OAuth state file (500 ms interval) until an access token for --server-url appears, then inject it. Times out at <sec> with exit 3 (auth_wait_timeout). Use after handing off to a human to complete OAuth in a browser. Unlike --use-stored-auth, this injects the freshly-landed access token directly (a token that just completed the browser flow is not expired), so it does not run the refresh grant. |
--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.
Token refresh (#1665). When the stored server state carries a refresh_token (plus the clientInformation the web inspector persists after a completed flow), --use-stored-auth runs the SDK's OAuth refresh_token grant to mint a fresh access token before connecting, then writes the rotated tokens back to the state file (owner-only 0o600, via the shared store writer) so web and CLI stay consistent. The auth-server metadata is reused from the stored state, or discovered from --server-url when absent. This covers both an absent and an expired stored access token — the persisted blob carries no expiry, so the refresh token is treated as the durable credential. If the refresh fails (revoked token, transient auth-server error) and a stored access token is also present, the CLI falls back to injecting that token rather than hard-failing; only when there is nothing to fall back on does it exit 3 (auth_required, envelope refresh_failed). Without a stored refresh_token the access token is injected as-is, and a stale one surfaces as an HTTP 401 → exit 3.
Because there is no stored expiry, a refresh_token is refreshed on every --use-stored-auth run. Two consequences with rotating refresh tokens: two concurrent invocations against the same state file race the single-use token (one wins), and a crash between a successful grant and the write-back leaves the rotated token unsaved. Both are narrow; re-authorize in the web inspector to recover.
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 is the canonical web deep-link (#1576) — http://<host>:<port>/?serverUrl=<url>&transport=<http|sse>&autoConnect=<token> — so navigating it in a browser reaches a connected inspector in one shot. transport is derived from the resolved server (--transport, else auto-detected from the URL path: /sse → sse, else http), not hardcoded. autoConnect is set to MCP_INSPECTOR_API_TOKEN; when that env var is unset the link is still emitted but a note field flags that the web app's autoConnect gate will reject it until the inspector is launched with a known token.
# 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.