I Audited 300 MCP-Related npm Packages. My Scanner Couldn’t Inspect Runtime Code in 217 .
I Audited 300 MCP-Related npm Packages. My Scanner Couldn’t Inspect Runtime Code in 217. So I created Driftward.This is not a malware count. It is a warning about security reports that say “zero findings” without showing how much executable code was actually inspected.The short versionI collected…
I Audited 300 MCP-Related npm Packages. My Scanner Couldn’t Inspect Runtime Code in 217. So I created Driftward.This is not a malware count. It is a warning about security reports that say “zero findings” without showing how much executable code was actually inspected.The short versionI collected 300 npm packages from 11 MCP-related registry searches and inspected their published tarballs without installing or executing them.The strongest result was a coverage failure, not a malware count.In 217 of 300 packages, or 72.3%, the tarball contained runtime code, but every runtime file fell under paths my precision-oriented scanner deliberately excludes, such as dist/, build/or minified output. The scanner could still read a manifest or README and produce a quiet result while inspecting none of the code that would actually run.217 coverage gaps, 80 packages with eligible runtime code, and 3 with no runtime code discovered.That does not mean 72.3% of MCP packages are invisible to every static-analysis technique. Bundles can be parsed. Source maps can sometimes recover original source. Specialized tools can deobfuscate or reason over generated JavaScript. It means something narrower and more useful:A negative static result is meaningless unless the result also tells you how much executable code was actually inspected.The frozen package list, exact versions, tarball integrity values, audit script, and raw results are available in the Driftward research directory.What this finding does and does not say: It says this scanner had no eligible runtime code in 217 tarballs. It does not say 217 packages were malicious, unsafe, or impossible for a different static analyzer to inspect.Why MCP changes the risk calculationA local MCP server is not merely a library imported into an application. With the common stdio transport, the MCP client starts it as a subprocess.The MCP project’s own security policy is explicit: the server runs with the same environment-level privileges as the client, and the SDK’s stdiotransport is not a sandbox. If the agent can read a credential, reach a network destination, or invoke a local tool, an unsandboxed server may inherit that reach.The official MCP security guidance recommends running local servers with minimal privileges and restricting filesystem and network access. That isolation is the host’s or operator’s responsibility.Source review still matters. But I wanted to know what source review can honestly tell us about the artifacts developers actually install.What I measuredThis was a convenience sample, not a census of the MCP ecosystem.The collector queried the npm registry using 11 search phrases, including mcp server, model context protocol, mcp connector, and modelcontextprotocol. It requested up to three 100-result pages per query, removed duplicates, retained package names containingmcp, and stopped at 300 names. Search ranking influences which packages enter the corpus; multiple packages from one publisher can appear, and a package whose name contains mcp is not necessarily an MCP server.For each package, I resolved the latest version available at collection time, downloaded the registry tarball directly, verified npm’s published integrity value, and examined its members in memory. All 300 integrity checks passed, and the audit completed with zero download or parse errors. Nothing was installed, extracted into an executable environment, or run.The scanner prioritizes precision. It looks for network APIs, literal URL hosts, subprocess execution, credential-path references, and a narrow set of prompt-injection phrases. It skips tests, fixtures, examples, generated dist/ and build/ trees, minified files, source maps, and files larger than 1 MB. Those exclusions reduce noisy findings. They also create measurable blind spots.ResultsCoverage belongs beside findings, not in a footnote. Frozen in results.json.The first row is the finding. The other rows must be read in light of it. For example, “zero credential references” does not establish that the corpus never accesses credentials. It means the scanner found none in the code it considered eligible, while 217 packages offered it no eligible runtime code at all.Four numbers worth retaining:217 packages had runtime code but zero eligible runtime files.107 of those packages shipped at least one source map; 110did not.50 packages exposed a network API or literal-host signal in eligible code.2 prompt-injection-shaped hits were both false positives after manual review.The two prompt-injection hits were false positivesThe scanner found injection-shaped phrases in two packages. Manual review showed that both were explanatory text. @mastra/mcp-docs-server@1.2.22 included evaluation examples containing phrases such as IGNORE PREVIOUS INSTRUCTIONS. @remnux/mcp-server@0.1.71 warns that malware samples may contain prompt-injection strings and explains how analysts should treat them.Neither hit was evidence that the server was trying to manipulate an agent. This is an important failure mode for keyword scanning: security documentation, test fixtures, and malicious instructions can contain identical strings. Context decides what they mean.A deliberately malicious test package returned zero riskOne package provided a useful coverage control. malicious-mcp-server@1.5.0 describes itself as “A deliberately malicious MCP server for E2E testing purposes.” Its published tarball contained three files: package.json (970 bytes), README.md (742 bytes), and dist/index.js(610,358 bytes).The source-first Driftward pass scanned the two small text files, excluded dist/index.js, reported no behavioral findings, and assigned a risk score of 0. A separate raw token count over the excluded bundle found six occurrences of exec, two of eval(, 52 of base64, and three of raw.githubusercontent.com.The correct result was not “clean.” It was “runtime coverage unavailable under this rule set.”Those strings alone do not prove execution or malicious behavior. In this case, the package’s own manifest supplies the context. The important fact is that the security-relevant bundle was never evaluated by the pass that produced the zero.Static analysis did not failSkipping generated output is a design choice, not a universal law of static analysis. A different scanner could parse the bundled JavaScript, inspect its AST, unpack modules, extract source maps, follow dependency metadata, compare provenance, or apply rules designed for minified code. Those techniques trade speed and precision for greater coverage.The mistake is treating “no findings” as a single state. It can mean at least three things:Relevant code was inspected, and no rule matched.Relevant code was inspected, but the rules missed the behavior.Relevant code was never inspected.Only the first is a meaningful clean static result, and even that is not proof of safety.What a useful static report should discloseA scanner should not collapse “nothing matched” and “nothing was inspected” into the same green result. For package artifacts, I would expect a coverage receipt beside every finding summary:runtime files discovered, files analyzed, and bytes analyzed;excluded files grouped by reason, including generated paths, size limits, unsupported formats, and parse errors;whether source maps exist and whether usable sources were recovered;the exact rule set and scanner version; anda result state that distinguishes no findings in inspected code from runtime coverage unavailable.This is a small reporting change with a large trust benefit. It lets reviewers decide whether a quiet result is evidence or merely absence of visibility.Why runtime behavior still mattersStatic review is a snapshot. Package behavior can change after an update, and a versionless command such as npx package-name can resolve to new code while the local MCP configuration remains unchanged.That is where runtime controls become complementary rather than competitive. Run each local server with its own identity and least-privilege policy. Record the destinations and capabilities it actually uses. Approve a baseline explicitly.Then compare later runs with that same server’s history. A configuration change should produce identity drift. A package update under an unchanged definition should still be visible if its runtime capabilities change.The current runtime model. macOS hard-pins egress to the loopback proxy; Linux currently provides filesystem and process confinement but not the same hard network pin.This is the model behind Driftward, the open-source tool I built while investigating the problem. Wrapped MCP servers run as separate principals such asmcp:github, each with its own policy, signed baseline, and drift history. The point is not to replace source review. It is to give source review a runtime backstop.The backend distinction matters. On macOS, Seatbelt hard-pins external sockets to Driftward’s loopback proxy. On Linux, bubblewrap currently provides filesystem and process confinement and supplies proxy environment variables, but the equivalent hard network pin is not yet shipped. The limitations document tracks that boundary explicitly.Recent research is moving in the same direction. The August 2026 preprint TrustShiftProbe models MCP servers that behave normally before changing at a later trust horizon, and evaluates runtime defenses based on behavioral baselines. It is a preprint, not settled consensus, but the temporal threat model is the right one: trust can change after review.LimitationsThe corpus is search-ranked and name-filtered, not random or representative.The numbers are a September 1, 2026 snapshot. npm packages and search results change.The audit measured package tarballs, not whether every package was a working MCP server.“Scanner blind” refers to this scanner’s exclusion rules, not all static analysis.Source-map presence was counted, but the audit did not determine whether a map embedded usable source.Literal URLs and API names are static signals, not proof that a code path executes.No package was detonated, so no claim is made about destinations contacted at runtime.A result with no findings is not a safety verdict.Reproduce the auditTo reproduce the exact tarball set, use the frozen results.json as the lock input:# self-bootstrapping — no install neededgit clone https://github.com/abdalhafeezbushara/driftward.gitcd driftwardpython3 research/mcp-npm-audit-2026-09-01/audit.py \ research/mcp-npm-audit-2026-09-01/results.json \ /tmp/mcp-audit-rerun.jsonTo collect a new 300-package snapshot from current npm search results:N=300 ./detonate/fetch-corpus.sh /tmp/mcp-packages.txtpython3 research/mcp-npm-audit-2026-09-01/audit.py \ /tmp/mcp-packages.txt /tmp/mcp-audit-current.jsonBoth paths download and inspect tarballs without installing or executing package code.A practical checklist for MCP usersPin what runs. Prefer an exact package version and integrity value over an unversioned one npx package-name.Review the published artifact. A clean repository is not enough if the installed tarball contains a generated bundle that was never inspected.Demand coverage numbers. Treat a report with no eligible runtime files as unavailable, not clean.Constrain each server separately. Give every local or bridged MCP server its own identity, filesystem scope, and host allow-list.Approve trust explicitly. Record a known-good run, sign the baseline, and fail CI or require review when capabilities change.The practical conclusionDo not abandon static analysis. Make its coverage explicit. For MCP servers and other agent-side tools, source review, artifact analysis, provenance checks, sandboxing, runtime recording, and behavioral drift detection solve different parts of the same problem.The most dangerous phrase in a security report may not be “malware detected.” It may be “zero findings” without an answer to the next question:Zero findings across how much code?This story is published on Generative AI. Connect with us on LinkedIn and follow Zeniteq to stay in the loop with the latest AI stories.Subscribe to our newsletter and YouTube channel to stay updated with the latest news and updates on generative AI. Let’s shape the future of AI together!I Audited 300 MCP-Related npm Packages. My Scanner Couldn’t Inspect Runtime Code in 217 . was originally published in Generative AI on Medium, where people are continuing the conversation by highlighting and responding to this story.Source: Generative AI Pub — Published — Category: Image AI