By now you've built MCP servers and connected agents to real tools. This week we confront the uncomfortable truth the earlier weeks glossed over: MCP, as a protocol, does not make any of that safe by default. Authentication, authorization, and trust in tool metadata are things you have to design in. Let's walk through what "secure" actually requires, using the same timeline the spec authors used to get there.
MCP authorization has gone through three distinct hardening stages in less than a year, and each one was a direct response to a real-world failure mode:
| Stage | Date | What changed | Why |
|---|---|---|---|
| 1 | March 2025 | OAuth 2.1 adopted as the standard authorization framework for MCP | Early MCP deployments had no consistent auth story; OAuth 2.1 (which drops legacy insecure flows like implicit grant) gave the ecosystem one baseline |
| 2 | June 2025 | MCP servers reclassified as OAuth 2.1 resource servers | Closed the "confused deputy" hole: a server that blindly forwards the client's access token to an upstream API can be tricked into acting with more authority than intended. Servers must now validate tokens themselves and must never pass client tokens through to other services |
| 3 | November 2025 | PKCE (Proof Key for Code Exchange) made mandatory for all client-side applications | Public clients (CLI tools, desktop agents, browser extensions) can't safely hold a client secret; PKCE binds the authorization code to the specific client that requested it, closing an interception window that plain OAuth 2.1 still left open |
The throughline across all three stages is the same: MCP servers are not just "APIs with extra steps" — they sit between an LLM (which can be socially engineered by content it reads) and real systems (which have real permissions). Every hardening step exists to keep a compromised or confused link in that chain from acting with someone else's authority.
Two supporting mechanisms make OAuth 2.1 workable in an ecosystem where any client might talk to any server:
resource parameter in both the authorization request and the token request, naming the specific MCP server the token is meant for. The authorization server then issues a token bound to that audience.Together, these two mechanisms prevent a token minted for Server A from being replayed against Server B — a critical property once you have many small MCP servers instead of one big API. A server that fails to check the audience claim on incoming tokens is, in effect, accepting anyone's key to any door.
Everything above assumes the attacker is trying to get past the front door. Tool poisoning walks in through a window you probably didn't know was unlocked: the tools/list response itself. When an MCP client connects to a server, it calls tools/list and gets back tool names, descriptions, and JSON schemas. Most client implementations feed that metadata directly into the LLM's context so the model knows what tools exist and how to call them. An attacker who controls (or has compromised) a server can embed hidden instructions inside a tool's description — invisible to the human user, but read and obeyed by the model. The model isn't calling the tool maliciously; it's being told by the tool's own metadata to do something the user never asked for. Real incidents are not hypothetical: a November 2025 WhatsApp MCP integration leaked message data this way, and CVE-2025-6514 documented a remote-code-execution flaw in mcp-remote rooted in the same class of trust failure. An empirical scan of 1,899 open-source MCP servers found 5.5% already exhibited tool-poisoning vulnerabilities in the wild — this isn't a theoretical corner case, it's a measurable, present risk in the packages you might install today.
Security researchers have been scanning public MCP servers, and the numbers should change how you think about "just spinning one up":
| Finding | Rate |
|---|---|
| Command-injection flaws | \~43% of popular servers |
| Path traversal allowed | \~22% |
| SSRF-exploitable, no default auth | \~30% |
| No authentication required at all (follow-up audit) | \~40% |
| Credentials handled in plaintext | \~79% |
| Publicly accessible, unauthenticated instances (July 2025 scan) | 1,862 servers |
Read that table again: two out of five audited servers require no authentication, and roughly four out of five mishandle credentials. This is the ecosystem you are deploying into, not a worst-case scenario.
"MCP is secure by default once it authenticates." False, and dangerously so. Authentication only confirms who is asking — it does nothing about what they're allowed to do, whether the token was meant for this server, or whether the tool itself is trustworthy. You must add OAuth 2.1 with mandatory PKCE, audience validation against RFC 8707's resource indicators, a strict no-token-passthrough rule, input allow-listing, and human confirmation gates for irreversible actions. None of that is automatic. "Tool inputs are the only untrusted data." Also false. As the tool-poisoning discussion above shows, tool descriptions, parameter schemas, and return values are all attacker-controlled surface once a server is compromised or malicious. Treat everything that flows from a server into the model's context as untrusted — not just the arguments a user types. "Sandboxing or containerizing the server makes it safe." Containerizing an MCP server limits the blast radius if it's exploited (it can't easily reach your host filesystem or other containers), but it does nothing to stop prompt injection, confused-deputy token forwarding, or a tool that simply has more permissions than it needs. Sandboxing is a layer, not a substitute for least-privilege scoping and per-user authorization.
Treat every MCP server — including ones you write yourself — as a resource server that must independently authenticate every request, validate that tokens were issued for it specifically, refuse to forward client tokens upstream, and treat its own tool metadata as something a human should be able to audit. Security here is a stack of habits, not a checkbox.