Nearly every guide to setting up an MCP server tells you to do this:
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp@latest"],
"env": { "STRIPE_SECRET_KEY": "sk_live_51J..." }
}
}
}
That is a live production key sitting in a file that gets committed to repositories, synced between machines, screenshotted for tutorials and pasted into bug reports. I have seen sk_live_
keys in all four.
It is also, in my experience, the single biggest reason MCP stalls at a company's security review. Not the protocol. Not the tooling. The config file.
So you wrap the server in your secret manager's CLI instead:
"args": ["run", "--env=prod", "--", "npx", "-y", "@stripe/mcp@latest"]
Better. The key is gone from the file. But those CLIs authenticate with a developer session stored in your OS keyring, and that session expires.
When it does, every MCP server wrapped this way stops starting at the same moment. And the error surfaces in your MCP client, which knows nothing about secret managers or expired logins. You get a generic startup failure on ten servers at once and go looking in entirely the wrong place.
I lost an afternoon to this before the pattern clicked.
Every serious secret manager has a concept for unattended access: a credential meant for machines rather than humans, with no TTL of its own. Infisical calls it a machine identity, 1Password calls it a service account, Vault calls it AppRole.
The credential is long-lived; what it produces is short-lived. You exchange it for a token at process start, use the token, and never think about it again.
That is the whole idea behind mcp-secrets-runner
. It sits between the MCP client and the server, authenticates with a machine credential, fetches the secrets and execs the real server:
{
"mcpServers": {
"stripe": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-secrets-runner", "--env=prod", "--path=/mcp",
"--", "npx", "-y", "@stripe/mcp@latest"]
}
}
}
No secret values in the file. No session to renew. The same config works on every machine that has the machine credentials in its environment, which is what makes it safe to commit.
The interesting part of building this was that the three supported backends work nothing alike:
op run
, which authenticates itself.A provider only has to answer one question: which child process do I start, and what environment does it get. That the three answers look so different is the best evidence I have that the interface is not overfitted to the first one I wrote.
"My MCP server won't start" is almost always one of four things, and an MCP client reports all four identically. So there is a doctor
:
$ mcp-secrets-runner doctor --provider=vault
mcp-secrets-runner doctor (provider: vault)
[ok ] Node 22.14.0
[ok ] instance
http://127.0.0.1:8200
[ok ] AppRole credentials present
[ok ] cached token available
[ok ] authentication succeeded
token valid for 20 min
All checks passed.
Missing CLI, missing credentials, wrong instance URL, unreadable secret path. One line each, instead of one opaque failure.
All diagnostics go to stderr. stdout belongs to the MCP protocol. One stray byte there corrupts the JSON-RPC stream and the client reports a parse error pointing nowhere useful.
It fails closed. If credentials are set but authentication fails, the runner exits. The alternative is worse than it sounds: a credential-less CLI invocation opens an interactive browser login, which hangs the MCP server on stdin forever while the client waits.
MIT, zero dependencies, Node 18+.