90 OF 100 FAIL THE FIRST CHECK

We scanned the top 100 public MCP servers' own install instructions with an open-source security scanner. Here's what came back.
Scan date: 2026-07-19 — llm-safe-haven scan --mcp v0.3.1, 8 detectors, offline + --online provenance check
Servers scanned
100
Unpinned install
90
No provenance
38
Credential literal
22
Typosquat FPs — found & fixed
5
Scan incomplete
0

The install snippet is the attack surface

On June 9, 2026, StepSecurity and BleepingComputer disclosed "Hades MCP-targeting" — the seventh wave of the Mini Shai-Hulud npm supply chain campaign, and the first to directly target the MCP server ecosystem instead of general-purpose npm/PyPI packages. The attacker published malicious packages impersonating popular MCP server libraries for Claude Code, Cursor, and GitHub Copilot integrations. A developer who installs a compromised MCP server doesn't just add a risky dependency — they hand the attacker a seat inside their coding agent's own tool-execution loop.

Nine days later, a second, unrelated cluster of advisories landed: the "June MCP-Server Insecure Default Wave" (June 18–26). mcp-pinot shipped with no auth and a public bind (CVSS 10.0, full cluster read/write). dbt-mcp leaked access and refresh tokens over an unauthenticated endpoint. backpropagate shipped an --auth flag that was a documented no-op — the backend never read the environment variable the flag was supposed to set. Five CVEs, one pattern: the server's default, out-of-the-box configuration — the thing its own README tells you to paste — was the vulnerability.

That's the premise of this research: scan --mcp reads configs, not packages. So we pointed it at the one thing every MCP server author controls and every user copies verbatim — the recommended install snippet in the README — for the 100 most-downloaded public MCP servers, and counted what came back.

Headline numbers

Out of 100 servers, using llm-safe-haven's own 8-detector scan --mcp suite (v0.3.1) against each server's own recommended install snippet:

Full methodology, the exact commands to reproduce this, and every source consulted: METHODOLOGY.md.

Per-detector breakdown

unpinned-execution — 90/100. The dominant finding by a wide margin, and the most avoidable: pin a version or a full git SHA (both count as pinned as of v0.3.1 — a floating @latest/@next/@canary tag, or nothing at all, does not).

provenance — 38/89 npm-resolvable servers lack an attestation (11 servers' recommended snippets don't use a directly-resolvable npx/uvx package spec — Docker, absolute binary paths, wrapped shell invocations, or remote-URL-only transports — so provenance is not-applicable for those; each still has an associated npm package, the install command just isn't the kind this detector can evaluate). Remember what this number does and doesn't mean: has-attestation means the package was published through npm's own provenance pipeline. It is not a claim that the code inside was audited or is safe — see the fidelity-limits section below.

insecure-endpoint — 1/100. Both hits come from a single server's recommended multi-server example: two HTTPS endpoints (the server's own remote transport, plus a third-party service endpoint shown in the same example block) with no recognized authentication header configured. Notably, neither hit is plain-HTTP — in this dataset the remote-transport failure mode is skipped authentication, not skipped TLS, mirroring the exact failure mode behind mcp-pinot/dbt-mcp above. Remote-transport snippets are rare in the top 100, so read this as a shape, not a rate.

credential-passthrough — 22/100. All 22 are the sensitive-name-literal rule (inlined literal value for a sensitive-sounding env var name) — none triggered the inlined-secret (a value that looks like an actual live credential) or broad-inheritance rules. In every case we inspected, the literal value is a documentation placeholder (YOUR_API_KEY, <YOUR_TOKEN>), not a real leaked credential. The finding is about the pattern the README teaches, not a specific leak: a JSON config file that says "put your real API key right here" is a config file that regularly ends up committed to a repo with the real key still in it.

scope-breadth — 1/100. One filesystem/shell-capable server recommends no bounded path argument in its default snippet. This detector is deliberately narrow by design (a small allowlist, false negatives accepted) — a low count here is expected, not reassuring.

tool-poisoning / tool-shadowing — 0/100. No injection-phrase or invisible-Unicode hits in any config-adjacent string across 100 servers. The tool-shadowing zero deserves an asterisk: the scan fixtures rewrite every server to a unique generated name (so two unrelated servers can't collide merely by both calling themselves filesystem), which also makes a within-batch name collision structurally impossible — that zero is by construction, not a measurement. Read both alongside the fidelity limits below — this detector is a static heuristic over config text, not a live tools/list inspection, so a clean result here says nothing about a server's actual runtime tool descriptions.

typosquat — 5/100, and all five were a confirmed detector false positive, not five real typosquats. @ui5/mcp-server, @cap-js/mcp-server, @launchdarkly/mcp-server, @hubspot/mcp-server, and @browserstack/mcp-server were each flagged scope-confusion because the allowlist's one @sentry/mcp-server entry seeded mcp-server as a "known" name half — and mcp-server turns out to be a generic leaf name that SAP, LaunchDarkly, HubSpot, and BrowserStack all independently and legitimately chose for their own official package. We named this as a detector bug, not a finding about any of those five vendors — every one of them is a real, official, first-party integration.

This one is found-and-fixed, not just filed. PR #86 shipped a token-composition filter in lib/mcp/detectors/typosquat.js: a full-spec allowlist entry's name half (like @sentry/mcp-server's mcp-server) now only seeds the exact-match scope-confusion pool when it is not composed entirely of the generic tokens mcp/server. @ui5/mcp-server and the other four stop being flagged as squats of Sentry's package, while a genuinely distinctive half like context7-mcp stays eligible — the fix is a token-composition rule, not a stoplist, so it doesn't quietly blind the detector to real squats that happen to contain those words. The regression tests are non-vacuous: a data-driven RED baseline confirmed all 5 known false positives failed against the unmodified detector before the fix landed, and true-positive coverage was checked to still hold after. This is exactly the kind of thing running your own detector at 25x the normal dogfood scale is supposed to surface — PR #81 caught a different false positive the same way one scale-order down, on our own machines, the day of a release.

Cross-set tool-shadowing

We did not compute a tool-name collision count across the 100-server set. Tool names are not statically recoverable from a recommended-install-snippet scrape — that would require parsing each server's own README tool-reference table, a different (and much larger) per-server extraction task than collecting one JSON snippet. Every server in this dataset is counted as skipped rather than reporting a fabricated collision figure. tool-shadowing findings above are the scanner's own static server-NAME collision proxy only (within each fixture batch), never a verified tool-level shadowing check at the protocol layer.

How this was collected

selection.json — top 100 by last-30-day npm downloads, union of the official modelcontextprotocol/servers reference implementations and an npm-search MCP-server heuristic (name/keyword match plus a real "does it have a bin entry" executable check). snippets.json — each server's own recommended mcpServers JSON block, extracted verbatim from its README (or synthesized, flagged, when no such block exists). dataset.json — the real scan --mcp --json --online envelope run against fixtures built from those snippets, merged mechanically by server name — every finding above traces back to an actual scan output, never hand-typed. Full detail, exact commands, and every documented deviation from the original methodology: METHODOLOGY.md.

What this can't see

This is the same honesty section docs/mcp-security.md §5 carries for the shipped tool, because this research ran the exact same static analysis with the exact same blind spots:

Check your own setup

Everything above comes from the same tool you can run against your own machine right now. Pin the version — that's the same lesson the headline number teaches, applied to the scanner itself:

npx llm-safe-haven@0.4.0 scan --mcp

scan --mcp now discovers configs across 10 agents — Claude Code, Cursor, Windsurf, Cline, Continue.dev, Codex CLI, Gemini CLI, Goose, Google Antigravity, and GitHub Copilot (VS Code) — not just the 5 this dataset's own scan used. See docs/mcp-security.md for the full per-agent path table.

Add --online to also check provenance attestations for any npx/uvx-resolvable servers you have configured, and --json for a CI-friendly machine-readable report.