MCPiranha
Security scanner for Model Context Protocol (MCP) servers: tool poisoning, cross-server shadowing, rug pulls, insecure configuration, vulnerable implementations and dangerous combinations of servers. It works fully offline. Design: docs/ARCHITECTURE.md.
Status: 0.1.0. 24 rules (see docs/RULES.md) run on client configs, snapshot
files and, with --connect, on what servers actually serve. Toxic-flow analysis across servers
is in; an experimental local classifier is optional; an LLM layer is planned.
Install
uvx mcpiranha scan # run without installing (needs uv)
pipx install mcpiranha # or install the command
pip install 'mcpiranha[ml]' # with the experimental classifier
Requires Python 3.12+. --connect uses Docker for its sandbox.
Usage
mcpiranha scan # every MCP client config on this machine + the current project
mcpiranha scan .mcp.json # one config file (Claude, Cursor, VS Code, Windsurf formats)
mcpiranha scan ./some-project # a project's .mcp.json, .cursor/mcp.json, .vscode/mcp.json
mcpiranha scan --connect # also start/reach each server and check its tools, prompts, resources
mcpiranha scan --connect npm:@scope/server@1.2.3 # vet a package before installing it
mcpiranha scan --connect https://mcp.example.com/mcp --oauth # sign in (opens a browser)
mcpiranha scan --connect https://mcp.example.com/mcp -H "Authorization: Bearer $TOKEN"
mcpiranha scan --connect --strict # CI: exit 2 if any server couldn't be checked
mcpiranha scan -f sarif -o mcpiranha.sarif # for GitHub code scanning
mcpiranha lock --connect # pin launch configs and tool definitions to mcp-lock.json
mcpiranha verify --connect # exit 1 if anything changed or was added since the lock
mcpiranha inspect --connect <target> # show the text the model reads, hidden characters revealed
mcpiranha flows # each client's tool capabilities and the toxic flows between them
mcpiranha scan --classifier # experimental: add the local ML classifier (pip install 'mcpiranha[ml]')
Supported clients: Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, Zed, Cline, Roo Code, Gemini CLI, Codex CLI, Continue, Goose, Amazon Q Developer CLI, Kiro, opencode and LM Studio, on macOS, Linux and Windows. See docs/CLIENTS.md for paths and formats. Reports never contain secret values.
Toxic flows
Servers that are harmless alone can be dangerous together. mcpiranha labels every tool (reads
untrusted content, reads private data, sends data out, runs code, destroys data) from its name,
its parameters and a table of well-known servers, then looks across all servers one client
loads. MCP-TF-001 reports the "lethal trifecta", e.g. github.get_issue -> filesystem.read_file -> slack.post_message: one poisoned issue can make the agent read your files and post them.
MCP-TF-002 reports untrusted content next to code execution or destructive tools. Flows across
servers are high severity (critical if the last step is auto-approved); flows inside a single
server, which are usually by design, are lower. Well-known servers count even without
--connect.
Experimental classifier
--classifier adds a local prompt-injection model (protectai/deberta-v3-base-prompt-injection-v2,
Apache-2.0, ~740 MB, downloaded once and verified against pinned hashes). It catches some reworded
attacks the phrase rules miss, but it was trained on chat prompts and flags ordinary usage
guidance in tool descriptions too, so its findings are low severity and meant as a review
queue. Measurements: calibration/README.md.
Policy file
.mcpiranha.yaml in the current directory (or --policy PATH) sets defaults and suppresses
findings you've reviewed. Suppressed findings stay in JSON and SARIF output, marked with their
reason.
fail_on: high
strict: true # exit 2 if any server couldn't be connected to
suppress:
- rule_id: MCP-CF-001
server: internal-db
reason: Secret is injected by vault at runtime # required
expires: 2027-01-01 # optional
- rule_id: "MCP-TP-*" # globs work for rule_id, server and tool
server: docs-search
reason: Reviewed 2026-10; descriptions quote attack examples on purpose
What --connect does
It sends only listing requests (initialize, tools/list, prompts/list, resources/list);
no tool is ever called. Tools are listed twice to catch servers that change them mid-session.
- stdio servers run in a Docker sandbox by default. The package is downloaded with network
access but without running any of its code (
npm install --ignore-scripts, wheels only for PyPI), then the server runs with no network, a read-only filesystem, no capabilities, a non-root user and resource limits. Servers that run local files can't be sandboxed this way;--sandbox noneruns servers directly on this machine, as your MCP client would. - Env vars get dummy values unless you pass
--real-credentials(which also expands${VAR}references). Most servers list their tools without valid credentials. - Remote servers are reached without credentials unless you pass
--real-credentials(the config's headers),--header(for a single URL target only, so a token can't reach another server) or--oauth(signs in through your browser; tokens stay in memory for that run). A server that answers without credentials is reported (MCP-CF-003). - Servers run in parallel, four at a time (
--jobs). npm and uv downloads are cached in the Docker volumesmcpiranha-npm-cacheandmcpiranha-uv-cache; remove them withdocker volume rmto start fresh. - Locking with
--connectalso pins the package version or image digest that actually ran, soverifycatches an unpinnednpx -y pkgquietly moving to a new release. - Opt-ins for servers that won't start otherwise:
--allow-network(network while running) and--allow-install-scripts(npm install scripts and PyPI source builds during download). Both are off by default. --save-snapshots DIRkeeps what each server served, so it can be rescanned offline.- Disabled servers (disabled in their client config) aren't started.
- Older servers that crash on the 2026
server/discoverprobe are retried in a fresh process with the classicinitializehandshake. - Versions can differ from your machine's: npm picks the newest release whose
enginesmatch the sandbox's Node 22 (MCPIRANHA_NODE_IMAGEoverrides the image), so an unpinned package may resolve differently than on a machine with a newer Node.
Calibration
The tool-metadata rules are tuned against the 100 most-downloaded servers in the official MCP registry: zero false positives on the 59 the sandbox could list, with the attack fixtures still detected. Method, scripts and results: calibration/README.md.
Development
uv sync --all-extras
uv run mcpiranha scan fixtures/metadata/unicode-tag-smuggling/snapshots.json
uv run mcpiranha scan fixtures/configs/claude_desktop_config.json
uv run mcpiranha inspect fixtures/metadata/unicode-tag-smuggling/snapshots.json
uv run pytest # MCPIRANHA_DOCKER_TESTS=1 also runs a real Docker sandbox
uv run ruff check . && uv run ruff format --check . && uv run pyright
Adding a rule
- Reserve an id in
docs/RULES.md. - Add a module under
src/mcpiranha/rules/builtin/with aRulesubclass decorated with@register(seeinvisible_unicode.py). Iterate text withiter_text_fieldsso nested schema text is covered. - List the id in
expected.jsonfor each fixture case it should fire on, and add a benign look-alike case.tests/test_fixtures.pythen checks it everywhere. - Build any invisible or look-alike characters with
chr();tests/test_source_hygiene.pyfails on raw ones in the source.
Third-party rule packs can register through the mcpiranha.rules entry-point group.
Metadata
Release files for MCPiranha 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mcpiranha-0.1.0.tar.gz | 219.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcpiranha-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 335.5 kB
Release files / mcpiranha-0.1.0.tar.gz
| Download URL | mcpiranha-0.1.0.tar.gz |
|---|---|
| Size | 219.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
628da2198e81a6664b992b8aafa2f0cd33a6c8d0745a34f1564fba323674e3fa
|
|
BLAKE2b-256 checksum How to use checksums |
2986cef527e0bab4278a3755caca3282369043dcac7c5692f13d556c34ed5b5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / mcpiranha-0.1.0-py3-none-any.whl
| Download URL | mcpiranha-0.1.0-py3-none-any.whl |
|---|---|
| Size | 116.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
296de3e98568e9f7599c16b74a487075c17d19e4e11fdc481d7dccf1c3353c7c
|
|
BLAKE2b-256 checksum How to use checksums |
a00dabae66eadff73b8128b04f2d766a74fe228144a0c8bcb6c7bb2be062e4d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for MCPiranha 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for mcpiranha-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.