MCP·Engineering
← All case studies

mcp-security-scanner — a static security scanner for MCP servers

// security category · 781 passing tests · Python + JS/TS parity · public repo
Python 781 passing tests 7 detector families JS/TS parity reachability + taint grading (Python) public repo

The 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/.tsx on 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 -q781 passed, 9 skipped (790 total; the 9 skips are the dogfood self-audit tests, which need MCP_SCANNER_FLEET_ROOT pointed 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/690/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-mcp server — 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 →