Skip to main content
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. 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.
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.

Docker

The Docker image 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: 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, 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. This section covers the risks.

The automatic plaintext fallback

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.
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): With a key (AES-256-GCM, key stretched with scrypt against a per-write random salt): 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 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

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.
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.

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: The daemon writes OAuth state and secrets through the same oauth.json and secret store as the other clients, so everything under Secret storage applies to it unchanged.