- 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 noOriginheader (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 ownMCP_INSPECTOR_API_TOKENdoes not change this, since a custom token is disclosed exactly like a generated one.DANGEROUSLY_OMIT_AUTH=trueremoves the guard completely. Anything that can reach the port can then spawn processes as you and use any OAuth token the Inspector holds. Onlytrueor1turns auth off; any other value, includingfalse, keeps it on.- Only
/api/*is gated. The page itself, its static assets andGET /healthzare served without the token./healthzreturns only{"status":"ok"}, for container and orchestrator probes.
Where the backend listens
The backend binds127.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.
Docker
The Docker image changes two things about the picture above.Publish the port on loopback only
Inside the container the Inspector must bind0.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 ofmcp.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
Treat that as a risk, not just a configuration fact. The file is written with mode0600, 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=memoryand 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, mode0600):
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_FILEis 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,USERon macOS and Linux) plus its configuredenv:. So a key inMCP_INSPECTOR_SECRET_KEYis not handed to it directly. - But it is the same user. A server can open
secrets.jsonitself, readoauth.jsonand your catalog, and usually read the Inspector’s environment through/proc/<pid>/environ. The environment allowlist is hygiene, not isolation.
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
mcpdocommand that starts the daemon generates a random 256-bit token and passes it to the daemon in its environment. The daemon publishes it tomcpdod.token(mode0600) in the daemon directory, so that anymcpdocommand 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 fresh0700directory under$TMPDIR/mcp-conn-<uid>/and exportsMCP_INSPECTOR_DAEMON_DIRandMCP_INSPECTOR_DAEMON_TOKENinto that shell, so the shell gets its own daemon and its own connections. The parentmcp-conn-<uid>directory is checked for ownership and symlinks before use, because$TMPDIRcan be shared.
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.
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_EXCLlock (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, orSIGINT/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
mcpdocommand starts a fresh daemon, which clears the stale socket, reclaims the lock and writes a new token. Connections must be re-established withmcpdo connect.
pgrep mcpdod. It sets its process title to mcpdod.
What it writes to disk
All in the daemon directory, which is0700:
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.