MCP in production
Auth and security for MCP servers
Securing MCP in production.
Video coming soon
Securing the wire
Local stdio servers running in your terminal are easy to secure: they have whatever permissions the user that spawned them has, and the trust model is "the user said run this." Production MCP, where servers are remote, vendor-hosted, or shared across teams, has a real auth surface to think about.
This lesson covers the auth patterns you'll see in practice and the security questions to ask before connecting an agent to a server.
What auth has to answer
Three questions:
Who is calling?
The server needs to know which user, agent, or organization is making a request. Without identity, you can't enforce per-caller policy or audit later.
Are they allowed to do this?
Identity isn't enough. A user might be allowed to read but not write. An agent's allow list (Track 2 Module 5) covers part of this; the server's own authorization layer covers the rest.
Did the message actually come from them?
Even if a request claims to be from user X, the server has to verify the claim. That's what auth tokens do: they're cryptographic evidence that the bearer was approved by some authority.
These three are the standard web-auth questions. MCP doesn't introduce new ones; it reuses HTTP's auth conventions.
Common auth patterns
Bearer tokens (most common)
The client sends an Authorization: Bearer <token> header on every request. The server validates the token (locally or against an auth service) before processing. Same pattern used by every modern HTTP API.
POST /mcp HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI...
Content-Type: application/json
{"jsonrpc": "2.0", "id": 1, "method": "tools/call", ...}For static tokens (a personal access token a user generated on a vendor's site), the client just stores the token and includes it on every call. For OAuth-style tokens, the client refreshes when expired.
OAuth 2.1 / dynamic client registration
For multi-user MCP servers (a vendor offering MCP to many customers), OAuth is the standard. The user authorizes the agent on the vendor's site; the vendor returns tokens; the agent uses those tokens for MCP calls.
MCP's auth specification standardizes the OAuth flow, including dynamic client registration so MCP clients can register themselves with the auth server without manual setup. This is what makes "I installed the GitHub MCP server in my agent and authorized it" feel as smooth as a normal OAuth login.
mTLS
Mutual TLS: both client and server present certificates. Useful inside corporate networks where you want strong identity for both sides without managing a separate token system. Heavy to set up; worth it for high-security environments.
Process-level (stdio only)
Stdio servers don't have a network auth surface. Trust comes from the OS: whoever spawned the process is who it serves. Auth tokens for the underlying capabilities (a GitHub PAT, a database password) come in through env vars or process arguments, set up by whatever spawned the server.
Where the secrets live
Bad: tokens in source code, in .mcp.json checked into git, in command-line arguments visible to other processes via ps.
OK: env vars set per-shell, ephemeral.
Better: a secret manager (cloud KMS, HashiCorp Vault, 1Password CLI). The host reads the secret from the manager at startup; the secret never lives on disk in plain text.
Best for vendor-hosted MCP: OAuth tokens scoped to a single user, with a short TTL and refresh tokens. No long-lived shared secret to leak.
Match the secret-handling discipline to the value of what the secret unlocks. A read-only token for a public dataset can be in env vars. A token that lets the agent deploy to production needs proper secret management.
Per-tool authorization on the server side
Auth gives you identity. The server still has to decide what each identity is allowed to do.
@mcp.tool()
def deploy(env: str, sha: str, *, _identity) -> dict:
if env == "production" and _identity.role != "ops":
return {"status": "denied", "reason": "production deploys require ops role"}
return _do_deploy(env, sha)Every tool that does anything risky should consult identity. The structure echoes Track 2 Module 5: allow lists, scopes, and gates apply at the server level too, not just at the agent level.
Auth at multiple layers
Production agent security uses defense in depth across layers:
| Layer | What it controls |
|---|---|
| Network (firewall, VPN, mTLS) | Who can reach the server at all |
| Transport auth (bearer, OAuth) | Which user/agent is making this request |
| Server-side authorization | Which tools and arguments this user can use |
| Agent-side allow list (Track 2 M5) | Which tools the agent can even attempt |
| Agent-side scopes (Track 2 M5) | Which arguments the agent can use |
A breach of one layer doesn't compromise the others. Even if a user steals an agent's bearer token, the server still applies its own authorization. Even if the server's authorization has a bug, the agent's allow list might prevent the call from being attempted.
OAuth scopes, intelligently
When using OAuth, you pick scopes during the authorization flow. The temptation is to ask for everything ("full access to your repo") so you don't have to come back later. Don't.
Scope to the minimum the agent actually needs:
- Read-only by default. Add write only when a write tool is requested.
- Per-resource if the provider supports it. "Read-only access to repos
foo/xandfoo/y" beats "read-only access to all your repos." - Time-bounded. Refresh tokens with short TTLs limit the window of damage if leaked.
Vendors that offer MCP often provide pre-defined scope bundles: "filesystem-read", "github-prs-write", etc. Pick the smallest bundle that does the job.
What an agent should NOT see
Some things the agent (and the host) really shouldn't have access to:
- The user's auth tokens. The host can hold tokens for transport, but they shouldn't show up in agent context, prompts, or logs.
- The user's password. Never; not for any reason.
- Long-lived secrets that should be hidden behind a service. If a tool needs a secret, the secret should live with the tool, not flow through the agent.
The pattern is: the agent reasons about intents ("post a message to #eng"); the server's identity-aware code does the actual privileged operation. Tokens stay in the auth layer, not in the agent.
A few things that go wrong
Hardcoded tokens that get rotated
A token is rotated; the agent stops working; nobody knows why. Fix by using a secret manager that surfaces fresh tokens on demand.
Token leakage via logs
A debug log dumps the request payload, including the auth header. Now the token is in your log aggregation system. Filter Authorization headers (and a list of other secret-like fields) out of logs at the source.
Refresh tokens that aren't refreshed
Long-running agents miss the refresh window because they cache the access token. Fix by checking expiry on every call (cheap) and refreshing proactively.
Servers that trust the client's identity claim
A server that takes the identity from a request header without verifying a signed token is open to spoofing. Always validate tokens, never trust unverified identity claims.
MCP doesn't sandbox tools for you
A common false assumption: "I'm running this in MCP, so it's somehow safer than a direct Python call." MCP standardizes the protocol, not the security model. A tool that does dangerous things does dangerous things whether it's exposed via MCP or a Python function. Treat every tool with the same care you'd use for any privileged code; MCP doesn't make it less risky, just more reusable.
Key takeaway
MCP auth follows web auth conventions: bearer tokens (or OAuth) for HTTP-based servers, process-level trust for stdio. Layer auth: network, transport, server-side authorization, agent-side allow lists, agent-side scopes. Use a secret manager. Scope OAuth tokens minimally. Never trust unverified identity. Tokens live in the auth layer, not in the agent. The next lesson covers orchestrating across multiple authenticated MCP servers in production.
Done with this lesson?