MCP·Engineering
← All case studies

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

// security category · 430 passing tests · Python + JS/TS parity · dogfooded on eight real servers · public repo
Python 430 passing tests 7 detector families JS/TS parity reachability + taint grading 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 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 -q430 passed, 9 skipped (439 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 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-mcp server — 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.

Book a scoping call  Next case study →