{"slug": "keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers", "title": "Keep your API keys out of your AI agent: a credential pattern for MCP servers", "summary": "A developer has published a credential-isolation pattern for MCP servers that places a trusted gateway between an AI agent and the MCP servers it calls, so the agent process never holds the API keys or OAuth client secrets that authorize its tool calls. The pattern, implemented with the open-source Apache-2.0 Go gateway Agenthof, registers each MCP server with the gateway, applies default-deny per-agent tool grants (including read-only modes), and brokers OAuth client-credentials tokens per call from the gateway's environment. The stated goal is to shrink the blast radius of a compromised agent from stolen live credentials to a revocable, per-run token.", "body_md": "If you've wired an MCP server into an agent, you've probably done something like this:\n\n```\n{\n  \"mcpServers\": {\n    \"billing\": {\n      \"command\": \"npx\",\n      \"args\": [\"billing-mcp\"],\n      \"env\": { \"BILLING_API_KEY\": \"sk-live-...\" }\n    }\n  }\n}\n```\n\nIt works. It's also handing your live billing key to the least trustworthy process in the system.\n\nAn agent is a program that decides what to do at runtime based on text it was given — some of which comes from the outside world (a webpage it read, a document it summarized, a tool result). That's the whole point, and it's also why the agent process is the wrong place to keep a secret.\n\nTwo things go wrong with the config above:\n\n**The credential lives in the agent's environment.** If the agent is compromised — prompt injection, a poisoned dependency, a tool that returns a malicious payload — the attacker is now one `os.environ` read away from your billing key. The blast radius of \"the agent did something dumb\" includes \"the agent's keys are gone.\"\n\n**The agent can call everything, and you can't prove what it did.** The MCP server exposes a set of tools; the agent can call any of them. When something goes wrong, your evidence is scattered across logs that the agent itself could have influenced.\n\nYou can't fix this by making the agent more careful. The agent is the untrusted part. You fix it by moving the trust boundary.\n\nInstead of letting the agent talk to the MCP server directly, put a small trusted process — a gateway — in the middle:\n\n```\nagent  ──►  gateway  ──►  MCP server\n           (holds the       (needs the\n            credential)      credential)\n```\n\nThe agent is *starved* of credentials. It can make tool calls, but it never possesses the secret that authorizes them. Compromise the agent and you get a revocable, per-run token — not the billing key.\n\nThis is more than a reverse proxy, because the gateway is a policy decision point. Since every call goes through it, it can also:\n\nHere's the pattern implemented with [Agenthof](https://github.com/agenthof/agenthof), an open-source (Apache-2.0, Go) governance gateway. The config is the useful part; the tool is just one way to run it.\n\n**1. Register the MCP server with the gateway — this is where the secret lives:**\n\n```\ntools:\n  ticket-search:\n    kind: mcp\n    url: https://tickets.internal/mcp\n    credential_source: static_env\n    token_env: TICKETS_MCP_TOKEN\n```\n\n`TICKETS_MCP_TOKEN` is read from the *gateway's* environment. The agent process never sees it.\n\n**2. Grant each agent only the tools it needs — default-deny:**\n\n```\nname: legacy-triage\nexecution: fronted\nendpoint: https://legacy.internal/agents/triage\ntools:\n  - resource: ticket-search\n    mode: all                            # every tool ticket-search exposes\n  - resource: billing-mcp\n    tools: [get_invoice, list_invoices]  # only these two of billing-mcp\n```\n\nAn agent gets access to a resource only if it's named here, and to the *tools* it names (or an explicit `mode: all`). Leaving the list off isn't \"allow everything\" — it's rejected at config load. Widest access is always something you typed on purpose, never something you got by omission.\n\nWant read-only? Say so, and the operator's classification decides what counts:\n\n```\ntools:\n  - resource: billing-mcp\n    mode: read-only        # only the tools billing-mcp is declared to expose read-only\n```\n\n**3. For OAuth-protected servers, even the client secret stays out of the agent.** The gateway's broker mints a token from your identity provider per call:\n\n```\ntools:\n  billing-mcp:\n    kind: mcp\n    url: https://billing.internal/mcp\n    credential_source: static_env\n    grant_type: client_credentials\n    issuer: https://idp.example.com/\n    token_endpoint: https://idp.example.com/oauth2/token\n    client_id_env: BILLING_MCP_CLIENT_ID\n    client_secret_env: BILLING_MCP_CLIENT_SECRET\n    scope: billing.read\n    read_only_tools: [get_invoice, list_invoices]\n```\n\nThe client id and secret are read from the gateway's environment; the agent gets the minted access token's *effects*, never the token, and never the secret that mints it.\n\n**4. Because everything flows through one point, every tool call and every refusal lands in a hash-chained, tamper-evident audit ledger** — so \"what did this agent actually do?\" has a single, ordered answer.\n\nBeing honest about the boundary is the whole point, so:\n\nThose aren't weaknesses to paper over; they're where the boundary actually sits, and knowing that is how you deploy the pattern correctly (gateway on a trusted host, agent in a sandbox, ledger shipped somewhere append-only).\n\nRegardless of which tool you use, the pattern is portable:\n\nThe agent stays the flexible, fallible thing it's supposed to be — and it stops being the thing that holds your keys.\n\nA working implementation of all of the above is at [github.com/agenthof/agenthof](https://github.com/agenthof/agenthof). If you're doing this differently — a sidecar, a service mesh, provider-side scoping — I'd genuinely like to hear how it's holding up.", "url": "https://wpnews.pro/news/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers", "canonical_source": "https://dev.to/rojaneerdev/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers-4cmo", "published_at": "2026-09-27 11:08:31+00:00", "updated_at": "2026-09-27 11:30:52.932482+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-safety", "developer-tools", "ai-tools"], "entities": ["Agenthof", "MCP", "OAuth"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers", "markdown": "https://wpnews.pro/news/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers.md", "text": "https://wpnews.pro/news/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers.txt", "jsonld": "https://wpnews.pro/news/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers.jsonld"}}