Blog/Security & compliance
MCP security checklist: tool poisoning, rug pulls and OAuth
MCP security checklist: the threat model, tool poisoning, rug pulls, RFC 9207 issuer checks, per-issuer credentials, scoped tokens and audit logs.
Balázs Csorba··10 min read
- MCP security
- Tool poisoning
- MCP OAuth
- Supply chain
- Audit logs

Key takeaways
- MCP trust ends in two places: where server-controlled text (tool descriptions, tool results) becomes model input, and where a token you hold is spent by a system the model can influence.
- Invariant Labs coined tool poisoning on 1 April 2025: a tool description whose hidden instructions make the model read SSH keys, plus shadowing, where one server rewrites another's tools.
- A rug pull is a description change after approval. Pin the server version and a hash of the canonical tool list, verify before the first tool call, and re-approve with a diff on any mismatch.
- The 2026-07-28 revision requires RFC 9207 iss validation (SEP-2468), keys credentials by issuer (SEP-2352), prefers Client ID Metadata Documents, and adds OTel trace context in _meta (SEP-414).
- Give each tool family its own least-privilege upstream token, never forward an inbound token, and log every tool call with a trace id: a poisoned result leaves only a normal-looking API call.
MCP security is the set of decisions that decides what a Model Context Protocol server may read, change and spend on behalf of a person or an agent, plus the checks that enforce those decisions. It is not a transport problem. JSON-RPC does not decide who is trusted, no schema validator will, and a correct response proves nothing. Every MCP server is a program you approved once, sitting inside a loop where the caller is a model and the arguments are model-generated.
This checklist starts with the threat model: servers, clients, hosts, and the three places where trust actually ends. Then the two attacks this protocol made cheap, tool poisoning and rug pulls, the authentication changes in the 2026-07-28 revision (RFC 9207 iss validation, Client ID Metadata Documents, per-issuer credentials), least-privilege upstream tokens, and the audit trail with the OpenTelemetry trace context the revision added. It ends with a checklist, a table mapping each risk to its control and the requirement behind it, and what the NSA's 2026 guidance adds.
What is the MCP threat model?
Name four parties before you write a single check. The server runs code you did not write, receives model-generated arguments and returns text that goes straight back into the model's context. The client is the host application that holds the model, the tool list and the user's credentials. The user approves servers and owns the consequences. The upstream is the API a server calls with a token you gave it. A server is therefore two things at once: a code-execution dependency in the ordinary software supply chain, and a channel that feeds text the model will follow.
Trust does not end at your process or network boundary. It ends in two places: at the moment server-controlled text becomes model input, a tool description at discovery time or a tool result at call time; and at the moment a token you hold is spent by a system a model can influence. Everything else here is a control on one of those two crossings.
The closest published taxonomy is the OWASP Top 10 for Agentic Applications (9 December 2025, for 2026). Two of its entries cover everything in this article: ASI02 Tool Misuse and ASI04 Agentic Supply Chain, the latter covering the server you install rather than the one you wrote. Use the taxonomy to name the risk, because "the agent got confused" is not a finding anyone can action.
How does tool poisoning work?
Tool poisoning is an attack on the description, not on the code. Invariant Labs published the write-up that named the class on 1 April 2025 (Luca Beurer-Kellner and Marc Fischer): malicious instructions embedded in a tool description, which they call a form of indirect prompt injection. Their example is an add tool whose description also says to read ~/.cursor/mcp.json and ~/.ssh/id_rsa and pass their contents as an argument. The user sees a tool that adds two numbers. The model sees the file paths.
The asymmetry is the mechanism. In their experiments against Cursor the confirmation dialog showed a tool name and a summary while the arguments, including the SSH key, were hidden behind a simplified UI. Invariant Labs' conclusion is blunt: MCP's security model assumes tool descriptions are trustworthy and benign, and it does not check. Their second finding, shadowing, needs no call to the attacker's own tool. A second server's description states extra behavior for a trusted tool, in their case that a send_email tool must redirect all mail to an attacker's address. The agent then sends mail to the attacker while the user asked for a different recipient, and nothing in the interaction log names the malicious server.
Their mitigations are three: make user-visible and model-visible instructions visibly different, pin the server and its tool definitions by hash, and enforce dataflow boundaries between servers. The first is a UI change, the second is the next section, the third is architectural. That a description is an injection channel is the subject of prompt injection as an architecture problem.
What is a rug pull, and how do you stop it?
A rug pull is the same attack with better timing: the server is legitimate at install time and changes the description afterwards. Invariant Labs compare it to replacing a package on PyPI after it has been approved. Pinning has two halves. Pin the version, which stops new code arriving, and pin the hash of the canonical tool list, which stops the text the model reads from changing. A version pin alone does not catch a description changed in a patch release.
# Pseudo-code: an approve-on-change gate in the client
approved = load_approved_hashes() # server id -> sha256 of the canonical tool list
def open_session(server):
tools = server.tools_list() # names, descriptions, JSON schemas
digest = sha256(canonical_json(tools)) # sort keys, sort tools, normalize whitespace
if approved.get(server.id) != digest:
show_diff(approved_tools.get(server.id), tools) # a human reads it
approved[server.id] = digest # re-approve, then re-pin
return Session(server, tools) Two details decide whether this works. Canonicalise: hashing a raw response means a reordered list reads as a change, and users learn to click through the prompt. Fail closed: if the list cannot be fetched or hashed, the session does not start. The revision's ttlMs and cacheScope on list results are a cost feature, not a control: a tool list verified an hour ago is not verified now.
Anthropic makes the local-versus-remote point in "How we contain Claude" (25 May 2026): "A locally installed tool is auditable. You can read the code, pin the version, and know it won't change under you. A remote tool, a hosted MCP server, a cloud connector, can change behavior at any point after you've approved it." Their advice for anything outside a reviewed directory: run it against fake data first, where a malicious tool's blast radius is contained.
How should you do MCP OAuth and API tokens?
Three changes in the 2026-07-28 revision matter, and all three are about who issued a token. First, RFC 9207 iss validation: authorization servers state their issuer in the authorization response, and clients must check a present iss against the recorded issuer before redeeming the code. That closes the mix-up class, where an attacker points a client at their own authorization server to redeem a code meant for yours. The revision makes it required under SEP-2468. Second, SEP-2352: persisted credentials are keyed by issuer identifier, are not reused with a different authorization server, and are re-registered when that server changes. Third, Client ID Metadata Documents are preferred over Dynamic Client Registration: the client id is an HTTPS URL pointing at a JSON document with at least client_id, client_name and redirect_uris, so the client is auditable by reading a document. DCR is deprecated, with removal no earlier than the first revision released on or after 28 July 2027.
# Pseudo-code: the two client-side checks the revision requires
if "iss" in authorization_response and authorization_response["iss"] != recorded_issuer:
raise AuthorizationError("issuer mismatch") # RFC 9207, before the code is redeemed
creds = key_store.get(authorization_response["iss"]) # SEP-2352: keyed by issuer
if creds is None or creds.issuer != authorization_response["iss"]:
register_again() # never reuse across serversOn the server side the job is narrower: accept only tokens minted for you, check the audience, and never forward the inbound token upstream. The best token hygiene is upstream, not inbound. Give each tool family its own credential with the smallest scope that makes it work, and keep read tools on read tokens, so a poisoned description in a Jira server can read issues but cannot transition them. The test: write the sentence "the worst thing this token can do" for every credential your server holds, and split any whose answer is broader than the tool's purpose.
What do you log, and what does a trace buy you?
The protocol's logging capability is deprecated in the 2026-07-28 revision, with OpenTelemetry and stderr as the replacements, and the log level now travels per request as io.modelcontextprotocol/logLevel. Server-side logs are your audit trail by default, not a chat channel.
Log per tool call, not per request: trace or session id, the subject the token represents, the server, the tool, a hash and size of the arguments and the result, the upstream URL, the decision, the duration. Never log tokens. SEP-414 adds documented _meta keys for OpenTelemetry trace context, so one traceparent joins the model call, the client, the gateway and your server. Without it you correlate on timestamps and hope.
The hard limit is worth stating plainly. Anthropic's point about their own connector is that a poisoned tool result can steer the agent into a call that looks, in the log, like a successful authorized API request: "once a poisoned tool return has steered the agent into exfiltrating data, the log just shows a successful, authorized API call. There's no after-the-fact signal to find." So log semantics, not just transport: which tool touched which resource, how often, at what rate, against a baseline. Their mitigation is a proxy in front of network-enabled tools that inspects return values before they enter the context.
What does the NSA guidance add?
In May 2026 the NSA published a Computer Security Information bulletin titled "Model Context Protocol (MCP): Security Design Considerations for AI-Driven Automation", summarized by Reed Smith on 4 June 2026. Its premise: adoption has outrun the safeguards, leaving organizations exposed to risks the protocol's designers did not anticipate. The risk list there covers uncontrolled automated actions, missing input screening, context poisoning, weak identity and access control, data leakage, missing human approval, credentials without expiry or revocation, and susceptibility to overload.
Read as a list, most of it is what this article already prescribes: treat every automated action as high-risk and keep it inside strict permission boundaries, separate systems and data by trust level, grant only the minimum access, screen inputs, run data processing locally where you can, and keep comprehensive activity logs integrated with your existing monitoring. Two recommendations are less common in MCP writing: use reliable, actively maintained tools from trusted providers, and subject them to your most rigorous review process, the same one you apply to new production software. It is guidance, not a standard, but it is a document a security team will recognize, which matters when you need the control funded.
MCP security checklist
The table is the summary: risk, control, and the requirement behind it.
| Risk | Control | Requirement that answers it |
|---|---|---|
| Instructions hidden in a tool description | Show model-only text to the human too; review descriptions at install | OWASP ASI02 and ASI04; no spec requirement |
| A server rewrites another server's tools (shadowing) | One trust level per client context; no untrusted server beside a credentialed one | NSA: separate systems and data by trust level |
| Definition changes after approval | Pin version and canonical tool-list hash, re-approve on change, fail closed | No spec answer; client policy |
| Code redeemed against the wrong authorization server | Validate iss before redeeming; key credentials by issuer | SEP-2468, SEP-2352, RFC 9207 |
| Unreviewable client identity | Client ID Metadata Documents instead of Dynamic Client Registration | 2026-07-28 prefers CIMD; DCR deprecated |
| Over-broad upstream token | One scoped token per tool family; never forward the inbound token | NSA: grant only the minimum access |
| No way to reconstruct an incident | Log every tool call with a propagated trace context | SEP-414; MCP Logging deprecated for OTel and stderr |
- Write the trust map down: which servers share one client context, which credentials each server holds, and which of those servers you did not write.
- Pin every server: version plus a hash of the canonical tool list, checked before the first tool call, failing closed.
- Re-approve on change and show the diff, so a changed description is a decision and not a surprise.
- Treat descriptions and tool results as untrusted input, and inspect what a network-enabled tool returns before it enters the context.
- Validate
issand key credentials by issuer if you ship a client; use CIMD rather than DCR for registration. - Give each tool family its own scoped token, accept only tokens minted for you, and never pass an inbound token upstream.
- Log every tool call with a trace context from
_meta, and alert on changes: new definitions, new egress destinations, new token scopes. - Review a new server like a new dependency: source or vendor, maintainer, install path, and the data it can reach.
Tool descriptions are also a design problem, because the same text must be cheap enough to keep in context and precise enough to pick correctly: see designing MCP tools agents pick correctly for that, and the 2026-07-28 migration guide for the transport changes that affect it. Putting MCP in front of a system that holds real credentials is the kind of work I do as an AI engineer.
Sources
- Invariant Labs: MCP Security Notification – Tool Poisoning Attacks (1 April 2025)
- OWASP: Top 10 for Agentic Applications for 2026 (9 December 2025)
- MCP specification 2026-07-28: changelog (SEP-2468, SEP-2352, SEP-414)
- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
- NSA: Model Context Protocol (MCP) – Security Design Considerations for AI-Driven Automation (May 2026)
- Reed Smith: NSA publishes security guidance on designing AI systems with MCP (4 June 2026)
- Anthropic: How we contain Claude across products (25 May 2026)
Frequently asked questions
What is MCP tool poisoning?
Tool poisoning is an attack on a tool's description rather than its code. Invariant Labs coined the term on 1 April 2025 for a class they describe as indirect prompt injection: the description of a harmless tool, for example one that adds two numbers, also instructs the model to read files such as an SSH private key and pass their contents as an argument. The user sees a name; the model sees the instructions and the arguments.
How do you stop an MCP server from rug pulling you?
Pin two things: the server version, and a hash of the canonical tool list, meaning the sorted names, descriptions and JSON schemas. Verify the hash before the first tool call in every session, canonicalize first so reordering does not look like a change, and fail closed if the list cannot be fetched. On a mismatch, show the human a diff and re-pin only after they accept it: a changed description is a new definition that needs approving.
What does RFC 9207 iss validation change for MCP clients?
RFC 9207 lets an authorization server state which issuer it is, and requires a client to check that value against the issuer it recorded before redeeming an authorization code. That closes the mix-up attack, where an attacker redirects a client to their own server to redeem a code meant for the real one. The 2026-07-28 revision of MCP makes the iss parameter required under SEP-2468, and adds SEP-2352, which keys credentials by issuer so they are never reused across servers.
What is a Client ID Metadata Document in MCP?
It replaces Dynamic Client Registration as the preferred way to identify an MCP client. Instead of registering at runtime and receiving an opaque client id, the client id is an HTTPS URL that points at a JSON document containing at least client_id, client_name and redirect_uris. Anyone can read that document, so a reviewer can check what a client claims to be before approving it. Authorization servers advertise support with client_id_metadata_document_supported.
How should an MCP server scope its API tokens?
Give each tool family its own credential with the smallest scope that makes the tool work, and keep read-only tools on read-only tokens. A Jira server whose tools are search, comment and transition should not hold one token that can do all three. Accept only tokens minted for your server, check the audience, and never forward the inbound token upstream. Write the sentence the worst thing this token can do, and split any credential that answers too broadly.