mcp-security-scanner — a static security scanner for MCP servers
Python 430 passing tests 7 detector families JS/TS parity reachability + taint grading public repoThe problem
MCP servers are a new attack surface built fast, often by one person, and pointed straight at a model that will do what a tool tells it to. The vulnerability classes are specific — codegen injection, tool-parameter injection into a shell, write tools on by default, secrets handed back in a tool response — but they hide in exactly the code a solo builder ships without a second pair of eyes. And a scanner that cries wolf on every line is ignored by week two, so the hard part isn't finding patterns; it's grading them honestly enough that a human trusts the queue.
What I built
mcp-security-scanner reads an MCP server's source and flags the vulnerability
classes that actually show up in production, each with a severity, a file:line, a
remediation, and an honest confidence. Seven detector families, with two grading
passes layered on top:
- Seven detector families — codegen/template injection, tool-param injection (shell/eval/pickle/unsafe-yaml/SSRF/path-traversal), auth & network posture, secret handling, write-tools-on-by-default, secret-leak-via-tool-response, and job hazards (cron/systemd/CI/deploy-script scope, destructive calls, success-reported-without-verification).
- Python + JS/TS parity — four families now cover
.js/.mjs/.cjs/.ts/.mts/.cts/.jsx/.tsxon a brace/string-aware regex basis (param-injection, tool-scope-creep, secret-leak-via-tool-response, and secret-in-log). JSX brace syntax was verified not to trip the detectors with a real mixed-fixture pair, not assumed. - Reachability grading — after the detectors run, a static call-graph labels each finding reachable (inside or transitively called from a registered tool handler), unreachable-by-tools, or unknown. Reachable raises confidence, unreachable lowers it — a finding is never dropped on this basis.
- Tool-parameter taint tracking — a second pass seeds every tool handler's parameters as taint sources and propagates them (assignments, f-strings, containers, same-repo calls up to two import hops) into the injection sinks, labelling each finding tainted / untainted / unknown.
Evidence you can check yourself
Public on purpose — every claim maps to a file you can open before you reply:
- Repo: github.com/jaimenbell/mcp-security-scanner
- Tests:
python -m pytest -q→430 passed, 9 skipped(439 total; the 9 skips are the dogfood self-audit tests, which needMCP_SCANNER_FLEET_ROOTpointed at real repos and skip cleanly in a fresh clone). - Every detector ships a matched pair of fixtures — a vulnerable one it must catch, a clean one it must not flag — so the false-positive floor is a tested invariant, not a hope.
- The dogfood proof (
--self-audit,tests/test_self_audit.py): the scanner runs against eight of my own in-production MCP servers. Six get a clean bill; two (github-mcp, discord-mcp) show a low-confidence P1 — the scanner honestly flagging their own obviously-fake test-fixture tokens rather than hiding them, exactly the "demote confidence, never suppress visibility" rule the tool is built on. The test asserts no high/medium-confidence P0/P1 on the clean set, and it passes. - See a real sample report: the actual generated deliverable, run against my own
rag-mcpserver — one low-confidence P2 hardening finding, a clean P0/P1 bill, and the honest capability boundary printed on every report.
Printed on every report on purpose: this is static analysis, not a prover. It
reads code; it does not run your server, and it does not prove any finding is remotely
exploitable. A "clean bill" means these detectors found no critical/high patterns — not
a security guarantee. Confidence is load-bearing: low findings are "a human should
glance at this" and produce false positives by design. The JS/TS coverage is line-based regex,
not a full AST like the Python path — and the README documents exactly which JS/TS shapes it
does and does not catch, rather than letting you assume parity it doesn't have.
Where it's going: Managed MCP Watch
The engine is MIT-licensed and the repo is public — you can clone it and gate your CI with
--fail-on P1 for $0, forever, and I say so in every sales conversation. What a public
repo can't give you is what the Managed MCP Watch subscription sells: a
monthly human-triaged digest of net-new findings against your baseline (so your team never chases
noise), rule freshness as the MCP threat landscape moves, and a human to ask when a finding is
confusing. The scan runs in your CI via the shipped GitHub Action — your source never
leaves your machine and I hold no credentials. The open core stays free; the service wrapped
around it is the product.
What it shows about how I work
An over-flagging scanner is easy; a trustworthy one is the actual engineering. The whole design bends toward honesty — never drop a finding, grade its confidence with a real call-graph and taint pass, prove the false-positive floor with paired fixtures, and dogfood the thing against my own production servers where a miss would be visible. That's the same standard I'd bring to auditing your fleet.