MCP Secret Management Patterns for Production
MCP servers expose API keys in plain-text config. Production patterns: env var injection, secret manager integration, key rotation without downtime.
MCP servers expose API keys in plain-text configuration files by default. The standard Claude Desktop config stores keys directly in JSON, and most MCP server tutorials show hardcoded credentials in example configs. This is acceptable for local development but dangerous for production, shared machines, or any environment where config files might be committed to git, backed up to cloud storage, or accessed by other users.
The default problem
A typical MCP server configuration looks like this, with API keys visible in plain text:
{
"mcpServers": {
"search": {
"command": "npx",
"args": ["-y", "@scavio/mcp-server"],
"env": {
"SCAVIO_API_KEY": "sc-live-abc123def456"
}
}
}
}This file sits in your home directory, is readable by any process running as your user, and is often included in dotfile backups. If you share your screen, the key is visible. If your machine is compromised, every API key in every MCP config is immediately exposed.
Pattern 1: Environment variable injection
The simplest improvement is referencing environment variables instead of hardcoding values. Set the key in your shell profile and reference it in the MCP config. This keeps keys out of config files that might be shared or committed.
# In ~/.zshrc or ~/.bashrc
export SCAVIO_API_KEY="sc-live-abc123def456"
export BRAVE_API_KEY="BSA-xyz789"
# Verify it is set
echo $SCAVIO_API_KEYThen your MCP config references the variable. Some MCP clients support variable expansion in env blocks; others require a wrapper script:
{
"mcpServers": {
"search": {
"command": "npx",
"args": ["-y", "@scavio/mcp-server"],
"env": {
"SCAVIO_API_KEY": "$SCAVIO_API_KEY"
}
}
}
}If your MCP client does not expand variables, use a shell wrapper:
#!/bin/bash
# ~/.local/bin/mcp-search-wrapper.sh
export SCAVIO_API_KEY="$SCAVIO_API_KEY"
exec npx -y @scavio/mcp-serverPattern 2: Secret manager integration
For production deployments and teams, use a proper secret manager. This adds a retrieval step before the MCP server starts, pulling keys from a secure store instead of local files.
import subprocess
import json
import os
def get_secret(secret_name: str, provider: str = "aws") -> str:
"""Retrieve a secret from a secret manager."""
if provider == "aws":
result = subprocess.run(
[
"aws", "secretsmanager", "get-secret-value",
"--secret-id", secret_name,
"--query", "SecretString",
"--output", "text",
],
capture_output=True, text=True,
)
return result.stdout.strip()
elif provider == "vault":
result = subprocess.run(
["vault", "kv", "get", "-field=value", f"secret/{secret_name}"],
capture_output=True, text=True,
)
return result.stdout.strip()
elif provider == "gcp":
result = subprocess.run(
[
"gcloud", "secrets", "versions", "access", "latest",
"--secret", secret_name,
],
capture_output=True, text=True,
)
return result.stdout.strip()
raise ValueError(f"Unknown provider: {provider}")
# Inject into environment before MCP server starts
os.environ["SCAVIO_API_KEY"] = get_secret("scavio-api-key", provider="aws")
os.environ["BRAVE_API_KEY"] = get_secret("brave-api-key", provider="aws")Pattern 3: Key rotation without downtime
API keys should be rotated periodically. The challenge is rotating without stopping running MCP servers. The pattern: generate a new key, update the secret store, restart the MCP server, then revoke the old key.
#!/bin/bash
# rotate-mcp-keys.sh
# Step 1: Generate new key (API-specific)
NEW_KEY=$(curl -s -X POST "https://api.scavio.dev/api/v1/keys/rotate" -H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.new_key')
# Step 2: Update secret store
aws secretsmanager update-secret --secret-id scavio-api-key --secret-string "$NEW_KEY"
# Step 3: Restart MCP server (picks up new env)
pkill -f "mcp-server"
# MCP client will auto-restart the server on next use
# Step 4: Revoke old key after grace period
sleep 300 # 5 minute grace period
curl -s -X POST "https://api.scavio.dev/api/v1/keys/revoke" -H "Authorization: Bearer $ADMIN_TOKEN" -d "{"key": "$OLD_KEY"}"
echo "Key rotation complete"Pattern 4: Per-project key scoping
Do not use one API key for all MCP servers and all projects. Create separate keys with scoped permissions for each project or environment. If a key is compromised, the blast radius is limited to one project.
{
"mcpServers": {
"search-production": {
"command": "bash",
"args": ["-c", "SCAVIO_API_KEY=$(aws secretsmanager get-secret-value --secret-id prod/scavio --query SecretString --output text) npx -y @scavio/mcp-server"]
},
"search-staging": {
"command": "bash",
"args": ["-c", "SCAVIO_API_KEY=$(aws secretsmanager get-secret-value --secret-id staging/scavio --query SecretString --output text) npx -y @scavio/mcp-server"]
}
}
}What to avoid
Do not store API keys in git repositories, even in private repos. Do not put keys in Docker images at build time. Do not share MCP config files between team members without stripping keys first. Do not use the same key for development and production. Do not skip key rotation because it is inconvenient.
The effort to set up proper secret management for MCP is about 30 minutes for environment variables, 1-2 hours for secret manager integration. The effort to recover from a leaked API key, revoke access, audit usage, and rotate everything downstream, is significantly more.
Minimum viable security
If you do nothing else: move API keys from MCP config files into environment variables, add your MCP config directory to .gitignore, and create separate keys for development and production. This eliminates the most common exposure vectors (git commits, screen sharing, backup leaks) with minimal setup effort.