mcp-security-scanner — a static security scanner for MCP servers
Python 781 passing tests 7 detector families JS/TS parity reachability + taint grading (Python) 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 over Python sources 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→781 passed, 9 skipped(790 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 measurement that matters — and it is not flattering. On 2026-07-29 I measured this scanner blind against five third-party MCP servers (notion, neon, qdrant, firecrawl, airtable), with ground truth from five independent audits run before the first scan — audits whose per-item record was not retained, so only the derived totals survive (noted 2026-09-09). It returned 0 true positives across 58 findings, and recall 0/69 — 0/7 even restricted to the three classes it implements. Its best case,
mcp-server-qdrant, found 0 of 9 real issues including a path traversal a blind auditor reproduced live; on firecrawl it flagged twelve fake test-fixture secrets and missed the one real credential. I pulled the paid audit tier that same day and blocked every outreach draft from citing a finding. Recall fixes the next day raised it to 2 findings that survive a hand audit — by my own README's accounting, 1 of real-world consequence. Pooled recall has not been re-scored since, so I make no claim about a current number. That is the whole record, including the part that improved. - Why it is still public: the detector engine is honest about what it cannot do — every finding carries a graded confidence, a report says which findings were never graded, and the false-positive floor is a tested invariant. That machinery is real and reusable. Its detection recall is what came back zero, and nothing on this page asks you to take that on faith.
- See a real sample report: the actual generated deliverable, run against my own
rag-mcpserver — shown so you can judge the report format and the capability boundary it prints. Read it as a sample of the deliverable, not as a security assurance: given the recall above, a clean bill from this tool carries no information.
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 — install it with
pip install jaimenbell-mcp-security-scanner (or clone it) and gate your CI with
mcp-scan --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, and prove the false-positive floor with paired fixtures. I also ran it against my own servers — but the blind third-party measurement above is what actually settled the question, and it settled it against the tool. Running it on your own code tells you very little; measuring it against ground truth you fixed in advance tells you everything. Learning that about my own work, and publishing it, is the standard I'd bring to auditing your fleet.
Book a scoping call See Managed MCP Watch → Next case study →