One read of Model Context Protocolmcp-20261008T130707Z
20 pages moved out of 356 read.
What this read moved
1-20 of 20docs/2026-07-28/tools/inspector Changed · +30 / -6 lines
docs/2026-07-28/tools/inspector/authorization Changed · +18 / -14 lines
docs/2026-07-28/tools/inspector/cli Changed · +66 / -14 lines
### CI gates ### Editing the catalog
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
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
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
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
docs/2026-07-28/tools/inspector/web Changed · +17 / -13 lines
docs/draft/tools/inspector Changed · +30 / -6 lines
docs/draft/tools/inspector/authorization Changed · +18 / -14 lines
docs/draft/tools/inspector/cli Changed · +66 / -14 lines
### CI gates ### Editing the catalog
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
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
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
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.