docs: add the v1 → v2 migration guide

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
This commit is contained in:
cliffhall 2026-08-10 19:23:43 -04:00
commit 2e89443739
5 changed files with 469 additions and 128 deletions

216
AGENTS.md

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,8 @@ npx @modelcontextprotocol/inspector --cli # CLI
npx @modelcontextprotocol/inspector --tui # TUI
```
> **Upgrading from v1?** Read the [v1 → v2 migration guide](./docs/v1-to-v2-migration.md) — CLI flags, the new `--config` vs. `--catalog` split, the Node engine bump, and what no longer ships.
> **Repo status.** This is the **v2** line of the Inspector. Active development happens on **`v2/main`** (the develop branch — all v2 PRs target it), which is merged into **`main`** at milestone releases; `main` is the default branch and holds the latest released v2, published to the npm `latest` tag. The legacy **v1** line lives on **`v1/main`** — security fixes only, published straight from that branch to the npm `v1-latest` tag (`npx @modelcontextprotocol/inspector@v1-latest`). See [`AGENTS.md`](./AGENTS.md) for branch/board conventions.
## Project layout
@ -38,7 +40,7 @@ inspector/
│ └── storage/ # File I/O helpers for the OAuth persist backends
├── test-servers/ # Composable MCP test servers + fixtures used by integration tests
├── scripts/ # Root build/verify tooling (install cascade, smokes, verify-build-gate, verify-format-coverage, pack:verify)
├── docs/ # Task-oriented guides (server configuration, MCP App review, launcher/config plan)
├── docs/ # Task-oriented guides (v1→v2 migration, server configuration, MCP App review, launcher/config plan)
├── specification/ # Design/build specifications
├── AGENTS.md # Contribution rules for agents AND humans (see below)
└── README.md # You are here
@ -49,6 +51,7 @@ Each client has its own README with client-specific detail:
Task-oriented guides live under [`docs/`](./docs):
- [Migrating from v1 to v2](./docs/v1-to-v2-migration.md) — the v1 → v2 map: CLI flag mapping, `--config` vs. `--catalog` semantics with before/after examples, the Node engine bump (`>=22.7.5` → `>=22.19.0`), env-var renames, and the sub-packages that no longer ship.
- [MCP server configuration](./docs/mcp-server-configuration.md) — which server(s) the Inspector connects to: `--catalog` vs. `--config`, ad-hoc targets, the `--` separator, the file format and its Inspector-specific per-server fields. Shared by all three clients; the cli and tui READMEs delegate their server-options sections to it.
- [Reviewing an MCP App](./docs/mcp-app-review.md) — the CLI-first → one-shot-web recipe for automated App-tool review: `--app-info` probe → deep-link navigate → rendered widget, plus OAuth handoff and proxy support.
- [Launcher and config consolidation](./docs/launcher-config-consolidation-plan.md) — why the launcher runs a client in-process rather than spawning it, and how the shared config processor fits in.
@ -130,18 +133,18 @@ This is what lets an Inspector connection negotiating `protocolEra: "auto" | "mo
Each config below is a ready-made server for exercising one feature by hand. Load one with `--config`, and unless noted, connect with **Protocol Era = Modern**.
| Config | Demonstrates | Issue |
| ----------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
| Config | Demonstrates | Issue |
| ----------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------- |
| `mcp-app-http.json` **(legacy era)** | An MCP App (UI resource + app tool) in the Apps tab | [#1859](https://github.com/modelcontextprotocol/inspector/issues/1859) |
| `modern-mrtr-http.json` | A single MRTR round-trip | — |
| `mrtr-showcase-http.json` | Every MRTR preset in one server | — |
| `modern-network-http.json` | Network tab: `Mcp-*` headers + error taxonomy | [#1628](https://github.com/modelcontextprotocol/inspector/issues/1628) |
| `xmcpheader-modern-http.json` | Tools tab: `x-mcp-header` mirroring and exclusions | [#1632](https://github.com/modelcontextprotocol/inspector/issues/1632) |
| `pagination-http.json` | Page-by-page list fetching | [#1721](https://github.com/modelcontextprotocol/inspector/issues/1721) |
| `advertised-extensions-http.json` | Tool registration gated on advertised extensions | [#1739](https://github.com/modelcontextprotocol/inspector/issues/1739) |
| `logging-{legacy,modern}-http.json` | Logging, both eras | [#1629](https://github.com/modelcontextprotocol/inspector/issues/1629) |
| `subscriptions-{legacy,modern}-http.json` | Resource subscriptions, both eras | [#1630](https://github.com/modelcontextprotocol/inspector/issues/1630) |
| `tasks-{legacy,modern}-http.json` | Tasks, both eras | [#1631](https://github.com/modelcontextprotocol/inspector/issues/1631) |
| `modern-mrtr-http.json` | A single MRTR round-trip | — |
| `mrtr-showcase-http.json` | Every MRTR preset in one server | — |
| `modern-network-http.json` | Network tab: `Mcp-*` headers + error taxonomy | [#1628](https://github.com/modelcontextprotocol/inspector/issues/1628) |
| `xmcpheader-modern-http.json` | Tools tab: `x-mcp-header` mirroring and exclusions | [#1632](https://github.com/modelcontextprotocol/inspector/issues/1632) |
| `pagination-http.json` | Page-by-page list fetching | [#1721](https://github.com/modelcontextprotocol/inspector/issues/1721) |
| `advertised-extensions-http.json` | Tool registration gated on advertised extensions | [#1739](https://github.com/modelcontextprotocol/inspector/issues/1739) |
| `logging-{legacy,modern}-http.json` | Logging, both eras | [#1629](https://github.com/modelcontextprotocol/inspector/issues/1629) |
| `subscriptions-{legacy,modern}-http.json` | Resource subscriptions, both eras | [#1630](https://github.com/modelcontextprotocol/inspector/issues/1630) |
| `tasks-{legacy,modern}-http.json` | Tasks, both eras | [#1631](https://github.com/modelcontextprotocol/inspector/issues/1631) |
#### MCP Apps
@ -261,17 +264,17 @@ Individual clients: `build:web`, `build:cli`, `build:tui`, `build:launcher`. The
Each client self-validates from its own folder; the root scripts chain them. There is **no** aggregate root `test` script — use `validate` (fast) or `coverage` (the gate).
| Script | What it does |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run validate` | Runs `verify:format-coverage` (asserts every tracked source file is format-gated) first, then `validate:core` (the shared `core/` `format:check` + `lint` gate), then per client: `format:check` + `lint` + **`typecheck`** (cli/tui only) + `build` + fast unit tests. The quick inner-loop check. |
| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project and covers the shared `core/` runtime (including `core/json` and `core/client`). |
| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web), plus two headless-Chromium smokes: a boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error — sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests), and an **MCP Apps** smoke (`smoke:web:app`) that drives connect → open app → `data-app-status="ready"` against a composable App server, covering the sandbox proxy and UI-protocol bridge. |
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run ci`. |
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs` + the exported helpers of `verify-typecheck-coverage.mjs`), one case per rule they encode. Runs in `validate` — and `verify:typecheck-coverage` guards *this* gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
| Script | What it does |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run validate` | Runs `verify:format-coverage` (asserts every tracked source file is format-gated) first, then `validate:core` (the shared `core/` `format:check` + `lint` gate), then per client: `format:check` + `lint` + **`typecheck`** (cli/tui only) + `build` + fast unit tests. The quick inner-loop check. |
| `npm run coverage` | The **per-file ≥90% gate** (lines/statements/functions/branches) under v8 instrumentation, per client. CI-enforced. For web this also runs the integration project and covers the shared `core/` runtime (including `core/json` and `core/client`). |
| `npm run smoke` | End-to-end smokes through the built launcher (`--help` dispatch + prod cli/tui/web), plus two headless-Chromium smokes: a boot smoke that runs the prod web bundle and asserts a clean first render (no uncaught error — sync exception or unhandled rejection, how a Node built-in reaching the browser bundle manifests), and an **MCP Apps** smoke (`smoke:web:app`) that drives connect → open app → `data-app-status="ready"` against a composable App server, covering the sandbox proxy and UI-protocol bridge. |
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run ci`. |
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs` + the exported helpers of `verify-typecheck-coverage.mjs`), one case per rule they encode. Runs in `validate` — and `verify:typecheck-coverage` guards _this_ gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package, `format:scripts` for the root `scripts/` tooling, and `format:shared` / `lint:shared` for the root "shared" surface (`test-servers/src/**`, `vitest.shared.mts`, the root `eslint.config.js`). Run `npm run format` before committing — the root `format` fixes `core/`, the root `scripts/`, the shared surface, and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file.
@ -287,7 +290,7 @@ The root `package.json` `"files"` allowlist is the source of truth for the tarba
- **No source maps.** The client bundlers set `sourcemap: false` (`clients/{cli,tui}/tsup.config.ts`, `clients/web/tsup.runner.config.ts`); Vite and the launcher's `tsc` already emit none. Maps are ~half the unpacked size and aren't needed at runtime — debug via `npm run dev` on the source.
- **`clients/web/build` ships via `clients/web/.npmignore`.** `clients/web/.gitignore` lists `build/`, and npm's packlist honors that nested `.gitignore` over the root `"files"` allowlist — so the prod web-server runner was silently missing from the tarball while `clients/web/dist` slipped through (its `.gitignore` only lists `dist-ssr`). `clients/web/.npmignore` overrides the `.gitignore` for publishing so both `build/` (runner) and `dist/` (SPA) ship. The other clients don't need this — none ship a nested `.gitignore`.
- **`clients/web/static` ships the MCP Apps sandbox proxy.** `clients/web/static/sandbox_proxy.html` is a committed source file (not a build artifact), read from disk at runtime by `clients/web/server/sandbox-controller.ts` as `<runner dir>/../static/sandbox_proxy.html`. It was missing from the root `"files"` allowlist entirely, so every published build failed the Apps tab with **"Sandbox not loaded"** ([#1859](https://github.com/modelcontextprotocol/inspector/issues/1859)) while working fine in the repo. Because the path is resolved *relative to* `clients/web/build`, the directory must ship at that exact location — `pack:verify` asserts both the tarball entry and the installed-on-disk path.
- **`clients/web/static` ships the MCP Apps sandbox proxy.** `clients/web/static/sandbox_proxy.html` is a committed source file (not a build artifact), read from disk at runtime by `clients/web/server/sandbox-controller.ts` as `<runner dir>/../static/sandbox_proxy.html`. It was missing from the root `"files"` allowlist entirely, so every published build failed the Apps tab with **"Sandbox not loaded"** ([#1859](https://github.com/modelcontextprotocol/inspector/issues/1859)) while working fine in the repo. Because the path is resolved _relative to_ `clients/web/build`, the directory must ship at that exact location — `pack:verify` asserts both the tarball entry and the installed-on-disk path.
- **A single version number, read from the root `package.json`.** The Inspector ships as one package with one version, so only the **root** `package.json` carries a `version` — the four `clients/*/package.json`s deliberately have none. Every Node client (CLI, TUI, and the web backend) resolves the version through the shared `readInspectorVersion()` reader in `core/node/version.ts`, which walks up to the root manifest (always present in the tarball). No client `package.json` is read at runtime, so none needs to ship. The web **browser** can't read the filesystem; it gets its version from the backend via `GET /api/config` (see [#1639](https://github.com/modelcontextprotocol/inspector/issues/1639)).
### `npm run pack:verify` — publish smoke against the real tarball

View file

@ -12,6 +12,8 @@ npx @modelcontextprotocol/inspector --cli node build/index.js
Supports tools, resources, and prompts (plus `--method servers/list` / `servers/show` for catalog entries without connecting).
> Coming from the v1 CLI? See the [v1 → v2 migration guide](../../docs/v1-to-v2-migration.md) — every v1 flag still exists, but exit codes, argument ordering, and the `--` separator changed.
### Examples
**Basic usage**

View file

@ -201,5 +201,6 @@ The CLI and TUI do not perform catalog CRUD yet — they are read consumers —
## Related
- [Migrating from v1 to v2](./v1-to-v2-migration.md) — why `--config` means something narrower than it did in v1, and what `--catalog` replaced.
- [Launcher and config consolidation](./launcher-config-consolidation-plan.md) — how the launcher and the shared config processor fit together.
- [Reviewing an MCP App](./mcp-app-review.md) — the CLI-first App review recipe.

325
docs/v1-to-v2-migration.md Normal file
View file

@ -0,0 +1,325 @@
# 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-latest` npm dist-tag. To stay on it, pin it explicitly:
>
> ```bash
> 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:
1. **`--config` no longer means what it did** — see [`--config` vs `--catalog`](#--config-vs---catalog).
2. **A failing tool call now exits non-zero** (`5`), where v1 exited `0`. CI scripts that ran `&&` chains past a failed call will start failing — correctly.
3. **`SERVER_PORT` was 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](https://github.com/modelcontextprotocol/inspector/issues/1636)).
```diff
{
"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_PORT` no longer selects a port you browse. (It is read as a fallback for the MCP Apps sandbox server's port — see `MCP_SANDBOX_PORT` below.)
- `MCP_PROXY_FULL_ADDRESS` has 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 published `6277` as well.
## Launching
```bash
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:
```bash
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:
- **`--config` in 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.
- **`--catalog` is 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.
```bash
# 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.
```bash
# 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](./mcp-server-configuration.md).
## 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](#--config-vs---catalog) |
| `--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](../clients/cli/README.md) 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:
```json
{
"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:
```bash
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:
```bash
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.
```bash
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` / `serverArgs` are gone — a URL can no longer ask the Inspector to spawn a process.
- `serverUrl` is restricted to `http:` / `https:`.
- `autoConnect` must equal the per-launch `MCP_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](../clients/web/README.md#deep-link-auto-connect).
**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
```bash
# 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=true` internally (a container must bind `0.0.0.0` to be reachable through `-p`). Setting a bare `HOST=0.0.0.0` **outside** 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 set `ALLOWED_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 `HEALTHCHECK` that assumes `--web`; add `--no-healthcheck` when running `--cli` / `--tui`.
- `:latest` now 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](../clients/web/README.md#host-binding--the-origin-allow-list).
**"I need v1 back."** `npx @modelcontextprotocol/inspector@v1-latest`.
## Related
- [MCP server configuration](./mcp-server-configuration.md) — the full `--catalog` / `--config` / ad-hoc model.
- [CLI README](../clients/cli/README.md) · [TUI README](../clients/tui/README.md) · [web README](../clients/web/README.md)
- [Reviewing an MCP App](./mcp-app-review.md) — the CLI-first App review recipe.