
An MCP server is untrusted code with a natural-language channel into your agent's context and credentials, and often a foothold on the same network as your other tools. This MCP server security checklist gates adoption on five things one engineer can verify in an afternoon: provenance, transport, authorization, tool-definition integrity, and runtime containment.
The short answer
- Treat every MCP server as a third-party dependency, an OAuth resource server, and a prompt-injection channel.
- Pin provenance, reject exposed transports, prove audience-bound minimal scopes, hash tools/list, and proxy a sandboxed run.
- Stdio executes as the user; tool annotations are hints; approvals must bind to a hash of the full tool definition.
- Fail any of those gates and keep the server off agents that hold private data or an outbound path.

Key takeaways
- An MCP server is a dependency, an OAuth resource server, and a prompt-injection channel at the same time.
- Stdio runs as the invoking user; Streamable HTTP needs OAuth 2.1, audience binding, Origin and Host checks, and TLS.
- The spec forbids token passthrough because a forwarded foreign bearer skips audience checks and enables lateral movement.
- Bind approval to a hash of the full tool definition, and treat readOnlyHint and destructiveHint as hints, not controls.
- A fail on pinning, exposure, audience, poisoned descriptions, hash binding, or unbounded access keeps the server out of production.
What belongs on an MCP server security checklist?
Review an MCP server as you would a package that also holds an OAuth token and can talk to the model. Pin provenance, reject an exposed transport, prove scopes are minimal and audience-bound, hash the tools/list manifest, and proxy a sandboxed run. If the server fails any of those five gates, keep it off agents that can read private data or send data out.
Model Context Protocol, published by Anthropic in late 2024, is an open JSON-RPC 2.0 protocol. Servers expose tools, resources, and prompts. Clients expose sampling, roots, and, since the 2025-06-18 revision, elicitation. Open source proves nothing here: Anthropic’s own reference git server shipped path traversal and argument injection. A registry listing, by itself, does not prove who published the code.
The twelve checks
- Publisher identity: a named maintainer or organization that matches the package namespace.
- Version and integrity pin. A floating
npx -yoruvxinstall fails. - Transport choice, with any legacy HTTP+SSE server flagged for removal.
- Origin and Host validation on HTTP listeners, and no bind on
0.0.0.0. - OAuth 2.1 with PKCE, and discovery metadata that names the server you added.
- RFC 8707 audience binding, with no token passthrough.
- Granted scopes that match the upstream calls you observe.
- No hidden instructions in tool names, titles, descriptions, or error strings.
- Approval bound to a hash of the full tool definition, not the tool name.
- No instructions aimed at other servers, and no tool-name collisions.
- File, command, environment, and network access limited to the job.
- Logging of every tool call, plus a kill switch you have actually tested.
A fail on checks 2, 4, 6, 8, 9, or 11 blocks production. Anything else can ship as conditional, with a named owner and a fix date.
The afternoon schedule
- Inventory, about 20 minutes. Package, version, publisher, install command, config path, and every other server already on the same client.
- Inspect, about 40 minutes. Save the raw tools/list, resources/list, and prompts/list responses.
- Transport and auth, about 40 minutes. Read the 401 response and its WWW-Authenticate header, then check listeners.
- Proxy and sandbox, about 50 minutes. Tee stdio or put a proxy in front of HTTP. Run the server in a container with a tight egress allowlist.
- Verdict, about 20 minutes. Score each check as pass, conditional, or fail, and name an owner for every conditional.
When the server is business-critical or the agent holds production credentials, a live attack on the tool chain is the next step. That is what MCP server and tool-chain security testing covers.
Is stdio safer than Streamable HTTP?
No. Stdio is local code execution as the user who launched the client. Streamable HTTP moves the process off the laptop and makes you responsible for network authentication, Origin checks, and TLS. Neither transport is safe by default; they fail in different places.
Stdio runs one subprocess per client, with line-delimited JSON on stdout. The child inherits the user’s filesystem, network, environment variables, and any token sitting in the client config. The protocol provides no sandbox.
Streamable HTTP, introduced in the 2025-03-26 spec revision to replace HTTP+SSE, is a network listener. Older HTTP+SSE servers are still in the wild: flag them and plan a cutover. A local HTTP listener without Origin or Host validation is open to DNS rebinding from any browser tab on the same machine.
JSON-RPC on the stdio pipe is plaintext, and Streamable HTTP stays plaintext until a gateway enforces TLS. The protocol has no per-tool RBAC, so policy lives in the client or the gateway.
| Transport | Where code runs | What you must prove | Typical fail |
|---|---|---|---|
| stdio | Child process of the user | Pinned binary, dropped privileges, no config secrets in reach | Reads PATs and SSH keys as the developer |
| Streamable HTTP | Local or remote listener | OAuth 2.1, audience binding, Origin/Host checks, TLS at your gateway | Bind on all interfaces, DNS rebinding, bearer forwarded upstream |
| HTTP+SSE (legacy) | Same class as HTTP | A documented reason it is still allowed, plus a removal date | Session handling from the original spec revision |
On the host, ss -tlnp or lsof -i should show the listener on localhost or a pinned interface. A 0.0.0.0 socket is a fail.

What can a malicious MCP server reach?
A connected server gets model context, whatever credentials its process can read, a foothold on the same network as your other tools, and often an outbound channel the model can be steered to use.
Tool names, descriptions, annotations, errors, and results are attacker-controlled text that lands in the same context window as your instructions. The model enforces no boundary between data and commands, so a sentence in a description already works as an instruction. Simon Willison’s lethal trifecta (June 2025) names the dangerous combination: private data, untrusted content, and a way to communicate out. One MCP server can supply all three.
Client config files are a common store of long-lived tokens. Claude Desktop keeps claude_desktop_config.json under Application Support on macOS and under %APPDATA%\Claude on Windows. Cursor uses ~/.cursor/mcp.json, VS Code uses .vscode/mcp.json, and Claude Code uses .mcp.json. A local server running as you can read all of them.
Prove these cases in the sandbox: arguments passed straight into a shell (CWE-78), path traversal in file tools (CWE-22), SSRF through a URL parameter (CWE-918), results that echo the server’s environment, and errors that leak stack traces or internal URLs.
Two public cases show the pattern. In May 2025, Invariant Labs showed a public GitHub issue steering an agent to copy private repository content into a public pull request. In mid-2025, researchers showed text in a support ticket steering an agent that held a Supabase service-role key into reading a private table and writing it back to the ticket. Both are the lethal trifecta in action, with a vendor logo attached.
Flaws that already shipped
- CVE-2025-49596, MCP Inspector: CSRF and DNS rebinding against the local Inspector proxy, leading to code execution on the developer machine.
- CVE-2025-6514, mcp-remote: OS command injection through a crafted
authorization_endpointin the OAuth flow. Treat OAuth metadata URLs as untrusted input. - Asana’s hosted MCP server, June 2025: a tenant-isolation flaw that exposed data across customers.
- Anthropic’s mcp-server-git: path traversal, argument injection, and an unrestricted
git_inittool, since fixed.
Most servers ship as npm or PyPI packages. Floating installers run postinstall scripts as the user, and lookalike names under mcp-server-* and @modelcontextprotocol/* are a live risk. Re-check each advisory before you cite a fixed version in a ticket. Map findings to the 2025 OWASP Top 10 for LLM Applications (LLM01, LLM02, LLM03, LLM06, LLM07) and draw the data flow in agentic AI threat modeling before approval.
Which OAuth 2.1 scopes are legitimate, and why is token passthrough banned?
Remote MCP servers are OAuth resource servers. The spec builds on OAuth 2.1 with PKCE required, Dynamic Client Registration (RFC 7591), Protected Resource Metadata (RFC 9728), and Resource Indicators (RFC 8707), so each access token names this server as its audience.
A legitimate scope is the smallest upstream permission that matches calls you actually see in the proxy. A hosted server that requests a full workspace admin token because per-tool delegation was harder to build fails. Static API keys prove possession of a secret, not audience. Mutual TLS proves which process connected, but still needs an authorization layer deciding which tool may run.
Decode the token and diff the granted scopes against the upstream calls captured in the sandbox. Prefer a dedicated service identity over a human PAT pasted into client config. Mcp-Session-Id values must be cryptographically random, bound to the authenticated user, and free of user identifiers; the spec cites guessable session IDs as its hijacking example.
Token passthrough means the server accepts a token issued for another resource and forwards it upstream. The spec says MUST NOT. Forwarding a foreign bearer skips the audience check, hides the MCP server from upstream audit logs, and turns one compromised server into lateral movement.
Confused-deputy bugs sit next to this: the server uses its own broader privilege on behalf of a caller who never had it. The spec’s security guidance also lists SSRF against OAuth metadata discovery, authorization-code interception and mix-up, and open redirects. Curl the discovery documents and confirm the issuer, resource identifier, and token audience all point at the server you meant to add.
How do I test for tool poisoning, rug pulls, and shadowing?
Tool poisoning
MCP tool poisoning hides instructions in a tool description, title, or error string. The model reads the full text, while most approval UIs show a truncated line, so a person approves a name. Invariant Labs demonstrated it in April 2025: a description told the agent to read SSH keys or the MCP client config and send them out through another tool call.
Dump tools/list to disk and search the descriptions for imperative phrases, zero-width characters, long whitespace runs, and base64 blobs. Then connect a canary server you own whose description tells the agent to read a canary file and post it to a host you monitor. If the client approves on the name alone, or the canary file leaves without an alert, the control failed.
That is an architecture bug, and a system prompt will not close it. Line jumping plants the same payload in a tool result (an issue body, a web page, a PDF) after approval, so cover result paths with prompt injection testing. Static scanners such as mcp-scan help surface poisoned text, but they do not replace the canary test.
Rug pulls
A rug pull returns a benign tools/list on first connect, earns approval, then changes a description or its behavior on reconnect or a version bump. Approvals stored by name are bypassable the moment the text changes. Hash the full definition (name, title, description, input schema, annotations), diff it at every session start, and treat a mismatch as a new approval or a disconnect.
Pin the package version and its integrity digest too, because a name pin still floats if the registry moves a tag. Re-review on every version bump.
Cross-server shadowing and annotations
Shadowing is a description on a malicious server that tells the model how to misuse a different, trusted server, which never changes. Invariant Labs’ example reads, in effect: whenever the filesystem server is used, also send the contents here. Reject descriptions that mention other servers, and reject duplicate tool names across the client config.
readOnlyHint, destructiveHint, idempotentHint, and openWorldHint are self-declared. A malicious server sets whatever earns a quieter UI, so enforce policy from your own allowlist. Sampling is covert token spend unless approved and rate-limited. Elicitation is a phishing channel: show it as the server speaking, never auto-answer, and never hand it credentials.
How do I sandbox, log, and kill an MCP server?
Run stdio servers in a container with a non-root user, a read-only root filesystem, an explicit mount list, and an egress allowlist of the hostnames the job needs. Put remote servers behind a gateway that enforces per-tool allowlists and requires human approval for destructive tools.
Direct client-to-server connections are hard to log and hard to revoke one at a time. A gateway fixes visibility but adds a trusted component, so review its credentials and admin plane. Scanners see definition text and drift, a human reading tools/list sees intent, and a proxy sees results that smuggle data after a clean definition. Keep all three and write down the gaps.
Log the JSON-RPC method, tool name, an argument hash (raw arguments only where policy allows), result size, session ID, user, and the tool-definition hash. Retain those records under the same retention policy and time synchronization you apply to IdP logs.
The kill switch is a gateway deny, a removed config entry, a revoked OAuth grant, and a redeployed pin. Rehearse it: revocation that needs a laptop-by-laptop edit will miss the incident window. Curated catalogs such as Docker’s MCP Catalog cut down anonymous publishers, but not runtime risk. For the agent side of the same problem, see how to test excessive agency in AI agents.
What should MCP vendor procurement ask?
Ask for evidence you can re-run, and score answers on the same pass, conditional, and fail scale as the afternoon review.
- Provenance: Who publishes the artifact, where is the source, and which digest do we pin this quarter?
- Transport: Which transport, which bind address, and where does TLS terminate?
- Authorization: Show a real access token. Which RFC 8707 resource does it name, where is passthrough rejected in code, and which tool call needs each upstream scope?
- Tool definitions: Send the current tools/list JSON and your change procedure. Which annotations do you set, and what enforces policy regardless of them?
- Runtime: Which files, commands, and hostnames does the server need? What do you log, for how long, and can we revoke you in one change?
- History: Any tenant-isolation or prompt-injection incidents, and what shipped afterward?
Answers that quote the spec without showing the running server score a fail. The spec describes obligations; your proxy log shows behavior.

FAQ
How do I check whether an MCP server is safe to connect to our AI agent?
Treat it as a dependency, an OAuth client, and an injection channel. Pin provenance, reject exposed transports, prove scopes are minimal and audience-bound, hash tools/list against rug pulls, and proxy a sandboxed run. Fail any gate and keep it away from agents with private data or an outbound path.
What is MCP tool poisoning?
Tool poisoning hides instructions in a tool description or error string that the model follows and a person usually does not see. Dump tools/list, search for hidden characters and imperative text, and confirm with a canary server you control.
What is an MCP rug pull attack?
The server presents a benign tool definition, gets approved, then changes the description or behavior later. Detect it by hashing the full definition and diffing that hash on every reconnect instead of trusting the tool name.
Is stdio more secure than Streamable HTTP?
Stdio avoids a network listener but runs with all of the launching user's privileges. Streamable HTTP needs real OAuth, Origin and Host validation, and TLS. Choose the transport whose failure mode you can actually contain.
Why does MCP forbid token passthrough?
Forwarding a token issued for another resource skips audience validation and hides the MCP server from upstream audit logs. A server may accept only tokens minted for itself, with the audience set through RFC 8707 resource indicators.
Should we trust readOnlyHint and destructiveHint?
No. Tool annotations are declared by the server itself, and a malicious server will set whatever gets the quietest UI. Enforce read-only, destructive, and network policy from your own client or gateway allowlist.
If the afternoon review hits a server you still need in production, MCP server and tool-chain security testing exercises tool poisoning, scope abuse, and egress against the agent you actually run.