Skip to content

IV.3IV · The protocol layerattack

The MCP assessment runbook

IV.2 · Model Context Protocol (MCP) tells you how MCP fails. This tells you whether it is failing in your estate - seven phases, run at a terminal, each one ending in a finding you can write down.

Phase 0 - Inventory: find the servers

Most MCP risk in an organization is not a server someone deployed, it is a server someone added to their editor. MCP09 (Shadow MCP Servers) is an inventory problem before it is a security one, and the answer set is small and enumerable: MCP clients keep their server lists in a handful of well-known files.

Enumerate configured MCP servers on an endpoint (authorized hosts only)
# macOS / Linux - the common client config locations
ls -la ~/.claude.json ~/.claude/settings.json 2>/dev/null
ls -la ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/null
ls -la ~/.cursor/mcp.json ./.cursor/mcp.json 2>/dev/null
ls -la ~/.vscode/mcp.json ./.vscode/mcp.json 2>/dev/null
# Windows
# %APPDATA%\Claude\claude_desktop_config.json
# %APPDATA%\Code\User\settings.json
# Sweep a fleet share / home directories for any MCP config, then read what they run
find /Users -maxdepth 4 \( -name 'mcp.json' -o -name 'claude_desktop_config.json' -o -name '.claude.json' \) 2>/dev/null
# What does each config actually launch, and does it carry secrets?
jq -r '.mcpServers // .servers | to_entries[] | "\(.key)\t\(.value.command // .value.url) \(.value.args // [] | join(" "))"' \
~/.cursor/mcp.json 2>/dev/null
grep -riE 'token|api[_-]?key|secret|password' ~/.cursor/mcp.json ~/.claude.json 2>/dev/null

Record for every server found: who added it, what it launches, what transport, and whether credentials sit in the config file. Secrets in a client config are MCP01 (token mismanagement) and they are readable by every process running as that user - including any MCP server on the same machine.

Phase 1 - Enumerate: what does the server expose?

List every primitive the server offers
# Local stdio server, via the official Inspector CLI
npx @modelcontextprotocol/inspector --cli <server-cmd> --method tools/list
npx @modelcontextprotocol/inspector --cli <server-cmd> --method resources/list
npx @modelcontextprotocol/inspector --cli <server-cmd> --method prompts/list
# Remote server, no client involved - this is also your unauthenticated-access test
curl -s -X POST https://<target>/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .

If that last command returns a tool list with no credential, stop and write the finding. You have just reproduced the single most common MCP defect, and everything downstream is now reachable by anyone who can route to the host.

Capture the initialize response too - it declares the server’s capabilities, and the reverse-channel ones decide which of the IV.2 · Model Context Protocol (MCP) attacks apply at all:

Which capabilities did the server negotiate?
curl -s -X POST https://<target>/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-11-25",
"capabilities":{},
"clientInfo":{"name":"assessment","version":"1.0"}}}' \
| jq '.result | {protocolVersion, capabilities, serverInfo, instructions}'
# Flag: sampling (can it drive your model?), sampling.tools (autonomous tool loop),
# elicitation (can it prompt your user?), tasks (durable handles), any extensions.
# Read `instructions` in full - it lands in the model's context verbatim.

Phase 2 - Read what the model reads

This is the highest-value step in the whole assessment and it needs no exploit. The tool description, the inputSchema property descriptions and the server instructions all reach the model before any approval prompt fires - so a server nobody has invoked can still steer the agent.

Pull every string the model will ingest, then review it as instructions
npx @modelcontextprotocol/inspector --cli <server-cmd> --method tools/list \
| jq -r '.tools[] | "=== \(.name)\n\(.description)\n--- schema descriptions:\n\(
[.inputSchema.properties // {} | to_entries[] | " \(.key): \(.value.description // "")"] | join("\n"))"'

Review the output for text addressed to the assistant rather than to a developer:

Grep forWhy it matters
IMPORTANT, <SYSTEM>, <IMPORTANT>The classic tool-poisoning wrapper
do not mention, do not tell the user, without further explanationConcealment - the tell that separates an attack from bad documentation
ignore previous, instead ofInstruction override aimed at other tools
read, ~/.ssh, .env, mcp.json, file pathsExfiltration staging inside a description
A parameter with no functional purpose (sidenote, context, debug)The channel the stolen data leaves through

Two things reviewers miss: schema property descriptions (most people read only the top-level description) and cross-tool targeting - a poisoned description on server A can impose behavior on a tool belonging to server B, because they share one context.

Tool annotations are not evidence. readOnlyHint, destructiveHint, idempotentHint and openWorldHint are asserted by the server you are assessing. Compare each annotation against what the tool actually does, and treat any mismatch as a finding in its own right - clients use these to decide auto-approval.

Phase 3 - Authorization: the three tests that fail most often

Of 119 OAuth-enabled remote MCP servers measured, every one had at least one authorization flaw. Test in descending order of observed failure rate:

#TestObserved failure rateWhat you are checking
1Malicious dynamic client registration95.8%Register a client with an attacker-controlled redirect_uri; does the server bind and honor it?
2PKCE downgrade68.1%Drop the code_challenge, or offer plain instead of S256; does the flow still complete?
3Consent-page bypass60.5%Replay the authorization request with a different client_id; is consent skipped because a cookie is present?
Authorization probes
# Does the server publish protected-resource metadata, and what does it point at? (RFC 9728)
curl -s https://<target>/.well-known/oauth-protected-resource | jq .
curl -si https://<target>/mcp -X POST -d '{}' | grep -i 'www-authenticate'
# Audience binding (RFC 8707): does a token minted for a DIFFERENT resource work here?
curl -s -X POST https://<target>/mcp \
-H "Authorization: Bearer $TOKEN_FOR_OTHER_AUDIENCE" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .
# A 200 here is token-audience confusion: the server accepted a token not issued to it.
# Scope minimization: does the server publish an omnibus scope catalog?
curl -s https://<target>/.well-known/oauth-authorization-server | jq '.scopes_supported'
# Wildcards, `all`, `full-access`, or every scope listed up front = over-broad by design.

Also test session handling, since the spec is explicit that servers MUST NOT use sessions for authentication: capture an Mcp-Session-Id, then replay it from a different source with no credential. If it works, the session is doing authentication’s job.

Phase 4 - Transport and network

Transport-layer checks
# Origin validation / DNS rebinding - servers MUST validate Origin and MUST 403 an invalid one
curl -si -X POST http://127.0.0.1:<port>/mcp -H 'Origin: https://evil.example' \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -1
# Expect 403. Then test the gap the spec leaves - omit Origin entirely and see what happens.
# Is a "local" server actually local?
ss -ltnp | grep -E ':(3000|8000|8080|11434)' || netstat -ano | findstr LISTENING
# A 0.0.0.0 bind on an MCP server is remote access with no auth story.
# Legacy transport - a bare SSE endpoint is a deprecated-transport finding
curl -si https://<target>/sse | head -5
# Estate sweep for exposed MCP and adjacent AI services
nuclei -tags mcp,exposure -severity medium,high,critical -u https://<target>

Phase 5 - Chain it

Individual findings are cheap; the chain is what moves a client. Work the IV.2 · Model Context Protocol (MCP) trust boundaries in order and look for a path from untrusted input to a privileged action:

  1. Untrusted content in - can attacker-controlled data reach the model through a resource, a tool result, or a fetched page?
  2. Instruction crossing - does that content get treated as instruction rather than data?
  3. Privileged tool out - is there a write, send, exec or payment tool in the same context?
  4. Egress - can the result leave, through a tool argument, an outbound fetch, or a rendered image?

All four present is a reportable exfiltration path, and it is the same lethal-trifecta test as II.2 · Prompt injection & the LLM attack surface - MCP just supplies more of the legs at once. Note that a multi-server host composes these across trust boundaries: server A supplies the injection, server B supplies the capability.

Phase 6 - Pin, then re-check

There is no protocol integrity control for tool descriptions, so a server that passed review can change afterwards. Establish the baseline as part of the assessment:

Pin what you approved, then diff it
# Trust-on-first-use pinning (Trail of Bits mcp-context-protector)
./mcp-context-protector.sh --command "<server-cmd>"
./mcp-context-protector.sh --url https://<target>/mcp
# Pinned state: ~/.mcp-context-protector/servers.json - diff it to catch a rug pull
# Or roll your own baseline: hash the exact strings the model consumes
npx @modelcontextprotocol/inspector --cli <server-cmd> --method tools/list \
| jq -S '.tools[] | {name, description, inputSchema}' | sha256sum > mcp-baseline.sha256

Scanners help, with one caveat that belongs in the method rather than a footnote:

Static scanning
uvx mcp-scan@latest # scans installed client configs
uvx mcp-scan@latest ~/.cursor/mcp.json

Phase 7 - Report

Map each finding to the taxonomy the client already reports against, so it lands in an existing risk register rather than a new one:

FindingMap to
No auth on a reachable serverMCP07 insufficient authentication · VIII.1
Instructions in a tool descriptionMCP03 tool poisoning · AML.T0051
Secrets in a client configMCP01 token mismanagement
Server discovered outside the registerMCP09 shadow MCP servers
Injection reaching a privileged toolMCP06 intent-flow subversion · OWASP LLM01 · ASI01
Token accepted that was not issued to this serverToken passthrough - explicitly forbidden by the spec

Score with AIVSS so the agentic amplifiers show up rather than being lost in a CVSS base (VIII.4 · ISO/IEC 42001, verification & maturity), and write it twice - technical chain plus the board sentence - per VI.5 · Running the engagement. The evidence-request and findings-table skeletons are in templates.