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.
The obvious fix has its own failure mode
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.
Machine identities do not expire
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.
Three backends, three different shapes
The interesting part of building this was that the three supported backends work nothing alike:
- Infisical mints a token over HTTPS, then hands off to the Infisical CLI.
-
1Password hands off to
op run, which authenticates itself. - Vault reads the secret over the HTTP API and starts your MCP server directly, with no CLI involved at all.
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.
When it does not work
"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.
Two details worth stealing
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+.
Top comments (2)
The failure mode you flag with the secret-manager-CLI wrapper is underrated: ten MCP servers dying simultaneously when a developer session expires, with the error surfacing in a client that has no concept of secret managers. That's the kind of cross-layer failure that eats an afternoon precisely because the symptom is maximally far from the cause.
The machine-identity → short-lived-token exchange is the right shape, and the thing I'd emphasize for anyone adopting it: the win isn't just "no secrets in the file," it's that the blast radius becomes scopeable and revocable. A committed config that resolves to a machine credential means you can rotate or kill that identity centrally without touching every dev's machine — which is the actual thing security review is asking for.
One question on the runner design: when the token exchange itself fails at process start (backend down, credential rotated), does
mcp-secrets-runnersurface a distinct error the client can render, or does it collapse back into the same generic startup failure you were trying to escape? Making that path legible feels like half the value.Some comments may only be visible to logged-in visitors. Sign in to view all comments.