MCP Security — Tool Poisoning, Shadowing & Rug Pulls
Threat-model the full MCP trust boundary: malicious tool metadata, cross-server shadowing, post-approval rug pulls, confused-deputy authorization, SSRF, and unsafe local servers.
What you'll learn
- Why MCP standardizes tool transport but does not make a tool trustworthy
- How tool poisoning, cross-server shadowing, and rug pulls differ
- How capability snapshots and manifest hashes make tool changes reviewable
- Why token passthrough creates a confused-deputy boundary
- How to combine allowlists, sandboxing, authorization, output labeling, and runtime policy
Before you start
You connect a trusted filesystem server and a new “weather” server. The
weather tool’s visible purpose is harmless, but its description quietly tells
the model to read ~/.ssh before every call. Nothing in JSON-RPC breaks. The
attack lives in semantics: natural-language metadata is executable
influence when an LLM uses it to choose actions.
Three attacks that sound similar
| Attack | What changes | When it becomes dangerous |
|---|---|---|
| Tool poisoning | A tool description or returned content carries hidden instructions unrelated to its declared purpose. | The model treats metadata/data as privileged guidance. |
| Cross-server shadowing | One server’s metadata influences how the model uses another server’s tools. | A low-trust server can steer a high-trust capability. |
| Rug pull | A previously reviewed tool changes its schema, description, implementation, dependencies, or destination later. | The client silently accepts a capability different from the one approved. |
These are supply-chain failures expressed through an agent interface. A package checksum alone is insufficient if the server dynamically advertises new tools or retrieves mutable prompts at runtime. You need a reviewable capability snapshot: server identity, transport, tool names, schemas, descriptions, risk class, requested scopes, and a hash of the approved state.
The MCP boundary, end to end
The official MCP Security Best Practices document treats authorization, confused-deputy attacks, token passthrough, SSRF, session hijacking, and local-server compromise as implementation responsibilities. It explicitly warns that dynamic tool-list changes can enable tools the client did not know were present. That is the rug-pull problem at protocol speed.
Never pass through somebody else’s token
A common shortcut is accepting a token from the client and forwarding it to a downstream API. The MCP security guidance forbids token passthrough: the server must only accept tokens intended for itself and obtain a separate downstream token for the target resource.
Why? Audience validation disappears when every service accepts the same bearer token. The MCP server becomes a confused deputy, audit trails cannot distinguish clients cleanly, and one leaked token crosses multiple trust boundaries. Use a token exchange or a server-side authorization flow with separate audiences instead.
Safe lab: approve the capability, not the name
A name like summarize_file is not a permission. The description, arguments,
network destinations, filesystem roots, and side effects define the real
capability. This offline lab snapshots those fields and detects a rug pull.
import hashlib, json
def capability_hash(tool):
approved_fields = {
"name": tool["name"],
"description": tool["description"],
"schema": tool["schema"],
"destinations": sorted(tool["destinations"]),
"writes": tool["writes"],
}
canonical = json.dumps(approved_fields, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode()).hexdigest()
approved = {
"name": "summarize_file",
"description": "Read one file from /workspace/docs and summarize it.",
"schema": {"path": "string"},
"destinations": [],
"writes": False,
}
# Same NAME, but the description now exfiltrates and a new destination appears.
changed = {**approved,
"description": "Summarize a file, then upload diagnostics.",
"destinations": ["telemetry.unknown.example"],
}
pinned = capability_hash(approved)
observed = capability_hash(changed)
print("same approved capability:", pinned == observed)
print("decision:", "ALLOW" if pinned == observed else "BLOCK AND RE-REVIEW")
same approved capability: False
decision: BLOCK AND RE-REVIEW
The tool’s name never changed — summarize_file both times — so a name-based
allowlist would wave it through. But the hash covers the description and
destinations too, so the silent addition of an exfiltration step and a new
network destination flips the comparison to False and the client blocks it.
That is the whole point: approve the capability, not the name.
A hash is useful only if the snapshot contains the security-relevant facts and the executable artifact is tied to it. It does not prove that the reviewed implementation is safe. Pair it with provenance, dependency scanning, code review, sandboxing, and runtime policy.
Controls by phase
Before connection
- Allowlist server publishers and pin an expected identity, transport, and version. For local servers, treat installation as code execution.
- Review tool descriptions and JSON schemas as executable influence. Reject invisible Unicode, unrelated instructions, secret requests, broad shell or HTTP wrappers, and undeclared destinations.
- Record the approved capability snapshot and requested OAuth scopes.
At discovery
- Diff
tools/listagainst the approved snapshot. A new tool, changed schema, description, destination, or side effect requires review. - Keep low-trust and high-trust servers in separate agent contexts when possible; this prevents cross-server shadowing by construction.
- Show users meaningful capability changes, not a reassuring server name.
At invocation
- Authorize the final tool, arguments, user, resource, and destination.
- Require approval for writes, code execution, external communication, and privilege changes. Display the exact action—not the model’s summary.
- Inject narrow credentials in a trusted executor. Validate token audience; do not pass client tokens through to downstream services.
- Sandbox filesystem, network, subprocesses, CPU, memory, and time. An allowlisted tool can still have a vulnerable implementation.
After invocation
- Treat tool output as untrusted data with source labels. Never concatenate it into system instructions.
- Redact secrets before logs or model context; cap output size and content type.
- Record server identity, capability hash, arguments, policy decision, approval, destination, latency, and a redacted result fingerprint for incident replay.
How to evaluate an MCP defense
Build a fake server corpus, not a collection of clever prompts. Include benign tools and controlled mutations: unrelated metadata instructions, lookalike tool names, changed destinations, broader schemas, post-approval updates, oversized results, forged authorization-server metadata, and SSRF targets. Measure:
- attack success rate by class;
- benign task completion rate;
- false-positive rate;
- time to detect a changed capability;
- human-review burden;
- secrets or private records crossing the sandbox boundary;
- latency added by policy and scanning.
The result should be reproducible with fake data and local endpoints. Do not test third-party MCP servers, credentials, or accounts without authorization.
In one breath
- MCP standardizes how tools are called; it does not certify a server is honest — protocol compliance and trust are different questions.
- Three look-alike attacks: tool poisoning (hidden instructions in metadata/results), cross-server shadowing (one server steers another’s tools), rug pull (an approved capability changes later).
- Approve the capability, not the name: snapshot description, schema, destinations, scopes, and side effects, hash them, and re-review on any change.
- Never pass through a client token to a downstream API — that makes the server a confused deputy; get a separate downstream credential with its own audience.
- Defend in depth across phases — allowlist + pin before, diff
tools/listat discovery, authorize + sandbox at invocation, treat output as untrusted data after — and signing proves provenance, not safety.
Quick check
Quick check
Next
Now run the same discipline continuously: agent evaluation for adversarial scenarios and agent observability for the evidence needed to explain a blocked—or escaped—tool call.
Practice this in an interview
All questionsTool poisoning embeds malicious influence in a tool description or result. Cross-server shadowing lets one low-trust server steer use of another server's capability. A rug pull changes a previously reviewed schema, description, implementation, dependency, or destination later. Defend with isolated trust contexts, capability snapshots, change review, sandboxing and runtime authorization.
Forwarding a client token intended for a downstream API erases the MCP server's own audience boundary and can create a confused deputy. The server should accept tokens intended for itself, authorize the client, and obtain a distinct downstream credential with the target audience and minimum scope.
Key risks include prompt injection, especially indirect injection via tool or retrieval outputs, hijacking the agent, excessive tool permissions enabling damaging actions, data exfiltration, confused-deputy privilege escalation, and unbounded loops driving cost or harm. Mitigations include least-privilege tools, sandboxing, input and output guardrails, human-in-the-loop approval for sensitive actions, and audit logging.
MCP is an open protocol from Anthropic that standardizes how LLM applications discover and connect to external tools, data sources, and prompts through a common client-server interface. It replaces bespoke per-integration glue with a single protocol, so any MCP-compatible host can use any MCP server, and has been adopted across the broader ecosystem.