Research / MCP ecosystem review

One in three popular MCP servers ships the lethal trifecta. The risk isn't in any file, it's in the graph.

A per-file scanner reads one config at a time, and at that altitude these servers look fine. We do something different: we build a system model of each one, its tools, the data and networks they reach, the trust boundaries they cross, and review the architecture. That is where the one-in-three finding lives, across 390 of the most popular open-source MCP servers, and it is what a per-file scanner structurally cannot produce.

390 servers modeled ≥ 200 stars each pinned commits system model, not a linter reproducible harness

The unit of analysis nobody scans

Almost nobody attacks an MCP server's source code. They attack what users actually run: the mcpServers block a README tells you to paste into Claude Desktop, Cursor, or VS Code. That block decides which packages auto-install at launch, which secrets sit in the environment, what the server can reach, and what else is live in the same agent session.

So that is what we modeled. 390 public MCP server repositories, each with at least 200 GitHub stars, cloned at a pinned commit and run through attestral, an open-source design-review tool for agentic systems. It does not lint files in isolation. It builds a system model of each server, the components, the edges between them, and the trust boundaries they cross, and reasons about what an injection could actually reach. Nothing was executed, no endpoint was contacted, no secret was read.

389 scanned cleanly, 387 distinct repositories after removing two that GitHub enumerated under both an old and a renamed slug. 233 distinct repos shipped a configuration concrete enough to model. Those 233 are the denominator for every percentage here, so nothing is inflated by repos that had nothing to scan, and nothing is counted twice.

The headline: one in three

390
repositories reviewed, pinned commits
233
shipped a modelable config, the denominator
32.6%
assemble the lethal trifecta
100%
of trifecta findings reproduce on re-scan

76 of 233 modelable configurations, 32.6%, assemble the lethal trifecta: private-data access, untrusted-input exposure, and an outbound channel, co-resident in one agent session. An agent that can read your data, ingest attacker-influenced content, and send anything out needs exactly one indirect prompt injection to become an exfiltration pipeline. No component is individually broken. The composition is the vulnerability.

Three things make the number worth taking seriously.

It is stable across corpus size

At 222 repositories the rate was 32.7%. At 390, 32.6%. Growing the corpus by three quarters moved the needle a tenth of a point. This is what the ecosystem looks like, not what one sample looks like.

Every finding reproduces

We re-scanned every trifecta finding from clean clones and got identical results, then hand-audited the tier of repositories that commit their configuration as files. All of them reproduce deterministically. There is no run-to-run noise in the headline.

The flagship is a real deployed fleet, verified by hand

The single named example, held for coordinated disclosure, is not a statistical abstraction. It is one popular server whose committed MCP config and committed sub-agent combine into a live trifecta, confirmed by reading the two files.

One caveat, stated plainly

The 32.6% counts documented configurations, and a minority assemble the trifecta only by combining several documented example blocks that nobody deploys together. Those stay in the aggregate but are never presented as single fleets. The per-fleet examples we stand on individually, like the flagship, are verified from the committed files, not inferred from a count.

What else the sweep found

Across the 233 modelable configurations, the per-server patterns are loud. Read the next section before you trust any single row of this table.

PatternShareWhat it means
Lethal trifecta (composition)32.6%One injection away from exfiltration
Auto-installing installer (npx -y / uvx)30.0%Every launch is a supply-chain event
Secrets handed to the process via env25.8%Tokens sit where tool output can echo them
Toxic flow: untrusted input reaches an action16.7%The injection has somewhere to go
Shell plus outbound network in one fleet15.0%Code execution with an exit
Mutable @latest / :latest pin15.0%Tomorrow's server is not the one you audited
Plaintext http:// transport9.4%Credentials and tool traffic in the clear
Remote endpoint genuinely unauthenticated0.9%See the next section

By category, database-backed servers were the worst slice (14 of 14 modelable configs had findings), followed by browser and web-automation servers (93%) and search servers (92%). The servers most useful to an agent are the ones that combine data access with reach.

The number that fell from 48% to under 1%

The most instructive finding in this study is one we deleted.

Early sweeps reported that nearly half of remote MCP endpoints shipped "without authentication." That number was wrong, and the way it was wrong matters. The MCP spec mandates OAuth 2.1 for HTTP transports. The token is negotiated interactively at connect time, so a spec-compliant hosted endpoint never has a credential in its static config. A scanner that reads "no token in the file" as "no auth" is manufacturing alarms out of correct behavior.

We fixed the rule to fire only on genuinely exposed endpoints: non-loopback, plaintext http://, no declared credential. The rate across the corpus fell to 0.9%. Two repos out of 233.

We found this, so we fixed it

Every loud per-server number deserves this treatment. A token in an env var is standard practice worth a footnote, not a headline. cmd /c npx on Windows is a launcher idiom, not a shell exposure. We publish the false positives we killed alongside the findings we kept, because a scanner that will not show you its noise floor should not be trusted with its signal.

ATL-109 ×5 false positive
"Five hosted servers expose a remote endpoint with no authentication."
Reality: OAuth. Five well-known hosted endpoints require OAuth 2.1; the token is obtained interactively and never appears in static config. "No token in the file" means compliant, not open.
ATL-202 merge artifact
"A large cloud vendor documents a lethal trifecta."
Reality: a stitched graph. One monorepo documents 132 separate example configs; modeling them as one fleet manufactures a trifecta across servers never deployed together. On the committed config there is none.
ATL-103 idiom, not shell
"This server exposes a shell."
Reality: the Windows launcher. "command":"cmd","args":["/c","npx",...] is how Windows starts an npm package, not a shell-exposing server.

Why the signal and the noise trade places

Notice the shape of what survived scrutiny and what did not.

Everything that dissolved was a property of a single component: this endpoint's auth, this installer's pin, this env var. Single-component facts have single-component explanations, and most turn out to be fine in context.

The finding that survived is a property of a relationship: a tool over here reads private data, a tool over there talks to the internet, and untrusted content can travel between them. No single file contains that finding. It only exists in the graph of the whole session, which is why per-file scanners, however many rules they carry, structurally cannot see it.

In the agentic era the loud, per-component alarms are mostly noise. The quiet, cross-component compositions are where the risk lives.

That is the bet behind attestral: build the system model first (components, edges, trust boundaries), then review the architecture. The trifecta check is not a rule that matches a string. It is a query over the graph. The information-flow field notes show how the same finding becomes a formal property you can discharge.

The one that survived, anonymized

Two files, both committed to a popular server's repository, combine into one agent session that can read private data, ingest untrusted input, and reach an outbound channel.

Untrusted input + exit
.mcp.json
A server profile enables an outbound web-browsing tool, which both fetches attacker-influenced content and provides a channel out.
+
Code execution + private data
.claude/agents/ sub-agent
A committed sub-agent carrying tools: Bash, Read, Glob, Grep, shell and filesystem access, in the same session.
# one indirect prompt injection is enough
web content (untrusted)
    the web-browsing tool ingests it into the session
    injected instruction reaches Bash + Read   # private data + code exec
    result leaves over the outbound tool        # exfiltration
ATL-202 lethal trifecta   (edges: ATL-203 shell+network, ATL-207 toxic flow)

Verified live against the current commit. This is a configuration default, not an exploited vulnerability; nothing was run and no secret was read. The maintainers were notified with a fix and a response window before this example was written down. It will be named in a follow-up once that window closes.

What to actually do

Four defaults close almost everything above.

01

Pin the installer

Exact versions, no -y. An install should be a deliberate act, not a side effect of launching your editor.

02

Keep secrets out of the pasted block

Reference a secret manager or OS keychain. Anything inline eventually transits model context.

03

Authenticate and encrypt every remote endpoint

OAuth per the MCP spec, TLS only. The 0.9% that are genuinely open are trivially findable.

04

Never co-locate the trifecta

Private data, untrusted input, and an outbound channel in one session need a human checkpoint. This is the one a linter cannot decide for you.

Check your own machine

The scanner is open source (Apache 2.0), runs locally, and needs no API key. Run the last line and you get a screenshot-ready card that says, in one screen, whether your own installed fleet assembles the trifecta.

pip install attestral
attestral scan --local           # audits the MCP servers configured on this machine
attestral scan <path>            # reviews a repo's agent + cloud design
attestral scan --local --card    # the one-screen self-audit, made to share

--local reads the MCP configs your clients already have installed (Claude Desktop, Cursor, VS Code, Windsurf) and tells you if you are running the trifecta right now.

Method, honestly

The whole point of a security claim is that someone can falsify it. So the harness, the pinned target list, and the per-repo output are reproducible: clone each target at its recorded commit, run the published CLI, and you get the same findings.

  • Corpus: 390 targets, at least 200 stars, hand-curated anchors plus API enumeration, every URL verified. One repo failed to clone; two were enumerated under both an old and renamed slug and are deduplicated everywhere (no rate moves more than 0.3 points). One deliberately vulnerable training repo is a known-positive harness anchor; removing it too gives 32.3%, still one in three.
  • Every finding is a configuration default, not an exploited vulnerability. Nothing was run; no endpoint was contacted; no secret was read.
  • Percentages are computed over the 233 distinct repos with a modelable config. Repos with no scannable surface are excluded from both numerator and denominator.
  • ML-based injection scoring ran but is excluded from every headline number; heuristic tiers flag benign instructional text too often to headline without review.
  • Withdrawn findings are documented, not deleted. Honesty by elimination is the method, not a disclaimer.