MCP servers need credentials to do useful work. A GitHub MCP server needs a personal access token. A database MCP server needs a connection string. A Stripe MCP server needs an API key.
The question is: where do those credentials live, and how do they get into the MCP server without also getting into the AI’s context window?
This guide covers the full landscape: the common anti-patterns, the correct architecture, and a working reference implementation.
The three anti-patterns
Anti-pattern 1: Credentials in the MCP config file
Many early MCP quickstart guides show this pattern:
{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxx" } } }}The credential is in a static JSON file on your filesystem. This file:
- Gets included in dotfiles repos that accidentally go public
- Is readable by any process running as your user
- Persists indefinitely — there’s no natural rotation pressure
Don’t put credentials in MCP config files.
Anti-pattern 2: MCP server returns credentials to the AI
Some MCP servers expose a get_secret or get_credential tool that returns the raw credential value to the AI. The AI can then pass it to other tools, include it in generated code, or use it in API calls visible in the context window.
This pattern is tempting because it’s simple. It’s also the most dangerous: every credential you manage this way is permanently accessible to the AI model, any future model update, any jailbreak, and any prompt injection attack that can get the AI to call the tool.
Never return plaintext credential values to the AI.
Anti-pattern 3: Separate credentials for each MCP server
When you have 10 MCP servers, you have 10 sets of credentials to manage. Each one:
- Needs to be rotated independently
- Has no unified audit log
- Gets configured in its own way
- Creates 10 separate attack surfaces
This isn’t wrong so much as it’s unscalable. The correct architecture centralizes credential management.
The correct architecture
The right approach uses a single credential broker MCP server that all other secrets flow through:
Claude │ ├── vault_run("gh pr list") ─────────────→ OpaqueVault (credential broker) │ │ │ ├── Decrypts GITHUB_TOKEN locally │ ├── Spawns: gh pr list │ │ └── env: GITHUB_TOKEN=xxx │ └── Returns: exit code + run metadata │ ├── vault_run("stripe customers list") ──→ OpaqueVault │ │ │ ├── Decrypts STRIPE_SECRET_KEY │ ├── Spawns: stripe customers list │ └── Returns: exit code + run metadata │ └── vault_list_secrets() ─────────────→ OpaqueVault └── Returns: [names only, no values]Key properties of this architecture:
- One MCP server manages all credentials — single audit trail, single rotation surface, single configuration point
- Credentials are decrypted locally — the broker (OpaqueVault) runs on your machine, decrypts in memory, injects into subprocess env
- The remote API lacks the keys to decrypt stored secret values — account and operational metadata remain visible
- Subprocess output is omitted from MCP responses — stdout and stderr go to the daemon terminal or agent log
A subprocess can still write or transmit an injected credential. Keep its permissions narrow and protect local output sinks; omitting output from MCP is not a sandbox.
How OpaqueVault implements this
The local ov agent daemon performs secret operations, and ov mcp serve bridges MCP clients to it. The interface exposes eight tools:
vault_run — runs an authorized command with selected secrets injected as environment variables. Returns exit code and run metadata; output goes to the daemon terminal or agent log.
vault_run( command: ["node", "scripts/migrate.js"], secret_names: ["DATABASE_URL"])vault_list_secrets — returns secret names and metadata without values.
vault_create_secret, vault_update_secret, vault_delete_secret — lifecycle operations. Enter new values through a trusted local workflow, not an AI conversation.
vault_secret_exists — checks whether a named secret exists.
vault_status — reports session and context status.
vault_check_context — verifies the intended app and environment.
Configure commands and MCP servers separately
vault_run executes commands; it is not a transparent stdio transport for another MCP server because its output is deliberately omitted. Connect other MCP servers through their documented client configuration and credential mechanisms.
For a CLI operation, store the credential through a trusted terminal and request a scoped command through OpaqueVault. For example, a vault_run request can run gh pr list with the secret name GITHUB_TOKEN, plus the expected app and environment. The response provides status, not the pull-request list; inspect the local output separately.
See the MCP tool reference for current parameters and limitations.
Organizing secrets for multiple services
For projects using many external services, scoping secrets by app and environment keeps credentials organized:
# Source controlov secret set GITHUB_TOKEN
# Databaseov secret set DATABASE_URLov secret set DATABASE_URL # after: ov env use production
# Payment processingov secret set STRIPE_SECRET_KEYov secret set STRIPE_SECRET_KEY # after: ov env use production
# AI services (for your app, not Claude itself)ov secret set OPENAI_API_KEY
# Emailov secret set SENDGRID_API_KEY
# Monitoringov secret set SENTRY_DSN # after: ov env use productionov secret set DATADOG_API_KEY # after: ov env use productionThe .ov.yaml in each project sets the default context:
app: my-projectenv: developmentWhen Claude calls vault_run, it uses the active app and environment context set by ov app use and ov env use (or the defaults in .ov.yaml). If you’re working across multiple projects, make sure the right context is active.
Audit logging
Every vault_run call generates an audit log entry:
2026-04-13T10:23:41Z vault_run my-project/development ACCESSED [hmac:a3f9...]2026-04-13T10:24:02Z vault_run my-project/development ACCESSED [hmac:a3f9...]2026-04-13T10:31:17Z vault_run my-project/production ACCESSED [hmac:b7c2...]Secret references in audit logs use HMAC tokens rather than plaintext names. This means a compromised audit log doesn’t become a map of what credentials you hold — an attacker needs the HMAC key to reverse a token back to a name. That key is held server-side (a root key in the API’s environment, plus a per-user subkey derived from it via HKDF-SHA256), not derived from your KEK. So the protection is against a leaked log rather than against the service itself: OpaqueVault can recompute the HMAC and resolve which secret a token refers to. To anyone who merely steals the log, it answers “was this credential accessed, and when?” without advertising your full secrets inventory.
Rotation workflow
When a credential needs to be rotated:
# 1. Issue a new key in Stripe's dashboard (don't revoke the old one yet)
# 2. Update the value in the vaultov secret set STRIPE_SECRET_KEY # updates the existing value
# 3. Verify the new value worksov run --secrets STRIPE_SECRET_KEY -- stripe customers list
# The CLI's `--secrets` flag corresponds to the `secret_names` parameter in the MCP `vault_run` tool.
# 4. If verification succeeds: revoke the old key in Stripe's dashboard# 5. If verification fails: re-run step 2 with the original key to roll backThe ordering matters: issue the new key first, update the vault, verify, then revoke the old key. This gives you a rollback path at every step. Because the credential is stored in one place, every subsequent vault_run automatically uses the updated value — no MCP config files to update, no .env files to edit, no teammates to notify.
For database connections and other long-lived sessions, connection pools may need draining before revoking old credentials — rotation is not always instantaneous.
Reference: full MCP config without credentials
This is the complete MCP configuration for a project using GitHub, Stripe, and a database — with no credentials in the config file:
{ "mcpServers": { "opaquevault": { "command": "ov", "args": ["mcp", "serve"] } }}That’s it. One MCP server. No credentials. All secrets live in the vault, get decrypted locally on demand, and are injected into subprocess environments without touching the AI’s context.
OpaqueVault is the MCP-native secret manager for AI coding agents. Get started free →
Related: MCP Secret Manager — How OpaqueVault Works · Zero-Knowledge Secret Manager
Corrections (April 2026):
- Connected CLI
--secretsflag to MCPvault_runtool’ssecret_namesparameter for reader clarity. - Added note on graceful rotation for long-lived connections (e.g., database connection pools).
- Softened “every official MCP quickstart guide” to acknowledge ecosystem maturation.
Updated October 23, 2026: aligned OpaqueVault’s daemon and MCP response descriptions with the current tool contract. Local commands and output sinks remain part of the trust boundary.