Zero Findings Across How Much Code? Auditing 300 MCP npm Packages
A tarball audit of 300 MCP server packages on npm found 217 with runtime code the scanner never read. Why a clean result needs a coverage receipt attached.
This article is published in English.
Three hundred npm packages with MCP in the name were pulled from the registry and examined as published tarballs, without ever being installed or run. The most striking outcome had nothing to do with a count of malware. It was a coverage failure: for 217 of the 300 packages (72.3%), the tarball held runtime code, yet every runtime file sat under a path that the scanner skips on purpose, such as dist/, build/ or minified output. The tool could read a manifest and a README, report a calm result, and look at none of the code that would really execute.
The split was 217 coverage gaps, 80 packages with eligible runtime code, and 3 where no runtime code turned up at all.
That does not mean 72.3% of MCP packages are invisible to static analysis in general. Bundles can be parsed, source maps sometimes restore the original code, and specialized tools can deobfuscate or reason about generated JavaScript. The finding is narrower and more useful:
A negative static result carries no meaning unless it also states how much executable code was actually inspected.
Scope matters here. The result means one particular scanner found nothing eligible to read in 217 tarballs. It makes no claim that those packages are malicious or unsafe, or that another analyzer could not inspect them.
Why MCP raises the stakes
A local MCP server is not just a library an app imports. Under the usual stdio transport, the client spawns it as a child process. The project's security policy spells out that the server holds the same environment-level privileges as the client and that the SDK's stdio transport offers no sandbox. If the agent can reach a credential, a network address or a local tool, an unsandboxed server can probably reach it as well.
Official MCP security guidance recommends starting local servers with minimal privileges and limiting filesystem and network access. Providing that isolation is up to the host or operator. Source review remains valuable, but the open question was what it can honestly say about the artifacts developers actually install.
How the sample was built
This was a convenience sample and not a census of the ecosystem.
The collector ran 11 npm registry searches, using phrases such as mcp server, model context protocol, mcp connector and modelcontextprotocol. It took up to three pages of 100 results per query, dropped duplicates, kept names containing mcp, and stopped at 300. Search ranking shapes who gets in, one publisher can appear many times, and a name containing mcp does not guarantee an MCP server.
For each package the latest version at collection time was resolved, the registry tarball was fetched directly, npm's published integrity value was verified, and the members were read in memory. All 300 integrity checks passed and no download or parse errors occurred. Nothing was installed, unpacked into an executable environment or run.
The scanner is tuned for precision. It searches for network APIs, literal URL hosts, subprocess calls, mentions of credential paths and a narrow set of injection phrases, while passing over test code, fixtures, examples, compiled output folders, minified files, source maps and anything above 1 MB. The skips reduce noise but also create quantifiable blind spots.
What came out
Coverage belongs next to the findings, not in a footnote. The first line of the results is the main finding, and every other line has to be read through it. A reading of zero credential references, for instance, does not show that the corpus never touches credentials. It shows only that the scanner found none in code it considered eligible, even though 217 packages gave it nothing eligible to read.
Four figures stand out:
- Runtime code but zero eligible runtime files: 217 packages.
- Of those 217, 107 included a source map and the other 110 did not.
- A network API or literal-host signal in eligible code: 50 packages.
- Prompt-injection-shaped matches: 2, and manual review cleared both as false positives.
Two false alarms
Injection-style phrases turned up in two packages, and a manual read showed both were explanatory text. One, the Mastra docs server at version 1.2.22, contained evaluation examples with strings like IGNORE PREVIOUS INSTRUCTIONS. The other, the REMnux MCP server at 0.1.71, warns that malware samples may carry prompt-injection strings and tells analysts how to treat them.
Neither was evidence of an attempt to manipulate an agent. This is a known weakness of keyword scanning: security documentation, test fixtures and hostile instructions can contain identical strings, and only context settles which is which.
A malicious test package that scored zero
One package worked as a coverage control. malicious-mcp-server 1.5.0 describes itself as a deliberately malicious MCP server for end-to-end testing. Its tarball held three files: package.json at 970 bytes, README.md at 742 bytes, and dist/index.js at 610,358 bytes.
The first-pass scan read the two small text files, skipped dist/index.js, found no behavioral signals and gave the package a risk score of 0. Counting raw tokens across the skipped bundle afterwards showed exec six times, eval( twice, base64 52 times and raw.githubusercontent.com three times.
So the honest verdict was not a clean bill of health. It was that runtime coverage could not be established under the rules in force. Those strings prove neither execution nor malice by themselves, and the manifest of the package supplies the missing context. The point is that the bundle carrying all the risk never reached the pass that emitted the zero.
The technique was not at fault
Leaving out generated output is a design decision, not a rule of the discipline. A different scanner might parse bundled JavaScript, inspect the syntax tree, unpack modules, pull out source maps, trace dependency metadata, compare provenance, or run rules written for minified code. Each of these gives up some speed and precision to gain reach.
The error lies in reading no findings as one single condition, because it can stand for at least three different situations:
- The relevant code was read and no rule fired.
- The relevant code was read, but the rules could not recognize the behavior.
- The relevant code was never read.
Only the first is a meaningful clean result, and even that is not proof of safety.
What a good report should disclose
A scanner should not merge nothing matched and nothing was inspected into one green result. For package artifacts, a coverage receipt should accompany every summary of findings:
- How many runtime files exist, how many were analyzed, and how many bytes that covered.
- Everything excluded, bucketed by cause: generated paths, size caps, unsupported formats, parse failures.
- The presence of source maps and whether real sources could be recovered from them.
- Which rule set and scanner version produced the result.
- A distinct result state for no findings in inspected code versus runtime coverage unavailable.
It is a small change in reporting with a large gain in trust, because reviewers can tell whether a quiet result is evidence or only a lack of visibility.
Why runtime behavior still counts
Static review is a snapshot. Behavior can change with an update, and an unpinned command like npx package-name can resolve to new code while the local MCP config stays the same. Runtime controls therefore complement static work instead of competing with it. Each local server should run under its own identity with a least-privilege policy. Its actual destinations and capabilities should be recorded and a baseline approved explicitly. Later runs are then compared with that server's own history, so that a config change produces identity drift, and a package update under an unchanged definition is still visible if runtime capabilities shift.
One open-source tool follows this model by wrapping each MCP server as its own principal, for example mcp:github, with a separate policy, signed baseline and drift record. It is meant as a runtime backstop for source review and not a substitute. Platform support is uneven: on macOS, Seatbelt pins outbound sockets to a loopback proxy, whereas on Linux bubblewrap so far provides filesystem and process confinement plus proxy environment variables, without the equivalent hard network pin.
Newer research moves along the same line. TrustShiftProbe, an August 2026 preprint, models MCP servers that act normally and later switch behavior at some trust horizon, and it tests runtime defenses founded on behavioral baselines. A preprint is not consensus, yet its time-aware threat model is the correct one, because trust can erode after a review is done.
Limits of the audit
- Selection came from search ranking and name filtering, so the sample is neither random nor representative.
- All figures describe 1 September 2026, and both npm contents and search results move.
- What was measured is tarballs, not whether each one is a functioning MCP server.
- The phrase scanner blind applies to this scanner's exclusion rules, not to static analysis overall.
- Counting source maps was done, yet nobody checked whether each map embeds usable source.
- Literal URLs and API names are static hints and do not show that a code path runs.
- Packages were never detonated, so network destinations at runtime remain unknown.
- Finding nothing is not a safety verdict.
Reproducing the run
To rebuild the exact tarball set, use the frozen results file as the lock input:
# self-bootstrapping — no install needed
git clone https://github.com/abdalhafeezbushara/driftward.git
cd driftward
python3 research/mcp-npm-audit-2026-09-01/audit.py \
research/mcp-npm-audit-2026-09-01/results.json \
/tmp/mcp-audit-rerun.json
To collect a fresh 300-package snapshot from current npm search results:
N=300 ./detonate/fetch-corpus.sh /tmp/mcp-packages.txt
python3 research/mcp-npm-audit-2026-09-01/audit.py \
/tmp/mcp-packages.txt /tmp/mcp-audit-current.json
Neither route installs a package or executes any of its code; both only fetch and read tarballs.
A short checklist for MCP users
- Pin versions. An exact version plus integrity value is better than an unversioned npx call.
- Look at the published artifact. A clean repository counts for little if the installed tarball contains a generated bundle that nobody opened.
- Ask for coverage figures. If a report shows no eligible runtime files, read it as unavailable and not as clean.
- Fence in each server individually, with its own identity, filesystem scope and allowed hosts, whether it runs locally or through a bridge.
- Make trust a deliberate step: record a known-good run, sign that baseline, and block CI or demand review once capabilities change.
Dropping static analysis is not the lesson; stating its coverage is. For MCP servers and similar tools used by agents, reading source, analyzing built artifacts, checking provenance, sandboxing, recording runtime behavior and detecting drift each cover a different slice of one problem. A security report's scariest line may not be malware detected but zero findings, left without the next question: zero findings across how much code?