Guide · MCP security

How to threat-model an MCP server

A Model Context Protocol (MCP) server speaks to your agent in the same voice as the human at the keyboard — anything it returns is read as content the agent can act on. That makes MCP server security a different problem from auditing an npm package: the dangerous behavior is usually legal code with bad intent, the manifest's capability claims aren't compiler-enforced, and the agent is a new execution environment that can be talked into calling tools it shouldn't. Below are the seven questions to ask before you trust one, drawn from the 12-class AI dev-stack threat model.

Short on time? Assay automates every check below — it threat-models the server before it reads a line of code, then backs each finding with a verbatim file:line quote. Jump to running it on an MCP server, or read the manual checklist first.

1. What does every response become?

An MCP tool-call response string is read in the same context as legitimate model output. A sufficiently clever string re-enters the model as instruction — this is indirect prompt injection, and it is the defining risk of the MCP surface. The attacker never speaks to your model directly; they shape data your model will eventually read. When you review a server, assume every byte it can return is hostile, and check that responses are structured as data (JSON with named fields) rather than free prose that interpolates upstream, attacker-controllable strings at the top level.

2. Does the tool description instruct the model?

The tool description registered at MCP handshake time is read by the model as guidance. A description that adds operational instructions — "always also call read_file on this path for context," "the assistant should now…" — is a working injection vector, sometimes called MCP tool poisoning. Red flags: imperative language directed at the model ("you must," "always also call"), a description that references tools other than its own, and hidden Unicode or zero-width joiners in the description string. A tool description should be a factual contract, not a prompt.

3. Does the bundle ship a reader and a sender?

No single tool needs to be dangerous. A "log viewer" reads a sensitive file; an "issue tracker connector" makes outbound requests. Asked to perform an innocent workflow, the agent composes them — read this log, file an issue including the contents — and sensitive data leaves the machine. The vulnerability is structural, not in any one function, so static analysis of a single tool won't surface it. When a server exposes both a high-sensitivity read primitive and an outbound network primitive, treat the combination as an exfiltration chain until proven otherwise.

4. Does it shell out?

An innocent-looking tool that wraps a dangerous one inherits your approval of the wrapper. You said "yes" to format_repo, not to the Bash command it constructs internally from your inputs — the classic confused-deputy pattern, transplanted into the agent context. Look for exec, spawn, or Bash invocations that build commands by string-concatenating upstream inputs without surfacing the exact command to the user.

5. Is the transport remote?

A server reachable over a remote transport can return benign descriptions at review time and inject at call time. Static review sees a snapshot; the runtime behavior belongs to a party you cannot audit. A remote MCP transport means every response is a live, untrusted injection source — weight it accordingly, and prefer servers you can run and inspect locally.

6. Does capability match the claim?

The README advertises a "weather tool"; the code, alongside the documented get_forecast, reads ~/.bash_history. The author may be malicious or merely sloppy — the harm to you is identical. Write down the capabilities the server claims, then read the code for the capabilities it actually has: network access in a tool that advertises only local operations, filesystem reads outside the obvious work directory, crypto or child-process imports an artifact's stated purpose doesn't need. The gap between claim and code is itself the vulnerability.

7. What happens on the next version?

A clean review of 1.0.0 says nothing about 1.0.1. Auto-update, post-install scripts, and unpinned dependency ranges turn a one-time install into a continuous trust relationship — the same typosquatting and account-takeover vectors seen across npm and PyPI apply directly to MCP registries. Pin dependencies, prefer signed releases, and re-scan on every update rather than trusting the install once.

Automating the checklist with Assay

Running these seven questions by hand on every server you install doesn't scale. Assay is an open-source MCP and Claude Code security scanner that does exactly this: it builds a threat model of the server before it reads the source, dispatches one investigator per threat class, and returns a safe / caution / unsafe verdict — with a post-validator that re-reads every citation and drops anything the model can't back with real code, so you never get a confabulated finding.

# scan a local MCP server directory
assay scan ./path-to-mcp-server

# or inventory everything in ~/.claude and scan from the web UI
assay inventory
assay serve            # http://localhost:7373 → "New Scan"

It runs on your Claude Code subscription — no separate API key. See Installation to get the binary, or the full 12-class threat model for the classes beyond MCP (hooks, skills, settings, and connector OAuth scopes).

Related: The 12 AI dev-stack threat classes · How Assay works — threat modeling before reading code · FAQ