{"_id":"@allenwu06/mcpaudit","name":"@allenwu06/mcpaudit","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@allenwu06/mcpaudit","version":"0.1.0","description":"Static pre-install security scanner for MCP (Model Context Protocol) servers — `npx mcpaudit <path>` flags command injection, credential/env exfiltration into LLM-visible output, over-broad filesystem/tool scope and dynamic eval before you wire a server i","type":"module","license":"MIT","private":false,"bin":{"mcpaudit":"bin/mcpaudit.js"},"engines":{"node":">=20"},"scripts":{"test":"vitest run","test:watch":"vitest","scan":"node bin/mcpaudit.js"},"keywords":["mcp","model-context-protocol","security","scanner","static-analysis","supply-chain","llm","ai-agents","cli"],"devDependencies":{"vitest":"^2.1.9"},"_id":"@allenwu06/mcpaudit@0.1.0","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-qkrDg1pr4671yREr7t5D3z9RVZZppzJYvOBL+kUQ3Iqdqk1jeALCp8oOtQy+7U/+LnkTNKpOwFrUZB3oH+PGbA==","shasum":"d2e559dc3a334dea869fcebb456d337f8727a765","tarball":"https://registry.npmjs.org/@allenwu06/mcpaudit/-/mcpaudit-0.1.0.tgz","fileCount":12,"unpackedSize":128916,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIQCAVa1Cin+O5aLn02bwJ8KtKhvAgPtOp7SFy8a/Wb30wwIfFFpUp1UlHMB36IuJYH+qcd1Y3/yWC6e0rGETZmBkmA=="}]},"_npmUser":{"name":"allenwu06","email":"allenwu@umich.edu"},"directories":{},"maintainers":[{"name":"allenwu06","email":"allenwu@umich.edu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mcpaudit_0.1.0_1779208939284_0.2819104700558177"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T16:42:19.207Z","0.1.0":"2026-05-19T16:42:19.435Z","modified":"2026-05-19T16:42:19.642Z"},"maintainers":[{"name":"allenwu06","email":"allenwu@umich.edu"}],"description":"Static pre-install security scanner for MCP (Model Context Protocol) servers — `npx mcpaudit <path>` flags command injection, credential/env exfiltration into LLM-visible output, over-broad filesystem/tool scope and dynamic eval before you wire a server i","keywords":["mcp","model-context-protocol","security","scanner","static-analysis","supply-chain","llm","ai-agents","cli"],"license":"MIT","readme":"# mcpaudit\n\n**A quick security X-ray for AI agent plugins, to run before you plug one in.**\n\nAn MCP server (MCP = Model Context Protocol, the standard way to give an AI\nassistant new tools) is code you download and let an AI agent run. mcpaudit\nreads that code *before* you trust it and points out the dangerous bits — the\nquick safety check that doesn't really exist for these plugins yet.\n\n```bash\nnpx <OWNER>/mcpaudit ./path-to-an-mcp-server\n```\n\nNo install, no setup, no API key, no internet needed. It reads the plugin's\nsource code and its settings file and flags risky patterns, ranked by how bad\nthey are, each with a concrete fix. It **never runs** the code it is checking —\nit only reads it.\n\n---\n\n## Why\n\nThese plugins run with real power *inside* the AI agent's loop — they can get a\nshell, your files, and the network on your machine, and whatever a plugin's\ntools output flows straight back into the AI's context where it can steer what\nthe AI does next. An independent 2026 audit\n([dev.to writeup](https://dev.to/ecap0/the-state-of-mcp-server-security-in-2026-118-findings-across-68-packages-4fkd),\ncorroborated by\n[The Register](https://www.theregister.com/security/2026/04/16/mcp-design-flaw-puts-200k-servers-at-risk-researcher/5222022))\nreported **118 security findings (5 critical, 9 high) across 68 of 194\nsurveyed MCP packages** — command injection, environment/credential leakage\ninto LLM-visible context, and over-broad filesystem/tool scope — and that\n**9 of 11 major MCP directories publish packages with zero automated\nsecurity review**.\n\n> Those numbers are **from that outside audit — this tool did not measure\n> them.** `mcpaudit` exists so you can run a check like that yourself, in\n> seconds, before letting someone else's plugin run inside your agent.\n\n## Install / run\n\nIt's zero-install via `npx`. A local path is scanned fully offline:\n\n```bash\n# scan a server you cloned / vendored\nnpx <OWNER>/mcpaudit ./vendor/some-mcp-server\n\n# machine-readable output for CI / tooling\nnpx <OWNER>/mcpaudit ./server --json\n\n# SARIF v2.1.0 (a standard scan-results format GitHub understands) —\n# upload it so findings show in GitHub's Code scanning tab\nnpx <OWNER>/mcpaudit ./server --sarif > mcpaudit.sarif\n\n# stricter gate: any high or critical fails the command\nnpx <OWNER>/mcpaudit ./server --fail-on high\n\n# continuous monitoring: accept current state, then gate only on NEW\n# regressions (offline, no accounts) — see \"Continuous monitoring\" below\nnpx <OWNER>/mcpaudit ./server --baseline-write .mcpaudit-baseline.json\nnpx <OWNER>/mcpaudit ./server --baseline .mcpaudit-baseline.json\n```\n\n> Scanning by **bare package name** (`npx mcpaudit some-mcp-pkg`) needs a\n> registry/tarball fetch wired up; the published build asks you to pass a\n> path instead (it does not silently do nothing and does not hit the\n> network). The path scan is the fully-functional path today.\n\n### Exit codes (so you can wire it into CI)\n\nCI (\"continuous integration\" — the automated checks that run on every code\npush) reads these exit codes:\n\n| code | meaning |\n|------|---------|\n| `0`  | scan completed, gate not tripped. **Also** internal error — see below. |\n| `1`  | scan completed and a finding met/exceeded `--fail-on` (default `high`). |\n| `2`  | usage error (bad arguments). |\n\n**Fails open on purpose:** if `mcpaudit` itself breaks (a bug, a folder it\ncan't read), it prints a loud error and exits `0`. A security checker that is\nitself broken should not block every build in your project. If you want it to\nbe a *hard* stop, make it a **required** check with `--fail-on` set, so a\nmissing or zero result is visible rather than silently passing.\n\n## What it detects\n\nThis is the detailed reference for developers. The rules are fixed and give\nthe same answer every time (no AI, no guessing). Each finding has a stable id,\na severity (how serious), the exact `file:line:col` location, a plain\nexplanation of **why it fired**, and how to fix it.\n\n| id | severity | what it flags |\n|----|----------|---------------|\n| `MCP001` | critical | **Command injection** — `child_process` `exec`/`execSync`/`spawn`/`fork` (or `execFile` with `shell:true`) built from a non-literal command (template interpolation, `+` concat, or a variable). A pure string literal does not fire. |\n| `MCP002` | high | **Credential / env exfiltration to the LLM** — `process.env` flowing into a tool result `text`, a returned value, or a tool/handler description. Env read into a local used only for outbound auth does not fire. |\n| `MCP003` | high | **Over-broad filesystem scope** — an MCP manifest granting `/`, `~`, a drive root, `*`, or a `../`-escaping path as an allowed directory. |\n| `MCP004` | medium | **Unrestricted tool scope** — a wildcard tool allowlist (`\"*\"`, `[\"*\"]`, `allowAllTools: true`). |\n| `MCP005` | high | **Dangerous dynamic eval** — a *bare global* `eval()` / `new Function()`, or the `vm` builtin's `runInThisContext`/`runInNewContext`/`runInContext`/`compileFunction`, with a non-literal argument. `eval(\"1+1\")` does not fire; a *method* of the same name on another object (`mathExpr.compile(x)`, `parser.eval(x)`) does not fire; a userland-bound `vm` does not fire (provenance). |\n| `MCP006` | medium | **Unpinned remote code execution** — `curl … \\| sh`, `npx …@latest`, `uvx`, etc. in source strings or the manifest start command. A pinned `pkg@1.2.3` does not fire. |\n| `MCP007` | high | **Prototype pollution** — a recursive/deep merge or deep-set (`_.merge`, `defaultsDeep`, `setWith`, `deepmerge`, …) from a non-literal source, or a computed `obj[key]=v` assignment where `key` can be `__proto__`/`constructor`. An inline-object-literal merge source and numeric array indices do not fire. |\n| `MCP008` | high | **SSRF-able outbound request** — `fetch`/`axios`/`got`/`https.request` with an attacker-influenceable URL **origin** (a bare variable, `${host}` in the authority, or a leading-variable concat). A hardcoded origin with only the path/query varying (`\"https://api.x/v1?q=\" + enc(q)`, `` `https://api.x/${id}` ``) does **not** fire. |\n| `MCP009` | critical | **Hardcoded secret in source** — a string literal that looks like a real credential (AWS/GitHub/Slack/Google key, an OpenAI- or Anthropic-style key, a PEM private key, a JWT). Obvious placeholders (`your-…`, `XXXX`, `<…>`, `example`) and comment-only mentions do not fire. |\n| `MCP010` | high | **Path traversal in a file tool** — an `fs.*` call whose path is a bare variable or a concat/template with no `path.join`/`resolve`/`normalize`/`basename` containment. Requires an `fs` binding (provenance). A pure literal path does not fire; a bare path variable whose *nearest prior assignment* is a `path.join`/`resolve`/`normalize`/`basename` expression (hoisted containment) does not fire. |\n| `MCP011` | critical / high | **Unsafe deserialization** — `node-serialize`/`serialize-javascript` `unserialize`/`deserialize` of non-literal data (critical, RCE), or `js-yaml` `load()` with the default schema (high). `JSON.parse` and `yaml.load(x, { schema: yaml.JSON_SCHEMA })` do not fire. |\n| `MCP012` | critical | **Dangerous npm lifecycle script** — a `preinstall`/`install`/`postinstall`/`prepare` script that pipes a network download into a shell, base64-decodes into a shell, or is an obvious obfuscated one-liner. A normal build hook (`tsc`, `node build.js`, `husky install`) does not fire; a curl in a non-lifecycle script does not fire. |\n| `MCP013` | critical | **Secret committed in a manifest** — a credential pattern (as MCP009) embedded in `package.json`/`mcp.json` (e.g. an `env` block). Placeholders and `${VAR}` references do not fire. |\n| `MCP014` | medium / low | **Risky declared dependency** — a `git+`/url/tarball dependency source that bypasses the registry/lockfile (medium); or, as a **low advisory only**, a dependency name one edit away from a popular package (typosquat *shape*). **Static and offline — no registry/network and no CVE/malware claim is ever made.** |\n\nThe rules are intentionally **conservative** — they aim to avoid the obvious\nfalse-positive patterns (literal `exec`/`eval`, env used only for auth,\nscoped relative directories, fixed-origin URLs, `path.join`-contained file\naccess, safe-schema YAML, placeholder secrets, normal build hooks, and scary\ntokens that are only in comments). The bundled `borderline` fixture is a\nlegit MCP server full of code that *looks* dangerous and must produce **zero\nfindings**; it is part of CI.\n\n### Output formats\n\n| flag | format | use |\n|------|--------|-----|\n| *(default)* | human | a developer reading the terminal before `npx`-ing a server |\n| `--json` | JSON | CI/tooling (stable schema, summary counts); includes a `baseline` block when `--baseline` is used |\n| `--sarif` | SARIF v2.1.0 | upload with `github/codeql-action/upload-sarif@v3` to populate the **Code scanning** tab; each result carries the stable finding id as a `partialFingerprint` so GitHub de-dupes across runs |\n| `--monitor-json` | JSON | (with `--baseline`) the structured monitoring record — the machine contract a hosted tier would consume; this build only prints it locally |\n\n### Continuous monitoring (baseline diff — free, offline, no accounts)\n\nA one-shot scan tells you today's state. A team usually wants *\"did anything\nget **worse** since we last reviewed this server?\"* That is a diff against a\ncommitted baseline — pure, deterministic, offline, no sign-up:\n\n```bash\n# 1. accept the current state into a baseline and commit it\nnpx <OWNER>/mcpaudit ./server --baseline-write .mcpaudit-baseline.json\ngit add .mcpaudit-baseline.json && git commit -m \"mcpaudit baseline\"\n\n# 2. in CI: re-scan and gate ONLY on NEW findings (regressions). An\n#    already-triaged finding no longer re-breaks every build; a freshly\n#    introduced one does.\nnpx <OWNER>/mcpaudit ./server --baseline .mcpaudit-baseline.json --fail-on high\n```\n\nThe baseline file is intentionally **timestamp/host/user-free** so re-writing\nan unchanged repo is byte-identical (clean, reviewable PR diffs); *when* a\nfinding appeared is git's job, not the file's.\n\n> **Note (honest scope):** the hosted/continuous-monitoring *product* — a\n> service that watches a server over time, alerts on a new critical, or shows\n> a fleet view — is **not in this repo**. This OSS CLI ships only the\n> baseline-diff mechanic and emits the machine record a hosted tier would\n> consume (`--monitor-json`). There is **no network call, no upload, no\n> account, no billing** anywhere in this codebase, by design.\n\n## GitHub Action\n\nA thin wrapper around the same scan. Copy\n[`examples/mcpaudit.yml`](examples/mcpaudit.yml) into\n`.github/workflows/`:\n\n```yaml\n- uses: <OWNER>/mcpaudit@v0\n  with:\n    path: \".\"\n    fail-on: \"high\"\n    sarif: \"true\"          # optional: write mcpaudit.sarif\n    # baseline: \".mcpaudit-baseline.json\"  # optional: gate on NEW only\n```\n\nIt posts a GitHub annotation per finding and sets outputs (`total`,\n`critical`, `high`, `medium`, `low`, `gate`, plus `new`/`fixed` with a\nbaseline and `sarif-file` with `sarif: true`). With `sarif: true` it writes\na SARIF v2.1.0 file you upload via `github/codeql-action/upload-sarif@v3`\n(see [`examples/mcpaudit.yml`](examples/mcpaudit.yml) for the\n`security-events: write` permission and upload step). No token or secret is\nneeded for the scan itself; the Action does **no network I/O** and never\nruns the scanned code. It fails open on internal error — make the job a\n**required** check for hard enforcement.\n\n## Limitations (read this)\n\n`mcpaudit` reads code and matches known-dangerous patterns. It does **not**\nrun the code in a locked box (a \"sandbox\") and it does **not** trace exactly\nhow a value flows from input to a dangerous spot (\"taint analysis\"). Be\nclear-eyed about what that means:\n\n- **It will miss things (false negatives).** Obfuscated code, vulnerability\n  reached through indirection/aliasing, dynamic `require`, behaviour that\n  only manifests at runtime, or a malicious dependency several layers deep\n  are largely **out of scope**. A clean result is **not** a security\n  guarantee or an audit — review the server's tools and scope yourself.\n- **It does not do taint tracking.** It cannot prove a sink is *reachable\n  from tool input*; it flags the dangerous shape and tells you to verify\n  reachability. Conversely it deliberately under-reports to stay quiet:\n  e.g. **MCP010 does not flag `path.join(\"./dir\", x)`** even though a `..`\n  in `x` can still escape — full path-containment analysis is beyond a\n  lexical scanner, so that is a *known, accepted false negative*, not a\n  guarantee the call is safe. Treat every `path.*`/merge/`fetch` on tool\n  input as worth a human look regardless of whether a rule fired.\n- **Provenance-gated, conservative by design (favor a false negative over\n  cry-wolf).** A few rules deliberately stay quiet on idiomatic-safe shapes:\n  - **MCP005** fires only on a *bare global* `eval(` / `new Function(`, and\n    on the `vm` builtin's `runInThisContext`/`runInNewContext`/`runInContext`/\n    `compileFunction`. A *method* call that merely shares those names —\n    `mathExpr.compile(x)`, `parser.eval(x)`, `engine.compile(tmpl)`, an ORM\n    `.run()` — does **not** fire (it is not the global / `vm`). The `vm.*`\n    sink is suppressed when `vm` is provably a userland binding (e.g.\n    `const vm = makeSandboxShim()`). Bare `eval(userInput)` still fires by\n    design — that *is* the sink.\n  - **MCP010** also treats *hoisted containment* as safe: when the `fs.*`\n    path is a bare variable whose nearest prior assignment is built from\n    `path.join`/`resolve`/`normalize`/`basename(...)` (e.g.\n    `const safe = path.resolve(BASE, path.basename(name)); fs.readFileSync(safe)`),\n    it does **not** fire. This look-back is the *nearest* declaration/\n    assignment of that identifier and is intentionally single-hop — a\n    containment value passed through *additional* indirection (further\n    aliasing, a helper return) is a *known, accepted false negative* (same\n    stance as the `path.join` note above), not a safety guarantee.\n- **It will sometimes be wrong (false positives).** The rules use a lexical\n  model (comments and static string text are excluded; template\n  interpolation is treated as code). Unusual code can still trip a rule.\n  Please [report misfires](FEEDBACK.md) — that is how it improves.\n- **No accuracy/benchmark numbers are claimed.** There is no published\n  labeled corpus behind this tool, so it ships with **no precision/recall or\n  detection-rate figures**. The 118-findings statistic above is **cited from\n  an external audit, not produced by `mcpaudit`.**\n- **The dependency layer is static and offline — and makes no CVE claim.**\n  `MCP014` flags non-registry dependency *sources* and, as a *low advisory\n  only*, names that are one edit from a popular package. It contacts **no\n  registry**, bundles **no vulnerability database**, and asserts **nothing**\n  about whether a given package/version is malicious or has a known CVE.\n  Pair it with a real SCA/advisory tool (`npm audit`, OSV, Dependabot).\n  Registry/tarball fetching for `mcpaudit <name>` remains an unimplemented,\n  honest interface — the published build asks you to pass a path.\n- **Scope is the documented MCP/Node patterns.** JS/TS source + JSON\n  manifests. Servers written in other languages, or that hide configuration\n  outside the manifest, are not fully covered. Minified/bundled,\n  binary/non-UTF8, and symlinked files are deliberately **skipped** (and\n  surfaced as diagnostics) — audit the original source, not build artifacts.\n- It is one layer. Use it alongside dependency scanning, least-privilege\n  configuration, and human review — not instead of them.\n\n## How it works\n\n- `src/rules.js` — the pure, deterministic analyzer (no I/O, no network, no\n  code execution): 14 source/manifest rules. Fully unit-tested.\n- `src/analyze.js` — the only filesystem touch: walks the tree, **never\n  follows symlinks** (cannot be steered out of the target), skips\n  minified/binary/non-UTF8 blobs and pathological depth, never throws\n  (errors are collected and returned), and never runs the project.\n- `src/format.js` — pure presentation: human, `--json`, and SARIF v2.1.0.\n- `src/baseline.js` — pure baseline build + diff for continuous monitoring;\n  contains the documented `// PAID TIER` seam (no network/account code).\n- `bin/mcpaudit.js` / `src/action.js` — thin CLI / Action glue.\n\nFinding ids are a deterministic hash of `rule|file|line|col|message`, so two\ndistinct findings at the same location stay distinct **and** the id is\nstable across runs and machines — CI baselines, SARIF de-dup, and\nsuppression lists are reproducible.\n\n## Development\n\n```bash\nnpm ci\nnpm test          # vitest — no network, no API key required\nnode bin/mcpaudit.js test/fixtures/vulnerable-server   # try it\nnode bin/mcpaudit.js test/fixtures/vulnerable-server --sarif | head\n```\n\nTests run against three fixture MCP servers — a clean one (zero findings), a\nvulnerable one (**every one of the 14 rules fires** with the right severity\nand location), and a borderline one (legit code that looks scary across all\n14 rules and must stay at **zero findings**) — plus dedicated suites for\nSARIF schema correctness, baseline-diff behaviour, and adversarial\nhardening (symlink escape, minified/binary/non-UTF8, deep trees,\nnever-throws).\n\n## Feedback\n\nFalse positives and missed vulns are the most valuable input. See\n[FEEDBACK.md](FEEDBACK.md): add the `mcpaudit-feedback` label to an issue, or\nuse the issue template. Reports are captured **verbatim** — read exactly as\nwritten, never paraphrased.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","readmeFilename":"README.md","_rev":"1-b760fa1492c6e65bfbeabc2aa47372b7"}