{
  "markdown": "<div align=\"center\">\n\n<img src=\"assets/logo.svg\" width=\"84\" alt=\"Heimdall\" />\n\n# Heimdall\n\n**The watchman at your agent's gate.**\n\nA **local, pre-flight security scanner** for **Model Context Protocol (MCP) servers** — vet\na server, or a whole agent config, before your agent trusts it. No account, no backend, and\nit never runs the server by default.\n\n[![npm](https://img.shields.io/npm/v/mcp-heimdall-scan)](https://www.npmjs.com/package/mcp-heimdall-scan)\n[![CI](https://github.com/caglarbozkurt/mcp-heimdall/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/caglarbozkurt/mcp-heimdall/actions/workflows/ci.yml)\n[![downloads](https://img.shields.io/npm/dm/mcp-heimdall-scan)](https://www.npmjs.com/package/mcp-heimdall-scan)\n[![license](https://img.shields.io/github/license/caglarbozkurt/mcp-heimdall)](LICENSE)\n[![node](https://img.shields.io/badge/node-%E2%89%A518.17-3fb950)](package.json)\n[![try in your browser](https://img.shields.io/badge/try-in%20your%20browser-e3b341)](https://caglarbozkurt.github.io/mcp-heimdall)\n\n[![mcp-heimdall MCP server](https://glama.ai/mcp/servers/caglarbozkurt/mcp-heimdall/badges/card.svg)](https://glama.ai/mcp/servers/caglarbozkurt/mcp-heimdall)\n\n</div>\n\n---\n\nMCP servers are unvetted code with a natural-language attack surface: their tool\ndescriptions go straight to your model, and the server runs with your machine's access.\nHeimdall scores **what a server can actually do** — not what it claims — and cites the\nexact evidence. It runs **entirely on your machine**, needs no account, and **never executes\nthe server by default**, so you can vet a package *before* you install it and gate it in CI.\n\n## Quickstart\n\n```bash\nnpx mcp-heimdall-scan firecrawl-mcp                   # scan a published server\nnpx mcp-heimdall-scan ./claude_desktop_config.json    # audit your whole agent config\nnpx mcp-heimdall-scan firecrawl-mcp --online          # + check its deps for known CVEs (OSV.dev)\nnpx mcp-heimdall-scan ./my-server --policy strict     # gate it in CI\n```\n\nNo install, runs locally, nothing leaves your machine.\n\n**Or try it in your browser:** [caglarbozkurt.github.io/mcp-heimdall](https://caglarbozkurt.github.io/mcp-heimdall)\n— the full scanner runs **100% client-side** (npm packages are fetched via jsDelivr; or paste a\n`tools.json` / MCP config). No backend, nothing uploaded. Local paths and `--handshake` need the CLI.\n\n## What it checks\n\n| Check | What it catches |\n|---|---|\n| 🧬 **Injection** | tool-poisoning across tools, resources & prompts — override, concealment, hidden chars, fake `<IMPORTANT>` tags |\n| 🔓 **Capability** | filesystem, network, shell, `eval`, and specific credential access (SSH / AWS / keychain / `.env`) |\n| 🎯 **Proven exfil paths** | data-flow that *proves* `secret → network` or `fetch → eval`, `file:line → file:line` |\n| 📦 **Provenance & deps** | install-time scripts, missing repo/license, capabilities inherited from dependencies |\n| 🛡️ **Known CVEs** *(opt-in)* | declared dependencies checked against the OSV.dev advisory DB — real CVE IDs, severity-ranked (`--online`) |\n| 🕸️ **Composition** | audits a whole config: cross-server exfiltration chains & tool-name collisions |\n| 🔁 **Drift** | fingerprints the surface — a silently changed tool description (rug-pull) is a hard fail |\n\nEvery finding cites `file:line` or `tool:name`. **Capability ≠ risk:** raw power is shown as\nan informational profile and never fails the scan — only hard **gates** and real **anomalies** do.\n\n## What makes it different\n\n- **Local, and it doesn't run the server.** Everything is static and offline by default — no\n  account, no backend, nothing uploaded — and it never executes untrusted code unless you opt\n  into `--handshake` (documented for a disposable VM only). You vet a package *before* installing.\n- **Proves the path.** Taint/data-flow turns \"reads files AND has network = fail\" into a\n  concrete, located flow — so it doesn't cry wolf on a config read plus an unrelated API call.\n- **Sees the whole gate.** It reasons across the *set* of servers you've configured — the\n  cross-server exfil path neither server shows alone. Most scanners look at one at a time.\n- **A gate you control.** Detectors emit facts; a **policy** you define turns them into\n  pass / warn / fail. Deny capabilities, require provenance, add audited waivers, gate CI.\n\n## Usage\n\n```bash\nheimdall <target> [options]\n```\n\n| Target | Example |\n|---|---|\n| local directory | `heimdall ./servers/my-mcp` |\n| npm package | `heimdall some-mcp-package` |\n| PyPI package | `heimdall pypi:some-mcp-server` |\n| git repository | `heimdall https://github.com/user/repo` |\n| tools/list dump | `heimdall tools.json` |\n| MCP client config | `heimdall ./claude_desktop_config.json` |\n\n<details>\n<summary><b>Options</b></summary>\n\n```\n--tools <file>     supplement analysis with a tools/list (or {tools,resources,prompts}) dump\n--policy <p>       \"default\", \"strict\", or a JSON policy file\n--baseline <file>  diff against a prior --json report (drift / rug-pull detection)\n--handshake        RUN the server(s) for the live tool list (untrusted code — VM/container only)\n--online           check declared deps against OSV.dev for known CVEs (sends dep names, not source)\n--json             machine-readable report\n--sarif            SARIF 2.1.0 (GitHub code-scanning / CI)\n--no-fail          always exit 0\n\nExit codes: 0 pass/warn · 1 fail · 2 error\n```\n</details>\n\n## Policies\n\nDetectors emit facts; a **policy** turns them into the verdict. Ship the default, pick\n`strict`, or write your own procurement/security criteria:\n\n<details>\n<summary><b>Example policy</b> (<code>policy.example.json</code>)</summary>\n\n```json\n{\n  \"name\": \"acme-procurement\",\n  \"denyCapabilities\": [\"exec\", \"dynamic-eval\", \"secret-access\"],\n  \"require\": [\"has_repository\", \"has_license\"],\n  \"failOnSeverity\": \"high\",\n  \"warnOnSeverity\": \"low\",\n  \"allow\": [{ \"id\": \"capability/scope-mismatch\", \"reason\": \"reviewed\", \"expires\": \"2026-12-31\" }]\n}\n```\nWaivers carry a reason and optional expiry — an expired waiver lapses and re-flags.\n</details>\n\n## Library\n\n```ts\nimport { scan } from \"mcp-heimdall-scan\";\n\nconst report = await scan(\"some-mcp-server\", { policy: \"strict\" });\nif (report.verdict === \"fail\") throw new Error(report.reasons.join(\"; \"));\n```\n\nAlso ships as a **Claude Code skill** (`skill/`) — vet a server in-conversation before installing.\n\n## Use it as an MCP server\n\nGive your agent a `scan_mcp_server` tool so it can vet a server **before** connecting to it —\n\"scan this before you add it.\" Add Heimdall to your MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"heimdall\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"--package\", \"mcp-heimdall-scan\", \"heimdall-mcp\"]\n    }\n  }\n}\n```\n\nThe tool takes `target` (npm package, `pypi:<name>`, path, GitHub URL, tools.json, or a client\nconfig), plus optional `policy` and `online`. It's **static-only** — it downloads but never\nexecutes the server, and the code-execution modes (`--handshake`, `validate`) are intentionally\nnot exposed to the agent.\n\n## Use it in CI (GitHub Action)\n\nGate every pull request — scan your MCP config (or a server) and **fail the build** if it's\nrisky. Add this to `.github/workflows/`:\n\n```yaml\n- uses: caglarbozkurt/mcp-heimdall@v1\n  with:\n    target: ./claude_desktop_config.json   # a path, npm/pypi package, github URL, or tools.json\n    policy: strict                          # \"default\", \"strict\", or a JSON policy file\n    online: true                            # also check deps for known CVEs (OSV.dev)\n    sarif: heimdall.sarif                   # optional: emit SARIF for code scanning\n```\n\n| Input | Default | Description |\n|---|---|---|\n| `target` | — | what to scan (required) |\n| `policy` | `default` | `default`, `strict`, or a path to a JSON policy |\n| `online` | `false` | check dependencies for known CVEs via OSV.dev |\n| `sarif` | — | write SARIF to this path (for `github/codeql-action/upload-sarif`) |\n| `fail-on-findings` | `true` | fail the job on a `FAIL` verdict (set `false` to report only) |\n| `version` | `latest` | pin the `mcp-heimdall-scan` version for reproducible CI |\n\nRuns entirely on your own CI runner — no backend, and free on public repos.\n\n## Validate (behavioral cross-check)\n\nStatic analysis says what a server *can* do. `heimdall validate` checks that against what it\n*actually does* — it runs the server with a capability recorder preloaded (hooking\n`fs` / `net` / `http(s)` / `child_process` / `vm` / `fetch` / `process.env`), drives each tool,\nand diffs observed runtime behavior against the static flags:\n\n```bash\nheimdall validate ./my-server            # one server: confirmed / missed / not-exercised\nheimdall validate --list servers.txt     # batch: a recall number over observed behavior\n```\n\n- **confirmed** — flagged *and* observed (the static claim held up).\n- **not exercised** — flagged but not triggered by naive args (a *lower bound*, **not** proof the flag is wrong).\n- **missed** — observed but **not** flagged → a real static gap to review (or an incidental library side effect).\n\nSo it's trustworthy for finding false **negatives** (static misses); it does not disprove a flag.\nEach server runs in a throwaway `HOME` + working directory with no inherited secrets, but it\nstill **runs the server and calls its tools** (network/exec side effects) — use a disposable VM/container.\n\n**Behavioral run over 200 real packages** ([`benchmarks/validate-run.md`](benchmarks/validate-run.md)):\n55 booted, 34 exercised an observable capability. Of the capabilities servers *actually\nexercised at runtime*, the static scan flagged **80.9%** (55/68) — up from 75.8% after the\nfirst run's misses became a fix-list (we widened dependency-based network detection, which\nroughly halved the network misses). The misses that remain are **structural**: a capability\nexercised *inside a dependency's internals or a subprocess*, which static analysis fundamentally\ncan't see — which is exactly why `validate` exists as the backstop. Honest recall, openly\nreported, improving run over run.\n\n## Tested at scale\n\nRun against **2,500 real MCP packages** from the npm registry (`benchmarks/`): **1,726 scanned**\nin ~5 minutes, **0.7% flagged** — robust on messy real-world code. Separately, it scores\n**100% on the small labeled fixture corpus** (`npm run eval`, ~10 benign/malicious fixtures\nincluding the Damn Vulnerable MCP project) — a calibration check, *not* a broad real-world\naccuracy number; the field scans above are unlabeled and used only for robustness. Full log:\n[`benchmarks/field-run.md`](benchmarks/field-run.md).\n\nWhat that scan says about the ecosystem your agent trusts:\n\n| Of 1,726 real MCP servers… | share |\n|---|---|\n| can **run shell commands** | 45% |\n| make **network calls** | 67% |\n| can **`eval` code at runtime** | 9% |\n| can do **both exec + network** | 34% |\n| touch **credential files** | 5% |\n\nThe 0.7% flagged were driven by install-time code execution and prompt-injection — including\nreal servers with **hidden zero-width characters embedded in their tool descriptions**, the\nkind of stealth tool-poisoning a keyword scanner sails past.\n\n> A robustness + distribution run is *not* an accuracy benchmark — the 2,500 servers are\n> unlabeled. A flag means **review this**, not proven malicious.\n\n## Security & limitations\n\nHeimdall is a **heuristic pre-flight check, not a guarantee** — a PASS isn't proof of safety.\nCapability, provenance, and CVE analysis cover **JS/TS and Python**; injection is\nlanguage-agnostic. Proven **taint/data-flow is JS/TS only** — Python falls back to\ncapability co-presence (a conservative gate, not a proven flow).\nEverything runs offline by default; `--online` is the one network call (it sends dependency\nnames + versions to OSV.dev, never your source), and the CVE match is against the declared\nrange, not a lockfile. `--handshake` **runs untrusted code** and is not a real sandbox. See\n[`SECURITY.md`](SECURITY.md) for the full threat model and how to report a vulnerability.\n\n## Contributing\n\nNew detection rules are the highest-value contribution — see\n[`CONTRIBUTING.md`](CONTRIBUTING.md). By participating you agree to the\n[Code of Conduct](CODE_OF_CONDUCT.md).\n\n## License\n\n[MIT](LICENSE) · built by [Çağlar Bozkurt](https://github.com/caglarbozkurt)\n",
  "bytes": 12304,
  "sha": "8f5aba288b3476fbe6bcc82de766e7d53314fb9ba7f22d170111bd60ba4fe773",
  "repo_slug": "caglarbozkurt/mcp-heimdall",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_caglarbozkurt_heimdall_e2ad95c6/readme"
}