Static analyzer for MCP server repos: 4 checks (BadHost / Starlette CVE-2026-48710, FastMCP wrapper-layer asyncio.run bug, loose @mcp.tool() schemas, subprocess command-injection w/ cross-function taint propagation).
Project description
mcp-audit
Static security + correctness audit for MCP server repos.
pip install mcpdone-audit # PyPI distribution (the plain 'mcp-audit' name is squatted; PEP 541 blocks it)
mcp-audit # scan the current directory
mcp-audit /path/to/repo # scan a specific repo
mcp-audit --json # machine-readable output
mcp-audit --check fastmcp_wrapper_layer # one check only
mcp-audit --list-checks # list available checks
Exit codes: 0 clean, 1 at least one finding, 2 usage error.
What it checks
| Check ID | Severity | What it finds |
|---|---|---|
starlette_badhost |
HIGH / MED | Starlette < 1.0.1 in pyproject.toml, requirements*.txt, uv.lock, poetry.lock, pdm.lock. BadHost (CVE-2026-48710) lets a crafted HTTP Host header bypass path-based authorization. Affects any HTTP/SSE-transport MCP server. Stdio servers are unaffected. |
fastmcp_wrapper_layer |
HIGH | Sync @mcp.tool() functions that call asyncio.run(...) inside their body. FastMCP invokes tools inside an already-running event loop; asyncio.run() raises RuntimeError. Looks fine in unit tests, dies on the first real protocol call. |
tool_input_validation |
LOW | @mcp.tool() parameters typed as bare str / bytes / Any / list[Any] / dict[..., Any] or with no annotation at all. The schema FastMCP exposes to the LLM is the substrate prompt-injection-via-tool-description attacks rely on; constraining it (Annotated[str, Field(max_length=N)], Literal[...], Pydantic models) closes the window without losing expressiveness. Hygiene check, not a CVE — expect findings even on well-written servers. Added in v0.2. |
command_injection |
HIGH | @mcp.tool() functions where a tool parameter (or a local tainted via assignment / .format() / string concat) flows into os.system, os.popen, or subprocess.* with shell=True or a tainted-interpolated command string. v0.4 added same-file cross-function taint propagation: the analyzer now follows local helper calls (positional + keyword binding, recursion-visited guard), so tool -> helper -> sink flows are caught. Cross-file taint remains out of scope. The list-of-args / no-shell pattern is correctly NOT flagged. Added in v0.3, cross-function in v0.4. |
More checks are landing — hard-coded secrets, write-API tools missing a FORBIDDEN_NAMES-style guardrail, read-only-by-default violations, path traversal in filesystem-touching servers.
Output format
$ mcp-audit examples/bad/
[HIGH ] starlette_badhost @ uv.lock
uv lockfile pins starlette==0.36.3 — vulnerable to BadHost (CVE-2026-48710). Patched in 1.0.1.
-> Upgrade Starlette to >=1.0.1 (the BadHost patch). If FastAPI pulls Starlette transitively, pin it explicitly. ...
[HIGH ] fastmcp_wrapper_layer @ server.py:18
tool 'fetch_url' (def) calls asyncio.run() inside its body. FastMCP invokes tools inside an already-running event loop, and asyncio.run() raises RuntimeError when nested. This will fail at the first real MCP protocol call even if every unit test passes.
-> Convert the tool to `async def` and replace `asyncio.run(...)` with `await`. ...
mcp-audit: 2 finding(s) — 2 high
--json emits one object: {"root": "...", "finding_count": N, "findings": [...]}. Each finding has check, severity, path, line, message, remediation.
What this is not
- It is not a runtime sandbox. Static analysis only.
- It does not install your venv to introspect it. It reads what's declared (manifests + lockfiles + source).
- It will not detect every vulnerability — only the classes its checks know about. Treat zero findings as "no known issues from this tool," not as a clean bill.
Background
- BadHost write-up: https://mcpdone.com/blog/badhost-mcp-servers
- FastMCP wrapper-layer bug write-up: https://mcpdone.com/blog/fastmcp-wrapper-layer-bug
Development
git clone https://github.com/Alienbushman/mcpdone-samples
cd mcpdone-samples/mcp-audit
pip install -e ".[dev]"
pytest
python smoke_test.py
To add a check: drop src/mcp_audit/checks/<name>.py exposing a module-level CHECK_ID and a check(root: Path) -> list[Finding] callable. Register it in src/mcp_audit/checks/__init__.py. Add fixtures + tests under tests/.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcpdone_audit-0.6.0.tar.gz.
File metadata
- Download URL: mcpdone_audit-0.6.0.tar.gz
- Upload date:
- Size: 25.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca99e6862658e197dbc722a3c6af211a18df07e615ebc7a0afe7c7e3c61a89e4
|
|
| MD5 |
747a3b2d24398878eae73f2268f1ef13
|
|
| BLAKE2b-256 |
cda86b19e496f27a75cc6d471470fc9642b99ee3d0349b4a53e41abb85f7eb8b
|
Provenance
The following attestation bundles were made for mcpdone_audit-0.6.0.tar.gz:
Publisher:
release.yml on Alienbushman/mcpdone-samples
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcpdone_audit-0.6.0.tar.gz -
Subject digest:
ca99e6862658e197dbc722a3c6af211a18df07e615ebc7a0afe7c7e3c61a89e4 - Sigstore transparency entry: 2040102846
- Sigstore integration time:
-
Permalink:
Alienbushman/mcpdone-samples@5c3b8748a4620894df348fd3dcfbb3c025a6604a -
Branch / Tag:
refs/tags/mcp-audit-v0.6 - Owner: https://github.com/Alienbushman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5c3b8748a4620894df348fd3dcfbb3c025a6604a -
Trigger Event:
push
-
Statement type:
File details
Details for the file mcpdone_audit-0.6.0-py3-none-any.whl.
File metadata
- Download URL: mcpdone_audit-0.6.0-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7968950b32895b438f8551f99032b21cfa21542cffdddc2ca43d397981a82148
|
|
| MD5 |
f7c308707b525ad554034c841c67ae0c
|
|
| BLAKE2b-256 |
a212ead4c71128a3eec38b0b8b572b8bb0c31947ed7e99b7a36124975705355d
|
Provenance
The following attestation bundles were made for mcpdone_audit-0.6.0-py3-none-any.whl:
Publisher:
release.yml on Alienbushman/mcpdone-samples
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcpdone_audit-0.6.0-py3-none-any.whl -
Subject digest:
7968950b32895b438f8551f99032b21cfa21542cffdddc2ca43d397981a82148 - Sigstore transparency entry: 2040102897
- Sigstore integration time:
-
Permalink:
Alienbushman/mcpdone-samples@5c3b8748a4620894df348fd3dcfbb3c025a6604a -
Branch / Tag:
refs/tags/mcp-audit-v0.6 - Owner: https://github.com/Alienbushman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5c3b8748a4620894df348fd3dcfbb3c025a6604a -
Trigger Event:
push
-
Statement type: