MCP servers and secrets: where your keys actually live now
Adding a Model Context Protocol (MCP) server to Claude Desktop, an IDE, or a CLI agent is a two-minute flow: pick a server from a directory, paste in an API key, click save. What you rarely pause to ask is the security question underneath: where does that key actually live now, and which processes on your machine can read it?
The short answer is that your keys now live in a few very specific places, and none of them are under the control of your password manager.
Where the key physically lives
When you configure an MCP server, the key ends up in one of three places:
- Client config files. Claude Desktop writes
claude_desktop_config.json; IDEs and CLI agents write.mcp.jsonor similar, often with anenvblock containing the plaintext value. - Server-side
.envfiles. Many servers are installed with instructions to create a.envnext to the server code, which the server loads at startup. - The server process environment. However the key gets in, the MCP server process ends up with it in its environment. That is the place that matters, because it is the place an attacker can read.
The config file is what you can see; the process environment is what an attacker reads.
The server process is the new attack surface
Every MCP server you add is a locally running process holding your credentials. The client does not sandbox it, and the server is usually installed from an npm or PyPI package you did not review. A malicious or compromised package can read its own environment, collect every key it was given, and send them anywhere — the process has network access by default. This is the classic supply-chain problem, with your secrets in hand at install time.
The second issue is over-provisioning: one long-lived key shared across servers means one compromised package compromises them all.
Tool output feeds the context window
There is a subtler path that does not require malice: MCP tools return stdout to the client, and that output goes straight into the model’s context window. If a server echoes its configuration or prints an error containing the key, the value lands in context — and from there into session transcripts, summaries, and provider-side request logs. The key does not need to be exfiltrated to be leaked.
Practical hardening
You can shrink this surface without giving up MCP:
- One scoped key per server. A GitHub token limited to one repo is worth less to an attacker than a token with org-wide scope.
- Inject at process launch instead of storing. Start the server through a
broker that resolves the secret from your existing
passor Bitwarden store and injects it into the subprocess environment:trustless run -s GITHUB_TOKEN -- npx -y some-mcp-server. The config file stays empty, the agent never sees the value, and stdout is scrubbed after the run. - Route HTTP-based tools through a proxy that injects credentials as headers per host, so no token is embedded in tool definitions or config.
- Audit access. A broker keeps an append-only log of which keys were resolved and when, so you can tell which servers actually touched a credential after an incident.
The pattern is the same as for agent subprocesses generally: the server can use the credential without ever possessing it.
trustless is a zero-dependency Go CLI that implements this injection and scrubbing pattern, with commands documented under /docs/commands/. It reads your existing pass or Bitwarden store, so there is no new vault to migrate to.
Next time you add an MCP server, can you say exactly which process will be holding your key — and what that process is allowed to do with it?