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.

··10 min read

  • MCP security
  • Tool poisoning
  • MCP OAuth
  • Supply chain
  • Audit logs
Concentric rings from outside in: NSA guidance, OWASP agentic risks, issuer-bound credentials, pinned tool definitions and a scoped core token.

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.

Where trust ends in an MCP deploymentFour columns from left to right: your machine, holding a workspace and the host app; the client, holding the agent, the model and the tool list; servers, local or remote, whose code you may not own; and data, that is repositories, APIs and tool results. Three dashed vertical lines mark the boundaries between them, labelled one, user consent, approve once; two, tool definitions, read by the model; three, upstream tokens, spent by the server. Arrows cross each boundary once, left to right. A note at the bottom says a change on the right can change what the model does on the left.MCP trust boundariestrust ends at the dashed linesHOSTCLIENTSERVERSDATAyour machineworkspace,host appclientagent, model,tools/listserverslocal orremotedatarepos, APIs,tool results1 · user consentapprove once2 · tool definitionsread by the model3 · upstream tokensspent by the servera change on the right can change what the model does on the left
The three MCP trust boundaries. Consent is a one-time dialog, tool definitions are read by the model on every session, and upstream tokens are spent by code you approved before the model had an opinion.

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.

Approving a tool definition, and re-approving when it changesA flow with four steps. The server returns tools/list, the client canonicalizes the names and schemas and hashes them, then compares the hash with the pinned one. If the hash matches, the session runs with no prompt. If it changed, the client shows the diff and a human re-approves, which re-pins the new hash and sends it back to the hash step.what you approve is the definition, not the nametools/listfrom the servercanonical hashnames, schemasequals pinnedhash?runno promptre-approveshow the diffsamechangedre-pin
The approve-on-change gate. Hash the canonical tool list, compare it with the approved hash before the first tool call, and treat any difference as a new definition that needs a human.
# 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 servers

On 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.

RiskControlRequirement that answers it
Instructions hidden in a tool descriptionShow model-only text to the human too; review descriptions at installOWASP 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 oneNSA: separate systems and data by trust level
Definition changes after approvalPin version and canonical tool-list hash, re-approve on change, fail closedNo spec answer; client policy
Code redeemed against the wrong authorization serverValidate iss before redeeming; key credentials by issuerSEP-2468, SEP-2352, RFC 9207
Unreviewable client identityClient ID Metadata Documents instead of Dynamic Client Registration2026-07-28 prefers CIMD; DCR deprecated
Over-broad upstream tokenOne scoped token per tool family; never forward the inbound tokenNSA: grant only the minimum access
No way to reconstruct an incidentLog every tool call with a propagated trace contextSEP-414; MCP Logging deprecated for OTel and stderr
  1. 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.
  2. Pin every server: version plus a hash of the canonical tool list, checked before the first tool call, failing closed.
  3. Re-approve on change and show the diff, so a changed description is a decision and not a surprise.
  4. Treat descriptions and tool results as untrusted input, and inspect what a network-enabled tool returns before it enters the context.
  5. Validate iss and key credentials by issuer if you ship a client; use CIMD rather than DCR for registration.
  6. Give each tool family its own scoped token, accept only tokens minted for you, and never pass an inbound token upstream.
  7. Log every tool call with a trace context from _meta, and alert on changes: new definitions, new egress destinations, new token scopes.
  8. 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

  1. Invariant Labs: MCP Security Notification – Tool Poisoning Attacks (1 April 2025)
  2. OWASP: Top 10 for Agentic Applications for 2026 (9 December 2025)
  3. MCP specification 2026-07-28: changelog (SEP-2468, SEP-2352, SEP-414)
  4. RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
  5. NSA: Model Context Protocol (MCP) – Security Design Considerations for AI-Driven Automation (May 2026)
  6. Reed Smith: NSA publishes security guidance on designing AI systems with MCP (4 June 2026)
  7. 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.

Sounds like what you need?

Tell me about your project or role – I’d love to hear from you.