Closes #1822 Adds docs/v1-to-v2-migration.md — the map for users arriving from v1: - CLI flag mapping (every v1 flag survives; lists what v2 adds) - `--config` vs `--catalog` semantics with before/after examples - Node engine bump (>=22.7.5 → >=22.19.0) - what no longer ships (the three sub-packages; v2 is one tarball) - the three behavior changes most likely to bite: exit code 5 on a failing tool call, target-must-come-first under --cli, and the reversed `--` separator - env-var mapping (MCP_PROXY_AUTH_TOKEN → MCP_INSPECTOR_API_TOKEN, MCP_PROXY_FULL_ADDRESS removed, SERVER_PORT repurposed), the v1 UI config settings, web query params, and Docker Linked from the root README (banner + docs list), the CLI README, and docs/mcp-server-configuration.md; AGENTS.md's docs/ tree entry updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
26 KiB
Migrating from Inspector v1 to v2
v2 is a rewrite. The package name and the npx @modelcontextprotocol/inspector entry point are unchanged, but the architecture, the CLI surface, and the way you point the Inspector at a server all changed.
This guide is the map. It covers what breaks, what it becomes, and the handful of places where a v1 command still runs in v2 but means something different — the ones worth reading before you conclude v2 is broken.
v1 is deprecated and receives security fixes only, published to the
v1-latestnpm dist-tag. To stay on it, pin it explicitly:npx @modelcontextprotocol/inspector@v1-latest
At a glance
| v1 | v2 | |
|---|---|---|
| Node | >=22.7.5 |
>=22.19.0 |
| npm packages | 4 (inspector + -client / -server / -cli) |
1 (@modelcontextprotocol/inspector) |
| Processes / ports | web UI on 6274 + MCP proxy on 6277 |
one web server on 6274 (plus a dynamic MCP Apps sandbox port) |
| Modes | web, --cli |
web, --cli, --tui |
| Server list | per-browser, in localStorage |
a catalog file on disk (~/.mcp-inspector/mcp.json) |
| Auth token env | MCP_PROXY_AUTH_TOKEN |
MCP_INSPECTOR_API_TOKEN |
| CLI exit codes | 0 / 1 |
0–5, plus a JSON error envelope on stderr |
The three changes most likely to bite an existing setup:
--configno longer means what it did — see--configvs--catalog.- A failing tool call now exits non-zero (
5), where v1 exited0. CI scripts that ran&&chains past a failed call will start failing — correctly. SERVER_PORTwas the proxy port and now isn't — it survives as a fallback for the MCP Apps sandbox port. Setting it no longer moves anything you can browse.
Requirements
v2 requires Node >=22.19.0 (v1 required >=22.7.5). The floor comes from undici@^8, used for HTTP proxy support. npx will refuse to run on an older Node rather than fail obscurely later.
What no longer ships
v1 published four packages:
@modelcontextprotocol/inspector@modelcontextprotocol/inspector-client@modelcontextprotocol/inspector-server@modelcontextprotocol/inspector-cli
v2 publishes only the first. It is a single tarball with a single version number, containing the web client, the CLI, the TUI, and the launcher that dispatches between them. The three sub-packages are frozen at 1.0.1, deprecated on npm, and will never see a 2.x.
If you depend on one of them directly, drop it and use the root package's mcp-inspector bin. There is no v2 equivalent of importing inspector-server as a library; the shared runtime lives in this repo's core/ and is not published separately yet (#1636).
{
"devDependencies": {
- "@modelcontextprotocol/inspector-cli": "^1.0.1"
+ "@modelcontextprotocol/inspector": "^2.0.0"
}
}
Architecture: the proxy is gone
v1 ran two processes: a React client on 6274 and an "MCP Proxy" on 6277 that held the actual MCP connections. The browser talked to the proxy over HTTP, authenticating with MCP_PROXY_AUTH_TOKEN.
v2 runs one web server on 6274. It serves the SPA and exposes /api/*, which the browser calls with x-mcp-remote-auth: Bearer <MCP_INSPECTOR_API_TOKEN>. There is no second port to expose, forward, or allow through a firewall.
Consequences:
SERVER_PORTno longer selects a port you browse. (It is read as a fallback for the MCP Apps sandbox server's port — seeMCP_SANDBOX_PORTbelow.)MCP_PROXY_FULL_ADDRESShas no v2 equivalent and is ignored. It existed to tell the browser where a non-default proxy lived; there is no proxy.- Docker only needs
-p 6274:6274. The v1 recipe published6277as well.
Launching
npx @modelcontextprotocol/inspector # web UI (unchanged default)
npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --tui # TUI (new in v2)
The mode flag must come first, immediately after the binary name; everything after it is forwarded to that client unchanged.
Passing a server ad-hoc works the same way it did:
npx @modelcontextprotocol/inspector node build/index.js
npx @modelcontextprotocol/inspector -e KEY=value -- node build/index.js --server-flag
--config vs --catalog
This is the change most likely to surprise you. Both versions have a --config flag, both take an mcpServers file, and they do not mean the same thing.
In v1, --config path.json --server name resolved one entry out of the file and launched against it. The file was read once and never written — but there was no other file, because v1's web UI kept its server list in browser localStorage.
v2 has a first-class server catalog on disk, and splits the two roles:
--catalog <path> |
--config <path> |
|
|---|---|---|
| Writable by the Inspector | Yes — this is the Inspector's own list | No. Served as-is; never written, seeded, or migrated |
| When the file is missing | created and seeded | errors |
| Default path | ~/.mcp-inspector/mcp.json, or MCP_CATALOG_PATH |
none — must be passed |
| Editable in the web UI | yes | no (catalog CRUD is hidden) |
So:
--configin v2 is the read-only role. Point it at a file you didn't write — a coworker's, a client application's, one checked into a repo — and the Inspector guarantees it won't touch the bytes, including any plaintext secrets in them. This is close to v1's behavior, minus the write-back that never existed anyway.--catalogis new and is what you want if you'd like the web UI to add, edit, and remove servers.- Neither is required. With no source flag, v2 uses the default catalog
~/.mcp-inspector/mcp.json, creating it if absent. v1 had no such default — it started empty every time.
Before / after
Your v1 file works unchanged. The format is the same mcpServers shape, type: "streamable-http" is still accepted (as is the "http" alias), and unknown fields such as v1's exported note are carried through rather than rejected.
# v1 — one entry out of a file
npx @modelcontextprotocol/inspector --config ./mcp.json --server everything
# v2 — same thing, same read-only guarantee
npx @modelcontextprotocol/inspector --config ./mcp.json --server everything
⚠️ One caveat on that last line: --server only selects under --cli. On the web client it is a no-op that logs a warning, and the TUI rejects it as an unknown option. In the web UI you pick the server from the list after it loads.
# v2 — let the Inspector manage the file (web UI can edit it)
npx @modelcontextprotocol/inspector --catalog ./mcp.json
# v2 — the default catalog, no flags at all
npx @modelcontextprotocol/inspector
Rules: --catalog and --config are mutually exclusive, and neither combines with an ad-hoc target (a positional command, --server-url, or --transport). v1 silently preferred the config file; v2 tells you.
If you export MCP_CATALOG_PATH in your shell, note that web and TUI read it unconditionally — so an ad-hoc invocation such as mcp-inspector --tui node build/index.js is rejected as a catalog/ad-hoc conflict. Unset it for that invocation. The CLI ignores the variable whenever an ad-hoc target is present.
The full model, including the Inspector-specific per-server fields v2 adds (protocolEra, roots, requestTimeout, oauth, …), is documented in MCP server configuration.
CLI flag mapping
Every v1 CLI flag still exists in v2 and means the same thing. Nothing was renamed or removed:
| v1 flag | v2 | Notes |
|---|---|---|
--method <method> |
same | v2 adds initialize, servers/list, servers/show; stream-only methods are rejected explicitly |
--tool-name <name> |
same | |
--tool-arg <k=v> |
same | |
--uri <uri> |
same | |
--prompt-name <name> |
same | |
--prompt-args <k=v> |
same | |
--log-level <level> |
same | |
--transport <sse|http|stdio> |
same | |
--server-url <url> |
same | |
--header "Name: Value" |
same | |
--metadata <k=v> |
same | |
--tool-metadata <k=v> |
same | |
-e KEY=VALUE |
same | |
--config <path> |
same flag, narrower meaning | see above |
--server <name> |
same | now --cli-only |
[target...] |
same | but see the target-ordering rule below |
New in v2, with no v1 equivalent:
| Flag | What it does |
|---|---|
--catalog <path> |
writable catalog file (see above) |
--cwd <path> |
working directory for a stdio server process |
--tool-args-json '<json>' |
tool arguments as one JSON object, passed verbatim (no key=value coercion, so "012" stays a string) |
--format text|json |
json emits a single {"result":…} object on stdout with no banners |
--app-info |
probe a tool's MCP App UI metadata without invoking the tool |
--connect-timeout <ms> |
connect timeout; defaults to 15000 for ad-hoc targets so a black-holed host fails fast |
--client-id / --client-secret / --client-metadata-url / --client-config |
OAuth client configuration |
--callback-url <url> |
OAuth loopback redirect URI (default http://127.0.0.1:6276/oauth/callback) |
--relogin / --stored-auth-only / --use-stored-auth / --wait-for-auth / --list-stored-auth / --print-handoff |
OAuth token reuse and web→CLI handoff |
v1 had no OAuth support in the CLI at all; a protected server simply failed. If you script against protected servers, --stored-auth-only is the flag to reach for in CI — it never opens a browser and fails fast with auth_required instead of hanging on a loopback callback.
See the CLI README for the full surface.
CLI behavior changes
The flags survived; three behaviors did not.
1. Exit codes and the error envelope
v1 exited 0 on success and 1 on any failure — including a tools/call that came back isError: true, which exited 0 because the call succeeded.
v2 maps failures onto a stable set:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Usage / unexpected error |
2 |
No MCP App on the tool (--app-info probe) |
3 |
Server requires authentication |
4 |
Server unreachable (DNS, refused, timeout) |
5 |
Tool error — isError: true, or the tool was not found |
and writes one JSON line to stderr on any non-zero exit:
{
"error": {
"code": "auth_required",
"message": "Unauthorized",
"status": 401,
"url": "https://api.example/mcp"
}
}
Migration note: a CI step that chained inspector --cli … --method tools/call … && next-step silently continued past failing tool calls in v1. In v2 it stops. That is the intended behavior, but it will surface as "v2 broke my pipeline" the first time a tool was failing all along. Parse the class with 2>&1 | tail -1 | jq .error rather than matching on prose.
2. The target must come first
v2's CLI reads the leading run of non-dash tokens as the server target. Anything after the first flag is no longer part of it:
mcp-inspector --cli node build/index.js --method tools/list # ✅
mcp-inspector --cli --method tools/list node build/index.js # ❌ target silently dropped
The second form does not error — the target is discarded and the Inspector falls back to your catalog, so it appears to work against the wrong server. v1 tolerated either order.
3. -- splits the other way under --cli
On web and TUI, everything after -- goes to the target command — same as v1:
mcp-inspector node build/index.js -- --config /etc/myserver.conf --verbose
Under --cli it is reversed: everything before -- is the target, everything after is the Inspector's own options.
mcp-inspector --cli node build/index.js -- --method tools/list
So the web example above, run under --cli, would have --config /etc/myserver.conf consumed as the Inspector's read-only-session flag (and then rejected as a catalog/ad-hoc conflict). There is currently no way to pass a leading-dash argument through to a stdio server on the --cli command line — put it in the server entry's args in a catalog or config file instead.
Stdout is otherwise compatible: the default text format still pretty-prints the result as JSON.stringify(result, null, 2), exactly as v1 did.
Environment variables
| v1 | v2 | Notes |
|---|---|---|
MCP_PROXY_AUTH_TOKEN |
MCP_INSPECTOR_API_TOKEN |
Renamed. Guards /api/* via x-mcp-remote-auth: Bearer <token>; the browser also receives it injected into index.html, so a bare reload keeps working |
MCP_PROXY_FULL_ADDRESS |
— | Removed; there is no proxy |
SERVER_PORT |
repurposed | Was the proxy port. Now only a fallback for the MCP Apps sandbox port |
CLIENT_PORT |
same | Web UI port, default 6274. Must be a fixed port — 0/dynamic is rejected, since the origin allow-list and sandbox CSP derive from it |
HOST |
same, guarded | An all-interfaces host (0.0.0.0, ::, and equivalent spellings) is now refused unless DANGEROUSLY_BIND_ALL_INTERFACES=true. Binding a specific IP or hostname needs no opt-in |
ALLOWED_ORIGINS |
same | Still comma-separated, still replaces the default list rather than merging. Entries must include the scheme |
DANGEROUSLY_OMIT_AUTH |
same | |
MCP_AUTO_OPEN_ENABLED |
same | Also gates non-TTY interactive OAuth in the CLI/TUI |
| — | DANGEROUSLY_BIND_ALL_INTERFACES |
New opt-in for a wildcard bind (the Docker image sets it) |
| — | MCP_CATALOG_PATH |
Default catalog path |
| — | MCP_SANDBOX_PORT |
MCP Apps sandbox server port (dynamic by default) |
| — | MCP_STORAGE_DIR, MCP_INSPECTOR_OAUTH_STATE_PATH, MCP_CLIENT_CONFIG_PATH |
Storage and OAuth state locations |
| — | MCP_OAUTH_CALLBACK_URL |
CLI/TUI OAuth loopback callback |
The v1 UI configuration settings
v1 exposed a Configuration panel writing localStorage keys, also settable by query param. Those were global; in v2 the equivalents are per-server settings, stored in the catalog entry and editable in Server Settings:
| v1 setting | v2 |
|---|---|
MCP_SERVER_REQUEST_TIMEOUT |
per-server requestTimeout (ms) |
| (no v1 equivalent) | per-server connectionTimeout (ms) |
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS |
always on; no longer configurable |
MCP_REQUEST_MAX_TOTAL_TIMEOUT |
no equivalent |
MCP_PROXY_FULL_ADDRESS |
no equivalent (no proxy) |
Web UI
Server list. v1 kept it in browser localStorage, so it was per-browser and invisible to the CLI. v2 keeps it in the catalog file, shared by all three clients — add a server in the web UI and the CLI and TUI see it.
Query params. v1 accepted ?transport=…&serverUrl=…&serverCommand=…&serverArgs=… plus ?MCP_PROXY_AUTH_TOKEN=…. v2's deep link is narrower and gated:
http://127.0.0.1:6274/?serverUrl=<url>&transport=http|sse&autoConnect=<token>
serverCommand/serverArgsare gone — a URL can no longer ask the Inspector to spawn a process.serverUrlis restricted tohttp:/https:.autoConnectmust equal the per-launchMCP_INSPECTOR_API_TOKEN, so a third-party-minted link can't drive a connect.?MCP_INSPECTOR_API_TOKEN=…replaces?MCP_PROXY_AUTH_TOKEN=….
Three further params (openApp, appArgs, autoOpen) land you on a rendered MCP App; see the web README.
Auth. v1's bearer-token field in the sidebar is replaced by per-server headers and real OAuth (including CIMD and enterprise-managed auth) configured in Server Settings and persisted in the catalog.
Docker
# v1
docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6277:6277 \
-e HOST=0.0.0.0 -e MCP_AUTO_OPEN_ENABLED=false \
ghcr.io/modelcontextprotocol/inspector:1.0.1
# v2 — one port; the image already sets the wildcard-bind opt-in
docker run --rm -p 6274:6274 ghcr.io/modelcontextprotocol/inspector:latest
Notes:
- The v2 image sets
DANGEROUSLY_BIND_ALL_INTERFACES=trueinternally (a container must bind0.0.0.0to be reachable through-p). Setting a bareHOST=0.0.0.0outside a container now exits with an error. - If you remap the published port (
-p 8080:6274), the browser's origin no longer matches the in-container port, so setALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080(or run-e CLIENT_PORT=8080 -p 8080:8080) or connects will 403. - The image runs as non-root and has a
HEALTHCHECKthat assumes--web; add--no-healthcheckwhen running--cli/--tui. :latestnow points at v2. To stay on the v1 image, pin its exact version tag (:1.0.1).
Troubleshooting
"--config errors that my file doesn't exist, but v1 created it." v1 never created config files either — but v2's default path is a catalog it will create. If you want a file created on demand, pass it as --catalog, not --config.
"--server is ignored." It selects only under --cli. Web warns and ignores it; the TUI errors on it as unknown.
"My CI step started failing after upgrading." Most likely exit code 5: a tools/call returning isError: true now exits non-zero. Check 2>&1 | tail -1 | jq .error.
"The Inspector connects to a different server than I named." Check flag order — under --cli the target must precede all flags, or it is silently dropped in favor of the catalog.
"HOST=0.0.0.0 exits with an error." That's the wildcard-bind guard. Bind a specific address instead, or set DANGEROUSLY_BIND_ALL_INTERFACES=true if you genuinely need all interfaces. See Host binding & the origin allow-list.
"I need v1 back." npx @modelcontextprotocol/inspector@v1-latest.
Related
- MCP server configuration — the full
--catalog/--config/ ad-hoc model. - CLI README · TUI README · web README
- Reviewing an MCP App — the CLI-first App review recipe.