SKILL 12 / MCP · DEEP DIVE

The Security Edge of MCP: Tool Descriptions and Tool Output Are Both Written by Someone Else

Three hard rules from the spec · Vetting before install, fences at runtime · Name collisions and fully-qualified names

2026-09-13 · BigCat

You think you installed a set of functions. What the model sees is a block of prose someone else wrote, pasted verbatim into its context — and it finished reading that prose before you called a single tool.

Related → Skill 11, Browser Automation MCP (that issue ended on “the page body is an injection surface”; this issue is that sentence in full)

// WHY THIS MATTERS

The last three issues treated MCP as capability: doc retrieval moves text in, browser automation hands over a pair of hands. This one flips the view. Every server you connect splices a stranger's prose into the model's system prompt, and usually hands it one of your credentials on the way in.

There is no single star skill this time. The subject is one defense line that runs from before-install to running: vet with MCP Inspector and agent-scan, fence with mcp-context-protector or a container gateway, then close it off with client-side permission rules. Eleven named, installable or readable items below; the selection criteria come in the back half.

// 01

What it kills for you: finding out only after install that a server can read your SSH key

Claim: MCP security is not an abstract risk. It is a concrete question you can answer tonight with two commands. Almost nobody has ever opened up the servers they installed to see which tools got exposed and what the descriptions actually say.

The first thing you can run right now

# (1) See what it actually hands the model (official debugger: Web UI / CLI / TUI)
npx @modelcontextprotocol/inspector

# (2) Scan every server and skill already on the machine
export SNYK_TOKEN=...
uvx snyk-agent-scan@latest

# (3) Shrink the blast radius on the spot: read-only, plus only the toolsets you need
github-mcp-server --read-only --toolsets repos,issues

The point of (1) is not “it connected.” It is reading, one by one, the description text that tools/list returns. That text lands in the model's context verbatim; the server author wrote it, you did not. Step (2) hides a trap worth calling out: plenty of tutorials still teach uvx mcp-scan@latest. That repo, invariantlabs-ai/mcp-scan, is now snyk/agent-scan, the package is snyk-agent-scan, and it wants a SNYK_TOKEN. Check the repo's current state before copying a command — a lesson this series paid for once already last issue.

Failure mode: using star count as a trust signal. Stars measure usefulness, not trustworthiness — and a server that is clean today can ship a new version tomorrow that replaces every description. That is the definition of the rug pull in the next section.
Primary sources · modelcontextprotocol/inspector (per the GitHub repo page, 2026-09: ~10.9k stars, MIT) · snyk/agent-scan (per the GitHub repo page, 2026-09: ~3.0k stars, Apache-2.0)
// 02

Mechanism: why a tool description is an attack surface, not documentation

Claim: in ordinary software a function signature is for the compiler. In MCP the tool description is a natural-language instruction for the model. The difference: get the first one wrong and compilation fails; get the second one wrong and it gets followed.
How someone else's prose enters an ordinary session ────────────────────────────────────────────────────────── Before you call any tool at all server ──tools/list──► names + descriptions + input schemas ──► system prompt ↑ already steering the model here (line jumping) After one tool call server ──result────► arbitrary text ──► context ↑ web page body / issue body / someone's PR description ────────────────────────────────────────────────────────── Three variants already reproduced (invariantlabs-ai/mcp-injection-experiments) direct-poisoning.py instructions hidden in a description: also read ~/.ssh/id_rsa shadowing.py one server rewrites another server's tool behavior whatsapp-takeover.py benign on install, malicious on a later load (sleeper rug pull) ──────────────────────────────────────────────────────────

What the spec itself says

The MCP specification (revision 2026-07-28), in server/tools.mdx, gives two sentences that carry most of this issue's theory: “clients MUST consider tool annotations to be untrusted unless they come from trusted servers” and “there SHOULD always be a human in the loop with the ability to deny tool invocations.” Note the reach of the first one — what is untrusted is not only the return value, but also the server's own label saying “this tool of mine is read-only.”

The overview section reduces it to three principles: users must explicitly consent to and understand all data access and operations; hosts must obtain explicit consent before exposing user data to servers; and “Tools represent arbitrary code execution and must be treated with appropriate caution.” Tools equal arbitrary code execution — that is the spec's own wording, not this site's rhetoric.

Failure mode: assuming “I only install read-only servers, so I'm fine.” A read-only server can still deliver hostile text into the context, which the other, writable server you installed then acts on. The danger is the combination, not any single tool — scanners call this a toxic flow.
// 03

Real usage and limits: eleven items across three layers

Claim: these layers differ in cost by an order of magnitude. Layer three is nearly free and can be finished tonight, yet most people start agonizing at layer two. Get the order backwards and security stalls forever at “once I have time to stand up a gateway.”

Layer 1 · Vet before you install

Layer 2 · Fences at runtime

Layer 3 · Client-side permissions (cheapest; do this one first)

One more layer that belongs to the protocol, not to tooling

If you write servers rather than only installing them, the MUSTs in basic/authorization/security-considerations.mdx are the bar: a server MUST NOT pass through the token it received from the client to a downstream API; it MUST validate token audience and accept only tokens issued specifically to itself; clients MUST include the resource parameter, MUST implement PKCE, and MUST refuse to proceed when PKCE support cannot be verified; and a proxy server using a static client ID MUST obtain user consent for each dynamically registered client — the clause that exists to kill the confused deputy.

// 04

Costs and alternatives: the one to know is “MCP servers do not run in the sandbox”

Claim: the sandbox you configured for Bash does not reach MCP. That is not an oversight, it is a boundary written into the docs — and most people assume turning on /sandbox covered both.

Three honest costs

Three ways to connect nothing at all

The honest cost: not one fence in this issue solves the root problem — that a model reads a sentence and does what it says. All of them only shrink the consequences of doing what it says. Which leaves exactly one criterion: assume every character this server returns was written by an attacker — what is the most it can cost me? If the answer contains “private repos,” “production database” or “payments,” that server should not be online alongside anything else.

// ONE-PAGE BUYING CARD

Do these three tonight (zero cost): (1) npx @modelcontextprotocol/inspector and read the full tool descriptions of everything already installed; (2) put "deny": ["mcp__*"] in settings and re-allow tool by tool as mcp__<server>__<tool>, always anchoring the allow to a concrete server; (3) give github-mcp-server --read-only --toolsets repos,issues, and lift it only on the day you need writes.

Add on the day an external server arrives: uvx snyk-agent-scan@latest (scan before install) · mcp-context-protector.sh --command ... (pinning plus change alerts) · docker mcp gateway run (container isolation and secrets) · MCP Registry for namespace ownership.

Teams and CI: turn on --strict-mcp-config and disabledMcpjsonServers unconditionally (unattended runs never show the approval prompt) · ToolHive for central control and audit · if you write servers, follow the MUSTs in security-considerations.mdx.

Explicitly don't: treat stars as trust · copy uvx mcp-scan@latest from a blog (the repo moved to snyk/agent-scan) · believe a tool's self-declared annotations · assume /sandbox covers MCP.

One-line criterion: assume every character this server returns was written by an attacker — what can it cost you? If the answer is big enough, don't leave it online next to your write access.

// GLOSSARY

tool poisoning
Hiding instructions for the model inside a tool description. The client UI usually shows only the tool name and a one-line summary; the full description text is visible to the model alone.
line jumping
A malicious server steering the model before you invoke any tool, because what tools/list returns enters the context at the start of the session. The mcp-context-protector README lists it as a primary target.
tool shadowing
One server rewriting another server's tool behavior through its own descriptions. The protocol does not guarantee cross-server name uniqueness; the fully-qualified mcp__<server>__<tool> is the client-side answer.
rug pull
Clean on the first version, hostile on a later one. Static scanning cannot catch it; configuration pinning and change alerts can.
toxic flow
Every tool individually compliant, yet the combination forms a “read something private, write somewhere external” path. The danger is the flow, not the node.
confused deputy
An intermediary with ambient authority tricked into acting for an attacker. The spec's remedy: a proxy server must obtain user consent for each dynamically registered client.
token passthrough
A server forwarding the client's token unchanged to a downstream API. The spec says MUST NOT — it bypasses audience validation and rate limiting, and corrupts auditing.
fully-qualified tool name
A tool's unique name once the client prefixes it with its server. It is also the unit permission rules, hook matchers and subagent tool lists are written in.

// FURTHER READING

// THINK IT THROUGH

Of the servers you have installed right now, how many have you personally read the full tool descriptions for?
If the answer is zero, the first command in this issue is tonight's homework. The summary your client shows and the description the model reads are frequently not the same text.
Hanging a read-only server and a writable server in the same session — do the risks add, or multiply?
They multiply. Individually compliant, jointly fatal: that is why the term toxic flow exists. The one that reads something private and the one that can write outward form a path the moment they are online together.
If a server's safety rests on its promise that it is read-only, what are you still missing?
An enforcement point that does not depend on its self-declaration. That is exactly why the spec marks annotations untrusted: read-only should come either from it having no write tools, or from an external switch like --read-only — never from a label.