Follow Discord
Sweep 08 Oct 2026 · 18:53Z Build v2.1.295 516 read Stable v2.1.286 Latest v2.1.295 Next v2.1.295 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One capture · mcp

One read of Model Context Protocolmcp-20261008T130707Z

20 pages moved out of 356 read.

Pages moved 20 significant first
Pages read 356 in this capture
Captured 13:07 UTC
Corpus hash 8fd0fb103ccb corpus-hash

What this read moved

1-20 of 20

docs/2026-07-28/tools/inspector Changed · +30 / -6 lines

from line 12
1212 
1313All three are built on the same shared core, so a connection behaves identically across them: the same transports, the same configuration files, the same OAuth state on disk, and the same [protocol-era](/docs/2026-07-28/tools/inspector/protocol-eras) negotiation (legacy vs. modern 2026-07-28).
1414 
15<Frame caption="The MCP Inspector web client, connected to a server, with the monitoring sidebar pinned so protocol traffic stays visible while you work.">
16 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=eef6e546b9831b3d169e26bba8c54ce3" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
15The package also installs **`mcpdo`**, an experimental [connection client](/docs/2026-07-28/tools/inspector/mcpdo) that connects to a server once and keeps it open across many commands, on the same core.
16 
17<Frame caption="The MCP Inspector web client after a tool call, with the monitoring sidebar pinned and widened so the JSON-RPC exchange stays readable while you work.">
18 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
1719</Frame>
1820 
1921## Quickstart
from line 32
3032 npx @modelcontextprotocol/inspector
3133 ```
3234 
33 The command prints a URL containing a one-time session token; open it in your browser. See [Web client](/docs/2026-07-28/tools/inspector/web).
35 The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](/docs/2026-07-28/tools/inspector/web).
3436 </Tab>
3537 
3638 <Tab title="CLI">
from line 83
8183 
8284Always read a server's own README first, since every server requires different commands and arguments.
8385 
86<Warning>
87 **The Inspector stores secrets (OAuth tokens, OAuth client secrets, and stdio
88 `env:` values) in the OS keychain when one is available.** On a machine with
89 no keychain (Linux without libsecret or a Secret Service, headless and SSH
90 sessions, Termux, and containers with a mounted volume) they are saved to
91 `~/.mcp-inspector/secrets.json` instead, **unencrypted unless you supply a
92 key**. See [Where secrets are
93 stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored)
94 for how to get a keychain back, encrypt the file, or keep secrets in memory
95 only.
96</Warning>
97 
8498## Launcher flags vs. client flags
8599 
86100`mcp-inspector`, the binary that `npx @modelcontextprotocol/inspector` runs, is a thin launcher. It owns only two things:
from line 113
99113</Note>
100114 
101115<Note>
102 `--help` behaves differently with and without a mode flag. Bare `mcp-inspector --help` prints the launcher's help and exits. With a mode flag it is
103 forwarded, so `mcp-inspector --cli --help` prints the CLI's full flag
104 reference instead.
116 `--help` behaves differently with and without a mode flag. Without one,
117 `-h`/`--help` anywhere on the command line (even after a server command)
118 prints the launcher's help and exits. With a mode flag it is forwarded, so
119 `mcp-inspector --cli --help` prints the CLI's full flag reference instead.
105120</Note>
106121 
107122## Where to go next
from line 147
132147 <Card title="Protocol eras" icon="code-branch" href="/docs/2026-07-28/tools/inspector/protocol-eras">
133148 Legacy vs. modern (2026-07-28) operation, and how every tab changes between
134149 protocol eras.
150 </Card>
151 
152 <Card title="mcpdo connection client" icon="plug" href="/docs/2026-07-28/tools/inspector/mcpdo">
153 Connect once, then run many commands against a named connection.
154 </Card>
155 
156 <Card title="Security" icon="shield-halved" href="/docs/2026-07-28/tools/inspector/security">
157 The threat model: the web backend's token, Docker, secret storage, stdio
158 servers, and the mcpdo daemon.
135159 </Card>
136160 
137161 <Card title="Recipes" icon="book" href="/docs/2026-07-28/tools/inspector/recipes">

docs/2026-07-28/tools/inspector/authorization Changed · +18 / -14 lines

from line 43
4343 
4444 <Step title="Exchange and retry">
4545 The code is exchanged for tokens, the tokens are persisted, and the original
46 connect (or, for a [mid-session challenge](#mid-session-re-authorization),
47 the request that was refused) is retried automatically.
46 connect is retried. For a [mid-session
47 challenge](#mid-session-re-authorization), the CLI retries the refused
48 request automatically, and the web client asks you to retry the action.
4849 </Step>
4950</Steps>
5051 
from line 59
5859 
5960| Surface | Default callback | Why |
6061| - | - | - |
61| **Web** | `http://localhost:6274/oauth/callback` | The main app server already has an HTTP listener. |
62| **Web** | `<origin you opened the Inspector at>/oauth/callback`, by default `http://127.0.0.1:6274/oauth/callback` | The main app server already has an HTTP listener. Copy the exact value from the **Redirect URI** field in Server Settings. |
6263| **CLI** | `http://127.0.0.1:6276/oauth/callback` | A dedicated loopback listener, so it doesn't collide with a running web Inspector. |
6364| **TUI** | `http://127.0.0.1:6276/oauth/callback` | The same listener as the CLI. |
6465 
6566**Register `http://127.0.0.1:6276/oauth/callback`** on any IdP that requires pre-registered redirect URIs before using the CLI or TUI. A predictable default is the point: you register once and reuse it.
6667 
67Override with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`.
68For the CLI and TUI, override it with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`. The web callback always follows the page's origin, so `localhost` and `127.0.0.1` produce different redirect URIs there too.
6869 
6970<Warning>
7071 The callback URL **must bind a loopback host**: `localhost`, `127.0.0.0/8`, or
from line 72
7172 `[::1]`. The listener receives the authorization code over plaintext `http`,
7273 so a non-loopback host is rejected with an error and there is no flag to
7374 override that. If your browser runs on a different machine, forward the
74 callback port to it; `--print-handoff` (below) prints a ready-made
75 `portForwardCmd`.
75 callback port to it, or complete the login in a web Inspector instead:
76 `--print-handoff` (below) prints a `portForwardCmd` for the web Inspector's
77 ports.
7678</Warning>
7779 
7880<Note>
from line 87
8587 
8688| File | Contents |
8789| - | - |
88| `~/.mcp-inspector/storage/oauth.json` | Tokens and client information, keyed by canonicalized server URL. Written owner-only. |
90| `~/.mcp-inspector/storage/oauth.json` | Non-secret OAuth state (discovery metadata, PKCE verifiers, granted scope, public client ids), keyed by canonicalized server URL. Written owner-only. |
8991| `~/.mcp-inspector/storage/client.json` | Install-level client settings (client metadata URL, enterprise IdP). The same file the web client's **Client Settings** dialog writes. |
9092| The server's `oauth` block in the [catalog file](/docs/2026-07-28/tools/inspector/configuration#catalog-file-format) | Per-server client id/secret, scopes, the enterprise-managed flag, and the [step-up](#mid-session-re-authorization) policy. |
9193 
9294The path to `oauth.json` is resolved in order: `MCP_INSPECTOR_OAUTH_STATE_PATH`, then `<MCP_STORAGE_DIR>/oauth.json` (see [Environment variables](/docs/2026-07-28/tools/inspector/configuration#environment-variables)), then the default above. All three clients resolve it the same way. Command-line `--client-id` / `--client-secret` / `--client-metadata-url` override `client.json`.
9395 
96The secrets themselves (access and refresh tokens, client secrets, registration access tokens, and IdP session tokens) are not in `oauth.json`. They go to the [secret store](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored), which is the OS keychain when one is reachable. `MCP_INSPECTOR_PERSIST_TOKENS` limits which acquired tokens are kept: `all` (default), `access` (no refresh tokens), or `none` (re-authorize every run). See [Secret store variables](/docs/2026-07-28/tools/inspector/configuration#secret-store-variables).
97 
9498## Mid-session re-authorization
9599 
96100A server can refuse a *single* request mid-session with a `401` or a `403 insufficient_scope`, and the Inspector handles both without dropping the connection:
from line 102
98102* **Re-authorization**: the token expired or was revoked. The Inspector parses the `WWW-Authenticate` challenge and re-runs the flow, then retries the failed request.
99103* **Step-up**: the request needs scopes the current token doesn't carry. The Inspector re-authorizes for the union of the held and required scopes, so the new token covers everything the old one did plus the newly required scopes.
100104 
101In the **web** client this surfaces as a re-authorization banner. In the **CLI** it prompts on stderr:
105In the **web** client, re-authorization shows a **Re-authentication required** banner, and step-up opens an **Additional permissions required** dialog listing the scopes, which you confirm with **Authorize**. Each server's **Insufficient-scope response** setting can turn step-up off so the `403` surfaces as an error instead. In the **CLI**, step-up prompts on stderr:
102106 
103107```
104108Proceed with step-up authorization? [y/N]
from line 128
124128 
125129| Flag | Behavior |
126130| - | - |
127| `--use-stored-auth` | Read the stored auth for `--server-url` and inject `Authorization: Bearer`. When a refresh token is stored, run the refresh grant first and inject the **fresh** token, persisting the rotation. Exits `3` (listing the stored server URLs) when nothing matches. |
128| `--wait-for-auth <sec>` | Poll the state file until a token for `--server-url` appears, then inject it. Times out at `<sec>` with exit `3`. Use after handing a login off to a human. |
131| `--use-stored-auth` | Read the stored auth for `--server-url` and inject `Authorization: Bearer`. When a refresh token is stored, run the refresh grant first and inject the **fresh** token, persisting the rotation. Exits `3` (`no_stored_token`, listing the stored server URLs) when nothing matches. |
132| `--wait-for-auth <sec>` | Poll the state file until a token for `--server-url` appears, then inject it. Times out at `<sec>` with exit `3` (`auth_wait_timeout`). Use after handing a login off to a human. |
129133| `--list-stored-auth` | Print `{ oauthStatePath, storedServerUrls }` and exit without connecting. |
130134| `--print-handoff` | Print a JSON block (`deepLink`, `portForwardCmd`, `oauthStatePath`, `apiToken`) for `--server-url` and exit; this is everything a remote script needs to drive the browser side. |
131| `--relogin` | Delete the stored OAuth for this server URL before connecting. HTTP/SSE only. |
135| `--relogin` | Delete the stored OAuth for this server URL before connecting, and revoke the grant at the authorization server (skip with `--no-revoke`). HTTP/SSE only. |
132136 
133A typical remote-VM sequence:
137A typical remote-VM sequence. It assumes a web Inspector is running on the VM with a known `MCP_INSPECTOR_API_TOKEN`, and the same value is exported in the shell below; without it, the handoff's `deepLink` carries no `autoConnect` token and the web client rejects it.
134138 
135139```bash theme={null}
136140# On the VM: print what the human needs in order to complete OAuth in their browser
from line 158
154158 
155159## Inspecting auth state
156160 
157* **Web**: the Connection Info panel shows discovery results, the registered client, granted scopes, and token state, and offers **Clear OAuth state** for the active server.
161* **Web**: the Connection Info panel shows the authorization status, the client registration type, the client ID, granted scopes, and the access token, and offers **Clear OAuth state and disconnect** for the active server. Clearing also revokes the grant unless the server's **Revoke tokens on clear** setting is off.
158162* **TUI**: the **Auth** tab (`a`) shows the same fields and clears state the same way.
159* **CLI**: `--list-stored-auth` shows what's on disk, and `--relogin` discards it and starts over.
163* **CLI**: `--list-stored-auth` shows what's on disk, and `--relogin` revokes and discards it and starts over.
160164 

docs/2026-07-28/tools/inspector/cli Changed · +66 / -14 lines

### CI gates ### Editing the catalog

from line 8
88npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
99```
1010 
11<Tip>
12 Running several commands against the same server, or a flow that spans one
13 session (tasks, subscriptions, elicitation)? The [mcpdo connection
14 client](/docs/2026-07-28/tools/inspector/mcpdo) connects once and keeps the
15 connection open between commands.
16</Tip>
17 
1118The examples below use the installed `mcp-inspector` binary. Without a global install, prefix each command with `npx @modelcontextprotocol/inspector` instead, as above.
1219 
1320## Choosing a server
from line 38
3138 
3239<Note>
3340 **The config file is the only durable way to give a run its
34 [roots](/specification/draft/client/roots):** there is no roots flag, and
35 `--method roots/set` applies only to that one short-lived connection. Roots
36 configured for a server are advertised at connect, so a server that calls
37 `roots/list` (as `@modelcontextprotocol/server-filesystem` does, to learn its
38 allowed directories) gets them.
41 [roots](/specification/draft/client/roots):** there is no roots flag, and the
42 CLI has no `roots/set` method. Roots configured for a server are advertised at
43 connect, so a server that calls `roots/list` (as
44 `@modelcontextprotocol/server-filesystem` does, to learn its allowed
45 directories) gets them.
3946</Note>
4047 
4148See [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration) for `--catalog` vs. `--config`, the `--` separator, and the shared server-selection flags.
from line 52
4552| `--method` | Required companions | Notes |
4653| - | - | - |
4754| `initialize` | None | Connect-only probe: `{serverInfo, protocolVersion, capabilities, instructions}`. |
48| `tools/list` | None | |
49| `tools/call` | `--tool-name`, plus `--tool-arg` / `--tool-args-json` | |
55| `tools/list` | None | `--strict` turns its schema portability check into a gate (see [CI gates](#ci-gates)). |
56| `tools/call` | `--tool-name`, plus optional `--tool-arg` / `--tool-args-json` | |
5057| `resources/list` | None | |
5158| `resources/read` | `--uri` | |
5259| `resources/templates/list` | None | |
60| `resources/directory/read` | `--uri`, plus optional `--cursor` | One page at a time; pass back the previous page's `nextCursor`. |
5361| `prompts/list` | None | |
54| `prompts/get` | `--prompt-name`, `--prompt-args` | |
62| `prompts/get` | `--prompt-name`, plus optional `--prompt-args` | |
5563| `logging/setLevel` | `--log-level` | Legacy era only; modern servers opt in per request instead. |
56| `servers/list`, `servers/show` | None | Read the catalog **without connecting** to anything. |
64| `skills/list` | None | `--verify` checks the skills returned (see [CI gates](#ci-gates)). |
65| `skills/get` | `--uri` | Same `--verify` option. |
66| `servers/list` | None | Read the catalog **without connecting** to anything. |
67| `servers/show` | `--server` | Same, for one entry. |
68| `servers/add` | `--server`, plus a positional target or `--server-url` | Write the catalog without connecting (see [Editing the catalog](#editing-the-catalog)). |
69| `servers/edit` | `--server`, plus what to change | Same. |
70| `servers/remove` | `--server` | Same. |
5771 
5872Stream- or session-only methods (`logging/tail`, for example) are rejected, since a process that exits can't hold a stream open.
5973 
6074### Passing arguments
6175 
62`--tool-arg` takes `key=value` and **coerces** values by JSON-parsing them, so `count=1` becomes a number and `"012"` becomes `12`:
76`--tool-arg` takes `key=value` and **coerces** each value by JSON-parsing it when it parses, so `count=1` and `zip=10001` both send numbers. A value that is not valid JSON (`zip=012`, a bare word) is sent as a string, and a string is then converted to the type the tool's input schema declares for that property, so `zip=012` against a numeric `zip` still sends `12`:
6377 
6478```bash theme={null}
6579mcp-inspector --cli <server> --method tools/call --tool-name mytool \
from line 80
6680 --tool-arg key=value --tool-arg count=1 --tool-arg 'options={"format":"json"}'
6781```
6882 
69`--tool-args-json` takes the whole argument object at once and passes it **verbatim**, with no coercion, so `"012"` stays the string `012`. The two are mutually exclusive:
83`--tool-args-json` takes the whole argument object at once and skips the `key=value` parsing, so `{"zip":"10001"}` sends the string `"10001"` rather than a number. The schema conversion still applies to string values, so this only keeps a string a string when the schema types that property as a string (or doesn't declare it). The two flags are mutually exclusive:
7084 
7185```bash theme={null}
7286mcp-inspector --cli <server> --method tools/call --tool-name mytool \
from line 110
96110 
97111Exit codes distinguish the outcomes: a tool with an app exits `0`, one with no app exits `2`, and a missing tool exits `5`, so a typo isn't mistaken for "no app". A probe failure (unreadable UI resource, malformed `resourceUri`) is reported in a `resourceError` field rather than aborting, so one bad tool never kills a whole listing.
98112 
113The CLI can't render an App, so by default it doesn't advertise the MCP Apps extension at `initialize`. A server that exposes its App tools only to clients claiming App support will then look app-less (exit `2`). Add `--advertise-apps` to claim the extension for that run.
114 
99115<Note>
100116 `tools/list --app-info` always emits NDJSON (one line per tool) regardless of
101117 `--format`; `--format json` reshapes only the single-tool output of
from line 125
109125| Code | Meaning |
110126| - | - |
111127| `0` | Success. |
112| `1` | Usage or unexpected error (the catch-all). |
128| `1` | Usage or unexpected error (the catch-all), including an unreadable secret store or OAuth state file. |
113129| `2` | No MCP App found on the tool (`--app-info` probe). |
114| `3` | Server requires authentication (401/403, `WWW-Authenticate`, OAuth). |
130| `3` | Server requires authentication (HTTP 401/403, or an SDK authorization error). |
115131| `4` | Server unreachable (DNS, connection refused, timeout, `fetch failed`). |
116132| `5` | Tool error: `tools/call` returned `isError: true`, or the tool wasn't found. |
133| `6` | `--strict` found an error-severity tool-schema portability problem. |
134| `7` | `--verify` found a skill that violates SEP-2640 (conformance, digest, or size mismatch). |
135| `8` | `--verify` couldn't check every skill within its read bounds. |
136| `9` | `--verify --require-digests` found a skill that advertises no digests. |
117137 
118138On any non-zero exit the CLI also writes a **single JSON line to stderr**:
119139 
from line 148
128148}
129149```
130150 
151The envelope carries `code` and `message` always, plus `cause` (the underlying error, such as a DNS failure), `status` (the HTTP status) and `url` when known. Query-string secrets in a URL are redacted. `code` is a stable name for the failure: `auth_required`, `unreachable`, `tool_not_found`, `tool_is_error`, `no_app`, `schema_unportable`, `store_unavailable`, `oauth_state_unrecognized`, or the catch-all `error`, among others.
152 
131153Because it's one line, a caller can parse it with `2>&1 | tail -1 | jq .error`.
132154 
133155A `tools/call` that returns `isError: true` still prints its payload, but exits `5`, so an `&&` chain doesn't proceed on a failed call.
from line 159
137159By default the CLI runs the same loopback OAuth flow as the TUI: it opens a browser and waits on a localhost callback that a CI job can't complete. Two flags make non-interactive runs predictable:
138160 
139161* `--stored-auth-only`: never start interactive OAuth or step-up, and never auto-open a browser. Use tokens from the shared store if present, otherwise fail immediately with `auth_required`. This is the flag CI wants.
140* `--use-stored-auth`: reuse a token that the web Inspector already obtained on this machine, refreshing it first when a refresh token is stored.
162* `--use-stored-auth`: reuse a token that the web Inspector already obtained on this machine, refreshing it first when a refresh token is stored. It looks the token up by URL, so it requires `--server-url`.
141163 
142164Without either, and with no TTY on stdin or stderr, the CLI fails fast with `auth_required` rather than hanging for fifteen minutes on a callback nobody will complete.
143165 
from line 178
156178 | jq -e '.result.tools | map(.name) | index("get_weather")' > /dev/null
157179```
158180 
181### CI gates
182 
183Three flags turn a check into a non-zero exit, each with its own code so a job can tell the failures apart:
184 
185* `--strict` (with `tools/list`): reports tool-schema portability problems in full on stderr (path, issue, suggested fix) and exits `6` if any is error-severity. Without it, `tools/list` prints only a one-line count. Under `--format json` the findings are folded into the output as `schemaFindings`.
186* `--verify` (with `skills/list` or `skills/get`): runs the SEP-2640 conformance and digest checks, writes one JSON report per skill on stdout, and exits `7` on a violation or `8` when the read bounds stopped it before every skill was checked. `--skill-catalog-max-skills` and `--skill-catalog-max-bytes` set those bounds.
187* `--require-digests` (with `--verify`): exits `9` for a skill that advertises no digests, instead of reporting it as unverifiable and exiting `0`.
188 
189```bash theme={null}
190mcp-inspector --cli <server> --method tools/list --strict > /dev/null
191mcp-inspector --cli <server> --method skills/list --verify --require-digests
192```
193 
159194### Branch on the failure class
160195 
161196```bash theme={null}
from line 225
190225 server `url` (userinfo or query tokens) or in stdio `args`. Treat raw URL and
191226 `detail` fields as sensitive before pasting them into an issue.
192227</Warning>
228 
229### Editing the catalog
230 
231`servers/add`, `servers/edit`, and `servers/remove` write the catalog without connecting to anything. They go through the same code as the web client's server list, so `env` values and client secrets land in the [secret store](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored) exactly as they would from the UI, and a running web client picks up the change.
232 
233```bash theme={null}
234# Add: the positional target or --server-url describes the entry, not a server to connect to
235mcp-inspector --cli node build/index.js -e API_KEY=secret --method servers/add --server my-server
236 
237# Edit: change the target, -e, --cwd, --header, or --protocol-era, or rename it
238mcp-inspector --cli --method servers/edit --server my-server --rename my-renamed-server
239 
240# Remove the entry and its stored secrets
241mcp-inspector --cli --method servers/remove --server my-renamed-server
242```
243 
244Only the writable catalog (`--catalog`, `MCP_CATALOG_PATH`, or the default) can be written; `--config` is refused. With an in-memory secret store, a write that supplies `-e` values (and a `--rename`) is refused, since the secrets would be lost when the run exits.
193245 
194246## Proxies
195247 

docs/2026-07-28/tools/inspector/configuration Changed · +129 / -25 lines

### Secret store variables ## Where secrets are stored ### How the store is chosen ### The file store ### Where the active store is reported ### Moving back to a keychain

from line 13
1313 
1414Everything below belongs to a client.
1515 
16<Note>
17 The package also installs a second, separate bin, `mcpdo`, the experimental
18 [connection client](/docs/2026-07-28/tools/inspector/mcpdo). It is not reached
19 through the launcher and takes no mode flag.
20</Note>
21 
1622## Choosing servers
1723 
1824### `--catalog` vs. `--config`
1925 
20All three clients resolve `--catalog` and `--config` through the same shared code, so each flag behaves the same in the web app, the CLI, and the TUI. Where the two differ from each other is the table below.
26The CLI and TUI resolve `--catalog` and `--config` through the same shared code, and the web client applies the same rules, so each flag means the same thing in all three. Where the two differ from each other is the table below.
2127 
2228| | `--catalog <path>` | `--config <path>` |
2329| - | - | - |
from line 33
2733| **Editable in the web UI?** | Yes. | No. |
2834| **Use it for** | Your own working set of servers. | A read-only session against someone else's config file. |
2935 
30The two are **mutually exclusive**, and neither combines with an ad-hoc target. Passing both is rejected identically by all three clients.
36The two are **mutually exclusive**, and neither combines with an ad-hoc target. Passing both is rejected identically by all three clients. The web client is stricter in one respect: it also rejects `--header` and `--protocol-era` alongside a file, where the CLI and TUI apply them on top of the file's settings.
3137 
3238<Note>
33 **What a freshly seeded catalog contains depends on the client.** The web backend seeds two sample servers, so a first launch has something to connect to immediately:
39 **What a freshly seeded catalog contains depends on the client.** The web backend seeds three sample servers, so a first launch has something to connect to immediately:
3440 
3541 ```json theme={null}
3642 {
from line 50
4450 "type": "stdio",
4551 "command": "npx",
4652 "args": ["-y", "@modelcontextprotocol/server-everything"]
53 },
54 "example-server-default": {
55 "type": "streamable-http",
56 "url": "https://example-server.modelcontextprotocol.io/mcp"
4757 }
4858 }
4959 }
from line 87
7787| - | - | - |
7888| `--catalog <path>` | Writable catalog file. | None |
7989| `--config <path>` | Read-only session file. | None |
80| `--server <name>` | Pick one named server out of the file. | **Web and CLI only.** The TUI loads every server in the file and lets you choose interactively. |
90| `--server <name>` | Pick one named server out of the file. | **Selects only in the CLI.** The web client accepts it but ignores it with a note, listing every server. The TUI doesn't define it (an unknown-option error) and lets you choose interactively. |
8191| `--transport <type>` | `stdio`, `sse`, or `http`. | Ad-hoc targets only. |
8292| `--server-url <url>` | Server URL for SSE/HTTP. | Ad-hoc targets only. |
8393| `--cwd <path>` | Working directory for a stdio server process. | None |
8494| `-e <KEY=VALUE>` | Environment variables for a stdio server. Repeatable. | None |
8595| `--header "Name: Value"` | HTTP headers for an HTTP/SSE server. Repeatable. | Requires an ad-hoc HTTP/SSE server on the web client. |
96| `--protocol-era <era>` | `legacy`, `auto`, or `modern`: the [protocol era](/docs/2026-07-28/tools/inspector/protocol-eras) to negotiate. | Requires an ad-hoc target on the web client. The CLI and TUI also let it override a file's `protocolEra`. |
97| `--skill-catalog-max-skills <n>` / `--skill-catalog-max-bytes <n>` | Budget for a skills verification run (most skills, most bytes read). | **CLI and TUI only.** Overrides the file's `skillCatalogMaxSkills` / `skillCatalogMaxBytes`. |
8698| `[target...]` | Positional command/URL for one ad-hoc server. | None |
8799 
88100### The `--` separator
89101 
90The **web and CLI** clients split their arguments at a bare `--` and pass everything after it to the target command as its own arguments. This is how you pass a flag that the Inspector would otherwise eat:
102The **web and TUI** clients pass everything after a bare `--` to the target command as its own arguments. This is how you pass a flag that the Inspector would otherwise eat:
91103 
92104```bash theme={null}
93105mcp-inspector node build/index.js -- --config /etc/myserver.conf --verbose
from line 107
95107 
96108Without the separator, `--config` would be read as the Inspector's own read-only-session flag.
97109 
110**The CLI splits the other way:** everything *before* `--` is the target, and everything after it is the Inspector's own options. Without a `--`, the CLI's target is only the leading run of arguments that don't start with a dash, so a server that takes flags needs the separator:
111 
112```bash theme={null}
113mcp-inspector --cli node build/index.js --config /etc/myserver.conf --verbose -- --method tools/list
114```
115 
98116## Web-only flags
99117 
100118| Flag | Meaning |
from line 129
111129| `--client-id <id>` | None | OAuth client ID for a static client. Overrides `client.json`. |
112130| `--client-secret <secret>` | None | OAuth client secret for confidential clients. Overrides `client.json`. |
113131| `--client-metadata-url <url>` | None | CIMD metadata URL. Overrides `client.json`. |
114| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | The redirect URI sent to the authorization server. Default `http://127.0.0.1:6276/oauth/callback`. Must be a loopback host (`127.0.0.1` or `localhost`): the local callback listener receives the authorization code over plaintext `http`, so any other host is rejected and there is no flag to override this. |
132| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | The redirect URI sent to the authorization server. Default `http://127.0.0.1:6276/oauth/callback`. Must be a loopback host (`localhost`, `127.0.0.1` or any other `127.x.x.x` address, or `[::1]`): the local callback listener receives the authorization code over plaintext `http`, so any other host is rejected and there is no flag to override this. |
115133 
116134## CLI-only flags
117135 
from line 137
119137 
120138| Group | Flags |
121139| - | - |
122| **What to invoke** | `--method`, `--tool-name`, `--tool-arg`, `--tool-args-json`, `--uri`, `--prompt-name`, `--prompt-args`, `--log-level`, `--metadata`, `--tool-metadata` |
123| **How to run it** | `--connect-timeout`, `--format`, `--app-info` |
124| **Auth** | `--use-stored-auth`, `--stored-auth-only`, `--relogin`, `--wait-for-auth`, `--list-stored-auth`, `--print-handoff` |
140| **What to invoke** | `--method`, `--tool-name`, `--tool-arg`, `--tool-args-json`, `--uri`, `--cursor`, `--prompt-name`, `--prompt-args`, `--log-level`, `--metadata`, `--tool-metadata`, `--rename` |
141| **How to run it** | `--connect-timeout`, `--format`, `-q` / `--quiet`, `--output`, `--output-format`, `--app-info`, `--advertise-apps`, `--strict`, `--verify`, `--require-digests`, `--completion <shell>` (`bash`, `zsh`, or `fish`) |
142| **Auth** | `--use-stored-auth`, `--stored-auth-only`, `--relogin`, `--no-revoke`, `--wait-for-auth`, `--list-stored-auth`, `--print-handoff` |
125143 
126144## Environment variables
127145 
128Environment variables split the same way as flags: two are read by the launcher itself, and the rest belong to the CLI and TUI or to the web backend.
146Environment variables split the same way as flags: two are read by the launcher itself, and the rest belong to the CLI and TUI, to the web backend, or to every client (the secret store).
129147 
130148### Read by the launcher
131149 
132150| Variable | Effect |
133151| - | - |
134| `MCP_DEBUG` | Append the error stack to a top-level failure. Only when set to a meaningful value: `0`, `false`, and empty read as off. |
152| `MCP_DEBUG` | Append the error stack to a top-level `--web` or `--tui` failure (a `--cli` failure always prints its [JSON error envelope](/docs/2026-07-28/tools/inspector/cli#exit-codes-and-error-envelopes) instead). Only when set to a meaningful value: `0`, `false`, and empty read as off. |
135153| `DEBUG` | Same, with the same meaningful-value rule, so a stray `DEBUG=0` doesn't turn stack traces on and `DEBUG` still works as the npm `debug` package's namespace filter. |
136154 
137155### CLI and TUI
138156 
139| Variable | Effect |
140| - | - |
141| `MCP_CATALOG_PATH` | Fallback for `--catalog`. Honored only when no ad-hoc target is given, so a shell that exports it can still run one-off ad-hoc invocations. |
142| `MCP_CLIENT_CONFIG_PATH` | Fallback for `--client-config`. |
143| `MCP_OAUTH_CALLBACK_URL` | Fallback for `--callback-url`. |
144| `MCP_STORAGE_DIR` | Directory for the OAuth state file (`<dir>/oauth.json`). |
145| `MCP_INSPECTOR_OAUTH_STATE_PATH` | Per-file override of the OAuth state path. Takes precedence over `MCP_STORAGE_DIR`. |
146| `MCP_AUTO_OPEN_ENABLED` | Controls browser auto-open and whether interactive OAuth may run without a TTY. `true` forces auto-open and allows OAuth prompts without a TTY, `false` never opens, and unset opens only on a TTY. |
157Some of these are read by the web backend too, as the **Read by** column shows.
147158 
159| Variable | Read by | Effect |
160| - | - | - |
161| `MCP_CATALOG_PATH` | Web, CLI, TUI | Fallback for `--catalog`. The CLI honors it only when no ad-hoc target is given, so a shell that exports it can still run one-off ad-hoc invocations. The web client and TUI apply it regardless, so combining it with an ad-hoc target is rejected there. |
162| `MCP_CLIENT_CONFIG_PATH` | CLI, TUI | Fallback for `--client-config`. |
163| `MCP_OAUTH_CALLBACK_URL` | CLI, TUI | Fallback for `--callback-url`. |
164| `MCP_STORAGE_DIR` | Web, CLI, TUI | Storage directory. Relocates the OAuth state file (`<dir>/oauth.json`) and the secrets file (`<dir>/secrets.json`). |
165| `MCP_INSPECTOR_OAUTH_STATE_PATH` | CLI, TUI | Per-file override of the OAuth state path. Takes precedence over `MCP_STORAGE_DIR`. |
166| `MCP_AUTO_OPEN_ENABLED` | Web, CLI | Controls browser auto-open. `false` never opens one. In the CLI, `true` forces auto-open and lets interactive OAuth run without a TTY, and unset opens only on a TTY. In the web client, unset opens the UI at launch. The TUI does not read it. |
167 
148168### Web backend environment variables
149169 
150170| Variable | Effect |
151171| - | - |
152172| `MCP_INSPECTOR_API_TOKEN` | Pin the [session token](/docs/2026-07-28/tools/inspector/web#the-session-token) instead of generating a random one per launch. |
153| `DANGEROUSLY_OMIT_AUTH` | Disable the `/api/*` token check entirely. |
154| `HOST` | Bind host. Defaults to `localhost`. |
173| `DANGEROUSLY_OMIT_AUTH` | Disable the `/api/*` token check entirely. Only `true` or `1` turn it off. |
174| `HOST` | Bind host. Defaults to `127.0.0.1`. |
155175| `CLIENT_PORT` | Web UI port. Defaults to `6274`. |
156176| `DANGEROUSLY_BIND_ALL_INTERFACES` | Required opt-in to bind a wildcard host (`0.0.0.0`, `::`, or any equivalent spelling). |
157177| `ALLOWED_ORIGINS` | Comma-separated origin allow-list. **Replaces** the default list rather than merging. |
158| `MCP_SANDBOX_PORT` | Pin the MCP Apps sandbox port, which is dynamic by default. |
178| `MCP_PROXY_AUTH_TOKEN` | Deprecated v1 name for `MCP_INSPECTOR_API_TOKEN`, used only when the new name is unset. |
179| `MCP_SANDBOX_PORT` | MCP Apps sandbox port. Defaults to `6275`; `0` asks the OS for a free port. |
180| `SERVER_PORT` | v1's proxy port, now only a fallback for the sandbox port when `MCP_SANDBOX_PORT` is unset or invalid. |
181| `MCP_APP_ORIGIN_PORT` | Port of the app-origin server used by MCP Apps that declare `_meta.ui.domain`. Defaults to `6278`; `0` asks the OS. |
182| `MCP_SANDBOX_FULL_ADDRESS` | Public URL of the MCP Apps sandbox proxy, for running behind a reverse proxy. |
183| `MCP_APP_ORIGIN_FULL_ADDRESS` | Public origin that `_meta.ui.domain` app documents are served from, for running behind a reverse proxy. |
184| `MCP_LOG_FILE` | Append the backend's structured (JSON lines) log to this file. |
185| `MCP_AUTO_OPEN_ENABLED` | `false` stops the browser from opening at launch. See [CLI and TUI](#cli-and-tui). |
186| `MCP_CATALOG_PATH`, `MCP_STORAGE_DIR` | As described under [CLI and TUI](#cli-and-tui). |
159187| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Standard proxy routing for outbound MCP connections. |
160188 
161189<Warning>
from line 191
163191 The web backend spawns processes and holds OAuth tokens, so anyone who can
164192 reach it can drive it.
165193</Warning>
194 
195What the token does and does not protect is laid out under [The web backend and its API token](/docs/2026-07-28/tools/inspector/security#the-web-backend-and-its-api-token).
196 
197### Secret store variables
198 
199Read by **every** client: web, CLI, TUI, and [mcpdo](/docs/2026-07-28/tools/inspector/mcpdo). How they combine is described under [Where secrets are stored](#where-secrets-are-stored).
200 
201| Variable | Effect |
202| - | - |
203| `MCP_INSPECTOR_SECRET_STORE` | `keyring`, `file`, or `memory` (case-insensitive) picks the store outright and skips the keychain probe. Empty counts as unset; any other value is ignored with a warning. |
204| `MCP_INSPECTOR_SECRET_FILE` | Path of the file store. Defaults to `secrets.json` in `MCP_STORAGE_DIR` when that is set, else `~/.mcp-inspector/secrets.json`. |
205| `MCP_INSPECTOR_SECRET_KEY_FILE` | Path of a file holding the passphrase that encrypts the file store (trailing line breaks removed). **Preferred**, and what Docker and Compose secrets are for. If the file is missing, unreadable or empty, the store refuses to read or write. |
206| `MCP_INSPECTOR_SECRET_KEY` | The passphrase itself. Use a generated, high-entropy value. Setting both key variables (with a non-blank `MCP_INSPECTOR_SECRET_KEY`) is an error. |
207| `MCP_INSPECTOR_PERSIST_TOKENS` | Which acquired OAuth tokens are persisted: `all` (default), `access` (no refresh tokens), or `none` (re-authorize every run). Client secrets are always persisted. |
208 
209## Where secrets are stored
210 
211The Inspector keeps credentials out of `mcp.json`, `client.json` and `oauth.json`, so that sharing, committing or syncing those files does not leak them. These are stored in a **secret store** instead:
212 
213* acquired OAuth tokens (access, refresh and IdP session tokens);
214* each server's OAuth client secret, and the enterprise IdP client secret from Client Settings;
215* each stdio server's `env:` values. When an entry is saved, each `env` key stays in `mcp.json` with an empty value and the real value goes to the store.
216 
217**`headers` are not moved.** They are saved in `mcp.json` exactly as written, so a header that carries a credential stays in the file.
218 
219### How the store is chosen
220 
221Each process picks one store, once, the first time it needs it: the web backend at startup, the CLI and TUI on first use. All clients use the same order:
222 
2231. **`MCP_INSPECTOR_SECRET_STORE`**, if set to `keyring`, `file` or `memory`. Nothing is probed.
2242. **The OS keychain**, if a probe reaches it: Keychain on macOS, Credential Manager on Windows, the Secret Service (libsecret, such as GNOME Keyring or KWallet) on Linux. Entries go under the service name `mcp-inspector`. Most desktop installs stop here.
2253. **A fallback**, announced on stderr:
226 * `memory` in a container whose secrets directory is **not** on a mounted volume, because a file in the container's writable layer would be lost anyway;
227 * `file` everywhere else.
228 
229| Where you run it | Store | Survives a restart? |
230| - | - | - |
231| Desktop macOS or Windows, or Linux with a Secret Service running | OS keychain | Yes |
232| Linux without libsecret or a Secret Service | File (`secrets.json`, mode `0600`) | Yes |
233| Headless server or SSH session with no D-Bus session | File | Yes |
234| Android/Termux | File | Yes |
235| Container with **no volume** on the secrets directory | Memory | No, this session only |
236| Container **with** a volume on the secrets directory | File | Yes |
237 
238<Warning>
239 **With no keychain, secrets go to a plaintext file, and you did not have to ask for it.** On a host where the keychain probe fails (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. Unless you supply a key, that file is **unencrypted**. Mode `0600` keeps out other non-root users, but not root, not backups or copies of your home directory, and not any program running as you, including the stdio servers the Inspector starts.
240 
241 Pick one:
242 
243 * **Get a keychain back:** install libsecret and run a Secret Service (for example `gnome-keyring`), or run the Inspector inside a desktop session. On the next start the Inspector moves the file's secrets into the keychain and deletes the file.
244 * **Encrypt the file:** supply a generated key with `MCP_INSPECTOR_SECRET_KEY_FILE` (preferred) or `MCP_INSPECTOR_SECRET_KEY`.
245 * **Don't write secrets to disk at all:** `MCP_INSPECTOR_SECRET_STORE=memory`, and re-enter them each session.
246 
247 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
248</Warning>
249 
250[mcpdo](/docs/2026-07-28/tools/inspector/mcpdo) never falls back to `memory`: its commands and its daemon are separate processes, so it uses the file store instead.
251 
252### The file store
253 
254The file lives at `MCP_INSPECTOR_SECRET_FILE` if set, else `secrets.json` inside `MCP_STORAGE_DIR` if that is set, else `~/.mcp-inspector/secrets.json`. The default sits **beside** the storage directory (`~/.mcp-inspector/storage`), not inside it.
255 
256* **Encryption is opt-in.** With a key, the file is encrypted with AES-256-GCM, the passphrase stretched by scrypt against a random salt regenerated on every write. Generate the key rather than choosing it, for example `(umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)`, and keep it away from the secrets file, its backups and any repository.
257* **Adding a key later is safe.** The next write upgrades a plaintext file in place.
258* **Changing or losing the key is not.** A file that no longer decrypts reads as empty, and the Inspector refuses to overwrite it. Restore the key, or delete the file and enter the values again.
259* **Permissions are enforced.** The file is written `0600` and re-tightened at startup. If it can't be (another owner, a read-only mount), the log and the settings footer say so.
260* **Concurrent Inspectors are safe.** A CLI run next to a web session serializes on a lock beside the file and verifies each write by reading it back.
261 
262### Where the active store is reported
263 
264* **On stderr**, when the store is selected: a warning on any keychain fallback, and another if the file is unencrypted, loosely permissioned or unreadable. Both end with a link to the Inspector's [secret storage guide](https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md). The web client's startup banner has a `Secrets:` line on every run.
265* **In the web client**, a footer in the **Client Settings**, **Server Settings** and **Add / Edit / Clone server** dialogs names the store, and turns into a warning when it is memory-only, unencrypted, loosely permissioned or unreadable.
266 
267### Moving back to a keychain
268 
269Install libsecret (or start a Secret Service) on a machine that was using the file store, and the next start selects the keychain and **moves the file's contents into it**. A value already in the keychain wins over the file's. The file is deleted only once every entry is accounted for, and a file that can't be decrypted is left in place. Setting `MCP_INSPECTOR_SECRET_STORE=keyring` triggers the same hand-off. Choosing `file` or `memory` never copies anything out of the keychain.
166270 
167271## Catalog file format
168272 

docs/2026-07-28/tools/inspector/mcpdo New page · 172 lines, new page

# mcpdo connection client ## Install ## Quickstart ## Choosing what to connect ## Addressing a connection ## Tasks ## Output ## Authorization ## Elicitation ## Protocol eras ## The daemon ## Isolating untrusted stdio servers ## Environment variables ## Using mcpdo from a coding agent

A whole new page. There's nothing to diff it against, so here is what it says.

# mcpdo connection client

> Connect to an MCP server once, then run many commands against that named connection

`mcpdo` is an **experimental** command-line client that ships in the `@modelcontextprotocol/inspector` package alongside `mcp-inspector`. Where the [CLI client](/docs/2026-07-28/tools/inspector/cli) connects, runs one `--method` and disconnects, `mcpdo` **connects once** and keeps the connection open, so you can run many commands against it from any shell, the way `ssh-agent` keeps keys loaded.

| | `mcp-inspector --cli` | `mcpdo` |
| - | - | - |
| **Lifecycle** | Connect, one `--method`, disconnect | Connect once, many commands |
| **State** | None between runs | Named connections held by a daemon |
| **Best for** | CI assertions, one JSON blob per run | Exploring a server, agent tool use, multi-step flows over one session (tasks, subscriptions, elicitation) |

Connections are held by a local background daemon, `mcpdod`, which `mcpdo` starts on demand and stops after its last connection closes. You never start it by hand. What it exposes, and how it is protected, is described under [The mcpdo connection daemon](/docs/2026-07-28/tools/inspector/security#the-mcpdo-connection-daemon).

Watch the [mcpdo tutorial video](https://www.youtube.com/watch?v=_a6eG0y156k).

## Install

`mcpdo` is a second bin in the same package, so install the package globally, or run it through `npx -p`:

```bash theme={null}
npm install -g @modelcontextprotocol/inspector
mcpdo --help

# or without installing
npx -p @modelcontextprotocol/inspector mcpdo --help
```

## Quickstart

```bash theme={null}
mcpdo servers/list                            # catalog entries you can connect
mcpdo connect my-server                       # connect a catalog entry
mcpdo @my-server tools/list
mcpdo @my-server tools/call echo message:=hi
mcpdo @my-server resources/read file:///tmp/notes.txt
mcpdo connections/list                        # what's open
mcpdo disconnect my-server
```

`mcpdo help`, or `mcpdo <command> --help`, prints the full, authoritative list of commands and flags.

## Choosing what to connect

`connect` takes a catalog entry name or an ad-hoc target:

```bash theme={null}
mcpdo connect my-server                        # from the default catalog
mcpdo connect my-server --config ./mcp.json    # from a read-only config file
mcpdo connect https://example.com/mcp          # ad-hoc HTTP/SSE server
mcpdo connect node build/index.js              # ad-hoc stdio server
```

The catalog and `--config` behave exactly as for the other clients (see [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration#choosing-servers)): the default catalog is `~/.mcp-inspector/mcp.json`, overridable with `--catalog` or `MCP_CATALOG_PATH`. `servers/list` and `servers/show <name>` read entries from disk. `connections/*` shows what the daemon currently holds. There are no commands to edit the catalog; edit the file directly.

A stdio server runs with the working directory and command resolution of the shell that ran `connect`, not of the daemon, so relative paths and bare command names mean what you would expect.

## Addressing a connection

A catalog connection is named after its entry. An ad-hoc one is named after its first token (`node`, `docker`, or the URL itself) unless you name it at connect time with `@name`:

```bash theme={null}
mcpdo connect @api https://example.com/mcp
```

Name the connection on each command with `@name`, or with `--connection <name>` (shorthand `--conn`). A connection named after a URL can only be addressed with `--conn <url>`, since `@name` takes only letters, digits, `_`, `.` and `-`:

```bash theme={null}
mcpdo @my-server tools/list
mcpdo --conn my-server tools/list
```

Omitting the name falls back to the most recently used connection, but only on an interactive TTY. From a script or an agent, where stdin is not a TTY, an unqualified command is an error, so that a background job never acts on whichever server you last used. Set `MCP_ALLOW_DEFAULT_CONNECTION=1` to allow the fallback anyway.

Connections **self-heal**: a dropped transport (an expired HTTP session, an exited stdio child) is re-dialed transparently on next use with the stored credentials. Only an `auth_required` error needs you to run `connect` again.

A connection runs **one call at a time**. Commands against it from other shells queue behind the call in flight, and while a call is parked on an [elicitation](#elicitation), new calls on that connection are refused until it is answered. A long call does not block other connections.

## Tasks

A tool that requires task support is refused by a plain `tools/call`; add `--task` to make a task-augmented call. It still **blocks** until the task finishes, then prints the final result:

```bash theme={null}
mcpdo @my-server tools/call --task start_job size:=large
```

If the task reaches `input_required`, its question is handled like any other [elicitation](#elicitation).

## Output

| Flag | Output |
| - | - |
| `--format text` (default) | Human-readable, with ANSI styling on a TTY unless `--plain` or `NO_COLOR` is set. |
| `--format json` | The pretty-printed payload, with no `{ result }` envelope. For scripts and agents. |

The global flags are `--format`, `--plain`, `--connection` / `--conn`, `--catalog` / `--config`, and `--stored-auth-only`. They may go before or after the subcommand, but before any `--`. The `@name` shorthand must come before the subcommand.

Terminal-bound text (results, elicitation prompts, daemon errors) has control characters stripped, so a server cannot rewrite your terminal. `--format json` stays verbatim.

## Authorization

`mcpdo` shares `oauth.json` and the secret store with the other Inspector clients, so a server you have already authorized elsewhere connects without a prompt. OAuth runs at **connect time**. Mid-session step-up is handled by the one-shot CLI, not by `mcpdo`.

| Command | Effect |
| - | - |
| `mcpdo connect <name> --relogin` (`-r`) | Clear this server's stored tokens before connecting (HTTP/SSE only), so it signs in fresh if the server requires auth. |
| `mcpdo disconnect <name> --clear-auth` (`-c`) | Close the connection **and** clear its stored tokens, so the next `connect` signs in fresh. |
| `mcpdo auth/list` | Stored credentials, each annotated with the catalog or connection names it is "known as", and `● live` when an open connection holds it. |
| `mcpdo auth/clear <url-or-name>` | Clear one stored credential, by store URL or friendly name. `--all --yes` clears every one. |
| `mcpdo auth/ema-login` / `auth/ema-status` / `auth/ema-logout` | Enterprise-managed authorization: sign in to the IdP once, then connect to EMA servers silently. |

When a browser sign-in is needed and neither stdin nor stderr is a TTY (and `MCP_AUTO_OPEN_ENABLED` is not forced on), `connect` exits `0` immediately with `pendingAuth: true` and an `authUrl`. Relay that URL to whoever will sign in. The connection completes on its own once they do: the next real command against it finishes the connection, and `connections/list` reports `pendingAuthSignedIn` in the meantime. Don't reconnect to fix a pending sign-in.

On a host with no OS keychain, `mcpdo` stores tokens in the shared `secrets.json` file, never in memory, because its commands and the daemon are separate processes. That file is plaintext unless you supply a key; see [Where secrets are stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored).

## Elicitation

When a server asks a question mid-call (a legacy `elicitation/create`, a modern MRTR round, or a task that reaches `input_required`):

* **On an interactive TTY**, `mcpdo` prompts inline. Form mode renders one prompt per field with a review step. URL mode prints the URL and waits for you to confirm you finished.
* **From a script or agent** (`--format json`, or no TTY), the command returns `elicitationPending` with an `elicitationId`, and the call stays **parked** on the daemon. Answer it from any shell:

```bash theme={null}
mcpdo elicitation/respond <elicitationId> approved:=true   # form fields as key:=value or JSON
mcpdo elicitation/respond <elicitationId> --done           # URL mode: I finished
mcpdo elicitation/respond <elicitationId> --decline        # form mode only
mcpdo elicitation/respond <elicitationId> --cancel         # either mode
```

A parked elicitation is cancelled after 10 minutes, and a connection holds one at a time; until it is answered, other calls on that connection are refused. URL mode is never auto-accepted. To keep a server from asking at all, connect with `--elicit off`. `--elicit url`, `form` or `both` (the default) choose which modes are advertised.

## Protocol eras

`mcpdo` negotiates the same [protocol eras](/docs/2026-07-28/tools/inspector/protocol-eras) as the other clients. `connect --era legacy|auto|modern` overrides a catalog entry's `protocolEra`, and is the only way to set it for an ad-hoc target. `connections/list` tags each connection `[legacy]` or `[modern]`, and `connections/show <name>` gives the negotiated version, server info, capabilities, and the supported-versions list when the server was probed.

## The daemon

| Command | Effect |
| - | - |
| `mcpdo daemon status` | Whether a daemon is running, and its connections and idle countdown. |
| `mcpdo daemon stop` | Close every connection and stop the daemon. |
| `eval "$(mcpdo private)"` | Give this shell its own daemon and connections. |

By default every shell you run `mcpdo` from shares one daemon, so a connection opened in one terminal is usable in another. `mcpdo private` exports `MCP_INSPECTOR_DAEMON_DIR` and `MCP_INSPECTOR_DAEMON_TOKEN` so the current shell gets a separate daemon. That keeps connections apart; it is not a security boundary against other processes running as you.

The daemon exits about a minute after its last connection closes. A daemon started before an upgrade keeps running the old code until then, so run `mcpdo daemon stop` after upgrading the package.

## Isolating untrusted stdio servers

The daemon's token controls who can **command** the daemon, not what a server can **do**. A stdio server runs with your full user privileges. To contain one you don't fully trust, make the stdio command a container:

```bash theme={null}
mcpdo connect @sandboxed -- docker run -i --rm --network none -v "$PWD:/work:ro" <server-image>
```

Without `@sandboxed`, the connection would be named `docker`.

Adjust the network and mount flags to what the server needs. HTTP and SSE servers run no local code, so they need no process isolation.

## Environment variables

`mcpdo` reads the same catalog, storage and secret-store variables as the other clients (see [Environment variables](/docs/2026-07-28/tools/inspector/configuration#environment-variables)), plus these:

| Variable | Effect |
| - | - |
| `MCP_INSPECTOR_DAEMON_DIR` | Directory holding the daemon's socket, lock, token and log. Defaults to `MCP_STORAGE_DIR` when set, else `~/.mcp-inspector`. Set by `mcpdo private`. |
| `MCP_INSPECTOR_DAEMON_TOKEN` | IPC token to present to, or start, the daemon. Unset, the `mcpdo` command that starts the daemon generates one, and the daemon publishes it to `mcpdod.token`. Set by `mcpdo private`. |
| `MCP_ALLOW_DEFAULT_CONNECTION` | `1` lets a command without `@name` / `--connection` use the most recently used connection even when stdin is not a TTY. |

## Using mcpdo from a coding agent

The package ships an agent skill that teaches an agent to drive `mcpdo`: connections it holds then extend the agent's toolset, and it knows to relay sign-in URLs and answer parked elicitations. `mcpdo agent-help` prints the guide, `mcpdo agent-help --skill-path` prints the path of the installable skill file, and `mcpdo agent-help --instructions` prints a block to append to a project's `CLAUDE.md` or `AGENTS.md`.

docs/2026-07-28/tools/inspector/protocol-eras Changed · +46 / -28 lines

## Cancellation

from line 6
66 
77## The `Protocol Era` setting
88 
9Each server carries a `protocolEra` of `legacy`, `auto`, or `modern`. In the web client it lives in **Server Settings**; in a catalog or config file it is the `protocolEra` field; in the CLI and TUI it comes from that same file.
9Each server carries a `protocolEra` of `legacy`, `auto`, or `modern`. In the web client it lives in **Server Settings**; in a catalog or config file it is the `protocolEra` field; the CLI and TUI read it from that same file, and their `--protocol-era <legacy|auto|modern>` flag overrides it for a single run.
1010 
1111| Era | What the Inspector does at connect |
1212| - | - |
from line 23
2323 configured.
2424</Note>
2525 
26Era selection works the same way in all three clients.
26Once connected, the negotiated era is shown as a badge on the **Protocol** tab's Messages header and in **Connection Info**. On a modern connection, `server/discover` also supplies `capabilities` (including `extensions`), `instructions`, and the list of `supportedVersions`. The server's name and version arrive in the result `_meta` under `io.modelcontextprotocol/serverInfo`.
2727 
28Once connected, the negotiated era is reported in the connection header and in **Connection Info**. On a modern connection, `server/discover` also supplies `capabilities` (including `extensions`), `instructions`, and the list of `supportedVersions`. The server's name and version arrive in the result `_meta` under `io.modelcontextprotocol/serverInfo`.
29 
3028<Frame caption="Server Settings: the Protocol Era selector, with all three choices.">
3129 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/settings-protocol-era.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=34566c45f97c8af0e2c0d9ee0493b572" width="3840" height="2160" data-path="images/inspector/settings-protocol-era.png" />
3230</Frame>
from line 31
3331 
3432## Reproducing each era locally
3533 
36Every section below ends with a **Reproduce with ...** pointer to a JSON config for one of the **composable test servers** shipped in the Inspector repository. Clone the repo, build the test servers, then point the Inspector at the config the section names.
34Every section below ends with a **Reproduce with ...** pointer to a JSON config for one of the **composable test servers** shipped in the Inspector repository. Clone the repo and build the test servers:
3735 
3836```bash theme={null}
3937git clone https://github.com/modelcontextprotocol/inspector
4038cd inspector && npm install && npm run build
41cd clients/web && npm run test-servers:build
39cd clients/web && npm run test-servers:build && cd ../..
4240```
4341 
42Then, from the repo root, start a server from the config a section names:
43 
44```bash theme={null}
45node test-servers/build/server-composable.js --config test-servers/configs/<name>.json
46```
47 
48The server prints its URL on stderr. If the config's port is already in use, it binds the next free port, so use the printed URL rather than assuming the port. Add that URL as a server in the Inspector, with the Protocol Era the section names.
49 
4450***
4551 
4652## Logging
from line 93
8793 </Tab>
8894 
8995 <Tab title="Modern">
90 The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus the `resourcesListChanged` opt-in. The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
96 The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
9197 
92 Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the Inspector reconnects by re-sending `subscriptions/listen`.
98 Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
9399 
94 Reproduce with `test-servers/configs/subscriptions-modern-http.json`.
100 Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
95101 </Tab>
96102</Tabs>
97103 
from line 119
113119 </Tab>
114120 
115121 <Tab title="Modern">
116 Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the *negotiated extension* rather than on `capabilities.tasks`.
122 Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
117123 
118124 Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
119125 
from line 154
148154| `mrtr_sample` | An embedded sampling request, routed to the Sampling panel. |
149155| `mrtr_roots` | An embedded `roots/list`, answered silently from configured roots (no modal). |
150156| `mrtr_edge` | An `inputRequests`-only round, then a `requestState`-only round. |
157| `mrtr_empty` | One elicitation round, then completes with an empty result (no `content`, no `structuredContent`). |
151158| `mrtr_loop` | Never completes, so the client stops at its `MRTR_MAX_ROUNDS` limit. |
152159 
153160<Note>
154 The legacy `collect_elicitation` pattern (a server calling
155 `server.elicitInput`) **errors** on a 2026-07-28 connection, because
161 The legacy pattern of a server calling `server.elicitInput` (the test servers'
162 `collect_elicitation` preset) **errors** on a 2026-07-28 connection, because
156163 server-to-client requests aren't allowed there. MRTR is its modern
157164 replacement.
158165</Note>
from line 168
161168 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/mrtr-pending-request.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=99f8acb7f845a12aed42bcea4d310fee" width="3840" height="2160" data-path="images/inspector/mrtr-pending-request.png" />
162169</Frame>
163170 
171<Frame caption="The monitoring sidebar's Protocol tab after mrtr_confirm completes. Both tools/call rounds sit inside one MRTR conversation, Round 1 tagged input_required and Round 2 complete, while unrelated traffic stays outside it.">
172 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/mrtr-protocol-conversation.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=b083d8127a7a67028fcd9702b25aeac1" width="3840" height="2160" data-path="images/inspector/mrtr-protocol-conversation.png" />
173</Frame>
174 
164175***
165176 
166177## Tools: mirrored headers and excluded tools
from line 185
174185 
175186Reproduce with `test-servers/configs/xmcpheader-modern-http.json`.
176187 
177<Warning>
178 **`Mcp-Param-*` mirroring is skipped by the SDK in the browser.** Calling a
179 mirrored tool from the *web* client omits the header, so a strict server
180 answers `-32020` (`HeaderMismatch`, see the [error
181 taxonomy](#network-and-protocol-headers-and-the-error-taxonomy) below). The
182 same tool called from the **CLI** or **TUI**, which both run on Node, mirrors
183 correctly. The header is dropped by an environment check inside the SDK,
184 outside the Inspector's control.
185</Warning>
188<Note>
189 On a modern connection the Inspector mirrors `x-mcp-header` arguments into
190 `Mcp-Param-*` headers itself, in all three clients. In the **web** client the
191 headers are added by the Inspector's Node backend, which issues the upstream
192 request, so they reach the server even though the browser never sends them.
193</Note>
186194 
187<Frame caption="get_weather shows its mirrored city -> Mcp-Param-City header, while invalid_header_tool is struck through under the Excluded (SEP-2243) divider.">
188 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tools-sep2243.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=98b020f6612b3a76b78b1a8d6159c2c0" width="3840" height="2160" data-path="images/inspector/tools-sep2243.png" />
195<Frame caption="get_weather shows its mirrored city -> Mcp-Param-City header, and the Network sidebar confirms the call carried mcp-param-city: Boston. invalid_header_tool is struck through under the Excluded (SEP-2243) divider.">
196 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/tools-sep2243.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=0e011c3012332c0a74d0f57fc84a5cf2" width="3840" height="2160" data-path="images/inspector/tools-sep2243.png" />
189197</Frame>
190198 
191199### `-32602` error panels
192200 
193Under the modern era a `tools/call` that rejects with `-32602` renders as a distinct **error panel**:
201A `tools/call` that rejects with `-32602` renders as a distinct **error panel**, on either era:
194202 
195203* **Unknown Tool**: when the message names a tool the server does not list. Reproduce by calling any name absent from the server's `tools/list`.
196204* **Invalid Parameters**: any other `-32602`. Reproduce with the `trigger_invalid_params` tool in the config above.
197205 
198Both eras reject with `-32602`; only the Inspector's presentation changes. On a legacy connection you get one generic JSON-RPC failure and have to read the message to tell which case you hit.
206The two share one error code, so the Inspector reads the message to tell them apart. Any other error code renders as a generic failure.
199207 
200208***
201209 
from line 233
225233 
226234***
227235 
236## Cancellation
237 
238On a legacy connection, cancelling an in-flight tool call sends `notifications/cancelled`. On a modern Streamable HTTP connection the Inspector instead closes that request's own SSE response stream, which is the 2026-07-28 cancellation signal. Over stdio, cancellation is still `notifications/cancelled`.
239 
240Reproduce with `test-servers/configs/cancellation-modern-http.json`: run `slow_task`, click **Cancel** after a few seconds, and the server's terminal prints how far the task got before it stopped.
241 
242***
243 
228244## Sessions
229245 
230246A legacy Streamable HTTP connection may carry a server-assigned session id (`Mcp-Session-Id`), which the client tears down with an HTTP `DELETE`. A modern connection is **sessionless and per-request**: with no session id the client SDK sends no `DELETE` to the server, so disconnect is purely local.
247 
248A legacy connection also opens a standalone `GET` notification stream after `initialize`, to carry notifications that do not belong to any request. The legacy-only **Suppress Notification Stream** option in **Server Settings** skips that stream, so you can inspect a server that cannot serve a second concurrent request. Modern connections never open the stream, so the option does not apply to them.
231249 
232250This has a practical consequence for your own test servers. A stateless modern handler constructed per request cannot hold state between calls, which is why `test-servers/configs/subscriptions-modern-http.json`, unlike its legacy counterpart, omits an `update_resource` tool: the mutation would run against a throwaway server instance and be invisible to the next read.
233251 

docs/2026-07-28/tools/inspector/recipes Changed · +132 / -24 lines

### Keeping your servers and secrets ### Health checks and other modes ## Keeping a connection open with mcpdo

from line 1
11# Recipes
22 
3> Practical guides for transports, importing configs, reviewing MCP Apps, Docker, and network hosting
3> Practical guides for transports, importing configs, reviewing MCP Apps, Docker, network hosting, and persistent connections with mcpdo
44 
55## Connecting stdio vs. HTTP servers
66 
from line 33
3333 
3434`--transport` accepts `http` (Streamable HTTP) and `sse`. If the server is protected, see [Authorization](/docs/2026-07-28/tools/inspector/authorization): no setup is needed in advance, because when the server answers `401` the Inspector runs the OAuth flow described there and retries the connection.
3535 
36For an HTTP server, also decide its [protocol era](/docs/2026-07-28/tools/inspector/protocol-eras). The default is `legacy`; set `modern` or `auto` in Server Settings (or `protocolEra` in the catalog file) to exercise the 2026-07-28 behavior.
36For an HTTP server, also decide its [protocol era](/docs/2026-07-28/tools/inspector/protocol-eras). The default is `legacy`; set `modern` or `auto` in Server Settings (or `protocolEra` in the catalog file) to exercise the 2026-07-28 behavior. For an ad-hoc target, pass it at launch with `--protocol-era legacy|auto|modern`.
3737 
3838## Importing an existing client config
3939 
from line 42
4242configs directly, and it also reads a server's own [MCP Registry](/registry/about) `server.json`.
4343 
4444Import merges into the active [catalog](/docs/2026-07-28/tools/inspector/configuration#choosing-servers)
45(the Inspector's writable server list), so existing entries aren't clobbered. If you'd rather
45(the Inspector's writable server list). When an imported server's id is already taken, you
46choose whether to overwrite, skip or rename it. If you'd rather
4647not touch your catalog at all, launch against the foreign file read-only instead:
4748 
4849```bash theme={null}
from line 64
6364 <Step title="Probe the security posture without calling the tool">
6465 ```bash theme={null}
6566 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
66 --method tools/call --tool-name <tool> --app-info
67 --method tools/call --tool-name <tool> --app-info --advertise-apps
6768 ```
6869 
70 `--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
71 
6972 One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
7073 
7174 ```json theme={null}
from line 101
98101 mcp-inspector --web &
99102 ```
100103 
101 Pinning `MCP_SANDBOX_PORT` matters here: the app's UI is served from a separate sandbox port that is dynamic by default, and your automation needs a fixed address to reach it.
104 Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
102105 </Step>
103106 
104107 <Step title="Navigate one deep link to a rendered widget">
from line 117
114117 
115118 | Selector | Attribute | Values |
116119 | - | - | - |
117 | `[data-testid="apps-form"]` | `data-app-status` | `ready` (on failure, `data-app-error` carries the reason) |
118 | `[data-testid="connection-status"]` | `data-status` | `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
120 | `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
121 | `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
119122 | `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
120123 </Step>
121124</Steps>
from line 128
125128A container image is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
126129 
127130```bash theme={null}
128docker run --rm -p 6274:6274 ghcr.io/modelcontextprotocol/inspector
131docker run --rm -p 127.0.0.1:6274:6274 ghcr.io/modelcontextprotocol/inspector
129132```
130133 
131134Read the [session token](/docs/2026-07-28/tools/inspector/web#the-session-token) from the container logs, or pin it with `-e MCP_INSPECTOR_API_TOKEN=<value>`.
132135 
133The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as a non-root user. It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
136The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as the non-root `node` user (uid `1000`). It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
134137 
135Its `HEALTHCHECK` probes the web UI, so add `--no-healthcheck` when running `--cli` or `--tui` (neither has a web server). `<target>` below is an [ad-hoc target](/docs/2026-07-28/tools/inspector/configuration#ad-hoc-targets): a positional stdio command, or `--server-url <url> --transport http`.
138<Warning>
139 **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
140 6274:6274` publishes on every host interface, putting a backend that spawns
141 processes, and the page that discloses its token, on your local network. The
142 image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
143 not the host's. See [Publish the port on loopback
144 only](/docs/2026-07-28/tools/inspector/security#publish-the-port-on-loopback-only).
145</Warning>
136146 
147To use the **Apps** tab, also publish the MCP Apps sandbox port, `6275`, and `6278` for an app that declares `_meta.ui.domain`. Publish each on the same port number inside and out, since the browser is handed the in-container port:
148 
137149```bash theme={null}
138docker run --rm --no-healthcheck ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
150docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6275:6275 \
151 ghcr.io/modelcontextprotocol/inspector
139152```
140153 
154### Keeping your servers and secrets
155 
156The server list, OAuth state and secrets live under `/home/node/.mcp-inspector`, in the container's writable layer, so `--rm` discards them and every run starts empty. Mount a volume there to keep them:
157 
158```bash theme={null}
159docker run --rm -p 127.0.0.1:6274:6274 \
160 -v mcp-inspector-data:/home/node/.mcp-inspector \
161 ghcr.io/modelcontextprotocol/inspector
162```
163 
164A container has no OS keychain, so where secrets go depends on that volume:
165 
166| Situation | Secret store | Survives a restart? |
167| - | - | - |
168| **No volume** on `/home/node/.mcp-inspector` | Memory | No, session only |
169| **With** that volume | `secrets.json` on the volume, mode `0600` | Yes |
170 
171If you bind-mount a host directory instead of a named volume, it keeps its host ownership, so on Linux add `--user "$(id -u):$(id -g)"` or `chown` it to uid `1000`, or saves fail with `EACCES`. Don't bind-mount the secrets file on its own: it isn't recognized as durable, and it can't be replaced atomically.
172 
141173<Warning>
174 **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
175 
176 Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
177 
178 ```bash theme={null}
179 mkdir -p ~/.config/mcp-inspector
180 (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
181 ```
182 
183 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/2026-07-28/tools/inspector/security#what-the-file-store-protects-against).
184</Warning>
185 
186Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
187 
188<Tabs>
189 <Tab title="docker run">
190 ```bash theme={null}
191 docker run --rm -p 127.0.0.1:6274:6274 \
192 -v mcp-inspector-data:/home/node/.mcp-inspector \
193 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
194 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
195 ghcr.io/modelcontextprotocol/inspector
196 ```
197 </Tab>
198 
199 <Tab title="Compose secrets">
200 ```yaml theme={null}
201 services:
202 inspector:
203 image: ghcr.io/modelcontextprotocol/inspector
204 ports: ["127.0.0.1:6274:6274"]
205 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
206 environment:
207 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
208 secrets: [mcp_inspector_secret_key]
209 secrets:
210 mcp_inspector_secret_key:
211 file: ${HOME}/.config/mcp-inspector/secret-key
212 volumes:
213 mcp-inspector-data:
214 ```
215 </Tab>
216</Tabs>
217 
218Without Swarm, Compose secrets are bind mounts that keep the host file's owner and mode, so the `0600` key file must be owned by uid `1000`. On a Linux host where your uid is different, run `sudo chown 1000 ~/.config/mcp-inspector/secret-key` rather than loosening its mode. Supply the **same** key on every run.
219 
220If the key file is missing, unreadable or empty, or both key variables are set, the Inspector **refuses to read or write the secrets file** rather than falling back to plaintext, and says why in the log and in the settings dialogs' footer. Everything else about the store (selection order, location, permissions) is under [Where secrets are stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored).
221 
222### Health checks and other modes
223 
224The image's `HEALTHCHECK` probes the web UI at the address `HOST` binds. `--cli` and `--tui` have no web server, so the probe detects those modes from the container's arguments and reports healthy while they run. An external orchestrator (a Kubernetes probe, a Compose `healthcheck`) can call `GET /healthz` on the web port, which needs no token and returns only `{"status":"ok"}`.
225 
226`<target>` below is an [ad-hoc target](/docs/2026-07-28/tools/inspector/configuration#ad-hoc-targets): a positional stdio command, or `--server-url <url> --transport http`.
227 
228```bash theme={null}
229docker run --rm ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
230```
231 
232<Warning>
142233 **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
143 8080:6274` the browser's origin becomes `http://localhost:8080`, which no
144 longer matches the in-container port, and connects will `403`. Either run `-e
145 CLIENT_PORT=8080 -p 8080:8080`, or set `-e
234 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
235 which no longer matches the in-container port, and connects will `403`. Either
236 run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
146237 ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
147238</Warning>
148239 
149240## Hosting on a network
150241 
151The Inspector binds `localhost` by default and its backend spawns processes, so treat exposing it to a network as a deliberate decision.
242The Inspector binds `127.0.0.1` by default and its backend spawns processes, so treat exposing it to a network as a deliberate decision.
152243 
153244The Inspector refuses to bind the **wildcard** all-interfaces addresses (`0.0.0.0`, `::`, and every equivalent spelling) unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding a **specific** address is allowed with no opt-in, because that's one deliberate exposure rather than every interface at once, which is the shape DNS-rebinding attacks target.
154245 
from line 246
155246| Goal | What to do |
156247| - | - |
157248| **Reach it from another machine on the LAN** | `HOST=192.168.1.50`. The default origin allow-list follows the bind host, so `http://192.168.1.50:6274` is accepted with no further config. |
158| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`. |
249| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`, and for MCP Apps set `MCP_SANDBOX_FULL_ADDRESS` (see below). |
159250| **Wildcard bind (containers)** | Set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Loopback access still works out of the box; reaching it at a non-loopback address needs `ALLOWED_ORIGINS`. |
160251 
161252<Warning>
from line 261
170261 
171262Two further caveats when going off loopback:
172263 
173* **MCP Apps need their sandbox port reachable too.** It's a separate, dynamic-by-default port; pin it with `MCP_SANDBOX_PORT` and expose or forward it. The Docker image publishes only `6274`.
174* **MCP Apps can't render over TLS or at a bare IPv6 literal.** The sandbox URL is always plain `http`, so an `https://` page blocks the iframe as mixed content; and a bracketed IPv6 literal isn't a valid CSP host-source, so browse at a name or an IPv4 address.
264* **MCP Apps need their sandbox port reachable too.** It's a separate listener (`6275` by default, set with `MCP_SANDBOX_PORT`), so expose or forward it alongside the web port.
265* **Behind TLS or a reverse proxy, give MCP Apps their public addresses.** By default the sandbox is advertised as `http://<bind host>:6275`, which an `https://` page blocks as mixed content. Set `MCP_SANDBOX_FULL_ADDRESS` (for example `https://inspector-sandbox.example.com/sandbox`) and, for apps that declare `_meta.ui.domain`, `MCP_APP_ORIGIN_FULL_ADDRESS` (for example `https://inspector-apps.example.com`). Each needs its own hostname or port; an address that shares the Inspector's origin is refused.
266* **MCP Apps can't render at a bare IPv6 literal.** A bracketed IPv6 literal isn't a valid CSP host-source, so browse at a name or an IPv4 address.
175267 
176Whatever the shape: keep authentication on. Do not set `DANGEROUSLY_OMIT_AUTH` on anything reachable by anyone but you.
268Whatever the shape: keep authentication on. Do not set `DANGEROUSLY_OMIT_AUTH` on anything reachable by anyone but you. The reasoning is under [Security](/docs/2026-07-28/tools/inspector/security#the-web-backend-and-its-api-token).
269 
270## Keeping a connection open with mcpdo
271 
272When an exploration spans many calls (a multi-step flow over one session, a log you watch, an agent using a server's tools mid-session), reconnecting for every `--cli` invocation gets in the way. [mcpdo](/docs/2026-07-28/tools/inspector/mcpdo) holds the connection for you:
273 
274```bash theme={null}
275mcpdo connect my-server
276mcpdo @my-server tools/call --task start_job size:=large # task-augmented; blocks until the task finishes
277mcpdo @my-server tools/call get_job_report
278mcpdo @my-server logging/tail # long-lived; Ctrl-C to stop
279mcpdo disconnect my-server
280```
281 
282A connection runs one call at a time, so commands against the same connection from other shells wait their turn.
283 
284The daemon that holds the connection starts automatically and exits about a minute after the last connection closes. Read [The mcpdo connection daemon](/docs/2026-07-28/tools/inspector/security#the-mcpdo-connection-daemon) before using it on a shared machine.
177285 
178286## Development workflow
179287 

docs/2026-07-28/tools/inspector/security New page · 188 lines, new page

# Security ## The web backend and its API token ### Where the backend listens ## Docker ### Publish the port on loopback only ### The data volume puts secrets on disk ## Secret storage ### The automatic plaintext fallback ### What the file store protects against ## stdio servers run as you ## The mcpdo connection daemon ### How clients reach it, and who else can ### How it authenticates commands ### What it holds in memory ### Lifetime ### What it writes to disk

A whole new page. There's nothing to diff it against, so here is what it says.

# Security

> The Inspector's threat model in one place, covering the web backend, Docker, secret storage, stdio servers, and the mcpdo daemon

The Inspector is a developer tool that holds real credentials and starts real processes. This page gathers everything it protects, what it trusts, and where the boundaries sit, so you can decide which setup fits your machine. The configuration and recipe pages link here rather than repeating the reasoning.

The short version:

* **The web backend can spawn processes.** Its API token is what stands between that capability and anything else that can reach the port.
* **Secrets go to the OS keychain when there is one.** Without one, they go to a file that is **plaintext unless you supply a key**, and that fallback happens automatically.
* **stdio servers run as you.** Anything the Inspector can read, a server it starts can usually read too.
* **The mcpdo daemon is a long-lived process holding live connections**, guarded by a token and same-user file permissions.

## The web backend and its API token

The web client is a browser app backed by a Node server that owns the MCP connections. That server can start stdio processes on request, so every `/api/*` route requires a per-launch bearer token (`x-mcp-remote-auth: Bearer <token>`). How the browser obtains it is described under [The session token](/docs/2026-07-28/tools/inspector/web#the-session-token).

What the token does and does not protect:

* **The token is the real guard for non-browser clients.** The origin allow-list (`ALLOWED_ORIGINS`) stops other web pages from driving the backend, but a request that arrives with **no** `Origin` header (curl, a script, any non-browser client) skips that check entirely.
* **`GET /` discloses the token.** The backend injects it into the served HTML so that a reload or a bookmark keeps working. Anyone who can load the page can therefore read the token, which is why the bind address matters more than the token's value. Pinning your own `MCP_INSPECTOR_API_TOKEN` does not change this, since a custom token is disclosed exactly like a generated one.
* **`DANGEROUSLY_OMIT_AUTH=true` removes the guard completely.** Anything that can reach the port can then spawn processes as you and use any OAuth token the Inspector holds. Only `true` or `1` turns auth off; any other value, including `false`, keeps it on.
* **Only `/api/*` is gated.** The page itself, its static assets and `GET /healthz` are served without the token. `/healthz` returns only `{"status":"ok"}`, for container and orchestrator probes.

### Where the backend listens

The backend binds `127.0.0.1` by default. Binding every interface (`0.0.0.0`, `::` and equivalent spellings) is refused unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding one specific address is allowed without that opt-in, because it is one deliberate exposure rather than all of them at once. See [Hosting on a network](/docs/2026-07-28/tools/inspector/recipes#hosting-on-a-network).

<Warning>
  Never combine `DANGEROUSLY_OMIT_AUTH` with a non-loopback bind. If you need
  the Inspector reachable by others, put a real access-control boundary in front
  of it: an authenticating reverse proxy, an SSH tunnel, or a private network.
</Warning>

## Docker

The [Docker image](/docs/2026-07-28/tools/inspector/recipes#docker) changes two things about the picture above.

### Publish the port on loopback only

Inside the container the Inspector must bind `0.0.0.0` to be reachable through `-p`, so the image sets `DANGEROUSLY_BIND_ALL_INTERFACES=true`. That opt-in governs the **container's** interfaces, not the host's. Which host interfaces see the Inspector is decided by how you publish the port:

| Publish flag | Reachable from |
| - | - |
| `-p 127.0.0.1:6274:6274` | This machine only. |
| `-p 6274:6274` (bare) | **Every host interface**, so your whole network. |

A bare `-p` puts a process-spawning backend, and the page that discloses its token, on your local network. Keep the `127.0.0.1:` prefix on every published port (`6274`, and `6275` / `6278` if you publish the MCP Apps listeners).

### The data volume puts secrets on disk

A container has no OS keychain. Without a volume on `/home/node/.mcp-inspector`, secrets stay **in memory** for the session and are lost when the container exits. Mounting that volume to keep your server list also switches secrets to a `secrets.json` file on the volume, and that file is **plaintext unless you supply a key**. It is then readable by root and every member of the host's `docker` group (which is equivalent to root), and by anyone who obtains a backup, snapshot or copy of the volume.

Supply the key as a file with `MCP_INSPECTOR_SECRET_KEY_FILE` (a Docker or Compose secret) rather than as an environment variable: a key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is visible to anyone who can run `docker inspect` or `docker exec`. The recipe shows both forms.

## Secret storage

The Inspector keeps credentials out of `mcp.json`, `client.json` and `oauth.json` so that sharing, committing or syncing those files does not leak them. These are stored as secrets:

* acquired OAuth tokens (access, refresh and ID tokens), subject to [`MCP_INSPECTOR_PERSIST_TOKENS`](/docs/2026-07-28/tools/inspector/configuration#secret-store-variables), and IdP session tokens from enterprise-managed authorization;
* each server's OAuth client secret, and the enterprise IdP client secret;
* dynamically registered client secrets and their registration access tokens;
* each stdio server's `env:` values.

**`headers` are not secrets.** They are saved in `mcp.json` exactly as written, so a header that carries a credential (an API key, a static `Authorization` value) stays in the file.

How the store is selected, and how to change it, is under [Where secrets are stored](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored). This section covers the risks.

### The automatic plaintext fallback

<Warning>
  On a host where the OS keychain cannot be reached (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. You do not have to ask for it, and unless you supply a key that file is **unencrypted**.

  The only signs are a warning on stderr when the store is selected and a footer in the web client's settings dialogs.
</Warning>

Treat that as a risk, not just a configuration fact. The file is written with mode `0600`, which keeps out other non-root users and nothing else. To close it, do one of the following:

* get a keychain back (install libsecret and run a Secret Service such as `gnome-keyring`, or run inside a desktop session);
* encrypt the file with a generated key in `MCP_INSPECTOR_SECRET_KEY_FILE`;
* or set `MCP_INSPECTOR_SECRET_STORE=memory` and re-enter secrets each session.

### What the file store protects against

The file store exists for machines without a keychain, and it is weaker than one. Treat keeping secrets in it, **even encrypted**, as a moderate risk.

**Without a key (plaintext, mode `0600`):**

| Threat | Protected? |
| - | - |
| Other non-root users on the machine, while the mode holds | Yes |
| Root, and on a container host every member of the `docker` group | No |
| Anyone with a copy of the file: a backup, a snapshot, a synced home directory, a commit | No |
| Any program running as your user, including the stdio servers the Inspector starts | No |

**With a key (AES-256-GCM, key stretched with scrypt against a per-write random salt):**

| Threat | Protected? |
| - | - |
| The file leaking on its own (a backup, snapshot, copy or commit), provided the key is high-entropy and did not leak with it | Yes |
| Anyone who can read the key where it lives | No |
| Root on the host, or the `docker` group: they can read the file, the key, or the process memory holding decrypted values | No |
| Code running as the same user | No |
| A weak passphrase: anyone holding the file can guess offline, quickly, because the scrypt cost is kept low for per-save derivation | No |

Where the key lives decides the second row. With `MCP_INSPECTOR_SECRET_KEY`, the key is in the Inspector's environment, readable through `/proc/<pid>/environ` by the same user or root, through `docker inspect` / `docker exec` for a container, and wherever you stored it for launching (a shell profile, an `.env` file, a Compose file). `MCP_INSPECTOR_SECRET_KEY_FILE` narrows that to whoever can read the key file, but the Inspector has to read it, so the same user can too. If the key sits beside the secrets file, in the same backup, volume or repository, encryption buys nothing.

In short, encryption turns "the file leaked" into "the file **and** the key leaked". It does not help against anyone who already has root, or the Inspector's own user, on the machine or in the container. When that is not acceptable, use a keychain or the memory store.

Two failure modes are deliberately loud rather than silent:

* If the key file is missing, unreadable or empty, if `MCP_INSPECTOR_SECRET_KEY_FILE` is set to an empty value, or if both key variables are set, the store **refuses to read or write** rather than falling back to plaintext.
* If the passphrase changes or is lost, the existing file can no longer be decrypted. The Inspector reads it as empty and **refuses to overwrite it**, so restore the passphrase, or delete the file and re-enter the values.

## stdio servers run as you

A stdio MCP server is a process the Inspector starts with your user's privileges, exactly as any MCP host would.

* **Environment:** the Inspector does not pass its own environment through. A stdio server gets a short allowlist (`HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `USER` on macOS and Linux) plus its configured `env:`. So a key in `MCP_INSPECTOR_SECRET_KEY` is not handed to it directly.
* **But it is the same user.** A server can open `secrets.json` itself, read `oauth.json` and your catalog, and usually read the Inspector's environment through `/proc/<pid>/environ`. The environment allowlist is hygiene, not isolation.

Only connect stdio servers you would trust with these secrets. To isolate one you don't, wrap its command in a container, for example `docker run -i --rm --network none <image>` as the stdio command. HTTP and SSE servers run no local code, so they need no process isolation.

## The mcpdo connection daemon

[mcpdo](/docs/2026-07-28/tools/inspector/mcpdo) keeps connections open between commands by handing them to a background daemon, `mcpdod`. It is started **automatically** the first time a command needs it (`mcpdo connect`, or any command against a connection). There is no separate "start the daemon" step to opt into. This section describes what that process exposes.

### How clients reach it, and who else can

| Platform | Endpoint |
| - | - |
| macOS / Linux | A Unix socket, `mcpdod.sock`, inside the daemon directory (default `~/.mcp-inspector`). |
| Windows | A named pipe, `\\.\pipe\mcp-conn-<hash>`, derived from the daemon directory. No directory permissions surround it, so the token is the guard. |

There is **no TCP port**, so nothing off the machine can reach the daemon. On Unix, the daemon directory is created, or tightened if it already exists, to mode `0700`, owned by you, and must be a real directory rather than a symlink. The socket and lock file inside it are `0600`. Other non-root users therefore cannot reach the socket. Root, and any process running as you, can.

The directory is chosen in this order: `MCP_INSPECTOR_DAEMON_DIR`, then `MCP_STORAGE_DIR`, then `~/.mcp-inspector`. A socket path longer than the platform's limit (about 104 bytes on macOS, 108 on Linux) is refused up front with an error naming the variable to shorten.

### How it authenticates commands

Every request must carry a bearer token. There is no unauthenticated request path.

* **Shared mode (the default):** the `mcpdo` command that starts the daemon generates a random 256-bit token and passes it to the daemon in its environment. The daemon publishes it to `mcpdod.token` (mode `0600`) in the daemon directory, so that any `mcpdo` command run by the same user can read it. Filesystem permissions on that file are the trust boundary, which is the same same-user boundary the socket has.
* **Private mode:** `eval "$(mcpdo private)"` creates a fresh `0700` directory under `$TMPDIR/mcp-conn-<uid>/` and exports `MCP_INSPECTOR_DAEMON_DIR` and `MCP_INSPECTOR_DAEMON_TOKEN` into that shell, so the shell gets its own daemon and its own connections. The parent `mcp-conn-<uid>` directory is checked for ownership and symlinks before use, because `$TMPDIR` can be shared.

Tokens are compared in constant time. A request line larger than 1 MiB is rejected. A command presenting the wrong token to a live daemon fails loudly. It never replaces that daemon.

<Note>
  Private mode separates connections and daemon state between shells. It is
  **not** a security boundary against other processes running as your user:
  anything with your UID that learns the daemon directory can read its token.
  For a hard boundary, use a separate user account or a container.
</Note>

### What it holds in memory

For as long as it runs, the daemon holds, for each open connection:

* the live MCP connection, and for stdio servers the child process, started with the daemon as its parent;
* the connection's resolved configuration, including stdio `env:` values pulled from the secret store;
* the OAuth tokens in use for HTTP connections, which it also re-reads from the store to re-dial a dropped transport;
* any elicitation a non-interactive command left parked, until it is answered or expires after 10 minutes.

The daemon is started with the environment of the `mcpdo` command that spawned it. It inherits that shell's variables (including `MCP_INSPECTOR_SECRET_KEY`, if set there, and its own IPC token in `MCP_INSPECTOR_DAEMON_TOKEN`) and keeps them for its whole lifetime, even after you change them in your shell. stdio servers it starts still receive only the allowlist above plus their `env:`, snapshotted from the shell that ran `mcpdo connect`.

On a keychain-less host, mcpdo never uses the memory store as its automatic fallback. Its front-end commands and the daemon are separate processes, so a per-process store could not carry a token from one to the other. mcpdo uses the **shared `secrets.json` file** instead, with the same plaintext-unless-keyed caveat as above. `MCP_INSPECTOR_SECRET_STORE=memory` set explicitly still wins. mcpdo prints the store warning once per `connect` rather than on every command.

### Lifetime

* **Start:** on the first command that needs it. If two start at once, an `O_EXCL` lock (`mcpdod.lock`) lets exactly one win. The lock left by a dead daemon is reclaimed, and a live daemon is never taken over.
* **Stop:** `mcpdo daemon stop`, or `SIGINT` / `SIGTERM`. It also exits by itself about **60 seconds after its last connection closes**. There is no maximum lifetime: while any connection is open, the daemon stays up.
* **On a clean stop:** new work is refused, in-flight requests get a short grace period, every connection is closed, and the socket, token file and lock are removed.
* **On a crash:** every connection it held is gone, and stdio servers lose their stdin, which normally ends them. Stored OAuth tokens and secrets are unaffected. The next `mcpdo` command starts a fresh daemon, which clears the stale socket, reclaims the lock and writes a new token. Connections must be re-established with `mcpdo connect`.

Find a stray daemon with `pgrep mcpdod`. It sets its process title to `mcpdod`.

### What it writes to disk

All in the daemon directory, which is `0700`:

| File | Mode | Contents |
| - | - | - |
| `mcpdod.sock` | `0600` | The IPC socket (Unix only). |
| `mcpdod.lock` | `0600` | The single-instance lock, holding the daemon's pid. |
| `mcpdod.token` | `0600` | The IPC bearer token. Written at start, removed at clean shutdown. |
| `mcpdod.log` | `0600` | The daemon's stderr, recreated on each start. Startup failures and the secret-store warning land here. |

The daemon writes OAuth state and secrets through the same `oauth.json` and secret store as the other clients, so everything under [Secret storage](#secret-storage) applies to it unchanged.

docs/2026-07-28/tools/inspector/tui Changed · +22 / -8 lines

from line 17
1717Unlike the CLI, the TUI has no `--server <name>` flag for picking one entry: it reads its servers from a catalog or config file, loads every server in it, and lets you pick from an on-screen list:
1818 
1919```bash theme={null}
20mcp-inspector --tui --catalog mcp.json # writable catalog, seeded empty if missing (unlike the web client)
20mcp-inspector --tui --catalog mcp.json # writable catalog, seeded empty if missing
2121mcp-inspector --tui --config mcp.json # read-only session, errors if absent
2222```
2323 
2424With neither `--catalog` nor `--config`, and no [ad-hoc target](/docs/2026-07-28/tools/inspector/configuration#ad-hoc-targets), it uses the default writable catalog `~/.mcp-inspector/mcp.json`. See [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration).
2525 
26The TUI also takes the [shared server-selection flags](/docs/2026-07-28/tools/inspector/configuration#shared-server-selection-flags) (`--server-url`, `--transport`, `--header`, `-e`, `--cwd`) plus `--protocol-era` for an ad-hoc server, and the [OAuth client flags](/docs/2026-07-28/tools/inspector/configuration#cli-and-tui-oauth-client-flags) (`--client-id`, `--client-secret`, `--client-metadata-url`, `--client-config`, `--callback-url`).
27 
2628## Tabs
2729 
2830| Tab | Key | What it shows |
2931| - | - | - |
30| **Info** | `i` | Server info, capabilities, and negotiated protocol details. |
31| **Auth** | `a` | OAuth state for the selected server, plus a **Clear OAuth state** action. |
32| **Info** | `i` | Server configuration, name, version and instructions, plus the roots the client advertises (press `e` to edit them). |
33| **Auth** | `a` | OAuth state for the selected HTTP or SSE server, plus a **Clear OAuth State** action (`s`), which also disconnects a live connection. |
3234| **Resources** | `r` | Browse and read resources. |
35| **Subscriptions** | `u` | Subscribe to and unsubscribe from resources (servers that support resource subscriptions). |
3336| **Prompts** | `m` | List prompts and render them with arguments. |
37| **Skills** | `k` | List a server's skills and verify their digests and frontmatter (servers that declare the skills extension). |
3438| **Tools** | `t` | View tools and execute them with form-like inputs. |
39| **Tasks** | `s` | List tasks, fetch their results, cancel them, and clear finished ones (servers that support Tasks). |
3540| **Protocol** | `p` | JSON-RPC request/response/notification history. |
3641| **Network** | `n` | HTTP traffic for SSE and [Streamable HTTP](/specification/latest/basic/transports) servers. |
3742| **Console** | `o` | `stderr` from a connected stdio server process. |
3843 
39The accelerators avoid collisions rather than always taking the first letter: **P**rotocol takes `p` so Pro**m**pts takes `m`, and **C**onsole takes `o` because `c` is the global Connect action.
44The accelerators avoid collisions rather than always taking the first letter: **P**rotocol takes `p` so Pro**m**pts takes `m`, and **C**onsole takes `o` because `c` is the global Connect action. Ta**s**ks takes `s`, its only free letter, so Subscriptions takes `u` and Skills takes `k`.
4045 
46Auth, Network, and Console appear only for the transports they apply to; Subscriptions, Skills, and Tasks appear only once a connected server supports them.
47 
4148## Navigation
4249 
4350| Key | Action |
4451| - | - |
45| `Left` / `Right` arrows or `Tab` | Switch tabs |
46| `Up` / `Down` arrows | Move through the current list |
52| `Tab` / `Shift+Tab` | Move focus: server list → tab bar → list → details |
53| `Left` / `Right` arrows (tab bar focused), or a tab's letter | Switch tabs |
54| `Up` / `Down` arrows | Select a server, move through a list, or scroll details, depending on which pane has focus |
4755| `Enter` | Select an item, execute a tool, or fetch a resource |
4856| `c` | Connect to the selected server |
4957| `d` | Disconnect |
50| `Esc` or `Ctrl+C` | Exit |
58| `/` | Filter the current list (`Enter` keeps the filter, `Esc` clears it) |
59| `+` | Open the details pane full screen |
60| `y` / `w` | In a details dialog: copy the value, or save it to a file (`w` also saves a tool's result) |
61| `?` | Show or hide the keybinding help |
62| `Esc` or `Ctrl+C` | Exit (`Esc` closes an open dialog first) |
5163 
64Press **`?`** whenever no dialog is open for the full keybinding reference, including the keys specific to the active tab.
65 
5266## Authorizing an HTTP server
5367 
54681. Select an HTTP or SSE server and press **`c`** to connect.
from line 83
6983 
7084See [Authorization](/docs/2026-07-28/tools/inspector/authorization) for the full picture.
7185 
72<Frame caption="The Auth tab. It shows the same OAuth fields as the web client's Connection Info, or reports that the server needs no authorization.">
86<Frame caption="The Auth tab. It shows the same OAuth fields as the web client's Connection Info or, before any authorization has happened, says there is no OAuth information yet.">
7387 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-auth.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=2e5ef574e80e81c4b49fa2ef0eac0528" width="2986" height="1832" data-path="images/inspector/tui-auth.png" />
7488</Frame>
7589 

docs/2026-07-28/tools/inspector/web Changed · +17 / -13 lines

from line 20
20202. A `?MCP_INSPECTOR_API_TOKEN=...` query string, the form used in that printed URL.
21213. `sessionStorage`, as a backstop.
2222 
23Set the `MCP_INSPECTOR_API_TOKEN` environment variable to pin a known token (useful for scripted launches), or set `DANGEROUSLY_OMIT_AUTH=true` to disable the check entirely, but only on a machine where nothing else can reach the port. Both are described under [Web backend environment variables](/docs/2026-07-28/tools/inspector/configuration#web-backend-environment-variables).
23Set the `MCP_INSPECTOR_API_TOKEN` environment variable to pin a known token (useful for scripted launches), or set `DANGEROUSLY_OMIT_AUTH=true` to disable the check entirely, but only on a machine where nothing else can reach the port. Both are described under [Web backend environment variables](/docs/2026-07-28/tools/inspector/configuration#web-backend-environment-variables). What the token does and does not protect is described under [Security](/docs/2026-07-28/tools/inspector/security#the-web-backend-and-its-api-token).
2424 
2525## Dev mode
2626 
from line 41
4141| **Tools** | `tools` capability | Browse schemas, fill arguments, call, inspect results. |
4242| **Prompts** | `prompts` capability | List prompts, supply arguments, preview generated messages. |
4343| **Resources** | `resources` capability | Browse, read, and subscribe to resources. |
44| **Skills** | The server declares the Skills extension (SEP-2640), in either era | Browse and fetch the server's skills. |
4445| **Tasks** | `capabilities.tasks` (legacy era) or the tasks extension (modern era) | Track long-running tool calls. |
4546| **Logs** | `logging` capability | Server `notifications/message` output, plus the era-appropriate level control. |
4647| **Protocol** | Always | The JSON-RPC transcript: requests, responses, notifications. |
from line 56
5556 
5657### The monitoring sidebar
5758 
58**Tasks**, **Logs**, **Protocol**, **Network**, and **Console** form a *monitor group*. Pin the group and they leave the tab bar and move into a resizable right-hand column, so you can watch traffic while working in Tools or Resources. The column width and the selected monitor tab persist across reloads.
59**Tasks**, **Logs**, **Protocol**, **Network**, and **Console** form a *monitor group*. Pin the group and they leave the tab bar and move into a resizable right-hand column, so you can watch traffic while working in Tools or Resources. Drag the column's edge to resize it, from 320 to 720 pixels wide. The column width and the selected monitor tab persist across reloads.
5960 
60<Frame caption="The monitoring sidebar pinned beside the Tools screen. The Protocol stream stays visible while you work.">
61 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=eef6e546b9831b3d169e26bba8c54ce3" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
61<Frame caption="The monitoring sidebar pinned beside the Tools screen and dragged to its full width, with the tool call's request and response expanded in the Protocol stream.">
62 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
6263</Frame>
6364 
6465## Servers
from line 75
7475| `--config <path>` | That file, read-only (never written or seeded) | No |
7576| `--server-url <url>` or a positional command | One ad-hoc server, held in memory | No |
7677 
77On a first launch the web client seeds the catalog with two sample servers: a filesystem server scoped to `/tmp` and the canonical "everything" reference server. See [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration) for the full rules, including why the CLI and TUI seed an empty catalog instead.
78On a first launch the web client seeds the catalog with three sample servers: a filesystem server scoped to `/tmp`, the canonical "everything" reference server, and the MCP org's hosted example server (Streamable HTTP, with OAuth via dynamic client registration). See [Configuration and flags](/docs/2026-07-28/tools/inspector/configuration) for the full rules, including why the CLI and TUI seed an empty catalog instead.
7879 
7980### Server Settings
8081 
8182* **Protocol Era**: `legacy` / `auto` / `modern`. See [Protocol eras](/docs/2026-07-28/tools/inspector/protocol-eras).
82* **Log level per request**: the level a modern-era connection stamps on each outgoing request by default, or `off` to opt out (see [Logging](/docs/2026-07-28/tools/inspector/protocol-eras#logging)).
83* **Log Level per Request**: the level a modern-era connection stamps on each outgoing request by default, or `off` to opt out (see [Logging](/docs/2026-07-28/tools/inspector/protocol-eras#logging)).
8384* **Advertised Extensions**: which extensions the Inspector declares in `capabilities.extensions`. A debugging knob: a server may legitimately change what it registers based on what you advertise. Uncheck the Tasks extension and reconnect against the `test-servers/configs/advertised-extensions-http.json` fixture (setup in [Reproducing each era locally](/docs/2026-07-28/tools/inspector/protocol-eras#reproducing-each-era-locally)) to watch a tool disappear.
8485* **Roots**: the roots advertised via the `roots` client capability. `@modelcontextprotocol/server-filesystem`, for instance, calls `roots/list` to learn its allowed directories.
85* **Headers**, **timeouts**, and **OAuth** fields.
86* **Fetch lists one page at a time**: when off, list results are auto-aggregated across pages on connect; when on, each list loads page 1 only with a **Load next page** control and an *N pages loaded* status. Reproduce with `test-servers/configs/pagination-http.json`, which paginates 12 tools, resources, and prompts into three pages each.
86* **Headers**, **timeouts**, and **OAuth** fields. Headers are saved in the catalog as written; OAuth client secrets and stdio `env:` values go to the [secret store](/docs/2026-07-28/tools/inspector/configuration#where-secrets-are-stored).
87* **OAuth Settings**: client ID and secret, a read-only **Redirect URI** to copy into a pre-registered client (it follows the origin you opened the Inspector at), **Scopes** (space-separated), **Request refresh token**, **Revoke tokens on clear**, additional authorization parameters, authorization and token URL overrides, and **Insufficient-scope response**, which decides whether a `403 insufficient_scope` triggers [step-up](/docs/2026-07-28/tools/inspector/authorization#mid-session-re-authorization) or surfaces the error.
88* **Fetch Lists One Page at a Time**: when off, list results are auto-aggregated across pages on connect; when on, each list loads page 1 only with a **Load next page** control and an *N pages loaded* status. Reproduce with `test-servers/configs/pagination-http.json`, which paginates 12 tools, resources, and prompts into three pages each.
8789 
90A footer at the bottom of **Server Settings**, **Client Settings** and the **Add / Edit / Clone server** dialogs names the secret store in use, so you see it where you type a secret. It turns into a warning when secrets are memory-only (lost on restart), in an unencrypted file, in a file with loose permissions, or in a file that can't be read.
91 
8892<Frame caption="Server Settings with Advertised Extensions expanded. Unchecking one changes what the Inspector declares at connect.">
8993 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-server-settings.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=d42be09ee8de7e45e58a8ff1a444ba52" width="3840" height="2160" data-path="images/inspector/web-server-settings.png" />
9094</Frame>
from line 121
117121 
118122## Apps
119123 
120[MCP Apps](/extensions/apps/overview) are tools that carry UI. The Apps tab renders one in a sandboxed iframe served from a **separate port**, exercises the `ui/*` bridge, and shows the view's `ui/message` submissions and its `notifications/message` logs in side panels.
124[MCP Apps](/extensions/apps/overview) are tools that carry UI. The Apps tab renders one in a sandboxed iframe served from a **separate port**, exercises the `ui/*` bridge, and shows the view's `ui/message` submissions and its `notifications/message` logs in panels below the frame.
121125 
122* The sandbox port is dynamic by default; pin it with `MCP_SANDBOX_PORT` if you need to expose or forward it.
126* The sandbox listens on its own port, `6275` by default (set it with `MCP_SANDBOX_PORT`). An app whose UI resource declares `_meta.ui.domain` has its document served from a third listener, the app origin, on `6278` by default (`MCP_APP_ORIGIN_PORT`). Expose or forward both along with the web port.
123127* The sandbox is gated by a `frame-ancestors` CSP, and a bracketed IPv6 literal is not a valid CSP host-source, so browse the Inspector at `localhost`, `127.0.0.1`, a hostname, or a LAN IPv4, **not** at a bare `http://[::1]:...`.
124* The sandbox URL is always plain `http`, so an `https://` Inspector page blocks the frame as mixed content. MCP Apps need a plain-`http` origin today.
128* By default the sandbox URL is plain `http` on the bind address, so an `https://` Inspector page blocks the frame as mixed content. Behind a TLS reverse proxy, set `MCP_SANDBOX_FULL_ADDRESS` (and `MCP_APP_ORIGIN_FULL_ADDRESS`) to the public `https://` address the browser reaches each listener at. Neither may share an origin with the Inspector UI; a value that does is ignored with a warning.
125129 
126130See [Recipes](/docs/2026-07-28/tools/inspector/recipes#reviewing-an-mcp-app) for the CLI-first automated review flow.
127131 
from line 141
137141* **Network**: the HTTP layer, for SSE and Streamable HTTP servers. Status codes, request and response headers, and bodies. On modern connections the standardized `Mcp-*` headers are highlighted and sentinel values decoded.
138142* **Console**: the connected stdio server process's `stderr`, which is where most stdio servers put their own diagnostics.
139143 
140Secrets are masked in these views, and entries can be cleared or exported.
144Secrets in Network headers and bodies are masked, with a control to reveal them; Protocol and Console show traffic as sent. Entries can be cleared or exported.
141145 
142146<Frame caption="The Protocol tab with an entry expanded, showing the full JSON-RPC exchange.">
143147 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-protocol.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=f31338c83a389c5588f11c0d5b2b97ed" width="3840" height="2160" data-path="images/inspector/web-protocol.png" />
from line 165
161165 
162166## Host binding and origins
163167 
164By default the Inspector binds `localhost` and accepts requests only from the loopback origins for its port. Treat both defaults as security boundaries, since the backend spawns processes on your machine.
168By default the Inspector binds `127.0.0.1` and accepts requests only from the loopback origins for its port. Treat both defaults as security boundaries, since the backend spawns processes on your machine.
165169 
166170Binding all interfaces (`HOST=0.0.0.0`) is **refused** unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding a *specific* non-loopback address is allowed with no opt-in, since that's a single deliberate exposure rather than every interface at once.
167171 

docs/draft/tools/inspector Changed · +30 / -6 lines

from line 12
1212 
1313All three are built on the same shared core, so a connection behaves identically across them: the same transports, the same configuration files, the same OAuth state on disk, and the same [protocol-era](/docs/draft/tools/inspector/protocol-eras) negotiation (legacy vs. modern 2026-07-28).
1414 
15<Frame caption="The MCP Inspector web client, connected to a server, with the monitoring sidebar pinned so protocol traffic stays visible while you work.">
16 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=eef6e546b9831b3d169e26bba8c54ce3" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
15The package also installs **`mcpdo`**, an experimental [connection client](/docs/draft/tools/inspector/mcpdo) that connects to a server once and keeps it open across many commands, on the same core.
16 
17<Frame caption="The MCP Inspector web client after a tool call, with the monitoring sidebar pinned and widened so the JSON-RPC exchange stays readable while you work.">
18 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
1719</Frame>
1820 
1921## Quickstart
from line 32
3032 npx @modelcontextprotocol/inspector
3133 ```
3234 
33 The command prints a URL containing a one-time session token; open it in your browser. See [Web client](/docs/draft/tools/inspector/web).
35 The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](/docs/draft/tools/inspector/web).
3436 </Tab>
3537 
3638 <Tab title="CLI">
from line 83
8183 
8284Always read a server's own README first, since every server requires different commands and arguments.
8385 
86<Warning>
87 **The Inspector stores secrets (OAuth tokens, OAuth client secrets, and stdio
88 `env:` values) in the OS keychain when one is available.** On a machine with
89 no keychain (Linux without libsecret or a Secret Service, headless and SSH
90 sessions, Termux, and containers with a mounted volume) they are saved to
91 `~/.mcp-inspector/secrets.json` instead, **unencrypted unless you supply a
92 key**. See [Where secrets are
93 stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored)
94 for how to get a keychain back, encrypt the file, or keep secrets in memory
95 only.
96</Warning>
97 
8498## Launcher flags vs. client flags
8599 
86100`mcp-inspector`, the binary that `npx @modelcontextprotocol/inspector` runs, is a thin launcher. It owns only two things:
from line 113
99113</Note>
100114 
101115<Note>
102 `--help` behaves differently with and without a mode flag. Bare `mcp-inspector --help` prints the launcher's help and exits. With a mode flag it is
103 forwarded, so `mcp-inspector --cli --help` prints the CLI's full flag
104 reference instead.
116 `--help` behaves differently with and without a mode flag. Without one,
117 `-h`/`--help` anywhere on the command line (even after a server command)
118 prints the launcher's help and exits. With a mode flag it is forwarded, so
119 `mcp-inspector --cli --help` prints the CLI's full flag reference instead.
105120</Note>
106121 
107122## Where to go next
from line 147
132147 <Card title="Protocol eras" icon="code-branch" href="/docs/draft/tools/inspector/protocol-eras">
133148 Legacy vs. modern (2026-07-28) operation, and how every tab changes between
134149 protocol eras.
150 </Card>
151 
152 <Card title="mcpdo connection client" icon="plug" href="/docs/draft/tools/inspector/mcpdo">
153 Connect once, then run many commands against a named connection.
154 </Card>
155 
156 <Card title="Security" icon="shield-halved" href="/docs/draft/tools/inspector/security">
157 The threat model: the web backend's token, Docker, secret storage, stdio
158 servers, and the mcpdo daemon.
135159 </Card>
136160 
137161 <Card title="Recipes" icon="book" href="/docs/draft/tools/inspector/recipes">

docs/draft/tools/inspector/authorization Changed · +18 / -14 lines

from line 43
4343 
4444 <Step title="Exchange and retry">
4545 The code is exchanged for tokens, the tokens are persisted, and the original
46 connect (or, for a [mid-session challenge](#mid-session-re-authorization),
47 the request that was refused) is retried automatically.
46 connect is retried. For a [mid-session
47 challenge](#mid-session-re-authorization), the CLI retries the refused
48 request automatically, and the web client asks you to retry the action.
4849 </Step>
4950</Steps>
5051 
from line 59
5859 
5960| Surface | Default callback | Why |
6061| - | - | - |
61| **Web** | `http://localhost:6274/oauth/callback` | The main app server already has an HTTP listener. |
62| **Web** | `<origin you opened the Inspector at>/oauth/callback`, by default `http://127.0.0.1:6274/oauth/callback` | The main app server already has an HTTP listener. Copy the exact value from the **Redirect URI** field in Server Settings. |
6263| **CLI** | `http://127.0.0.1:6276/oauth/callback` | A dedicated loopback listener, so it doesn't collide with a running web Inspector. |
6364| **TUI** | `http://127.0.0.1:6276/oauth/callback` | The same listener as the CLI. |
6465 
6566**Register `http://127.0.0.1:6276/oauth/callback`** on any IdP that requires pre-registered redirect URIs before using the CLI or TUI. A predictable default is the point: you register once and reuse it.
6667 
67Override with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`.
68For the CLI and TUI, override it with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`. The web callback always follows the page's origin, so `localhost` and `127.0.0.1` produce different redirect URIs there too.
6869 
6970<Warning>
7071 The callback URL **must bind a loopback host**: `localhost`, `127.0.0.0/8`, or
from line 72
7172 `[::1]`. The listener receives the authorization code over plaintext `http`,
7273 so a non-loopback host is rejected with an error and there is no flag to
7374 override that. If your browser runs on a different machine, forward the
74 callback port to it; `--print-handoff` (below) prints a ready-made
75 `portForwardCmd`.
75 callback port to it, or complete the login in a web Inspector instead:
76 `--print-handoff` (below) prints a `portForwardCmd` for the web Inspector's
77 ports.
7678</Warning>
7779 
7880<Note>
from line 87
8587 
8688| File | Contents |
8789| - | - |
88| `~/.mcp-inspector/storage/oauth.json` | Tokens and client information, keyed by canonicalized server URL. Written owner-only. |
90| `~/.mcp-inspector/storage/oauth.json` | Non-secret OAuth state (discovery metadata, PKCE verifiers, granted scope, public client ids), keyed by canonicalized server URL. Written owner-only. |
8991| `~/.mcp-inspector/storage/client.json` | Install-level client settings (client metadata URL, enterprise IdP). The same file the web client's **Client Settings** dialog writes. |
9092| The server's `oauth` block in the [catalog file](/docs/draft/tools/inspector/configuration#catalog-file-format) | Per-server client id/secret, scopes, the enterprise-managed flag, and the [step-up](#mid-session-re-authorization) policy. |
9193 
9294The path to `oauth.json` is resolved in order: `MCP_INSPECTOR_OAUTH_STATE_PATH`, then `<MCP_STORAGE_DIR>/oauth.json` (see [Environment variables](/docs/draft/tools/inspector/configuration#environment-variables)), then the default above. All three clients resolve it the same way. Command-line `--client-id` / `--client-secret` / `--client-metadata-url` override `client.json`.
9395 
96The secrets themselves (access and refresh tokens, client secrets, registration access tokens, and IdP session tokens) are not in `oauth.json`. They go to the [secret store](/docs/draft/tools/inspector/configuration#where-secrets-are-stored), which is the OS keychain when one is reachable. `MCP_INSPECTOR_PERSIST_TOKENS` limits which acquired tokens are kept: `all` (default), `access` (no refresh tokens), or `none` (re-authorize every run). See [Secret store variables](/docs/draft/tools/inspector/configuration#secret-store-variables).
97 
9498## Mid-session re-authorization
9599 
96100A server can refuse a *single* request mid-session with a `401` or a `403 insufficient_scope`, and the Inspector handles both without dropping the connection:
from line 102
98102* **Re-authorization**: the token expired or was revoked. The Inspector parses the `WWW-Authenticate` challenge and re-runs the flow, then retries the failed request.
99103* **Step-up**: the request needs scopes the current token doesn't carry. The Inspector re-authorizes for the union of the held and required scopes, so the new token covers everything the old one did plus the newly required scopes.
100104 
101In the **web** client this surfaces as a re-authorization banner. In the **CLI** it prompts on stderr:
105In the **web** client, re-authorization shows a **Re-authentication required** banner, and step-up opens an **Additional permissions required** dialog listing the scopes, which you confirm with **Authorize**. Each server's **Insufficient-scope response** setting can turn step-up off so the `403` surfaces as an error instead. In the **CLI**, step-up prompts on stderr:
102106 
103107```
104108Proceed with step-up authorization? [y/N]
from line 128
124128 
125129| Flag | Behavior |
126130| - | - |
127| `--use-stored-auth` | Read the stored auth for `--server-url` and inject `Authorization: Bearer`. When a refresh token is stored, run the refresh grant first and inject the **fresh** token, persisting the rotation. Exits `3` (listing the stored server URLs) when nothing matches. |
128| `--wait-for-auth <sec>` | Poll the state file until a token for `--server-url` appears, then inject it. Times out at `<sec>` with exit `3`. Use after handing a login off to a human. |
131| `--use-stored-auth` | Read the stored auth for `--server-url` and inject `Authorization: Bearer`. When a refresh token is stored, run the refresh grant first and inject the **fresh** token, persisting the rotation. Exits `3` (`no_stored_token`, listing the stored server URLs) when nothing matches. |
132| `--wait-for-auth <sec>` | Poll the state file until a token for `--server-url` appears, then inject it. Times out at `<sec>` with exit `3` (`auth_wait_timeout`). Use after handing a login off to a human. |
129133| `--list-stored-auth` | Print `{ oauthStatePath, storedServerUrls }` and exit without connecting. |
130134| `--print-handoff` | Print a JSON block (`deepLink`, `portForwardCmd`, `oauthStatePath`, `apiToken`) for `--server-url` and exit; this is everything a remote script needs to drive the browser side. |
131| `--relogin` | Delete the stored OAuth for this server URL before connecting. HTTP/SSE only. |
135| `--relogin` | Delete the stored OAuth for this server URL before connecting, and revoke the grant at the authorization server (skip with `--no-revoke`). HTTP/SSE only. |
132136 
133A typical remote-VM sequence:
137A typical remote-VM sequence. It assumes a web Inspector is running on the VM with a known `MCP_INSPECTOR_API_TOKEN`, and the same value is exported in the shell below; without it, the handoff's `deepLink` carries no `autoConnect` token and the web client rejects it.
134138 
135139```bash theme={null}
136140# On the VM: print what the human needs in order to complete OAuth in their browser
from line 158
154158 
155159## Inspecting auth state
156160 
157* **Web**: the Connection Info panel shows discovery results, the registered client, granted scopes, and token state, and offers **Clear OAuth state** for the active server.
161* **Web**: the Connection Info panel shows the authorization status, the client registration type, the client ID, granted scopes, and the access token, and offers **Clear OAuth state and disconnect** for the active server. Clearing also revokes the grant unless the server's **Revoke tokens on clear** setting is off.
158162* **TUI**: the **Auth** tab (`a`) shows the same fields and clears state the same way.
159* **CLI**: `--list-stored-auth` shows what's on disk, and `--relogin` discards it and starts over.
163* **CLI**: `--list-stored-auth` shows what's on disk, and `--relogin` revokes and discards it and starts over.
160164 

docs/draft/tools/inspector/cli Changed · +66 / -14 lines

### CI gates ### Editing the catalog

from line 8
88npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
99```
1010 
11<Tip>
12 Running several commands against the same server, or a flow that spans one
13 session (tasks, subscriptions, elicitation)? The [mcpdo connection
14 client](/docs/draft/tools/inspector/mcpdo) connects once and keeps the
15 connection open between commands.
16</Tip>
17 
1118The examples below use the installed `mcp-inspector` binary. Without a global install, prefix each command with `npx @modelcontextprotocol/inspector` instead, as above.
1219 
1320## Choosing a server
from line 38
3138 
3239<Note>
3340 **The config file is the only durable way to give a run its
34 [roots](/specification/draft/client/roots):** there is no roots flag, and
35 `--method roots/set` applies only to that one short-lived connection. Roots
36 configured for a server are advertised at connect, so a server that calls
37 `roots/list` (as `@modelcontextprotocol/server-filesystem` does, to learn its
38 allowed directories) gets them.
41 [roots](/specification/draft/client/roots):** there is no roots flag, and the
42 CLI has no `roots/set` method. Roots configured for a server are advertised at
43 connect, so a server that calls `roots/list` (as
44 `@modelcontextprotocol/server-filesystem` does, to learn its allowed
45 directories) gets them.
3946</Note>
4047 
4148See [Configuration and flags](/docs/draft/tools/inspector/configuration) for `--catalog` vs. `--config`, the `--` separator, and the shared server-selection flags.
from line 52
4552| `--method` | Required companions | Notes |
4653| - | - | - |
4754| `initialize` | None | Connect-only probe: `{serverInfo, protocolVersion, capabilities, instructions}`. |
48| `tools/list` | None | |
49| `tools/call` | `--tool-name`, plus `--tool-arg` / `--tool-args-json` | |
55| `tools/list` | None | `--strict` turns its schema portability check into a gate (see [CI gates](#ci-gates)). |
56| `tools/call` | `--tool-name`, plus optional `--tool-arg` / `--tool-args-json` | |
5057| `resources/list` | None | |
5158| `resources/read` | `--uri` | |
5259| `resources/templates/list` | None | |
60| `resources/directory/read` | `--uri`, plus optional `--cursor` | One page at a time; pass back the previous page's `nextCursor`. |
5361| `prompts/list` | None | |
54| `prompts/get` | `--prompt-name`, `--prompt-args` | |
62| `prompts/get` | `--prompt-name`, plus optional `--prompt-args` | |
5563| `logging/setLevel` | `--log-level` | Legacy era only; modern servers opt in per request instead. |
56| `servers/list`, `servers/show` | None | Read the catalog **without connecting** to anything. |
64| `skills/list` | None | `--verify` checks the skills returned (see [CI gates](#ci-gates)). |
65| `skills/get` | `--uri` | Same `--verify` option. |
66| `servers/list` | None | Read the catalog **without connecting** to anything. |
67| `servers/show` | `--server` | Same, for one entry. |
68| `servers/add` | `--server`, plus a positional target or `--server-url` | Write the catalog without connecting (see [Editing the catalog](#editing-the-catalog)). |
69| `servers/edit` | `--server`, plus what to change | Same. |
70| `servers/remove` | `--server` | Same. |
5771 
5872Stream- or session-only methods (`logging/tail`, for example) are rejected, since a process that exits can't hold a stream open.
5973 
6074### Passing arguments
6175 
62`--tool-arg` takes `key=value` and **coerces** values by JSON-parsing them, so `count=1` becomes a number and `"012"` becomes `12`:
76`--tool-arg` takes `key=value` and **coerces** each value by JSON-parsing it when it parses, so `count=1` and `zip=10001` both send numbers. A value that is not valid JSON (`zip=012`, a bare word) is sent as a string, and a string is then converted to the type the tool's input schema declares for that property, so `zip=012` against a numeric `zip` still sends `12`:
6377 
6478```bash theme={null}
6579mcp-inspector --cli <server> --method tools/call --tool-name mytool \
from line 80
6680 --tool-arg key=value --tool-arg count=1 --tool-arg 'options={"format":"json"}'
6781```
6882 
69`--tool-args-json` takes the whole argument object at once and passes it **verbatim**, with no coercion, so `"012"` stays the string `012`. The two are mutually exclusive:
83`--tool-args-json` takes the whole argument object at once and skips the `key=value` parsing, so `{"zip":"10001"}` sends the string `"10001"` rather than a number. The schema conversion still applies to string values, so this only keeps a string a string when the schema types that property as a string (or doesn't declare it). The two flags are mutually exclusive:
7084 
7185```bash theme={null}
7286mcp-inspector --cli <server> --method tools/call --tool-name mytool \
from line 110
96110 
97111Exit codes distinguish the outcomes: a tool with an app exits `0`, one with no app exits `2`, and a missing tool exits `5`, so a typo isn't mistaken for "no app". A probe failure (unreadable UI resource, malformed `resourceUri`) is reported in a `resourceError` field rather than aborting, so one bad tool never kills a whole listing.
98112 
113The CLI can't render an App, so by default it doesn't advertise the MCP Apps extension at `initialize`. A server that exposes its App tools only to clients claiming App support will then look app-less (exit `2`). Add `--advertise-apps` to claim the extension for that run.
114 
99115<Note>
100116 `tools/list --app-info` always emits NDJSON (one line per tool) regardless of
101117 `--format`; `--format json` reshapes only the single-tool output of
from line 125
109125| Code | Meaning |
110126| - | - |
111127| `0` | Success. |
112| `1` | Usage or unexpected error (the catch-all). |
128| `1` | Usage or unexpected error (the catch-all), including an unreadable secret store or OAuth state file. |
113129| `2` | No MCP App found on the tool (`--app-info` probe). |
114| `3` | Server requires authentication (401/403, `WWW-Authenticate`, OAuth). |
130| `3` | Server requires authentication (HTTP 401/403, or an SDK authorization error). |
115131| `4` | Server unreachable (DNS, connection refused, timeout, `fetch failed`). |
116132| `5` | Tool error: `tools/call` returned `isError: true`, or the tool wasn't found. |
133| `6` | `--strict` found an error-severity tool-schema portability problem. |
134| `7` | `--verify` found a skill that violates SEP-2640 (conformance, digest, or size mismatch). |
135| `8` | `--verify` couldn't check every skill within its read bounds. |
136| `9` | `--verify --require-digests` found a skill that advertises no digests. |
117137 
118138On any non-zero exit the CLI also writes a **single JSON line to stderr**:
119139 
from line 148
128148}
129149```
130150 
151The envelope carries `code` and `message` always, plus `cause` (the underlying error, such as a DNS failure), `status` (the HTTP status) and `url` when known. Query-string secrets in a URL are redacted. `code` is a stable name for the failure: `auth_required`, `unreachable`, `tool_not_found`, `tool_is_error`, `no_app`, `schema_unportable`, `store_unavailable`, `oauth_state_unrecognized`, or the catch-all `error`, among others.
152 
131153Because it's one line, a caller can parse it with `2>&1 | tail -1 | jq .error`.
132154 
133155A `tools/call` that returns `isError: true` still prints its payload, but exits `5`, so an `&&` chain doesn't proceed on a failed call.
from line 159
137159By default the CLI runs the same loopback OAuth flow as the TUI: it opens a browser and waits on a localhost callback that a CI job can't complete. Two flags make non-interactive runs predictable:
138160 
139161* `--stored-auth-only`: never start interactive OAuth or step-up, and never auto-open a browser. Use tokens from the shared store if present, otherwise fail immediately with `auth_required`. This is the flag CI wants.
140* `--use-stored-auth`: reuse a token that the web Inspector already obtained on this machine, refreshing it first when a refresh token is stored.
162* `--use-stored-auth`: reuse a token that the web Inspector already obtained on this machine, refreshing it first when a refresh token is stored. It looks the token up by URL, so it requires `--server-url`.
141163 
142164Without either, and with no TTY on stdin or stderr, the CLI fails fast with `auth_required` rather than hanging for fifteen minutes on a callback nobody will complete.
143165 
from line 178
156178 | jq -e '.result.tools | map(.name) | index("get_weather")' > /dev/null
157179```
158180 
181### CI gates
182 
183Three flags turn a check into a non-zero exit, each with its own code so a job can tell the failures apart:
184 
185* `--strict` (with `tools/list`): reports tool-schema portability problems in full on stderr (path, issue, suggested fix) and exits `6` if any is error-severity. Without it, `tools/list` prints only a one-line count. Under `--format json` the findings are folded into the output as `schemaFindings`.
186* `--verify` (with `skills/list` or `skills/get`): runs the SEP-2640 conformance and digest checks, writes one JSON report per skill on stdout, and exits `7` on a violation or `8` when the read bounds stopped it before every skill was checked. `--skill-catalog-max-skills` and `--skill-catalog-max-bytes` set those bounds.
187* `--require-digests` (with `--verify`): exits `9` for a skill that advertises no digests, instead of reporting it as unverifiable and exiting `0`.
188 
189```bash theme={null}
190mcp-inspector --cli <server> --method tools/list --strict > /dev/null
191mcp-inspector --cli <server> --method skills/list --verify --require-digests
192```
193 
159194### Branch on the failure class
160195 
161196```bash theme={null}
from line 225
190225 server `url` (userinfo or query tokens) or in stdio `args`. Treat raw URL and
191226 `detail` fields as sensitive before pasting them into an issue.
192227</Warning>
228 
229### Editing the catalog
230 
231`servers/add`, `servers/edit`, and `servers/remove` write the catalog without connecting to anything. They go through the same code as the web client's server list, so `env` values and client secrets land in the [secret store](/docs/draft/tools/inspector/configuration#where-secrets-are-stored) exactly as they would from the UI, and a running web client picks up the change.
232 
233```bash theme={null}
234# Add: the positional target or --server-url describes the entry, not a server to connect to
235mcp-inspector --cli node build/index.js -e API_KEY=secret --method servers/add --server my-server
236 
237# Edit: change the target, -e, --cwd, --header, or --protocol-era, or rename it
238mcp-inspector --cli --method servers/edit --server my-server --rename my-renamed-server
239 
240# Remove the entry and its stored secrets
241mcp-inspector --cli --method servers/remove --server my-renamed-server
242```
243 
244Only the writable catalog (`--catalog`, `MCP_CATALOG_PATH`, or the default) can be written; `--config` is refused. With an in-memory secret store, a write that supplies `-e` values (and a `--rename`) is refused, since the secrets would be lost when the run exits.
193245 
194246## Proxies
195247 

docs/draft/tools/inspector/configuration Changed · +129 / -25 lines

### Secret store variables ## Where secrets are stored ### How the store is chosen ### The file store ### Where the active store is reported ### Moving back to a keychain

from line 13
1313 
1414Everything below belongs to a client.
1515 
16<Note>
17 The package also installs a second, separate bin, `mcpdo`, the experimental
18 [connection client](/docs/draft/tools/inspector/mcpdo). It is not reached
19 through the launcher and takes no mode flag.
20</Note>
21 
1622## Choosing servers
1723 
1824### `--catalog` vs. `--config`
1925 
20All three clients resolve `--catalog` and `--config` through the same shared code, so each flag behaves the same in the web app, the CLI, and the TUI. Where the two differ from each other is the table below.
26The CLI and TUI resolve `--catalog` and `--config` through the same shared code, and the web client applies the same rules, so each flag means the same thing in all three. Where the two differ from each other is the table below.
2127 
2228| | `--catalog <path>` | `--config <path>` |
2329| - | - | - |
from line 33
2733| **Editable in the web UI?** | Yes. | No. |
2834| **Use it for** | Your own working set of servers. | A read-only session against someone else's config file. |
2935 
30The two are **mutually exclusive**, and neither combines with an ad-hoc target. Passing both is rejected identically by all three clients.
36The two are **mutually exclusive**, and neither combines with an ad-hoc target. Passing both is rejected identically by all three clients. The web client is stricter in one respect: it also rejects `--header` and `--protocol-era` alongside a file, where the CLI and TUI apply them on top of the file's settings.
3137 
3238<Note>
33 **What a freshly seeded catalog contains depends on the client.** The web backend seeds two sample servers, so a first launch has something to connect to immediately:
39 **What a freshly seeded catalog contains depends on the client.** The web backend seeds three sample servers, so a first launch has something to connect to immediately:
3440 
3541 ```json theme={null}
3642 {
from line 50
4450 "type": "stdio",
4551 "command": "npx",
4652 "args": ["-y", "@modelcontextprotocol/server-everything"]
53 },
54 "example-server-default": {
55 "type": "streamable-http",
56 "url": "https://example-server.modelcontextprotocol.io/mcp"
4757 }
4858 }
4959 }
from line 87
7787| - | - | - |
7888| `--catalog <path>` | Writable catalog file. | None |
7989| `--config <path>` | Read-only session file. | None |
80| `--server <name>` | Pick one named server out of the file. | **Web and CLI only.** The TUI loads every server in the file and lets you choose interactively. |
90| `--server <name>` | Pick one named server out of the file. | **Selects only in the CLI.** The web client accepts it but ignores it with a note, listing every server. The TUI doesn't define it (an unknown-option error) and lets you choose interactively. |
8191| `--transport <type>` | `stdio`, `sse`, or `http`. | Ad-hoc targets only. |
8292| `--server-url <url>` | Server URL for SSE/HTTP. | Ad-hoc targets only. |
8393| `--cwd <path>` | Working directory for a stdio server process. | None |
8494| `-e <KEY=VALUE>` | Environment variables for a stdio server. Repeatable. | None |
8595| `--header "Name: Value"` | HTTP headers for an HTTP/SSE server. Repeatable. | Requires an ad-hoc HTTP/SSE server on the web client. |
96| `--protocol-era <era>` | `legacy`, `auto`, or `modern`: the [protocol era](/docs/draft/tools/inspector/protocol-eras) to negotiate. | Requires an ad-hoc target on the web client. The CLI and TUI also let it override a file's `protocolEra`. |
97| `--skill-catalog-max-skills <n>` / `--skill-catalog-max-bytes <n>` | Budget for a skills verification run (most skills, most bytes read). | **CLI and TUI only.** Overrides the file's `skillCatalogMaxSkills` / `skillCatalogMaxBytes`. |
8698| `[target...]` | Positional command/URL for one ad-hoc server. | None |
8799 
88100### The `--` separator
89101 
90The **web and CLI** clients split their arguments at a bare `--` and pass everything after it to the target command as its own arguments. This is how you pass a flag that the Inspector would otherwise eat:
102The **web and TUI** clients pass everything after a bare `--` to the target command as its own arguments. This is how you pass a flag that the Inspector would otherwise eat:
91103 
92104```bash theme={null}
93105mcp-inspector node build/index.js -- --config /etc/myserver.conf --verbose
from line 107
95107 
96108Without the separator, `--config` would be read as the Inspector's own read-only-session flag.
97109 
110**The CLI splits the other way:** everything *before* `--` is the target, and everything after it is the Inspector's own options. Without a `--`, the CLI's target is only the leading run of arguments that don't start with a dash, so a server that takes flags needs the separator:
111 
112```bash theme={null}
113mcp-inspector --cli node build/index.js --config /etc/myserver.conf --verbose -- --method tools/list
114```
115 
98116## Web-only flags
99117 
100118| Flag | Meaning |
from line 129
111129| `--client-id <id>` | None | OAuth client ID for a static client. Overrides `client.json`. |
112130| `--client-secret <secret>` | None | OAuth client secret for confidential clients. Overrides `client.json`. |
113131| `--client-metadata-url <url>` | None | CIMD metadata URL. Overrides `client.json`. |
114| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | The redirect URI sent to the authorization server. Default `http://127.0.0.1:6276/oauth/callback`. Must be a loopback host (`127.0.0.1` or `localhost`): the local callback listener receives the authorization code over plaintext `http`, so any other host is rejected and there is no flag to override this. |
132| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | The redirect URI sent to the authorization server. Default `http://127.0.0.1:6276/oauth/callback`. Must be a loopback host (`localhost`, `127.0.0.1` or any other `127.x.x.x` address, or `[::1]`): the local callback listener receives the authorization code over plaintext `http`, so any other host is rejected and there is no flag to override this. |
115133 
116134## CLI-only flags
117135 
from line 137
119137 
120138| Group | Flags |
121139| - | - |
122| **What to invoke** | `--method`, `--tool-name`, `--tool-arg`, `--tool-args-json`, `--uri`, `--prompt-name`, `--prompt-args`, `--log-level`, `--metadata`, `--tool-metadata` |
123| **How to run it** | `--connect-timeout`, `--format`, `--app-info` |
124| **Auth** | `--use-stored-auth`, `--stored-auth-only`, `--relogin`, `--wait-for-auth`, `--list-stored-auth`, `--print-handoff` |
140| **What to invoke** | `--method`, `--tool-name`, `--tool-arg`, `--tool-args-json`, `--uri`, `--cursor`, `--prompt-name`, `--prompt-args`, `--log-level`, `--metadata`, `--tool-metadata`, `--rename` |
141| **How to run it** | `--connect-timeout`, `--format`, `-q` / `--quiet`, `--output`, `--output-format`, `--app-info`, `--advertise-apps`, `--strict`, `--verify`, `--require-digests`, `--completion <shell>` (`bash`, `zsh`, or `fish`) |
142| **Auth** | `--use-stored-auth`, `--stored-auth-only`, `--relogin`, `--no-revoke`, `--wait-for-auth`, `--list-stored-auth`, `--print-handoff` |
125143 
126144## Environment variables
127145 
128Environment variables split the same way as flags: two are read by the launcher itself, and the rest belong to the CLI and TUI or to the web backend.
146Environment variables split the same way as flags: two are read by the launcher itself, and the rest belong to the CLI and TUI, to the web backend, or to every client (the secret store).
129147 
130148### Read by the launcher
131149 
132150| Variable | Effect |
133151| - | - |
134| `MCP_DEBUG` | Append the error stack to a top-level failure. Only when set to a meaningful value: `0`, `false`, and empty read as off. |
152| `MCP_DEBUG` | Append the error stack to a top-level `--web` or `--tui` failure (a `--cli` failure always prints its [JSON error envelope](/docs/draft/tools/inspector/cli#exit-codes-and-error-envelopes) instead). Only when set to a meaningful value: `0`, `false`, and empty read as off. |
135153| `DEBUG` | Same, with the same meaningful-value rule, so a stray `DEBUG=0` doesn't turn stack traces on and `DEBUG` still works as the npm `debug` package's namespace filter. |
136154 
137155### CLI and TUI
138156 
139| Variable | Effect |
140| - | - |
141| `MCP_CATALOG_PATH` | Fallback for `--catalog`. Honored only when no ad-hoc target is given, so a shell that exports it can still run one-off ad-hoc invocations. |
142| `MCP_CLIENT_CONFIG_PATH` | Fallback for `--client-config`. |
143| `MCP_OAUTH_CALLBACK_URL` | Fallback for `--callback-url`. |
144| `MCP_STORAGE_DIR` | Directory for the OAuth state file (`<dir>/oauth.json`). |
145| `MCP_INSPECTOR_OAUTH_STATE_PATH` | Per-file override of the OAuth state path. Takes precedence over `MCP_STORAGE_DIR`. |
146| `MCP_AUTO_OPEN_ENABLED` | Controls browser auto-open and whether interactive OAuth may run without a TTY. `true` forces auto-open and allows OAuth prompts without a TTY, `false` never opens, and unset opens only on a TTY. |
157Some of these are read by the web backend too, as the **Read by** column shows.
147158 
159| Variable | Read by | Effect |
160| - | - | - |
161| `MCP_CATALOG_PATH` | Web, CLI, TUI | Fallback for `--catalog`. The CLI honors it only when no ad-hoc target is given, so a shell that exports it can still run one-off ad-hoc invocations. The web client and TUI apply it regardless, so combining it with an ad-hoc target is rejected there. |
162| `MCP_CLIENT_CONFIG_PATH` | CLI, TUI | Fallback for `--client-config`. |
163| `MCP_OAUTH_CALLBACK_URL` | CLI, TUI | Fallback for `--callback-url`. |
164| `MCP_STORAGE_DIR` | Web, CLI, TUI | Storage directory. Relocates the OAuth state file (`<dir>/oauth.json`) and the secrets file (`<dir>/secrets.json`). |
165| `MCP_INSPECTOR_OAUTH_STATE_PATH` | CLI, TUI | Per-file override of the OAuth state path. Takes precedence over `MCP_STORAGE_DIR`. |
166| `MCP_AUTO_OPEN_ENABLED` | Web, CLI | Controls browser auto-open. `false` never opens one. In the CLI, `true` forces auto-open and lets interactive OAuth run without a TTY, and unset opens only on a TTY. In the web client, unset opens the UI at launch. The TUI does not read it. |
167 
148168### Web backend environment variables
149169 
150170| Variable | Effect |
151171| - | - |
152172| `MCP_INSPECTOR_API_TOKEN` | Pin the [session token](/docs/draft/tools/inspector/web#the-session-token) instead of generating a random one per launch. |
153| `DANGEROUSLY_OMIT_AUTH` | Disable the `/api/*` token check entirely. |
154| `HOST` | Bind host. Defaults to `localhost`. |
173| `DANGEROUSLY_OMIT_AUTH` | Disable the `/api/*` token check entirely. Only `true` or `1` turn it off. |
174| `HOST` | Bind host. Defaults to `127.0.0.1`. |
155175| `CLIENT_PORT` | Web UI port. Defaults to `6274`. |
156176| `DANGEROUSLY_BIND_ALL_INTERFACES` | Required opt-in to bind a wildcard host (`0.0.0.0`, `::`, or any equivalent spelling). |
157177| `ALLOWED_ORIGINS` | Comma-separated origin allow-list. **Replaces** the default list rather than merging. |
158| `MCP_SANDBOX_PORT` | Pin the MCP Apps sandbox port, which is dynamic by default. |
178| `MCP_PROXY_AUTH_TOKEN` | Deprecated v1 name for `MCP_INSPECTOR_API_TOKEN`, used only when the new name is unset. |
179| `MCP_SANDBOX_PORT` | MCP Apps sandbox port. Defaults to `6275`; `0` asks the OS for a free port. |
180| `SERVER_PORT` | v1's proxy port, now only a fallback for the sandbox port when `MCP_SANDBOX_PORT` is unset or invalid. |
181| `MCP_APP_ORIGIN_PORT` | Port of the app-origin server used by MCP Apps that declare `_meta.ui.domain`. Defaults to `6278`; `0` asks the OS. |
182| `MCP_SANDBOX_FULL_ADDRESS` | Public URL of the MCP Apps sandbox proxy, for running behind a reverse proxy. |
183| `MCP_APP_ORIGIN_FULL_ADDRESS` | Public origin that `_meta.ui.domain` app documents are served from, for running behind a reverse proxy. |
184| `MCP_LOG_FILE` | Append the backend's structured (JSON lines) log to this file. |
185| `MCP_AUTO_OPEN_ENABLED` | `false` stops the browser from opening at launch. See [CLI and TUI](#cli-and-tui). |
186| `MCP_CATALOG_PATH`, `MCP_STORAGE_DIR` | As described under [CLI and TUI](#cli-and-tui). |
159187| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Standard proxy routing for outbound MCP connections. |
160188 
161189<Warning>
from line 191
163191 The web backend spawns processes and holds OAuth tokens, so anyone who can
164192 reach it can drive it.
165193</Warning>
194 
195What the token does and does not protect is laid out under [The web backend and its API token](/docs/draft/tools/inspector/security#the-web-backend-and-its-api-token).
196 
197### Secret store variables
198 
199Read by **every** client: web, CLI, TUI, and [mcpdo](/docs/draft/tools/inspector/mcpdo). How they combine is described under [Where secrets are stored](#where-secrets-are-stored).
200 
201| Variable | Effect |
202| - | - |
203| `MCP_INSPECTOR_SECRET_STORE` | `keyring`, `file`, or `memory` (case-insensitive) picks the store outright and skips the keychain probe. Empty counts as unset; any other value is ignored with a warning. |
204| `MCP_INSPECTOR_SECRET_FILE` | Path of the file store. Defaults to `secrets.json` in `MCP_STORAGE_DIR` when that is set, else `~/.mcp-inspector/secrets.json`. |
205| `MCP_INSPECTOR_SECRET_KEY_FILE` | Path of a file holding the passphrase that encrypts the file store (trailing line breaks removed). **Preferred**, and what Docker and Compose secrets are for. If the file is missing, unreadable or empty, the store refuses to read or write. |
206| `MCP_INSPECTOR_SECRET_KEY` | The passphrase itself. Use a generated, high-entropy value. Setting both key variables (with a non-blank `MCP_INSPECTOR_SECRET_KEY`) is an error. |
207| `MCP_INSPECTOR_PERSIST_TOKENS` | Which acquired OAuth tokens are persisted: `all` (default), `access` (no refresh tokens), or `none` (re-authorize every run). Client secrets are always persisted. |
208 
209## Where secrets are stored
210 
211The Inspector keeps credentials out of `mcp.json`, `client.json` and `oauth.json`, so that sharing, committing or syncing those files does not leak them. These are stored in a **secret store** instead:
212 
213* acquired OAuth tokens (access, refresh and IdP session tokens);
214* each server's OAuth client secret, and the enterprise IdP client secret from Client Settings;
215* each stdio server's `env:` values. When an entry is saved, each `env` key stays in `mcp.json` with an empty value and the real value goes to the store.
216 
217**`headers` are not moved.** They are saved in `mcp.json` exactly as written, so a header that carries a credential stays in the file.
218 
219### How the store is chosen
220 
221Each process picks one store, once, the first time it needs it: the web backend at startup, the CLI and TUI on first use. All clients use the same order:
222 
2231. **`MCP_INSPECTOR_SECRET_STORE`**, if set to `keyring`, `file` or `memory`. Nothing is probed.
2242. **The OS keychain**, if a probe reaches it: Keychain on macOS, Credential Manager on Windows, the Secret Service (libsecret, such as GNOME Keyring or KWallet) on Linux. Entries go under the service name `mcp-inspector`. Most desktop installs stop here.
2253. **A fallback**, announced on stderr:
226 * `memory` in a container whose secrets directory is **not** on a mounted volume, because a file in the container's writable layer would be lost anyway;
227 * `file` everywhere else.
228 
229| Where you run it | Store | Survives a restart? |
230| - | - | - |
231| Desktop macOS or Windows, or Linux with a Secret Service running | OS keychain | Yes |
232| Linux without libsecret or a Secret Service | File (`secrets.json`, mode `0600`) | Yes |
233| Headless server or SSH session with no D-Bus session | File | Yes |
234| Android/Termux | File | Yes |
235| Container with **no volume** on the secrets directory | Memory | No, this session only |
236| Container **with** a volume on the secrets directory | File | Yes |
237 
238<Warning>
239 **With no keychain, secrets go to a plaintext file, and you did not have to ask for it.** On a host where the keychain probe fails (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. Unless you supply a key, that file is **unencrypted**. Mode `0600` keeps out other non-root users, but not root, not backups or copies of your home directory, and not any program running as you, including the stdio servers the Inspector starts.
240 
241 Pick one:
242 
243 * **Get a keychain back:** install libsecret and run a Secret Service (for example `gnome-keyring`), or run the Inspector inside a desktop session. On the next start the Inspector moves the file's secrets into the keychain and deletes the file.
244 * **Encrypt the file:** supply a generated key with `MCP_INSPECTOR_SECRET_KEY_FILE` (preferred) or `MCP_INSPECTOR_SECRET_KEY`.
245 * **Don't write secrets to disk at all:** `MCP_INSPECTOR_SECRET_STORE=memory`, and re-enter them each session.
246 
247 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/draft/tools/inspector/security#what-the-file-store-protects-against).
248</Warning>
249 
250[mcpdo](/docs/draft/tools/inspector/mcpdo) never falls back to `memory`: its commands and its daemon are separate processes, so it uses the file store instead.
251 
252### The file store
253 
254The file lives at `MCP_INSPECTOR_SECRET_FILE` if set, else `secrets.json` inside `MCP_STORAGE_DIR` if that is set, else `~/.mcp-inspector/secrets.json`. The default sits **beside** the storage directory (`~/.mcp-inspector/storage`), not inside it.
255 
256* **Encryption is opt-in.** With a key, the file is encrypted with AES-256-GCM, the passphrase stretched by scrypt against a random salt regenerated on every write. Generate the key rather than choosing it, for example `(umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)`, and keep it away from the secrets file, its backups and any repository.
257* **Adding a key later is safe.** The next write upgrades a plaintext file in place.
258* **Changing or losing the key is not.** A file that no longer decrypts reads as empty, and the Inspector refuses to overwrite it. Restore the key, or delete the file and enter the values again.
259* **Permissions are enforced.** The file is written `0600` and re-tightened at startup. If it can't be (another owner, a read-only mount), the log and the settings footer say so.
260* **Concurrent Inspectors are safe.** A CLI run next to a web session serializes on a lock beside the file and verifies each write by reading it back.
261 
262### Where the active store is reported
263 
264* **On stderr**, when the store is selected: a warning on any keychain fallback, and another if the file is unencrypted, loosely permissioned or unreadable. Both end with a link to the Inspector's [secret storage guide](https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md). The web client's startup banner has a `Secrets:` line on every run.
265* **In the web client**, a footer in the **Client Settings**, **Server Settings** and **Add / Edit / Clone server** dialogs names the store, and turns into a warning when it is memory-only, unencrypted, loosely permissioned or unreadable.
266 
267### Moving back to a keychain
268 
269Install libsecret (or start a Secret Service) on a machine that was using the file store, and the next start selects the keychain and **moves the file's contents into it**. A value already in the keychain wins over the file's. The file is deleted only once every entry is accounted for, and a file that can't be decrypted is left in place. Setting `MCP_INSPECTOR_SECRET_STORE=keyring` triggers the same hand-off. Choosing `file` or `memory` never copies anything out of the keychain.
166270 
167271## Catalog file format
168272 

docs/draft/tools/inspector/mcpdo New page · 172 lines, new page

# mcpdo connection client ## Install ## Quickstart ## Choosing what to connect ## Addressing a connection ## Tasks ## Output ## Authorization ## Elicitation ## Protocol eras ## The daemon ## Isolating untrusted stdio servers ## Environment variables ## Using mcpdo from a coding agent

A whole new page. There's nothing to diff it against, so here is what it says.

# mcpdo connection client

> Connect to an MCP server once, then run many commands against that named connection

`mcpdo` is an **experimental** command-line client that ships in the `@modelcontextprotocol/inspector` package alongside `mcp-inspector`. Where the [CLI client](/docs/draft/tools/inspector/cli) connects, runs one `--method` and disconnects, `mcpdo` **connects once** and keeps the connection open, so you can run many commands against it from any shell, the way `ssh-agent` keeps keys loaded.

| | `mcp-inspector --cli` | `mcpdo` |
| - | - | - |
| **Lifecycle** | Connect, one `--method`, disconnect | Connect once, many commands |
| **State** | None between runs | Named connections held by a daemon |
| **Best for** | CI assertions, one JSON blob per run | Exploring a server, agent tool use, multi-step flows over one session (tasks, subscriptions, elicitation) |

Connections are held by a local background daemon, `mcpdod`, which `mcpdo` starts on demand and stops after its last connection closes. You never start it by hand. What it exposes, and how it is protected, is described under [The mcpdo connection daemon](/docs/draft/tools/inspector/security#the-mcpdo-connection-daemon).

Watch the [mcpdo tutorial video](https://www.youtube.com/watch?v=_a6eG0y156k).

## Install

`mcpdo` is a second bin in the same package, so install the package globally, or run it through `npx -p`:

```bash theme={null}
npm install -g @modelcontextprotocol/inspector
mcpdo --help

# or without installing
npx -p @modelcontextprotocol/inspector mcpdo --help
```

## Quickstart

```bash theme={null}
mcpdo servers/list                            # catalog entries you can connect
mcpdo connect my-server                       # connect a catalog entry
mcpdo @my-server tools/list
mcpdo @my-server tools/call echo message:=hi
mcpdo @my-server resources/read file:///tmp/notes.txt
mcpdo connections/list                        # what's open
mcpdo disconnect my-server
```

`mcpdo help`, or `mcpdo <command> --help`, prints the full, authoritative list of commands and flags.

## Choosing what to connect

`connect` takes a catalog entry name or an ad-hoc target:

```bash theme={null}
mcpdo connect my-server                        # from the default catalog
mcpdo connect my-server --config ./mcp.json    # from a read-only config file
mcpdo connect https://example.com/mcp          # ad-hoc HTTP/SSE server
mcpdo connect node build/index.js              # ad-hoc stdio server
```

The catalog and `--config` behave exactly as for the other clients (see [Configuration and flags](/docs/draft/tools/inspector/configuration#choosing-servers)): the default catalog is `~/.mcp-inspector/mcp.json`, overridable with `--catalog` or `MCP_CATALOG_PATH`. `servers/list` and `servers/show <name>` read entries from disk. `connections/*` shows what the daemon currently holds. There are no commands to edit the catalog; edit the file directly.

A stdio server runs with the working directory and command resolution of the shell that ran `connect`, not of the daemon, so relative paths and bare command names mean what you would expect.

## Addressing a connection

A catalog connection is named after its entry. An ad-hoc one is named after its first token (`node`, `docker`, or the URL itself) unless you name it at connect time with `@name`:

```bash theme={null}
mcpdo connect @api https://example.com/mcp
```

Name the connection on each command with `@name`, or with `--connection <name>` (shorthand `--conn`). A connection named after a URL can only be addressed with `--conn <url>`, since `@name` takes only letters, digits, `_`, `.` and `-`:

```bash theme={null}
mcpdo @my-server tools/list
mcpdo --conn my-server tools/list
```

Omitting the name falls back to the most recently used connection, but only on an interactive TTY. From a script or an agent, where stdin is not a TTY, an unqualified command is an error, so that a background job never acts on whichever server you last used. Set `MCP_ALLOW_DEFAULT_CONNECTION=1` to allow the fallback anyway.

Connections **self-heal**: a dropped transport (an expired HTTP session, an exited stdio child) is re-dialed transparently on next use with the stored credentials. Only an `auth_required` error needs you to run `connect` again.

A connection runs **one call at a time**. Commands against it from other shells queue behind the call in flight, and while a call is parked on an [elicitation](#elicitation), new calls on that connection are refused until it is answered. A long call does not block other connections.

## Tasks

A tool that requires task support is refused by a plain `tools/call`; add `--task` to make a task-augmented call. It still **blocks** until the task finishes, then prints the final result:

```bash theme={null}
mcpdo @my-server tools/call --task start_job size:=large
```

If the task reaches `input_required`, its question is handled like any other [elicitation](#elicitation).

## Output

| Flag | Output |
| - | - |
| `--format text` (default) | Human-readable, with ANSI styling on a TTY unless `--plain` or `NO_COLOR` is set. |
| `--format json` | The pretty-printed payload, with no `{ result }` envelope. For scripts and agents. |

The global flags are `--format`, `--plain`, `--connection` / `--conn`, `--catalog` / `--config`, and `--stored-auth-only`. They may go before or after the subcommand, but before any `--`. The `@name` shorthand must come before the subcommand.

Terminal-bound text (results, elicitation prompts, daemon errors) has control characters stripped, so a server cannot rewrite your terminal. `--format json` stays verbatim.

## Authorization

`mcpdo` shares `oauth.json` and the secret store with the other Inspector clients, so a server you have already authorized elsewhere connects without a prompt. OAuth runs at **connect time**. Mid-session step-up is handled by the one-shot CLI, not by `mcpdo`.

| Command | Effect |
| - | - |
| `mcpdo connect <name> --relogin` (`-r`) | Clear this server's stored tokens before connecting (HTTP/SSE only), so it signs in fresh if the server requires auth. |
| `mcpdo disconnect <name> --clear-auth` (`-c`) | Close the connection **and** clear its stored tokens, so the next `connect` signs in fresh. |
| `mcpdo auth/list` | Stored credentials, each annotated with the catalog or connection names it is "known as", and `● live` when an open connection holds it. |
| `mcpdo auth/clear <url-or-name>` | Clear one stored credential, by store URL or friendly name. `--all --yes` clears every one. |
| `mcpdo auth/ema-login` / `auth/ema-status` / `auth/ema-logout` | Enterprise-managed authorization: sign in to the IdP once, then connect to EMA servers silently. |

When a browser sign-in is needed and neither stdin nor stderr is a TTY (and `MCP_AUTO_OPEN_ENABLED` is not forced on), `connect` exits `0` immediately with `pendingAuth: true` and an `authUrl`. Relay that URL to whoever will sign in. The connection completes on its own once they do: the next real command against it finishes the connection, and `connections/list` reports `pendingAuthSignedIn` in the meantime. Don't reconnect to fix a pending sign-in.

On a host with no OS keychain, `mcpdo` stores tokens in the shared `secrets.json` file, never in memory, because its commands and the daemon are separate processes. That file is plaintext unless you supply a key; see [Where secrets are stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored).

## Elicitation

When a server asks a question mid-call (a legacy `elicitation/create`, a modern MRTR round, or a task that reaches `input_required`):

* **On an interactive TTY**, `mcpdo` prompts inline. Form mode renders one prompt per field with a review step. URL mode prints the URL and waits for you to confirm you finished.
* **From a script or agent** (`--format json`, or no TTY), the command returns `elicitationPending` with an `elicitationId`, and the call stays **parked** on the daemon. Answer it from any shell:

```bash theme={null}
mcpdo elicitation/respond <elicitationId> approved:=true   # form fields as key:=value or JSON
mcpdo elicitation/respond <elicitationId> --done           # URL mode: I finished
mcpdo elicitation/respond <elicitationId> --decline        # form mode only
mcpdo elicitation/respond <elicitationId> --cancel         # either mode
```

A parked elicitation is cancelled after 10 minutes, and a connection holds one at a time; until it is answered, other calls on that connection are refused. URL mode is never auto-accepted. To keep a server from asking at all, connect with `--elicit off`. `--elicit url`, `form` or `both` (the default) choose which modes are advertised.

## Protocol eras

`mcpdo` negotiates the same [protocol eras](/docs/draft/tools/inspector/protocol-eras) as the other clients. `connect --era legacy|auto|modern` overrides a catalog entry's `protocolEra`, and is the only way to set it for an ad-hoc target. `connections/list` tags each connection `[legacy]` or `[modern]`, and `connections/show <name>` gives the negotiated version, server info, capabilities, and the supported-versions list when the server was probed.

## The daemon

| Command | Effect |
| - | - |
| `mcpdo daemon status` | Whether a daemon is running, and its connections and idle countdown. |
| `mcpdo daemon stop` | Close every connection and stop the daemon. |
| `eval "$(mcpdo private)"` | Give this shell its own daemon and connections. |

By default every shell you run `mcpdo` from shares one daemon, so a connection opened in one terminal is usable in another. `mcpdo private` exports `MCP_INSPECTOR_DAEMON_DIR` and `MCP_INSPECTOR_DAEMON_TOKEN` so the current shell gets a separate daemon. That keeps connections apart; it is not a security boundary against other processes running as you.

The daemon exits about a minute after its last connection closes. A daemon started before an upgrade keeps running the old code until then, so run `mcpdo daemon stop` after upgrading the package.

## Isolating untrusted stdio servers

The daemon's token controls who can **command** the daemon, not what a server can **do**. A stdio server runs with your full user privileges. To contain one you don't fully trust, make the stdio command a container:

```bash theme={null}
mcpdo connect @sandboxed -- docker run -i --rm --network none -v "$PWD:/work:ro" <server-image>
```

Without `@sandboxed`, the connection would be named `docker`.

Adjust the network and mount flags to what the server needs. HTTP and SSE servers run no local code, so they need no process isolation.

## Environment variables

`mcpdo` reads the same catalog, storage and secret-store variables as the other clients (see [Environment variables](/docs/draft/tools/inspector/configuration#environment-variables)), plus these:

| Variable | Effect |
| - | - |
| `MCP_INSPECTOR_DAEMON_DIR` | Directory holding the daemon's socket, lock, token and log. Defaults to `MCP_STORAGE_DIR` when set, else `~/.mcp-inspector`. Set by `mcpdo private`. |
| `MCP_INSPECTOR_DAEMON_TOKEN` | IPC token to present to, or start, the daemon. Unset, the `mcpdo` command that starts the daemon generates one, and the daemon publishes it to `mcpdod.token`. Set by `mcpdo private`. |
| `MCP_ALLOW_DEFAULT_CONNECTION` | `1` lets a command without `@name` / `--connection` use the most recently used connection even when stdin is not a TTY. |

## Using mcpdo from a coding agent

The package ships an agent skill that teaches an agent to drive `mcpdo`: connections it holds then extend the agent's toolset, and it knows to relay sign-in URLs and answer parked elicitations. `mcpdo agent-help` prints the guide, `mcpdo agent-help --skill-path` prints the path of the installable skill file, and `mcpdo agent-help --instructions` prints a block to append to a project's `CLAUDE.md` or `AGENTS.md`.

docs/draft/tools/inspector/protocol-eras Changed · +46 / -28 lines

## Cancellation

from line 6
66 
77## The `Protocol Era` setting
88 
9Each server carries a `protocolEra` of `legacy`, `auto`, or `modern`. In the web client it lives in **Server Settings**; in a catalog or config file it is the `protocolEra` field; in the CLI and TUI it comes from that same file.
9Each server carries a `protocolEra` of `legacy`, `auto`, or `modern`. In the web client it lives in **Server Settings**; in a catalog or config file it is the `protocolEra` field; the CLI and TUI read it from that same file, and their `--protocol-era <legacy|auto|modern>` flag overrides it for a single run.
1010 
1111| Era | What the Inspector does at connect |
1212| - | - |
from line 23
2323 configured.
2424</Note>
2525 
26Era selection works the same way in all three clients.
26Once connected, the negotiated era is shown as a badge on the **Protocol** tab's Messages header and in **Connection Info**. On a modern connection, `server/discover` also supplies `capabilities` (including `extensions`), `instructions`, and the list of `supportedVersions`. The server's name and version arrive in the result `_meta` under `io.modelcontextprotocol/serverInfo`.
2727 
28Once connected, the negotiated era is reported in the connection header and in **Connection Info**. On a modern connection, `server/discover` also supplies `capabilities` (including `extensions`), `instructions`, and the list of `supportedVersions`. The server's name and version arrive in the result `_meta` under `io.modelcontextprotocol/serverInfo`.
29 
3028<Frame caption="Server Settings: the Protocol Era selector, with all three choices.">
3129 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/settings-protocol-era.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=34566c45f97c8af0e2c0d9ee0493b572" width="3840" height="2160" data-path="images/inspector/settings-protocol-era.png" />
3230</Frame>
from line 31
3331 
3432## Reproducing each era locally
3533 
36Every section below ends with a **Reproduce with ...** pointer to a JSON config for one of the **composable test servers** shipped in the Inspector repository. Clone the repo, build the test servers, then point the Inspector at the config the section names.
34Every section below ends with a **Reproduce with ...** pointer to a JSON config for one of the **composable test servers** shipped in the Inspector repository. Clone the repo and build the test servers:
3735 
3836```bash theme={null}
3937git clone https://github.com/modelcontextprotocol/inspector
4038cd inspector && npm install && npm run build
41cd clients/web && npm run test-servers:build
39cd clients/web && npm run test-servers:build && cd ../..
4240```
4341 
42Then, from the repo root, start a server from the config a section names:
43 
44```bash theme={null}
45node test-servers/build/server-composable.js --config test-servers/configs/<name>.json
46```
47 
48The server prints its URL on stderr. If the config's port is already in use, it binds the next free port, so use the printed URL rather than assuming the port. Add that URL as a server in the Inspector, with the Protocol Era the section names.
49 
4450***
4551 
4652## Logging
from line 93
8793 </Tab>
8894 
8995 <Tab title="Modern">
90 The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus the `resourcesListChanged` opt-in. The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
96 The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
9197 
92 Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the Inspector reconnects by re-sending `subscriptions/listen`.
98 Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
9399 
94 Reproduce with `test-servers/configs/subscriptions-modern-http.json`.
100 Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
95101 </Tab>
96102</Tabs>
97103 
from line 119
113119 </Tab>
114120 
115121 <Tab title="Modern">
116 Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the *negotiated extension* rather than on `capabilities.tasks`.
122 Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
117123 
118124 Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
119125 
from line 154
148154| `mrtr_sample` | An embedded sampling request, routed to the Sampling panel. |
149155| `mrtr_roots` | An embedded `roots/list`, answered silently from configured roots (no modal). |
150156| `mrtr_edge` | An `inputRequests`-only round, then a `requestState`-only round. |
157| `mrtr_empty` | One elicitation round, then completes with an empty result (no `content`, no `structuredContent`). |
151158| `mrtr_loop` | Never completes, so the client stops at its `MRTR_MAX_ROUNDS` limit. |
152159 
153160<Note>
154 The legacy `collect_elicitation` pattern (a server calling
155 `server.elicitInput`) **errors** on a 2026-07-28 connection, because
161 The legacy pattern of a server calling `server.elicitInput` (the test servers'
162 `collect_elicitation` preset) **errors** on a 2026-07-28 connection, because
156163 server-to-client requests aren't allowed there. MRTR is its modern
157164 replacement.
158165</Note>
from line 168
161168 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/mrtr-pending-request.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=99f8acb7f845a12aed42bcea4d310fee" width="3840" height="2160" data-path="images/inspector/mrtr-pending-request.png" />
162169</Frame>
163170 
171<Frame caption="The monitoring sidebar's Protocol tab after mrtr_confirm completes. Both tools/call rounds sit inside one MRTR conversation, Round 1 tagged input_required and Round 2 complete, while unrelated traffic stays outside it.">
172 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/mrtr-protocol-conversation.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=b083d8127a7a67028fcd9702b25aeac1" width="3840" height="2160" data-path="images/inspector/mrtr-protocol-conversation.png" />
173</Frame>
174 
164175***
165176 
166177## Tools: mirrored headers and excluded tools
from line 185
174185 
175186Reproduce with `test-servers/configs/xmcpheader-modern-http.json`.
176187 
177<Warning>
178 **`Mcp-Param-*` mirroring is skipped by the SDK in the browser.** Calling a
179 mirrored tool from the *web* client omits the header, so a strict server
180 answers `-32020` (`HeaderMismatch`, see the [error
181 taxonomy](#network-and-protocol-headers-and-the-error-taxonomy) below). The
182 same tool called from the **CLI** or **TUI**, which both run on Node, mirrors
183 correctly. The header is dropped by an environment check inside the SDK,
184 outside the Inspector's control.
185</Warning>
188<Note>
189 On a modern connection the Inspector mirrors `x-mcp-header` arguments into
190 `Mcp-Param-*` headers itself, in all three clients. In the **web** client the
191 headers are added by the Inspector's Node backend, which issues the upstream
192 request, so they reach the server even though the browser never sends them.
193</Note>
186194 
187<Frame caption="get_weather shows its mirrored city -> Mcp-Param-City header, while invalid_header_tool is struck through under the Excluded (SEP-2243) divider.">
188 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tools-sep2243.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=98b020f6612b3a76b78b1a8d6159c2c0" width="3840" height="2160" data-path="images/inspector/tools-sep2243.png" />
195<Frame caption="get_weather shows its mirrored city -> Mcp-Param-City header, and the Network sidebar confirms the call carried mcp-param-city: Boston. invalid_header_tool is struck through under the Excluded (SEP-2243) divider.">
196 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/tools-sep2243.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=0e011c3012332c0a74d0f57fc84a5cf2" width="3840" height="2160" data-path="images/inspector/tools-sep2243.png" />
189197</Frame>
190198 
191199### `-32602` error panels
192200 
193Under the modern era a `tools/call` that rejects with `-32602` renders as a distinct **error panel**:
201A `tools/call` that rejects with `-32602` renders as a distinct **error panel**, on either era:
194202 
195203* **Unknown Tool**: when the message names a tool the server does not list. Reproduce by calling any name absent from the server's `tools/list`.
196204* **Invalid Parameters**: any other `-32602`. Reproduce with the `trigger_invalid_params` tool in the config above.
197205 
198Both eras reject with `-32602`; only the Inspector's presentation changes. On a legacy connection you get one generic JSON-RPC failure and have to read the message to tell which case you hit.
206The two share one error code, so the Inspector reads the message to tell them apart. Any other error code renders as a generic failure.
199207 
200208***
201209 
from line 233
225233 
226234***
227235 
236## Cancellation
237 
238On a legacy connection, cancelling an in-flight tool call sends `notifications/cancelled`. On a modern Streamable HTTP connection the Inspector instead closes that request's own SSE response stream, which is the 2026-07-28 cancellation signal. Over stdio, cancellation is still `notifications/cancelled`.
239 
240Reproduce with `test-servers/configs/cancellation-modern-http.json`: run `slow_task`, click **Cancel** after a few seconds, and the server's terminal prints how far the task got before it stopped.
241 
242***
243 
228244## Sessions
229245 
230246A legacy Streamable HTTP connection may carry a server-assigned session id (`Mcp-Session-Id`), which the client tears down with an HTTP `DELETE`. A modern connection is **sessionless and per-request**: with no session id the client SDK sends no `DELETE` to the server, so disconnect is purely local.
247 
248A legacy connection also opens a standalone `GET` notification stream after `initialize`, to carry notifications that do not belong to any request. The legacy-only **Suppress Notification Stream** option in **Server Settings** skips that stream, so you can inspect a server that cannot serve a second concurrent request. Modern connections never open the stream, so the option does not apply to them.
231249 
232250This has a practical consequence for your own test servers. A stateless modern handler constructed per request cannot hold state between calls, which is why `test-servers/configs/subscriptions-modern-http.json`, unlike its legacy counterpart, omits an `update_resource` tool: the mutation would run against a throwaway server instance and be invisible to the next read.
233251 

docs/draft/tools/inspector/recipes Changed · +132 / -24 lines

### Keeping your servers and secrets ### Health checks and other modes ## Keeping a connection open with mcpdo

from line 1
11# Recipes
22 
3> Practical guides for transports, importing configs, reviewing MCP Apps, Docker, and network hosting
3> Practical guides for transports, importing configs, reviewing MCP Apps, Docker, network hosting, and persistent connections with mcpdo
44 
55## Connecting stdio vs. HTTP servers
66 
from line 33
3333 
3434`--transport` accepts `http` (Streamable HTTP) and `sse`. If the server is protected, see [Authorization](/docs/draft/tools/inspector/authorization): no setup is needed in advance, because when the server answers `401` the Inspector runs the OAuth flow described there and retries the connection.
3535 
36For an HTTP server, also decide its [protocol era](/docs/draft/tools/inspector/protocol-eras). The default is `legacy`; set `modern` or `auto` in Server Settings (or `protocolEra` in the catalog file) to exercise the 2026-07-28 behavior.
36For an HTTP server, also decide its [protocol era](/docs/draft/tools/inspector/protocol-eras). The default is `legacy`; set `modern` or `auto` in Server Settings (or `protocolEra` in the catalog file) to exercise the 2026-07-28 behavior. For an ad-hoc target, pass it at launch with `--protocol-era legacy|auto|modern`.
3737 
3838## Importing an existing client config
3939 
from line 42
4242configs directly, and it also reads a server's own [MCP Registry](/registry/about) `server.json`.
4343 
4444Import merges into the active [catalog](/docs/draft/tools/inspector/configuration#choosing-servers)
45(the Inspector's writable server list), so existing entries aren't clobbered. If you'd rather
45(the Inspector's writable server list). When an imported server's id is already taken, you
46choose whether to overwrite, skip or rename it. If you'd rather
4647not touch your catalog at all, launch against the foreign file read-only instead:
4748 
4849```bash theme={null}
from line 64
6364 <Step title="Probe the security posture without calling the tool">
6465 ```bash theme={null}
6566 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
66 --method tools/call --tool-name <tool> --app-info
67 --method tools/call --tool-name <tool> --app-info --advertise-apps
6768 ```
6869 
70 `--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
71 
6972 One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
7073 
7174 ```json theme={null}
from line 101
98101 mcp-inspector --web &
99102 ```
100103 
101 Pinning `MCP_SANDBOX_PORT` matters here: the app's UI is served from a separate sandbox port that is dynamic by default, and your automation needs a fixed address to reach it.
104 Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
102105 </Step>
103106 
104107 <Step title="Navigate one deep link to a rendered widget">
from line 117
114117 
115118 | Selector | Attribute | Values |
116119 | - | - | - |
117 | `[data-testid="apps-form"]` | `data-app-status` | `ready` (on failure, `data-app-error` carries the reason) |
118 | `[data-testid="connection-status"]` | `data-status` | `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
120 | `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
121 | `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
119122 | `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
120123 </Step>
121124</Steps>
from line 128
125128A container image is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
126129 
127130```bash theme={null}
128docker run --rm -p 6274:6274 ghcr.io/modelcontextprotocol/inspector
131docker run --rm -p 127.0.0.1:6274:6274 ghcr.io/modelcontextprotocol/inspector
129132```
130133 
131134Read the [session token](/docs/draft/tools/inspector/web#the-session-token) from the container logs, or pin it with `-e MCP_INSPECTOR_API_TOKEN=<value>`.
132135 
133The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as a non-root user. It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
136The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as the non-root `node` user (uid `1000`). It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
134137 
135Its `HEALTHCHECK` probes the web UI, so add `--no-healthcheck` when running `--cli` or `--tui` (neither has a web server). `<target>` below is an [ad-hoc target](/docs/draft/tools/inspector/configuration#ad-hoc-targets): a positional stdio command, or `--server-url <url> --transport http`.
138<Warning>
139 **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
140 6274:6274` publishes on every host interface, putting a backend that spawns
141 processes, and the page that discloses its token, on your local network. The
142 image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
143 not the host's. See [Publish the port on loopback
144 only](/docs/draft/tools/inspector/security#publish-the-port-on-loopback-only).
145</Warning>
136146 
147To use the **Apps** tab, also publish the MCP Apps sandbox port, `6275`, and `6278` for an app that declares `_meta.ui.domain`. Publish each on the same port number inside and out, since the browser is handed the in-container port:
148 
137149```bash theme={null}
138docker run --rm --no-healthcheck ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
150docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6275:6275 \
151 ghcr.io/modelcontextprotocol/inspector
139152```
140153 
154### Keeping your servers and secrets
155 
156The server list, OAuth state and secrets live under `/home/node/.mcp-inspector`, in the container's writable layer, so `--rm` discards them and every run starts empty. Mount a volume there to keep them:
157 
158```bash theme={null}
159docker run --rm -p 127.0.0.1:6274:6274 \
160 -v mcp-inspector-data:/home/node/.mcp-inspector \
161 ghcr.io/modelcontextprotocol/inspector
162```
163 
164A container has no OS keychain, so where secrets go depends on that volume:
165 
166| Situation | Secret store | Survives a restart? |
167| - | - | - |
168| **No volume** on `/home/node/.mcp-inspector` | Memory | No, session only |
169| **With** that volume | `secrets.json` on the volume, mode `0600` | Yes |
170 
171If you bind-mount a host directory instead of a named volume, it keeps its host ownership, so on Linux add `--user "$(id -u):$(id -g)"` or `chown` it to uid `1000`, or saves fail with `EACCES`. Don't bind-mount the secrets file on its own: it isn't recognized as durable, and it can't be replaced atomically.
172 
141173<Warning>
174 **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
175 
176 Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
177 
178 ```bash theme={null}
179 mkdir -p ~/.config/mcp-inspector
180 (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
181 ```
182 
183 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/draft/tools/inspector/security#what-the-file-store-protects-against).
184</Warning>
185 
186Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
187 
188<Tabs>
189 <Tab title="docker run">
190 ```bash theme={null}
191 docker run --rm -p 127.0.0.1:6274:6274 \
192 -v mcp-inspector-data:/home/node/.mcp-inspector \
193 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
194 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
195 ghcr.io/modelcontextprotocol/inspector
196 ```
197 </Tab>
198 
199 <Tab title="Compose secrets">
200 ```yaml theme={null}
201 services:
202 inspector:
203 image: ghcr.io/modelcontextprotocol/inspector
204 ports: ["127.0.0.1:6274:6274"]
205 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
206 environment:
207 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
208 secrets: [mcp_inspector_secret_key]
209 secrets:
210 mcp_inspector_secret_key:
211 file: ${HOME}/.config/mcp-inspector/secret-key
212 volumes:
213 mcp-inspector-data:
214 ```
215 </Tab>
216</Tabs>
217 
218Without Swarm, Compose secrets are bind mounts that keep the host file's owner and mode, so the `0600` key file must be owned by uid `1000`. On a Linux host where your uid is different, run `sudo chown 1000 ~/.config/mcp-inspector/secret-key` rather than loosening its mode. Supply the **same** key on every run.
219 
220If the key file is missing, unreadable or empty, or both key variables are set, the Inspector **refuses to read or write the secrets file** rather than falling back to plaintext, and says why in the log and in the settings dialogs' footer. Everything else about the store (selection order, location, permissions) is under [Where secrets are stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored).
221 
222### Health checks and other modes
223 
224The image's `HEALTHCHECK` probes the web UI at the address `HOST` binds. `--cli` and `--tui` have no web server, so the probe detects those modes from the container's arguments and reports healthy while they run. An external orchestrator (a Kubernetes probe, a Compose `healthcheck`) can call `GET /healthz` on the web port, which needs no token and returns only `{"status":"ok"}`.
225 
226`<target>` below is an [ad-hoc target](/docs/draft/tools/inspector/configuration#ad-hoc-targets): a positional stdio command, or `--server-url <url> --transport http`.
227 
228```bash theme={null}
229docker run --rm ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
230```
231 
232<Warning>
142233 **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
143 8080:6274` the browser's origin becomes `http://localhost:8080`, which no
144 longer matches the in-container port, and connects will `403`. Either run `-e
145 CLIENT_PORT=8080 -p 8080:8080`, or set `-e
234 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
235 which no longer matches the in-container port, and connects will `403`. Either
236 run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
146237 ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
147238</Warning>
148239 
149240## Hosting on a network
150241 
151The Inspector binds `localhost` by default and its backend spawns processes, so treat exposing it to a network as a deliberate decision.
242The Inspector binds `127.0.0.1` by default and its backend spawns processes, so treat exposing it to a network as a deliberate decision.
152243 
153244The Inspector refuses to bind the **wildcard** all-interfaces addresses (`0.0.0.0`, `::`, and every equivalent spelling) unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding a **specific** address is allowed with no opt-in, because that's one deliberate exposure rather than every interface at once, which is the shape DNS-rebinding attacks target.
154245 
from line 246
155246| Goal | What to do |
156247| - | - |
157248| **Reach it from another machine on the LAN** | `HOST=192.168.1.50`. The default origin allow-list follows the bind host, so `http://192.168.1.50:6274` is accepted with no further config. |
158| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`. |
249| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`, and for MCP Apps set `MCP_SANDBOX_FULL_ADDRESS` (see below). |
159250| **Wildcard bind (containers)** | Set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Loopback access still works out of the box; reaching it at a non-loopback address needs `ALLOWED_ORIGINS`. |
160251 
161252<Warning>
from line 261
170261 
171262Two further caveats when going off loopback:
172263 
173* **MCP Apps need their sandbox port reachable too.** It's a separate, dynamic-by-default port; pin it with `MCP_SANDBOX_PORT` and expose or forward it. The Docker image publishes only `6274`.
174* **MCP Apps can't render over TLS or at a bare IPv6 literal.** The sandbox URL is always plain `http`, so an `https://` page blocks the iframe as mixed content; and a bracketed IPv6 literal isn't a valid CSP host-source, so browse at a name or an IPv4 address.
264* **MCP Apps need their sandbox port reachable too.** It's a separate listener (`6275` by default, set with `MCP_SANDBOX_PORT`), so expose or forward it alongside the web port.
265* **Behind TLS or a reverse proxy, give MCP Apps their public addresses.** By default the sandbox is advertised as `http://<bind host>:6275`, which an `https://` page blocks as mixed content. Set `MCP_SANDBOX_FULL_ADDRESS` (for example `https://inspector-sandbox.example.com/sandbox`) and, for apps that declare `_meta.ui.domain`, `MCP_APP_ORIGIN_FULL_ADDRESS` (for example `https://inspector-apps.example.com`). Each needs its own hostname or port; an address that shares the Inspector's origin is refused.
266* **MCP Apps can't render at a bare IPv6 literal.** A bracketed IPv6 literal isn't a valid CSP host-source, so browse at a name or an IPv4 address.
175267 
176Whatever the shape: keep authentication on. Do not set `DANGEROUSLY_OMIT_AUTH` on anything reachable by anyone but you.
268Whatever the shape: keep authentication on. Do not set `DANGEROUSLY_OMIT_AUTH` on anything reachable by anyone but you. The reasoning is under [Security](/docs/draft/tools/inspector/security#the-web-backend-and-its-api-token).
269 
270## Keeping a connection open with mcpdo
271 
272When an exploration spans many calls (a multi-step flow over one session, a log you watch, an agent using a server's tools mid-session), reconnecting for every `--cli` invocation gets in the way. [mcpdo](/docs/draft/tools/inspector/mcpdo) holds the connection for you:
273 
274```bash theme={null}
275mcpdo connect my-server
276mcpdo @my-server tools/call --task start_job size:=large # task-augmented; blocks until the task finishes
277mcpdo @my-server tools/call get_job_report
278mcpdo @my-server logging/tail # long-lived; Ctrl-C to stop
279mcpdo disconnect my-server
280```
281 
282A connection runs one call at a time, so commands against the same connection from other shells wait their turn.
283 
284The daemon that holds the connection starts automatically and exits about a minute after the last connection closes. Read [The mcpdo connection daemon](/docs/draft/tools/inspector/security#the-mcpdo-connection-daemon) before using it on a shared machine.
177285 
178286## Development workflow
179287 

docs/draft/tools/inspector/security New page · 188 lines, new page

# Security ## The web backend and its API token ### Where the backend listens ## Docker ### Publish the port on loopback only ### The data volume puts secrets on disk ## Secret storage ### The automatic plaintext fallback ### What the file store protects against ## stdio servers run as you ## The mcpdo connection daemon ### How clients reach it, and who else can ### How it authenticates commands ### What it holds in memory ### Lifetime ### What it writes to disk

A whole new page. There's nothing to diff it against, so here is what it says.

# Security

> The Inspector's threat model in one place, covering the web backend, Docker, secret storage, stdio servers, and the mcpdo daemon

The Inspector is a developer tool that holds real credentials and starts real processes. This page gathers everything it protects, what it trusts, and where the boundaries sit, so you can decide which setup fits your machine. The configuration and recipe pages link here rather than repeating the reasoning.

The short version:

* **The web backend can spawn processes.** Its API token is what stands between that capability and anything else that can reach the port.
* **Secrets go to the OS keychain when there is one.** Without one, they go to a file that is **plaintext unless you supply a key**, and that fallback happens automatically.
* **stdio servers run as you.** Anything the Inspector can read, a server it starts can usually read too.
* **The mcpdo daemon is a long-lived process holding live connections**, guarded by a token and same-user file permissions.

## The web backend and its API token

The web client is a browser app backed by a Node server that owns the MCP connections. That server can start stdio processes on request, so every `/api/*` route requires a per-launch bearer token (`x-mcp-remote-auth: Bearer <token>`). How the browser obtains it is described under [The session token](/docs/draft/tools/inspector/web#the-session-token).

What the token does and does not protect:

* **The token is the real guard for non-browser clients.** The origin allow-list (`ALLOWED_ORIGINS`) stops other web pages from driving the backend, but a request that arrives with **no** `Origin` header (curl, a script, any non-browser client) skips that check entirely.
* **`GET /` discloses the token.** The backend injects it into the served HTML so that a reload or a bookmark keeps working. Anyone who can load the page can therefore read the token, which is why the bind address matters more than the token's value. Pinning your own `MCP_INSPECTOR_API_TOKEN` does not change this, since a custom token is disclosed exactly like a generated one.
* **`DANGEROUSLY_OMIT_AUTH=true` removes the guard completely.** Anything that can reach the port can then spawn processes as you and use any OAuth token the Inspector holds. Only `true` or `1` turns auth off; any other value, including `false`, keeps it on.
* **Only `/api/*` is gated.** The page itself, its static assets and `GET /healthz` are served without the token. `/healthz` returns only `{"status":"ok"}`, for container and orchestrator probes.

### Where the backend listens

The backend binds `127.0.0.1` by default. Binding every interface (`0.0.0.0`, `::` and equivalent spellings) is refused unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding one specific address is allowed without that opt-in, because it is one deliberate exposure rather than all of them at once. See [Hosting on a network](/docs/draft/tools/inspector/recipes#hosting-on-a-network).

<Warning>
  Never combine `DANGEROUSLY_OMIT_AUTH` with a non-loopback bind. If you need
  the Inspector reachable by others, put a real access-control boundary in front
  of it: an authenticating reverse proxy, an SSH tunnel, or a private network.
</Warning>

## Docker

The [Docker image](/docs/draft/tools/inspector/recipes#docker) changes two things about the picture above.

### Publish the port on loopback only

Inside the container the Inspector must bind `0.0.0.0` to be reachable through `-p`, so the image sets `DANGEROUSLY_BIND_ALL_INTERFACES=true`. That opt-in governs the **container's** interfaces, not the host's. Which host interfaces see the Inspector is decided by how you publish the port:

| Publish flag | Reachable from |
| - | - |
| `-p 127.0.0.1:6274:6274` | This machine only. |
| `-p 6274:6274` (bare) | **Every host interface**, so your whole network. |

A bare `-p` puts a process-spawning backend, and the page that discloses its token, on your local network. Keep the `127.0.0.1:` prefix on every published port (`6274`, and `6275` / `6278` if you publish the MCP Apps listeners).

### The data volume puts secrets on disk

A container has no OS keychain. Without a volume on `/home/node/.mcp-inspector`, secrets stay **in memory** for the session and are lost when the container exits. Mounting that volume to keep your server list also switches secrets to a `secrets.json` file on the volume, and that file is **plaintext unless you supply a key**. It is then readable by root and every member of the host's `docker` group (which is equivalent to root), and by anyone who obtains a backup, snapshot or copy of the volume.

Supply the key as a file with `MCP_INSPECTOR_SECRET_KEY_FILE` (a Docker or Compose secret) rather than as an environment variable: a key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is visible to anyone who can run `docker inspect` or `docker exec`. The recipe shows both forms.

## Secret storage

The Inspector keeps credentials out of `mcp.json`, `client.json` and `oauth.json` so that sharing, committing or syncing those files does not leak them. These are stored as secrets:

* acquired OAuth tokens (access, refresh and ID tokens), subject to [`MCP_INSPECTOR_PERSIST_TOKENS`](/docs/draft/tools/inspector/configuration#secret-store-variables), and IdP session tokens from enterprise-managed authorization;
* each server's OAuth client secret, and the enterprise IdP client secret;
* dynamically registered client secrets and their registration access tokens;
* each stdio server's `env:` values.

**`headers` are not secrets.** They are saved in `mcp.json` exactly as written, so a header that carries a credential (an API key, a static `Authorization` value) stays in the file.

How the store is selected, and how to change it, is under [Where secrets are stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored). This section covers the risks.

### The automatic plaintext fallback

<Warning>
  On a host where the OS keychain cannot be reached (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. You do not have to ask for it, and unless you supply a key that file is **unencrypted**.

  The only signs are a warning on stderr when the store is selected and a footer in the web client's settings dialogs.
</Warning>

Treat that as a risk, not just a configuration fact. The file is written with mode `0600`, which keeps out other non-root users and nothing else. To close it, do one of the following:

* get a keychain back (install libsecret and run a Secret Service such as `gnome-keyring`, or run inside a desktop session);
* encrypt the file with a generated key in `MCP_INSPECTOR_SECRET_KEY_FILE`;
* or set `MCP_INSPECTOR_SECRET_STORE=memory` and re-enter secrets each session.

### What the file store protects against

The file store exists for machines without a keychain, and it is weaker than one. Treat keeping secrets in it, **even encrypted**, as a moderate risk.

**Without a key (plaintext, mode `0600`):**

| Threat | Protected? |
| - | - |
| Other non-root users on the machine, while the mode holds | Yes |
| Root, and on a container host every member of the `docker` group | No |
| Anyone with a copy of the file: a backup, a snapshot, a synced home directory, a commit | No |
| Any program running as your user, including the stdio servers the Inspector starts | No |

**With a key (AES-256-GCM, key stretched with scrypt against a per-write random salt):**

| Threat | Protected? |
| - | - |
| The file leaking on its own (a backup, snapshot, copy or commit), provided the key is high-entropy and did not leak with it | Yes |
| Anyone who can read the key where it lives | No |
| Root on the host, or the `docker` group: they can read the file, the key, or the process memory holding decrypted values | No |
| Code running as the same user | No |
| A weak passphrase: anyone holding the file can guess offline, quickly, because the scrypt cost is kept low for per-save derivation | No |

Where the key lives decides the second row. With `MCP_INSPECTOR_SECRET_KEY`, the key is in the Inspector's environment, readable through `/proc/<pid>/environ` by the same user or root, through `docker inspect` / `docker exec` for a container, and wherever you stored it for launching (a shell profile, an `.env` file, a Compose file). `MCP_INSPECTOR_SECRET_KEY_FILE` narrows that to whoever can read the key file, but the Inspector has to read it, so the same user can too. If the key sits beside the secrets file, in the same backup, volume or repository, encryption buys nothing.

In short, encryption turns "the file leaked" into "the file **and** the key leaked". It does not help against anyone who already has root, or the Inspector's own user, on the machine or in the container. When that is not acceptable, use a keychain or the memory store.

Two failure modes are deliberately loud rather than silent:

* If the key file is missing, unreadable or empty, if `MCP_INSPECTOR_SECRET_KEY_FILE` is set to an empty value, or if both key variables are set, the store **refuses to read or write** rather than falling back to plaintext.
* If the passphrase changes or is lost, the existing file can no longer be decrypted. The Inspector reads it as empty and **refuses to overwrite it**, so restore the passphrase, or delete the file and re-enter the values.

## stdio servers run as you

A stdio MCP server is a process the Inspector starts with your user's privileges, exactly as any MCP host would.

* **Environment:** the Inspector does not pass its own environment through. A stdio server gets a short allowlist (`HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `USER` on macOS and Linux) plus its configured `env:`. So a key in `MCP_INSPECTOR_SECRET_KEY` is not handed to it directly.
* **But it is the same user.** A server can open `secrets.json` itself, read `oauth.json` and your catalog, and usually read the Inspector's environment through `/proc/<pid>/environ`. The environment allowlist is hygiene, not isolation.

Only connect stdio servers you would trust with these secrets. To isolate one you don't, wrap its command in a container, for example `docker run -i --rm --network none <image>` as the stdio command. HTTP and SSE servers run no local code, so they need no process isolation.

## The mcpdo connection daemon

[mcpdo](/docs/draft/tools/inspector/mcpdo) keeps connections open between commands by handing them to a background daemon, `mcpdod`. It is started **automatically** the first time a command needs it (`mcpdo connect`, or any command against a connection). There is no separate "start the daemon" step to opt into. This section describes what that process exposes.

### How clients reach it, and who else can

| Platform | Endpoint |
| - | - |
| macOS / Linux | A Unix socket, `mcpdod.sock`, inside the daemon directory (default `~/.mcp-inspector`). |
| Windows | A named pipe, `\\.\pipe\mcp-conn-<hash>`, derived from the daemon directory. No directory permissions surround it, so the token is the guard. |

There is **no TCP port**, so nothing off the machine can reach the daemon. On Unix, the daemon directory is created, or tightened if it already exists, to mode `0700`, owned by you, and must be a real directory rather than a symlink. The socket and lock file inside it are `0600`. Other non-root users therefore cannot reach the socket. Root, and any process running as you, can.

The directory is chosen in this order: `MCP_INSPECTOR_DAEMON_DIR`, then `MCP_STORAGE_DIR`, then `~/.mcp-inspector`. A socket path longer than the platform's limit (about 104 bytes on macOS, 108 on Linux) is refused up front with an error naming the variable to shorten.

### How it authenticates commands

Every request must carry a bearer token. There is no unauthenticated request path.

* **Shared mode (the default):** the `mcpdo` command that starts the daemon generates a random 256-bit token and passes it to the daemon in its environment. The daemon publishes it to `mcpdod.token` (mode `0600`) in the daemon directory, so that any `mcpdo` command run by the same user can read it. Filesystem permissions on that file are the trust boundary, which is the same same-user boundary the socket has.
* **Private mode:** `eval "$(mcpdo private)"` creates a fresh `0700` directory under `$TMPDIR/mcp-conn-<uid>/` and exports `MCP_INSPECTOR_DAEMON_DIR` and `MCP_INSPECTOR_DAEMON_TOKEN` into that shell, so the shell gets its own daemon and its own connections. The parent `mcp-conn-<uid>` directory is checked for ownership and symlinks before use, because `$TMPDIR` can be shared.

Tokens are compared in constant time. A request line larger than 1 MiB is rejected. A command presenting the wrong token to a live daemon fails loudly. It never replaces that daemon.

<Note>
  Private mode separates connections and daemon state between shells. It is
  **not** a security boundary against other processes running as your user:
  anything with your UID that learns the daemon directory can read its token.
  For a hard boundary, use a separate user account or a container.
</Note>

### What it holds in memory

For as long as it runs, the daemon holds, for each open connection:

* the live MCP connection, and for stdio servers the child process, started with the daemon as its parent;
* the connection's resolved configuration, including stdio `env:` values pulled from the secret store;
* the OAuth tokens in use for HTTP connections, which it also re-reads from the store to re-dial a dropped transport;
* any elicitation a non-interactive command left parked, until it is answered or expires after 10 minutes.

The daemon is started with the environment of the `mcpdo` command that spawned it. It inherits that shell's variables (including `MCP_INSPECTOR_SECRET_KEY`, if set there, and its own IPC token in `MCP_INSPECTOR_DAEMON_TOKEN`) and keeps them for its whole lifetime, even after you change them in your shell. stdio servers it starts still receive only the allowlist above plus their `env:`, snapshotted from the shell that ran `mcpdo connect`.

On a keychain-less host, mcpdo never uses the memory store as its automatic fallback. Its front-end commands and the daemon are separate processes, so a per-process store could not carry a token from one to the other. mcpdo uses the **shared `secrets.json` file** instead, with the same plaintext-unless-keyed caveat as above. `MCP_INSPECTOR_SECRET_STORE=memory` set explicitly still wins. mcpdo prints the store warning once per `connect` rather than on every command.

### Lifetime

* **Start:** on the first command that needs it. If two start at once, an `O_EXCL` lock (`mcpdod.lock`) lets exactly one win. The lock left by a dead daemon is reclaimed, and a live daemon is never taken over.
* **Stop:** `mcpdo daemon stop`, or `SIGINT` / `SIGTERM`. It also exits by itself about **60 seconds after its last connection closes**. There is no maximum lifetime: while any connection is open, the daemon stays up.
* **On a clean stop:** new work is refused, in-flight requests get a short grace period, every connection is closed, and the socket, token file and lock are removed.
* **On a crash:** every connection it held is gone, and stdio servers lose their stdin, which normally ends them. Stored OAuth tokens and secrets are unaffected. The next `mcpdo` command starts a fresh daemon, which clears the stale socket, reclaims the lock and writes a new token. Connections must be re-established with `mcpdo connect`.

Find a stray daemon with `pgrep mcpdod`. It sets its process title to `mcpdod`.

### What it writes to disk

All in the daemon directory, which is `0700`:

| File | Mode | Contents |
| - | - | - |
| `mcpdod.sock` | `0600` | The IPC socket (Unix only). |
| `mcpdod.lock` | `0600` | The single-instance lock, holding the daemon's pid. |
| `mcpdod.token` | `0600` | The IPC bearer token. Written at start, removed at clean shutdown. |
| `mcpdod.log` | `0600` | The daemon's stderr, recreated on each start. Startup failures and the secret-store warning land here. |

The daemon writes OAuth state and secrets through the same `oauth.json` and secret store as the other clients, so everything under [Secret storage](#secret-storage) applies to it unchanged.

docs/draft/tools/inspector/tui Changed · +22 / -8 lines

from line 17
1717Unlike the CLI, the TUI has no `--server <name>` flag for picking one entry: it reads its servers from a catalog or config file, loads every server in it, and lets you pick from an on-screen list:
1818 
1919```bash theme={null}
20mcp-inspector --tui --catalog mcp.json # writable catalog, seeded empty if missing (unlike the web client)
20mcp-inspector --tui --catalog mcp.json # writable catalog, seeded empty if missing
2121mcp-inspector --tui --config mcp.json # read-only session, errors if absent
2222```
2323 
2424With neither `--catalog` nor `--config`, and no [ad-hoc target](/docs/draft/tools/inspector/configuration#ad-hoc-targets), it uses the default writable catalog `~/.mcp-inspector/mcp.json`. See [Configuration and flags](/docs/draft/tools/inspector/configuration).
2525 
26The TUI also takes the [shared server-selection flags](/docs/draft/tools/inspector/configuration#shared-server-selection-flags) (`--server-url`, `--transport`, `--header`, `-e`, `--cwd`) plus `--protocol-era` for an ad-hoc server, and the [OAuth client flags](/docs/draft/tools/inspector/configuration#cli-and-tui-oauth-client-flags) (`--client-id`, `--client-secret`, `--client-metadata-url`, `--client-config`, `--callback-url`).
27 
2628## Tabs
2729 
2830| Tab | Key | What it shows |
2931| - | - | - |
30| **Info** | `i` | Server info, capabilities, and negotiated protocol details. |
31| **Auth** | `a` | OAuth state for the selected server, plus a **Clear OAuth state** action. |
32| **Info** | `i` | Server configuration, name, version and instructions, plus the roots the client advertises (press `e` to edit them). |
33| **Auth** | `a` | OAuth state for the selected HTTP or SSE server, plus a **Clear OAuth State** action (`s`), which also disconnects a live connection. |
3234| **Resources** | `r` | Browse and read resources. |
35| **Subscriptions** | `u` | Subscribe to and unsubscribe from resources (servers that support resource subscriptions). |
3336| **Prompts** | `m` | List prompts and render them with arguments. |
37| **Skills** | `k` | List a server's skills and verify their digests and frontmatter (servers that declare the skills extension). |
3438| **Tools** | `t` | View tools and execute them with form-like inputs. |
39| **Tasks** | `s` | List tasks, fetch their results, cancel them, and clear finished ones (servers that support Tasks). |
3540| **Protocol** | `p` | JSON-RPC request/response/notification history. |
3641| **Network** | `n` | HTTP traffic for SSE and [Streamable HTTP](/specification/latest/basic/transports) servers. |
3742| **Console** | `o` | `stderr` from a connected stdio server process. |
3843 
39The accelerators avoid collisions rather than always taking the first letter: **P**rotocol takes `p` so Pro**m**pts takes `m`, and **C**onsole takes `o` because `c` is the global Connect action.
44The accelerators avoid collisions rather than always taking the first letter: **P**rotocol takes `p` so Pro**m**pts takes `m`, and **C**onsole takes `o` because `c` is the global Connect action. Ta**s**ks takes `s`, its only free letter, so Subscriptions takes `u` and Skills takes `k`.
4045 
46Auth, Network, and Console appear only for the transports they apply to; Subscriptions, Skills, and Tasks appear only once a connected server supports them.
47 
4148## Navigation
4249 
4350| Key | Action |
4451| - | - |
45| `Left` / `Right` arrows or `Tab` | Switch tabs |
46| `Up` / `Down` arrows | Move through the current list |
52| `Tab` / `Shift+Tab` | Move focus: server list → tab bar → list → details |
53| `Left` / `Right` arrows (tab bar focused), or a tab's letter | Switch tabs |
54| `Up` / `Down` arrows | Select a server, move through a list, or scroll details, depending on which pane has focus |
4755| `Enter` | Select an item, execute a tool, or fetch a resource |
4856| `c` | Connect to the selected server |
4957| `d` | Disconnect |
50| `Esc` or `Ctrl+C` | Exit |
58| `/` | Filter the current list (`Enter` keeps the filter, `Esc` clears it) |
59| `+` | Open the details pane full screen |
60| `y` / `w` | In a details dialog: copy the value, or save it to a file (`w` also saves a tool's result) |
61| `?` | Show or hide the keybinding help |
62| `Esc` or `Ctrl+C` | Exit (`Esc` closes an open dialog first) |
5163 
64Press **`?`** whenever no dialog is open for the full keybinding reference, including the keys specific to the active tab.
65 
5266## Authorizing an HTTP server
5367 
54681. Select an HTTP or SSE server and press **`c`** to connect.
from line 83
6983 
7084See [Authorization](/docs/draft/tools/inspector/authorization) for the full picture.
7185 
72<Frame caption="The Auth tab. It shows the same OAuth fields as the web client's Connection Info, or reports that the server needs no authorization.">
86<Frame caption="The Auth tab. It shows the same OAuth fields as the web client's Connection Info or, before any authorization has happened, says there is no OAuth information yet.">
7387 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-auth.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=2e5ef574e80e81c4b49fa2ef0eac0528" width="2986" height="1832" data-path="images/inspector/tui-auth.png" />
7488</Frame>
7589 

docs/draft/tools/inspector/web Changed · +17 / -13 lines

from line 20
20202. A `?MCP_INSPECTOR_API_TOKEN=...` query string, the form used in that printed URL.
21213. `sessionStorage`, as a backstop.
2222 
23Set the `MCP_INSPECTOR_API_TOKEN` environment variable to pin a known token (useful for scripted launches), or set `DANGEROUSLY_OMIT_AUTH=true` to disable the check entirely, but only on a machine where nothing else can reach the port. Both are described under [Web backend environment variables](/docs/draft/tools/inspector/configuration#web-backend-environment-variables).
23Set the `MCP_INSPECTOR_API_TOKEN` environment variable to pin a known token (useful for scripted launches), or set `DANGEROUSLY_OMIT_AUTH=true` to disable the check entirely, but only on a machine where nothing else can reach the port. Both are described under [Web backend environment variables](/docs/draft/tools/inspector/configuration#web-backend-environment-variables). What the token does and does not protect is described under [Security](/docs/draft/tools/inspector/security#the-web-backend-and-its-api-token).
2424 
2525## Dev mode
2626 
from line 41
4141| **Tools** | `tools` capability | Browse schemas, fill arguments, call, inspect results. |
4242| **Prompts** | `prompts` capability | List prompts, supply arguments, preview generated messages. |
4343| **Resources** | `resources` capability | Browse, read, and subscribe to resources. |
44| **Skills** | The server declares the Skills extension (SEP-2640), in either era | Browse and fetch the server's skills. |
4445| **Tasks** | `capabilities.tasks` (legacy era) or the tasks extension (modern era) | Track long-running tool calls. |
4546| **Logs** | `logging` capability | Server `notifications/message` output, plus the era-appropriate level control. |
4647| **Protocol** | Always | The JSON-RPC transcript: requests, responses, notifications. |
from line 56
5556 
5657### The monitoring sidebar
5758 
58**Tasks**, **Logs**, **Protocol**, **Network**, and **Console** form a *monitor group*. Pin the group and they leave the tab bar and move into a resizable right-hand column, so you can watch traffic while working in Tools or Resources. The column width and the selected monitor tab persist across reloads.
59**Tasks**, **Logs**, **Protocol**, **Network**, and **Console** form a *monitor group*. Pin the group and they leave the tab bar and move into a resizable right-hand column, so you can watch traffic while working in Tools or Resources. Drag the column's edge to resize it, from 320 to 720 pixels wide. The column width and the selected monitor tab persist across reloads.
5960 
60<Frame caption="The monitoring sidebar pinned beside the Tools screen. The Protocol stream stays visible while you work.">
61 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=eef6e546b9831b3d169e26bba8c54ce3" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
61<Frame caption="The monitoring sidebar pinned beside the Tools screen and dragged to its full width, with the tool call's request and response expanded in the Protocol stream.">
62 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
6263</Frame>
6364 
6465## Servers
from line 75
7475| `--config <path>` | That file, read-only (never written or seeded) | No |
7576| `--server-url <url>` or a positional command | One ad-hoc server, held in memory | No |
7677 
77On a first launch the web client seeds the catalog with two sample servers: a filesystem server scoped to `/tmp` and the canonical "everything" reference server. See [Configuration and flags](/docs/draft/tools/inspector/configuration) for the full rules, including why the CLI and TUI seed an empty catalog instead.
78On a first launch the web client seeds the catalog with three sample servers: a filesystem server scoped to `/tmp`, the canonical "everything" reference server, and the MCP org's hosted example server (Streamable HTTP, with OAuth via dynamic client registration). See [Configuration and flags](/docs/draft/tools/inspector/configuration) for the full rules, including why the CLI and TUI seed an empty catalog instead.
7879 
7980### Server Settings
8081 
8182* **Protocol Era**: `legacy` / `auto` / `modern`. See [Protocol eras](/docs/draft/tools/inspector/protocol-eras).
82* **Log level per request**: the level a modern-era connection stamps on each outgoing request by default, or `off` to opt out (see [Logging](/docs/draft/tools/inspector/protocol-eras#logging)).
83* **Log Level per Request**: the level a modern-era connection stamps on each outgoing request by default, or `off` to opt out (see [Logging](/docs/draft/tools/inspector/protocol-eras#logging)).
8384* **Advertised Extensions**: which extensions the Inspector declares in `capabilities.extensions`. A debugging knob: a server may legitimately change what it registers based on what you advertise. Uncheck the Tasks extension and reconnect against the `test-servers/configs/advertised-extensions-http.json` fixture (setup in [Reproducing each era locally](/docs/draft/tools/inspector/protocol-eras#reproducing-each-era-locally)) to watch a tool disappear.
8485* **Roots**: the roots advertised via the `roots` client capability. `@modelcontextprotocol/server-filesystem`, for instance, calls `roots/list` to learn its allowed directories.
85* **Headers**, **timeouts**, and **OAuth** fields.
86* **Fetch lists one page at a time**: when off, list results are auto-aggregated across pages on connect; when on, each list loads page 1 only with a **Load next page** control and an *N pages loaded* status. Reproduce with `test-servers/configs/pagination-http.json`, which paginates 12 tools, resources, and prompts into three pages each.
86* **Headers**, **timeouts**, and **OAuth** fields. Headers are saved in the catalog as written; OAuth client secrets and stdio `env:` values go to the [secret store](/docs/draft/tools/inspector/configuration#where-secrets-are-stored).
87* **OAuth Settings**: client ID and secret, a read-only **Redirect URI** to copy into a pre-registered client (it follows the origin you opened the Inspector at), **Scopes** (space-separated), **Request refresh token**, **Revoke tokens on clear**, additional authorization parameters, authorization and token URL overrides, and **Insufficient-scope response**, which decides whether a `403 insufficient_scope` triggers [step-up](/docs/draft/tools/inspector/authorization#mid-session-re-authorization) or surfaces the error.
88* **Fetch Lists One Page at a Time**: when off, list results are auto-aggregated across pages on connect; when on, each list loads page 1 only with a **Load next page** control and an *N pages loaded* status. Reproduce with `test-servers/configs/pagination-http.json`, which paginates 12 tools, resources, and prompts into three pages each.
8789 
90A footer at the bottom of **Server Settings**, **Client Settings** and the **Add / Edit / Clone server** dialogs names the secret store in use, so you see it where you type a secret. It turns into a warning when secrets are memory-only (lost on restart), in an unencrypted file, in a file with loose permissions, or in a file that can't be read.
91 
8892<Frame caption="Server Settings with Advertised Extensions expanded. Unchecking one changes what the Inspector declares at connect.">
8993 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-server-settings.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=d42be09ee8de7e45e58a8ff1a444ba52" width="3840" height="2160" data-path="images/inspector/web-server-settings.png" />
9094</Frame>
from line 121
117121 
118122## Apps
119123 
120[MCP Apps](/extensions/apps/overview) are tools that carry UI. The Apps tab renders one in a sandboxed iframe served from a **separate port**, exercises the `ui/*` bridge, and shows the view's `ui/message` submissions and its `notifications/message` logs in side panels.
124[MCP Apps](/extensions/apps/overview) are tools that carry UI. The Apps tab renders one in a sandboxed iframe served from a **separate port**, exercises the `ui/*` bridge, and shows the view's `ui/message` submissions and its `notifications/message` logs in panels below the frame.
121125 
122* The sandbox port is dynamic by default; pin it with `MCP_SANDBOX_PORT` if you need to expose or forward it.
126* The sandbox listens on its own port, `6275` by default (set it with `MCP_SANDBOX_PORT`). An app whose UI resource declares `_meta.ui.domain` has its document served from a third listener, the app origin, on `6278` by default (`MCP_APP_ORIGIN_PORT`). Expose or forward both along with the web port.
123127* The sandbox is gated by a `frame-ancestors` CSP, and a bracketed IPv6 literal is not a valid CSP host-source, so browse the Inspector at `localhost`, `127.0.0.1`, a hostname, or a LAN IPv4, **not** at a bare `http://[::1]:...`.
124* The sandbox URL is always plain `http`, so an `https://` Inspector page blocks the frame as mixed content. MCP Apps need a plain-`http` origin today.
128* By default the sandbox URL is plain `http` on the bind address, so an `https://` Inspector page blocks the frame as mixed content. Behind a TLS reverse proxy, set `MCP_SANDBOX_FULL_ADDRESS` (and `MCP_APP_ORIGIN_FULL_ADDRESS`) to the public `https://` address the browser reaches each listener at. Neither may share an origin with the Inspector UI; a value that does is ignored with a warning.
125129 
126130See [Recipes](/docs/draft/tools/inspector/recipes#reviewing-an-mcp-app) for the CLI-first automated review flow.
127131 
from line 141
137141* **Network**: the HTTP layer, for SSE and Streamable HTTP servers. Status codes, request and response headers, and bodies. On modern connections the standardized `Mcp-*` headers are highlighted and sentinel values decoded.
138142* **Console**: the connected stdio server process's `stderr`, which is where most stdio servers put their own diagnostics.
139143 
140Secrets are masked in these views, and entries can be cleared or exported.
144Secrets in Network headers and bodies are masked, with a control to reveal them; Protocol and Console show traffic as sent. Entries can be cleared or exported.
141145 
142146<Frame caption="The Protocol tab with an entry expanded, showing the full JSON-RPC exchange.">
143147 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-protocol.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=f31338c83a389c5588f11c0d5b2b97ed" width="3840" height="2160" data-path="images/inspector/web-protocol.png" />
from line 165
161165 
162166## Host binding and origins
163167 
164By default the Inspector binds `localhost` and accepts requests only from the loopback origins for its port. Treat both defaults as security boundaries, since the backend spawns processes on your machine.
168By default the Inspector binds `127.0.0.1` and accepts requests only from the loopback origins for its port. Treat both defaults as security boundaries, since the backend spawns processes on your machine.
165169 
166170Binding all interfaces (`HOST=0.0.0.0`) is **refused** unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding a *specific* non-loopback address is allowed with no opt-in, since that's a single deliberate exposure rather than every interface at once.
167171 
Feedback