How should secrets (API keys, credentials) be handled inside an MCP server's configuration?
Quick Answer
Secrets an MCP server needs (API keys, database credentials, OAuth client secrets) should be supplied through the server's own environment/configuration — environment variables, a secrets manager, or a host's secret-storage feature where available — never hardcoded in source, never committed to version control, and never passed as a tool argument, since arguments are visible to the model and can end up echoed into its output, logged, or leaked via a crafted prompt. Where a host's config file is the delivery mechanism (as with Claude Desktop's env field), treat that file with the same care as any other secrets store, since it typically sits in plaintext on disk. For remote/HTTP servers, per-user credentials should flow through the OAuth authorization framework rather than being embedded in the server's own static configuration at all, so the server never needs to hold one shared, highly-privileged secret on behalf of every user.
Detailed Answer
Secrets handling for an MCP server follows the same discipline as any backend service, with one MCP-specific trap worth calling out explicitly.
The MCP-specific trap: secrets as tool arguments
# Wrong: the model can see, echo, or leak this argument
@mcp.tool()
def query_database(api_key: str, sql: str) -> str: ...
# Right: the server already holds its own credential from its environment;
# the model never sees it at all
@mcp.tool()
def query_database(sql: str) -> str:
return db.execute(DB_CREDENTIAL, sql)
If a secret is a tool argument, it's part of the same context the model reasons over. It can be tricked, via prompt injection or simply by mistake, into repeating it back, logging it in a visible trace, or passing it somewhere it shouldn't go. Configuration-level secrets the server reads once at startup never enter that channel.
Standard secret hygiene, applied here
- Never hardcode or commit secrets to the server's source. Read them from environment variables or a secrets manager at startup.
- Fail fast if a required secret is missing, rather than proceeding and failing confusingly later.
- Scope credentials narrowly. A server talking to one specific upstream API should hold a credential scoped to exactly what it needs, not a broad admin-level key "just in case" — this ties back to the least-privilege principle.
- Rotate and revoke. Treat MCP server credentials like any other service credential in your rotation and incident-response process.
Where a host's config file is the delivery mechanism
{ "mcpServers": { "acme": { "command": "python", "args": ["server.py"],
"env": { "ACME_API_KEY": "sk-xxxxx" } } } }
This file typically lives in plaintext in the user's local config directory. Protect it with the same file-permission discipline you'd apply to any credentials file. Anyone with read access to that file, or to a backup of it, can read the embedded secret in full.
The remote-server distinction
For a server serving many users over HTTP, the right model usually isn't "the server holds one static secret representing all users" at all. It's "each user authorizes through OAuth, and the server obtains a token scoped to that specific user's access." This sidesteps the confused-deputy risk covered earlier by design, rather than trying to bolt safety on top of one shared credential after the fact.