MCP Security in 2026: The Protocol Got Hardened. The Ecosystem Didn't.
I run a Firecracker microVM cloud that other people's AI agents execute code in. A growing share of that code arrives via the Model Context Protocol: an agent calls a tool, the tool calls my API, a sandbox boots. So I have a professional interest in a question that got asked a lot this year: is MCP safe to put in production?
The honest answer in September 2026 is: the protocol is in far better shape than it was eighteen months ago, and the thing that will hurt you is almost never the protocol. It is the server someone npx-installed from a README, the token it was handed, and the fact that nobody is reading what the tool descriptions say.
This post is my attempt to separate those layers. I'll go through what the specification actually changed, the incidents and CVEs that forced those changes, what the guidance bodies now say, and the controls I would refuse to ship without. Every claim is linked. Where I could not verify something against a primary source I say so rather than round up.
What the spec changed, and why each change exists
MCP shipped in November 2024 as a JSON-RPC protocol with two transports and essentially no security model. Each revision since has been a response to something that broke. Reading the changelogs back-to-back is like reading incident reports.
| Revision | Security-relevant change | The problem it answers |
|---|---|---|
| 2025-03-26 | OAuth 2.1 authorization; tool annotations (readOnlyHint, destructiveHint) |
No standard way to authenticate remote servers; clients had no signal about what a tool does |
| 2025-06-18 | Servers become OAuth Resource Servers; clients must send RFC 8707 resource indicators; new Security Best Practices page; structured tool output | Token passthrough and confused-deputy attacks where a malicious server obtains a token meant for someone else |
| 2025-11-25 | OIDC discovery; incremental scope consent; Client ID Metadata Documents replace dynamic client registration; 403 on bad Origin |
Over-broad scopes granted up front; DNS-rebinding against local servers; unmanageable client registration |
| 2026-07-28 | Stateless core, session IDs retired; mandatory Mcp-Method / Mcp-Name HTTP headers; RFC 9207 iss validation required; dynamic client registration formally deprecated; extensions framework |
Gateways and WAFs could not enforce policy without parsing JSON bodies; mix-up attacks across authorization servers; session hijacking |
The July 2026 revision is the big one. The maintainers call it the largest rewrite since launch, and the security motivation is explicit: putting the method and tool name in HTTP headers means a proxy can say "deny tools/call for delete_repository from this client" without touching the payload. That is the primitive every enterprise gateway wanted and none of them had.
Two smaller 2026 items matter as much in practice:
- In March the maintainers published a post on tool annotations that says, in as many words, that annotations are not guaranteed to describe tool behaviour and clients must treat them as untrusted unless the server is trusted. A
readOnlyHint: truefrom a server you don't control is a suggestion, not a fact. - In June the Enterprise-Managed Authorization extension went stable. It is built on the IETF identity-assertion grant draft and lets an identity provider issue tokens for a specific MCP server on behalf of an already-authenticated user. Okta is the first IdP, and Asana, Atlassian, Linear, Supabase and Figma are among the first servers. This is the part that makes "the agent has my Jira access" auditable instead of ambient.
Governance moved too. Anthropic donated MCP to the Linux Foundation's Agentic AI Foundation in December 2025 with AWS, Google, Microsoft, OpenAI, Cloudflare and Block as platinum members. The official registry launched in preview the previous September, and as of this month it still carries the preview label. It stores metadata, verifies namespaces, and explicitly delegates security scanning to package registries. It is not a vetted app store, and you should not treat it as one.
The incidents that forced the changes
I've plotted the CVEs with published CVSS scores in order of disclosure. The scale is worth staring at: eight of nine are High or Critical, and the two most recent are 9.8s.
A few of these deserve more than a bar.
Tool poisoning and rug pulls (April 2025). Invariant Labs showed that hidden instructions inside a tool's description could make Cursor read ~/.ssh/id_rsa and pass it as a tool argument. The model sees the description; the user sees a friendly one-line summary. The "rug pull" variant is worse: the server presents a benign description at approval time and swaps it later. There is no CVE because it is not a bug. It is the protocol working as designed against a client that trusts descriptions.
The GitHub MCP exploit (May 2025). Same team, same shape at a different layer. A public issue in a repository contained instructions; an agent with access to a private repository read the issue, followed the instructions, and leaked private data into a public pull request. No vulnerability in the server. The vulnerability is giving one agent read access to untrusted input and write access to something valuable in the same session.
Supabase and the lethal trifecta (July 2025). Simon Willison's write-up is the clearest statement of the pattern: private data, untrusted content, and an exfiltration channel. A support ticket's text drove a Cursor session holding a service_role key to dump an integration-tokens table. Supabase responded with read-only and project-scoped modes two months later, which is the right response and also an admission that the default was wrong.
mcp-remote, CVE-2025-6514 (July 2025). JFrog found that the most popular bridge between stdio clients and remote servers passed the authorization_endpoint value from a server's metadata into a shell. Any remote MCP server you connected to could execute commands on your machine. Over 437,000 downloads were affected. This is the one that should have ended the practice of npx some-package as an MCP installation method. It did not.
The "Mother of All AI Supply Chains" (April 2026). OX Security reported that the stdio transport in the Python, TypeScript, Java and Rust SDKs passes the configured command and args to the shell without sanitisation. Anyone who can influence the MCP config of an application can execute commands. That produced a cluster of CVEs across LiteLLM (fixed in 1.83.7), Windsurf, Agent Zero, DocsGPT and others. OX reported that Anthropic considered the SDK behaviour by design, and I think that is defensible: the config is a command to run. The lesson is that a config file that launches processes is a credential, and should be protected like one.
Cursor DuneSlide (July 2026). Cato Networks disclosed two 9.8s: prompt injection, including via MCP tool responses, could escape Cursor's sandbox through path traversal and symlink tricks. Fixed in Cursor 3.0. This is the third Cursor MCP-adjacent critical in twelve months, which tells you something about how hard it is to bolt a sandbox onto an editor after the fact.
There is also the exposure problem that has no CVE at all. Trend Micro found 492 unauthenticated MCP servers on the public internet in mid-2025 and reported the number had nearly tripled to 1,467 by their late-2025 update. Most were on major cloud providers. These are not exploits. They are people who ran a server on 0.0.0.0 and walked away.
What the guidance bodies now say
Three documents landed in the past nine months that I now hand to anyone asking me to review an MCP deployment.
The OWASP Top 10 for Agentic Applications 2026 (December 2025) covers agents generally. The OWASP MCP Top 10 is still an incubator project at v0.1 beta, with a full release planned for next month, but the categories are already useful: token mismanagement, scope creep, tool poisoning, supply chain, command injection, intent-flow subversion, weak authentication, missing audit telemetry, shadow servers, and context over-sharing. Every incident above maps to at least one.
The NSA's Artificial Intelligence Security Center published a Cybersecurity Information Sheet on MCP in May 2026. I could not retrieve the PDF directly, so I'm working from secondary summaries, but the recommendations are consistent across them: OS-level sandboxing of servers with seccomp, AppArmor or SELinux; signed JSON-RPC payloads with replay protection; a server inventory; audit logging; and approval workflows for sensitive tools. It also states plainly that the protocol has no native role-based access control or token lifecycle semantics. Those live in your gateway or nowhere.
And the spec's own Security Best Practices page is normative and short. Servers must not accept tokens that were not issued to them. Sessions must not be used for authentication. Clients should block requests to private and link-local ranges. Clients must show the full command before launching a local server and should sandbox it. If your client does not do these things, it is not compliant, regardless of what its marketing says.
The six controls I would not ship without
This is the list I apply to my own platform and to consulting clients. It is deliberately boring. Everything exciting about MCP security is in the incidents; everything effective is here.
1. Pin tool descriptions and diff them on every tools/list
The rug pull works because clients re-fetch descriptions and never compare. Fix that at the client, or with a wrapper. The script below uses the official Python SDK to snapshot every tool's name, description and input schema, hash them, and fail loudly when anything changes. It runs in a few hundred milliseconds against a stdio server and I run it in CI against every server we depend on.
#!/usr/bin/env python3
"""Snapshot MCP tool definitions and fail if any have changed since the last run."""
import asyncio, hashlib, json, sys
from pathlib import Path
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
LOCK = Path("mcp-tools.lock.json")
async def snapshot(cmd: str, args: list[str]) -> dict[str, str]:
params = StdioServerParameters(command=cmd, args=args)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = (await session.list_tools()).tools
out = {}
for t in tools:
canon = json.dumps(
{"name": t.name, "description": t.description, "schema": t.inputSchema},
sort_keys=True, separators=(",", ":"),
)
out[t.name] = hashlib.sha256(canon.encode()).hexdigest()
return out
async def main(cmd: str, *args: str) -> int:
current = await snapshot(cmd, list(args))
if not LOCK.exists():
LOCK.write_text(json.dumps(current, indent=2, sort_keys=True))
print(f"wrote {LOCK} with {len(current)} tools")
return 0
pinned = json.loads(LOCK.read_text())
changed = {n for n in current if pinned.get(n) != current[n]}
removed = set(pinned) - set(current)
if changed or removed:
print("TOOL DEFINITIONS CHANGED — refusing to proceed")
for n in sorted(changed): print(f" changed: {n}")
for n in sorted(removed): print(f" removed: {n}")
return 1
print("tool definitions match lockfile")
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main(*sys.argv[1:])))
Usage: python pin_tools.py npx -y @modelcontextprotocol/server-filesystem /srv/data. Commit the lockfile. A description change now needs a human to approve it, which is the whole point. Snyk's Agent Scan, the successor to Invariant's mcp-scan, does the same thing with a nicer interface plus poisoning heuristics, and is worth running as a second opinion.
2. Run every server in its own sandbox, with an egress allowlist
A stdio MCP server is an arbitrary process with your user's permissions. Treat it like one. The minimum on Linux is a separate user, a read-only filesystem except for a declared workspace, and a network namespace whose only route is to the hosts the server needs. Claude Code's sandbox mode does this with bubblewrap on Linux and seatbelt on macOS and includes domain allowlists; it is the right default and most other clients still lack it.
On my platform the answer is a microVM per server with a default-deny egress policy, because a kernel boundary is the only isolation I trust for code I did not write. I've written before about why isolation stops a compromised process from reaching other tenants but does nothing about it draining your API quota. Both controls are necessary. Neither is sufficient.
3. Scope the token to the task, not the user
The Supabase incident happened because the agent held a key that could do everything. Every incident in the "lethal trifecta" family has this property. Since June, the enterprise-managed authorization extension gives you a standard way to mint server-specific, user-bound tokens from your IdP. Before that, and for servers that don't support it, use the narrowest credential the vendor offers: GitHub fine-grained tokens scoped to one repository, Supabase project-scoped read-only mode, database roles with SELECT only.
The rule I use: if a tool has destructiveHint: true, the credential behind it should be different from the one behind the read-only tools, and the client should require a human click before calling it.
4. Put a gateway in front of remote servers, and use the new headers
The July spec's Mcp-Method and Mcp-Name headers exist so that policy can be enforced without a JSON parser in the hot path. Cloudflare's MCP Server Portals (April 2026), Kong's MCP gateway (October 2025) and AWS Bedrock AgentCore Gateway all do a version of: authenticate the client, expose a curated subset of tools, default-deny writes, and log every tools/call with the arguments.
That last one is the one you actually want at 2 AM. When something goes wrong with an agent, the question is "what did it call, with what arguments, and who was it acting as". If your logs cannot answer that, you do not have an audit trail, you have a vibe.
5. Never let the agent read untrusted content and write to something valuable in the same session
This is the architectural fix for prompt injection, and it is the only one that works reliably today. Google DeepMind's CaMeL formalised it: a privileged planner that never sees untrusted data, and a quarantined model that reads untrusted data but cannot call tools. Meta's "Rule of Two" says the same thing more bluntly: an agent gets at most two of untrusted input, private data access, and the ability to change state or communicate externally.
Concretely for MCP: the session that reads GitHub issues should not be the session that has write access to the repository. If your product requires both, put a human approval between them, and make the approval show the actual tool arguments, not a summary.
6. Inventory the servers, including the ones you didn't install
The OWASP category "shadow MCP servers" is real. Developers add servers to .cursor/mcp.json and claude_desktop_config.json and forget them. GitGuardian's 2026 scan reportedly found tens of thousands of secrets in MCP config files committed to public GitHub; I could not verify their exact figures against the primary report, so take the magnitude and not the number. What I can say from my own fleet is that config files that launch processes get committed to repositories at roughly the rate .env files do, and the consequence is worse, because a .env file leaks a secret and an MCP config file leaks a secret and a command.
Scan for these files in CI. Treat any command field as a deploy-time change requiring review. Rotate anything that ever appeared in one.
What I'd tell someone deciding today
MCP in September 2026 has an authorization model that is genuinely good, a wire format that gateways can police, a foundation behind it, and a maintainer team that has responded to every class of incident with a spec change. The TypeScript and Python SDKs have each passed a billion downloads. It is not going away and it is not a toy.
It also has an ecosystem of tens of thousands of servers, most of them unaudited, a registry that explicitly does not vet them, clients that are still shipping 9.8s, and an installation culture built on npx -y. The spec cannot fix that. Your deployment discipline can.
The way I think about it: MCP moved the problem from "can this server authenticate" to "should this server be trusted with what it has been given". That is progress. It is also exactly the problem we have had with every plugin system since browser extensions, and the answers are the same. Least privilege. Isolation. An audit log you can actually query. A human between untrusted input and irreversible action.
None of that is new. It is just, once again, the whole job.
Related: Isolation Is Not an Abuse Control, and Container vs microVM vs gVisor for agent workloads.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents. Everything above comes from running it in production.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
The Worms Learned to Use Your AI Agent: A Year of npm Supply-Chain Attacks
From s1ngularity invoking Claude Code to hunt for wallets, to Shai-Hulud self-publishing across 500 packages, to IronWorm stealing AI API keys — the twelve months that changed how I install software.
13 minSep 3, 2026AI Agent Cloud: What the Term Means and Who Actually Has One
An AI agent cloud needs more than a sandbox: state, hosting, triggers, audit. I map 15 platforms against that stack and show where each one stops short.
10 minSep 3, 2026AI Agent Sandboxes: What They Are and How to Choose One
What an AI agent sandbox is at the systems level, the four properties that decide one, and how 15 platforms compare — from an engineer who built one of them.
10 min