13 popular MCP servers, 1,170 raw findings, 1 genuine vulnerability
static analysis aggregate-only, no attribution honest false-positive triage 762 passing tests (mcp-security-scanner)
What this is: I ran mcp-security-scanner against 13 popular,
actively-maintained public MCP server repositories and aggregated the results. This page
publishes only counts, distributions, and anonymized patterns -- no finding
below is tied to a named repository, and the list of which 13 repos were scanned is
deliberately not published either, so it cannot be used to narrow down which one had the
real issue. Exactly one repo had a genuine, previously-undisclosed, actionable vulnerability.
Responsible disclosure is in progress for that finding; it is not detailed further here, by
design.
Methodology
13 repos were chosen for diversity -- official-org servers, frameworks, monorepos, and
single-purpose servers, spanning Python and JS/TS, all with recent commit activity. Each was
shallow-cloned (--depth 1), scanned read-only, and the JSON output was
hand-reviewed before any conclusion was drawn.
What "hand-reviewed" means here: every P0 finding (6 total) and every P1 finding outside test/fixture/example paths (roughly 90 of 194) was individually read in source to judge genuine reportability -- not just tallied. That distinction matters because a static scanner's raw finding count, taken uncritically, overstates real risk. See the results below for exactly how much it overstated it here.
Scanner version used for this scan: mcp-security-scanner at
pinned commit 15b5460, which had 264 passing tests (271 total) at that exact
commit -- static AST-based analysis for Python, regex-based for JS/TS.
The scanner has since grown. Current mcp-security-scanner canonical is 762
passing tests (771 total), live-verified 2026-08-04 at commit 915d1a3. Those are two
different numbers on purpose -- one is "what ran this scan," the other is "what the tool can
do right now" -- and this page is explicit about which is which rather than letting them blur
together.
The scanner never runs, imports, or sandboxes target code -- it reads git-tracked source text
and parses it (Python via ast; JS/TS via regex). Its stated philosophy is
"over-flag, never silently drop," which is the right default for a security tool but means raw
counts need a human pass before they mean anything. That human pass, and being explicit about
what it found, is the actual point of this page.
Results -- aggregate
| Metric | Value |
|---|---|
| Repos scanned | 13 |
| Total files scanned (union) | ~7,600 |
| Repos with 1 or more finding at any severity | 12 / 13 (92%) |
| Repos where the scanner returned no P0/P1 | 3 / 13 (23%) |
| Repos with 1 or more P0/P1 raw finding | 10 / 13 (77%) |
| Repos with a genuinely reportable P0/P1 vulnerability (post hand-review) | 1 / 13 (8%) |
| Total raw findings, all severities | 1,170 |
| Raw severity split | P0: 6 · P1: 194 · P2: 969 · P3: 1 |
| Confidence split | high: 365 · medium: 470 · low: 335 |
| Reachability split (discoverable tool call-graph) | reachable: 317 · unknown: 853 |
The headline gap is between raw severity and reviewed severity. 200 raw P0/P1 findings came out of the scanner; after reading every one in source, exactly one repo had a genuine, actionable, previously-undisclosed vulnerability at that severity. That is not a knock on the scanner -- an "over-flag, never drop" static tool is supposed to produce a review queue, not a verdict. It's the number for anyone tempted to use a raw finding count as a credibility score without doing the human pass: don't.
Detector-class distribution (raw, all 13 repos, all severities)
| Detector class | Findings | Share |
|---|---|---|
| Path traversal | 314 | 27% |
| Tool-scope-creep (ungated mutating tool) | 185 | 16% |
| SSRF | 168 | 14% |
| Network exposure (bind/debug posture) | 157 | 13% |
| Secret-in-log | 93 | 8% |
| Shell/command injection | 77 | 7% |
| Hardcoded secret | 59 | 5% |
| Job hazard: unverified success | 58 | 5% |
| Job hazard: destructive-no-confirm | 18 | 2% |
| Missing auth on mutating route | 10 | <1% |
| Debug console enabled | 8 | <1% |
| Code eval (exec/Function) | 6 | <1% |
| Secret leak via tool response | 6 | <1% |
| Codegen injection (template autoescape) | 5 | <1% |
| Unsafe deserialization | 3 | <1% |
| Tracked secret-bearing file | 2 | <1% |
| No rate limiter | 1 | <1% |
The honest false-positive classes
This is the part that matters more than the raw table above: what the 200 raw P0/P1 flags actually turned out to be once read in source, named generically (no repo identified):
- "Read-only" doc/proxy tools flagged as ungated mutating tools. More than one repo had a pure documentation-search or read-proxy tool -- no destructive verb, no actual mutating sink -- trip the tool-scope-creep detector. A real false-positive class: the detector's mutating-sink heuristic over-triggered on a downstream proxy call it couldn't distinguish from a write.
- Custom in-body permission checks not recognized as gates. At least one repo genuinely guards a delete-shaped tool with an explicit readonly-mode check that returns "not permitted" -- a real, working authorization gate the detector's gate-recognizer doesn't pattern-match because it isn't one of the handful of decorator/env-flag/named-check shapes it looks for. A "no visible permission gate" finding on this scanner means "no gate the heuristic recognizes," not "definitely unguarded" -- a false positive that should worry a tool author less than the opposite miss would.
- Self-declared "destructive" tools flagged as if undisclosed. A computer-use-style MCP server (application/window/input control) had tools whose own code already labels them destructive via the MCP tool-annotation spec -- the maintainer had already disclosed the exact risk through the protocol's own mechanism. The scanner has no path today to read that self-declared metadata and downgrade accordingly, so it flagged the tool as if the risk were hidden. Worth a scanner enhancement, not a maintainer disclosure.
- Pagination cursors tripping the secret-name heuristic. Every observed "secret-leak-via-tool-response" finding outside a test file was a cursor-style pagination field whose name merely contains the substring "token" -- an opaque continuation handle, not a credential. The name-shape heuristic needs a pagination-cursor exclusion list to cut this noise class.
- Almost all non-test shell-injection findings were the maintainers' own build/release tooling -- package-manager installs and builds, VCS tag/push commands, license-checker invocations, install/uninstall CLI scripts -- none reachable from a registered MCP tool, none attacker-influenced. Same story for essentially all "destructive-call-with-no- confirm" job-hazard findings: cleanup commands against the maintainers' own temp directories inside their own CI workflows, force-push/reset commands against their own release branches. Real hardening advice for a CI-hygiene pass, not attacker-reachable vulnerabilities in a live server.
- A JS-regex sink detector confused two unrelated functions that share a name. Observed 3 times against one repo's own internal tooling script -- a naming collision the line-based JS heuristic can't resolve without real parsing. Disclosed as a known scanner limitation, not a target-repo issue.
- A "hardcoded secret" hit turned out to be a widely-published placeholder credential from a major SDK's own public example documentation, already marked with an explicit allowlist-suppression comment by the maintainer. The secret-shape heuristic can't distinguish a famous public placeholder from a real leaked value without a denylist of known-dummy literals.
- A tracked "secret-bearing file" was a self-signed TLS test certificate -- the common pattern of committing a throwaway cert/key pair to spin up a local HTTPS test server in CI, not a real secret leak. The extension-based heuristic can't tell a disposable test fixture from a production key without content inspection.
The one genuine finding
Of the 200 raw P0/P1 flags across 13 repos, one was a template/codegen-injection pattern --
the exact class this scanner is validated to catch (it's the tool's own --self-audit
proof point) -- in a code-generation tool that renders a caller-influenced identifier
directly into generated source with autoescape explicitly disabled and a narrow rationale
that didn't address the code-injection risk specifically. Responsible disclosure is in
progress. No repo is named here, no scope table is published alongside this report, and
this finding will not be detailed further until disclosure has run its course.
Honest scanner blind spots (disclosed, not fixed here)
- Reachability/taint cross-file resolution is bounded (one hop for tool-scope-creep/ secret-leak low-level-SDK paths; two hops for taint) -- deeper flows are labeled unknown, never guessed.
- No dynamic analysis -- a "clean bill" means the detectors found no critical/high static pattern, not a proof of runtime safety.
- JS/TS coverage is regex-based, not AST-based -- the function-name-collision false positive above is a direct consequence.
- The scanner does not read MCP tool-annotation metadata (e.g. a maintainer-set destructive-tool flag) that a maintainer may have already declared, so it cannot credit self-disclosed risk.
- Gate recognition is pattern-based (decorator, env-flag, or a short list of named-check shapes) -- a project-specific custom gate function can go unrecognized.
Bottom line
Across 13 well-known public MCP servers, the ecosystem is not littered with undisclosed critical vulnerabilities -- it's littered with a scanner producing an honest, over-inclusive review queue that a human still has to read. One genuinely reportable finding came out of 200 raw P0/P1 flags. That ratio is itself the credibility pitch: a scanner that publicly counts its own false positives is more trustworthy than one that reports a scarier number nobody checked.