MCP Security: A Practitioner's Guide
Key takeaways
- MCP security means treating every MCP server as untrusted code, because tool descriptions, returned data and error messages all land in the model's context as instructions.
- Tool poisoning, return-content injection and planted handles attack the model through text, while command injection, path traversal and unauthenticated transports attack the server through ordinary bugs and defaults.
- Most real exposure is boring: servers bound to 0.0.0.0 with no authentication, floating versions, plaintext secrets and tool call logs nobody reads.
- A static scanner such as mcpscan is a pre-install gate that catches poisoned metadata, dangerous sinks and leaked secrets, but it cannot see runtime behavior or content that arrives next week.
- Least privilege, approval on write tools, egress allowlists and per-request authorization are what turn a missed finding from a breach into an annoyance.
MCP security is the practice of treating every Model Context Protocol server as untrusted code: checking it for tool poisoning, command injection, path traversal, planted prompts, missing authentication and supply-chain tampering before an AI agent is allowed to call it. This guide maps everything I have published on the topic. Each section gives a definition, the evidence, and a link to the article where I worked through it.
What is MCP, and where are its trust boundaries?
An MCP server is a program that reads instructions from outside your trust boundary and executes actions inside it, which is the textbook definition of an attack surface. Treating it like a REST API (check auth, validate inputs, rate limit) misses the core issue: an MCP server does not just process data, it instructs a model. Tool descriptions, field names and even error messages land in the model’s context, and the model treats them as guidance (What Is MCP Security?).
That gives you four boundaries worth naming:
- The text the model reads. Every piece of text an agent reads is a candidate instruction, and your system prompt has no special authority over any of it (No CVE needed).
- The code behind each tool. Arguments the model passes end up in shells, file paths and URLs, one layer below anything the model can see.
- The transport. A stdio server inherits your own user’s boundary. The moment a server listens on a socket it needs an identity story, and in 2026 several shipped without one by default (MCP Servers Had a Rough 48 Hours).
- The deployment. On a laptop there is one client and one trust boundary. Behind a gateway there are many clients, tenants and write tools that need approval (What Actually Changes When an MCP Server Leaves Your Laptop).
Underneath all four sits the agent, a confused deputy by design. You contain it with permissions, not a smarter prompt, which makes least privilege the actual security boundary (Your AI agent is the most over-privileged account you own).
MCP attack classes at a glance
“Where it’s caught” is the earliest control that realistically stops each class, based on what the linked article found.
| Attack class | What it looks like | Where it’s caught | Deep dive |
|---|---|---|---|
| Tool poisoning (description injection) | A description tells the model to read ~/.ssh/id_rsa, sometimes in invisible Unicode | Static metadata scan before install; diff descriptions on update | The MCP attack your code review cannot see |
| Command injection | A tool argument reaches grep -i "{query}" or shell=True unescaped | Argument-to-sink tracing in a static scan | What Is MCP Security? |
| Path traversal (read and write) | ../../../../etc/passwd through read_file, or an attacker-chosen write destination | Confinement to a declared root; scanner rules covering write sinks | My MCP Security Scanner Missed 2026’s Worst MCP RCE |
| Return-content injection (planted prompts) | A GitHub issue returned by a tool carries instructions | Rarely statically; approval on, read and write split | No CVE needed |
| Supply chain and fake servers | A trojanized server wrapped in farmed personas and forks | Vetting costly-to-fake signals; first run in a container with egress watch | A fake MCP server spent three months earning trust |
| Unauthenticated servers | SSE or HTTP on 0.0.0.0 exposing every tool with no auth | Bind check, unauthenticated curl probe, authenticating proxy | MCP Servers Had a Rough 48 Hours |
| Planted handles in the stateless spec | A handle in the context window gets replayed by injected text | Bind handles to a principal; validate per request | MCP 2026-07-28 Went Stateless |
| Context cost | Tool schemas fill most of the context window before any work | Deferred tool loading behind a one-line index | MCP Costs 72% of Your Context |
Tool poisoning: the payload lives in metadata
Tool poisoning is when a malicious instruction hides inside a tool’s own description and the model obeys it as part of the tool’s contract. The example in my primer is a description that says to read ~/.ssh/id_rsa and include it “for validation purposes”. The schema validates, and the model still exfiltrates a private key. When I scanned my own servers, I found one tool whose description told the model to always prefer it over competitors: not exfiltration, but silent hijacking no API scanner would flag (What Is MCP Security?).
The invisible variant is worse. Zero-width spaces, bidi overrides and Unicode tag characters can spell out an instruction inside a description that renders as nothing. The diff shows nothing. This is the attack that made me build mcpscan (The MCP attack your code review cannot see).
The NSA’s MCP guidance lands in the same place: treat descriptions as untrusted input, read them before install, and diff them on every update (The NSA published 17 pages on MCP security).
Command injection: the old bug, one layer down
Command injection in MCP is when a tool argument reaches a shell command without escaping, so the agent becomes the injection vector. In one server I tested, a search_notes tool ran grep -i "{query}" notes/ internally. I passed query="; cat /etc/passwd" and the model relayed it happily, because to the model it was a documented tool call. The injection happened in code the model cannot see (What Is MCP Security?).
One 60-day window in 2026 produced more than 30 CVEs against MCP servers, and roughly 43 percent of them were command injection (The MCP attack your code review cannot see). A related failure: IBM’s mcp-contextforge-gateway sandbox substring-scanned for dangerous dunder names, and the payload built those names at runtime. A list of names is not a security boundary (MCP Servers Had a Rough 48 Hours).
Path traversal: reads got the attention, writes got the CVEs
Path traversal is when a tool takes a file path from the caller and fails to normalize and confine it to an intended directory. I tested a document server by asking for ../../../../etc/passwd through a legitimate read_file call, and it returned the file. In my own setup this was the category where servers failed most consistently (What Is MCP Security?).
The write side is worse. When I checked mcpscan’s path traversal rule, MCP007, against the four real path traversal CVEs disclosed against MCP servers in 2026, it would have missed every one, because it was scoped to reads and every exploit was a write. The worst was CVE-2026-27825 in mcp-atlassian, a CVSS 9.1 unauthenticated RCE chained with an SSRF. The fix added write-sink patterns and rates those hits HIGH, because an attacker-controlled destination is a worse primitive than an attacker-controlled source (My MCP Security Scanner Missed 2026’s Worst MCP RCE).
Thirteen of the 29 mcp-atlassian CVE records published in September share one primitive: an upload tool that opens a caller-controlled file_path. One bypassed the project’s own March fix because a defaulted base directory was omitted at some call sites, which is why I treat path confinement as an interface property, not a call-site habit (I Traced 29 CVEs in One MCP Server to 4 Root Causes).
Return-content injection: when the data talks back
Return-content injection, which I called planted prompts in my primer, is when the server hides the payload in data a tool returns, such as a document, a database row or an error message. It is indirect prompt injection, and the hardest class to catch with static scanning because the payload can live anywhere in the server’s data (What Is MCP Security?).
The clearest public example needs no malicious server at all. Invariant Labs showed that a malicious issue in a public repo made an agent using the official GitHub MCP server leak private repository data into a public pull request. There was nothing to patch, because it is an architectural issue at the agent system level. What I changed in my own setup: approval stays on, one repo per session with scoped tokens, read and write split so a human approves writes, and a line in every prompt that touches external content telling the agent that tool results are data, not instructions (No CVE needed).
The reverse also happens. My own agent once reported a prompt injection that the logs did not support; it most likely misread its own corrupted context. Persist the raw context on anomaly and search everything before believing anything (My AI agent said we were under attack).
Supply chain: fake servers and backdoored dependencies
An MCP supply-chain attack is when the server or one of its dependencies is malicious before you ever run it, so the trust signals you check at install time may be manufactured. Straiker STAR Labs documented SmartLoader spending three months building five GitHub accounts with AI-generated personas and cross-forked repos around a trojanized Oura Ring MCP server, then submitting it to a legitimate MCP market registry. What stays expensive to fake is issue history with real back and forth, a company that exists outside GitHub, a boring release rhythm, and accounts that do not reappear across several “different” projects (A fake MCP server spent three months earning trust).
Dependencies fail the same way. Two backdoored litellm releases reached PyPI on March 24, 2026, which is why exact version pins plus hash pinning matter for any gateway that holds your provider keys (The LiteLLM Supply Chain Backdoor). Agent skills are going through the same era: Koi Security found 341 malicious skills out of roughly 2,632 on ClawHub (The fake browser extension playbook is back).
Unauthenticated servers and broken authorization
An unauthenticated MCP server is one whose network transport treats “can reach my port” as “may call my tools”. In about 35 hours, NVD published CVE records for four MCP servers with this exact failure: Bifrost, mysql-mcp-server, IBM’s mcp-contextforge-gateway and @zereight/mcp-gitlab. In the GitLab case one request could read /proc/self/environ and leak the access token. LiteLLM’s MCP authentication bypass, CVE-2026-59822, accepted a single-character bearer token and is on CISA’s KEV list (MCP Servers Had a Rough 48 Hours).
Authorization breaks in quieter ways too. In mcp-atlassian, the CVSS 10.0 record described an HTTP transport that fell back to the operator’s own credentials for unauthenticated callers, and another showed a tool allowlist applied at listing but not rechecked at dispatch, so a client that knew a hidden tool name could call it (I Traced 29 CVEs in One MCP Server to 4 Root Causes).
At scale, OX Security’s April disclosure put vulnerable MCP instances at roughly 200,000, mostly through human mistakes: services bound to every interface, no authentication, plaintext secrets, stale versions (200,000 exposed MCP servers later). The answer is per-request authentication at a gateway, tenant scoping at the edge, and expiring write approvals bound to one request and tool (What Actually Changes When an MCP Server Leaves Your Laptop). A happy-path demo proves none of this (A Happy-Path MCP Demo Proves Almost Nothing About Tenant Isolation).
The stateless spec: a planted prompt is a credential
The stateless MCP revision is the 2026-07-28 specification, which removes the initialize handshake and the Mcp-Session-Id header so any request can land on any instance. State now travels as a handle the model passes back as an argument, so a handle planted or read through prompt injection works as a valid credential. It can arrive through retrieved content, tool output, or MCP Apps HTML rendered next to the agent (MCP 2026-07-28 Went Stateless).
My position is that sessions were never authentication, but the new model moves enforcement to per-request validation, and nothing in the SDK does it for you. The checks I treat as mandatory: bind each handle to the authenticated principal at mint time, validate the presenter on every use, treat handles as untrusted input with short lifetimes, flag instruction-like content in tool outputs, and require human confirmation before the next write-capable call when a result looked like an instruction (stateless spec).
Context cost: the tax that shapes your design
Context cost is the share of the model’s context window consumed by tool schemas before the agent does any work. Perplexity’s CTO said three MCP servers consumed 143,000 of a 200,000-token window, a 72% tax. The reusable fix behind Pi’s Codemode is deferred loading: a one-line index per tool, with full schemas fetched on first use (MCP Costs 72% of Your Context).
Note that Pi’s QuickJS sandbox is a token-efficiency mechanism, not a security boundary, and it does not protect against prompt injection or malicious tool output (context cost).
The MCP security checklist
The merged checklist. Each line links to where I explain it.
Before you install a server
- Run a static scan of the repository and its
.claude/directory (tool poisoning) - Read every tool description like code, because it is (primer)
- Trace each argument to its execution sink and assume shell wrapping is injectable until proven otherwise (primer)
- Confirm path tools normalize and confine to a declared root, on writes as well as reads (write sinks)
- Verify who is behind it outside the repo, and compare permissions requested against purpose (fake server)
- Do the first run in a container with no credentials and an egress watch (fake server)
When you run it
- Inventory every MCP server you actually run, including what credentials it holds (NSA checklist)
- Pin exact versions and watch the release feed (boring checks)
- Check bind addresses and probe for unauthenticated access; put an authenticating reverse proxy in front of any SSE or HTTP server (unauthenticated CVEs)
- Enforce authorization at dispatch, not just at listing (29 CVEs)
- Run each server with the least filesystem and network access it can survive on (primer)
- Use an egress allowlist that denies by default and logs denials (least privilege)
When you host it for others
- Authenticate every request, enforce tenant scope at the gateway, and bind write approvals to a request (hosted MCP)
- Bind stateless handles to a principal and validate them on every use (stateless spec)
- Keep tool call approval on and split read from write (GitHub issue)
- Log tool calls with arguments, results and session identifiers somewhere a human looks (NSA checklist)
How to scan an MCP server with mcpscan
mcpscan is the static scanner I built for MCP servers and Claude Code projects: Python 3.9+, zero runtime dependencies, no network calls, no telemetry. It only reads files. The quickest way to see what it does is to scan its own deliberately malicious fixture (The MCP attack your code review cannot see):
git clone https://github.com/glatinone/mcpscan && cd mcpscan
python -m mcpscan tests/fixtures/vulnerable
Beyond tool poisoning, it flags command injection, .claude/ hooks that pipe curl into sh, wildcard grants like Bash(*), committed API keys, vulnerable SDK versions, path traversal, SSRF, insecure deserialization, disabled TLS and unauthenticated remote servers. Output is text, JSON or SARIF, so it can fail a CI build. --fix only patches unambiguous cases such as yaml.load to yaml.safe_load.
The scanning order I use is in the primer: tool descriptions, argument sinks, path boundaries, then return content. My first version only checked descriptions and missed the path traversal in my own document server.
I also test the scanner itself. A harness of synthetic attack fixtures fired 6 of 7 on the first pass; the miss, an API key in a JSON config, became a fix, and two obfuscation variants were documented as open rather than claimed as solved (My Scanner Passed Until I Built a Harness That Lied to It on Purpose). For my separate preflight tool, a benchmark of 5 synthetic manifests with 24 tools detected 14 of 14 planted findings with zero false positives, which measures the rule engine, not real-world detection quality (Benchmarking My MCP Security Scanner).
Know the limits. mcpscan cannot see runtime behavior, so it is a pre-install gate, not a red-team harness. A regex rule will not catch a path built three functions away from its sink, and no static scan sees a GitHub issue someone opens next Tuesday. Scanning raises the floor; it does not close the hole.
Read the deep dives
Start here
- What Is MCP Security? Common Attacks and How to Scan Your MCP Servers
- Your AI agent is the most over-privileged account you own
- The NSA published 17 pages on MCP security. Here it is as a checklist you can run today
Attacks through text the model reads
- The MCP attack your code review cannot see
- No CVE needed: how a GitHub issue hijacked an AI agent
- MCP 2026-07-28 Went Stateless: A Planted Prompt Is a Credential
- My AI agent said we were under attack. The logs said otherwise.
Real CVEs and exposure
- MCP Servers Had a Rough 48 Hours: 4 Unauthenticated CVEs
- I Traced 29 CVEs in One MCP Server to 4 Root Causes
- 200,000 exposed MCP servers later, the boring checks still win
Supply chain
- A fake MCP server spent three months earning trust. The tells were there
- The LiteLLM Supply Chain Backdoor: What 3 Hours Means for Your AI Agent Gateway
- The fake browser extension playbook is back. This time it ships as agent skills
Hosting MCP for a team
- What Actually Changes When an MCP Server Leaves Your Laptop
- A Happy-Path MCP Demo Proves Almost Nothing About Tenant Isolation
- MCP Costs 72% of Your Context: The Fix Pi Shipped
Building and testing the scanner
- My MCP Security Scanner Missed 2026’s Worst MCP RCE: Here Is the One-Rule Fix
- My Scanner Passed Until I Built a Harness That Lied to It on Purpose
- Benchmarking My MCP Security Scanner: 14 of 14 Findings, Zero False Positives
- I Wrote an MCP Server Auth Test Harness for the 2026-07-28 Spec. Here Is What Broke First
- I Pointed 3 Static Scanners at an MCP Server That Fails 8 Auth Tests. They Found 0, Including Mine
Browse everything on the topic at MCP Security.
Frequently asked questions
- What is MCP security?
- MCP security is the practice of treating every Model Context Protocol server as untrusted code and checking it for tool poisoning, command injection, path traversal, planted prompts, missing authentication and supply-chain tampering before an AI agent is allowed to call it.
- What are the most common MCP vulnerabilities?
- The classes that keep recurring are tool poisoning in tool descriptions, command injection through tool arguments, path traversal on both read and write sinks, prompt injection through returned content, fake or backdoored servers, and HTTP or SSE servers that expose every tool with no authentication.
- How do I scan an MCP server for security issues?
- Run a static scanner such as mcpscan against the repository before you install it, then check tool descriptions for instruction-like text, trace each argument to its execution sink, confirm path tools are confined to a declared root, and probe any network endpoint for unauthenticated access.
- Is a local stdio MCP server safer than a remote one?
- A stdio server inherits your own user's boundary, which is why the mysql-mcp-server advisory notes stdio deployments are unaffected. Once a server listens on a socket it needs its own authentication, tenant isolation and approval story.
- Can the model just refuse malicious instructions in tool descriptions?
- No. Tool descriptions and tool results are context, and models follow instructions in their context. Assume the model complies and contain the damage with permissions instead.
- What is the fastest MCP security win?
- Least privilege per server: a restricted user, no broad filesystem mounts, no unneeded secrets and no network exposure it does not need. That alone turns most findings from critical to low.
About the author
Security Engineer & Technical Writer
Security engineer and technical writer based in Batam, Indonesia. I review and harden MCP servers and AI agents, build security automation, and write up what holds and what fails. I also train on DNS security as a PANDI .id Academy trainer.
Related articles
-
7 min read
I Pointed 3 Static Scanners at an MCP Server That Fails 8 Auth Tests. They Found 0, Including Mine
mcpscan, Semgrep (200 rules) and Bandit gave a broken MCP server and its fixed twin the same report. Five custom Semgrep rules caught 3 of 8 auth bugs.
- MCP Security
- AI Security
-
7 min read
No CVE needed: how a GitHub issue hijacked an AI agent
In a demo, one GitHub issue hijacked an AI agent via the GitHub MCP server and leaked private repo data. Why it needs no CVE, and what I changed.
- AI Security
- MCP Security
-
3 min read
A fake MCP server spent three months earning trust. The tells were there
A fake MCP server from SmartLoader earned trust for three months. See which signals attackers can farm, which are costly to fake, and a pre-install checklist.
- MCP Security
- AI Security
All writing | RSS | Markdown