Static security scanner for MCP (Model Context Protocol) servers.
mcpwn reads the tools, prompts and resources an MCP server advertises and flags the ones that can turn an agent against its user.
Scanning never launches an MCP server and never calls a tool. mcpwn audit
does both, deliberately, and only under an engagement file that names the
target.
mcpwn is published for security research, education, and testing systems you are entitled to test.
discover, view and scan only read. mcpwn audit launches processes and
calls tools on a live server, which acts on it: it creates, sends, writes and
spends whatever the tools it is allowed to call do. Point it only at systems you
own or have written permission to assess. Having that permission is your
responsibility, and so is what you do with the results.
brew install mlab-sh/mcpwn/mcpwnOr from source:
cargo install --locked --git https://github.com/mlab-sh/mcpwnPrebuilt binaries for macOS and Linux, x86_64 and arm64, are attached to each release.
Find the MCP configs on this machine, without analysing them:
mcpwn discoverSee everything a server exposes, tools, prompts and resources, with no analysis:
mcpwn view --url https://example.com/mcpScan them:
mcpwn scanScan a project directory, or a single endpoint:
mcpwn scan ./my-repomcpwn scan --url https://example.com/mcp -H "Authorization: Bearer $TOKEN"Record a baseline so later scans can tell you what changed:
mcpwn scan --write-lockUnderstand a finding:
mcpwn explain MCPWN-CAP-001Exit codes: 0 clean, 1 findings at or above the threshold, 2 error.
36 rules. mcpwn explain lists them all; mcpwn explain <ID> gives the detail.
| Family | Rules | What |
|---|---|---|
| Configuration | MCPWN-CFG-001..004 |
Plaintext credentials, unpinned launch packages, http:// endpoints, credentials in URLs |
| Capability | MCPWN-CAP-001..005 |
Command execution, code evaluation, filesystem and network access, x-mcp-header mirroring |
| Obfuscation | MCPWN-OBF-001..006 |
Unicode tag characters, zero-width characters, bidi overrides, homoglyphs, encoded payloads, in tools, prompts and resources alike |
| Rug pull | MCPWN-RUG-001..003 |
Tools that changed, disappeared or appeared since the lockfile |
| Shadowing | MCPWN-SHA-001..003 |
Colliding tool names, look-alike names, a server giving instructions about another server's tool |
| Toxic flow | MCPWN-FLOW-001 |
An ingest, a source and a sink coexisting in one environment |
| Reconnaissance | MCPWN-NET-001..006 |
Needs --probe: unenforced credentials, missing auth discovery, deprecated transport, plaintext downgrade, unvalidated protocol and headers |
| Confirmed defects | MCPWN-ACT-001..008 |
mcpwn audit only: path traversal, command injection, SQL injection, SSRF, session fixation, header injection, crash and error leaks under malformed input |
--probe asks a remote endpoint the questions enumeration does not: does it
need the credential it was given, how does it advertise authentication, what
else does it serve, and does it validate the protocol as the specification
requires.
mcpwn scan --url https://example.com/mcp --probemcpwn view --url https://example.com/mcp --probeRead-only: no tool is ever called and nothing is written. It still sends extra requests and touches paths you did not name, so it is off by default. Point it only at servers you are entitled to examine.
mcp.lock records what each tool looked like when you approved it. Later scans
compare against it.
mcpwn scan --write-lock # record the baseline
mcpwn scan # compare against it
mcpwn scan --update-lock # accept the changes, after reviewing them
mcpwn diff old.lock mcp.lockmcpwn scan --fail-on high --format sarif > mcpwn.sarifmcpwn init-policy > mcpwn.toml creates a policy file for rule overrides and
accepted findings:
fail-on = "high"
[rules]
"MCPWN-CFG-002" = "off"
[[ignore]]
rule = "MCPWN-CAP-001"
scope = "shell::run_command"
reason = "reviewed 2026-08; this server exists to run commands"The GitHub Action wraps all of it:
- uses: mlab-sh/mcpwn@main
with:
policy: mcpwn.toml
lock: mcp.lock
fail-on: highSee the action documentation for a complete workflow and the full list of inputs.
Active testing of a server you are entitled to act on. Every other command
reads; audit calls tools, which means it acts on the target.
mcpwn audit init > engagement.toml
mcpwn audit run --dry-run
mcpwn audit run
mcpwn audit run --format sarif --fail-on high > audit.sarifThe engagement file is the only way in. There is no --url and no config
discovery, so one command can never reach every server on a machine.
target = "https://mcp.example.com/mcp"
authorized_by = "you@example.com"
reference = "PT-2026-014"
[limits]
rate_per_second = 2
max_requests = 500
[tools]
allow = ["read_file", "fetch_url"] # nothing else is called
allow_dangerous = false # tools that take a command lineSeven probes. Four poison one parameter of one tool: path traversal, command
injection, SQL injection, SSRF to the cloud metadata service. Three test the
transport itself: session fixation, header injection, and malformed JSON-RPC.
mcpwn audit probes lists them; each looks for a specific oracle.
- Nothing destructive is sent.
; echoyes,; rmnever. - Every hit is re-checked against a control call that must come back clean.
- Tools that take a command line are skipped unless
allow_dangerousis set. protocol-fuzzis the only probe that can take a target down, so it runs only when an engagement names it. Nesting stops at 200 levels and payloads at 64 KiB.- Header injection needs the request written by hand over a socket, so it covers
http://targets only. Anhttps://one is reported as not covered. - Every request and response is written to a JSONL transcript as it happens.
--format,--policyand--fail-onwork exactly as they do forscan, so a rule accepted for a server is accepted whichever command found it. The JSON output carries the engagement and the transcript path alongside the findings.
A stdio: target launches the server, with no shell, a minimal environment, a
deadline and a kill on every exit path. scan, view and discover still
never launch anything and never call a tool.
Claude Desktop, Cursor, Windsurf, Continue, Zed, Codex, VS Code. Global locations are searched by default; a path argument searches project-level dotfolders instead.
Only JSON configs are parsed. TOML (Codex) and YAML (Continue) files are discovered and listed, but reported as not yet parseable.
- stdio servers are never launched, so their tools, prompts and resources are never listed. Only the configuration checks apply to them.
- Enumerating a remote server is a network request. The analysis is static; the tool list has to come from somewhere.
- Tool poisoning detection is not implemented yet.
- Resources are read as advertised: mcpwn never calls
resources/read, so what a resource actually returns is out of scope for a scan. mcpwn auditnever runs without an engagement file, and never calls a tool the engagement did not name.
- How MCP works, with sources
- How the static checks work
- How the active audit works
- Using mcpwn in GitHub Actions
just testjust ci