{
  "markdown": "<!-- aicom-mirror-notice -->\n> **🔄 Synced from a monorepo — but with a live history.** `warden` mirrors the\n> canonical AI-Factory monorepo. History here is append-only (no force-push).\n> **Pull requests are welcome** — merged PRs are imported back into the monorepo\n> and re-synced here, so your contribution becomes canonical.\n> 💬 **[Issues](https://github.com/alexar76/warden/issues)** · **[Pull requests](https://github.com/alexar76/warden/pulls)** both welcome.\n\n# WARDEN — MCP server\n\n<!-- mcp-name: io.github.alexar76/warden -->\n\n<!-- aicom-readme-badges -->\n<p align=\"center\">\n  <a href=\"https://github.com/alexar76/warden/actions/workflows/ci.yml\"><img src=\"https://raw.githubusercontent.com/alexar76/warden/refs/heads/main/docs/badges/ci.svg\" alt=\"CI\" /></a>\n  <a href=\"https://warden.modelmarket.dev/\"><img src=\"https://raw.githubusercontent.com/alexar76/warden/refs/heads/main/docs/badges/deps.svg\" alt=\"0 runtime deps\" /></a>\n  <a href=\"https://warden.modelmarket.dev/\"><img src=\"https://img.shields.io/npm/v/@aimarket/warden.svg\" alt=\"npm @aimarket/warden\" /></a>\n  <img src=\"https://raw.githubusercontent.com/alexar76/warden/refs/heads/main/docs/badges/tests.svg\" alt=\"96 tests passing\" />\n  <img src=\"https://raw.githubusercontent.com/alexar76/warden/refs/heads/main/docs/badges/node.svg\" alt=\"node >=20\" />\n  <img src=\"https://raw.githubusercontent.com/alexar76/warden/refs/heads/main/docs/badges/warden.svg\" alt=\"WARDEN MCP firewall\" />\n  <a href=\"https://github.com/alexar76/warden/blob/main/LICENSE\"><img src=\"https://raw.githubusercontent.com/alexar76/warden/refs/heads/main/docs/badges/license.svg\" alt=\"License: MIT\" /></a>\n</p>\n<!-- /aicom-readme-badges -->\n\n<p align=\"center\">\n  <a href=\"https://warden.modelmarket.dev/\">\n    <img src=\"docs/screenshots/readme/hero-3d.png\" alt=\"WARDEN — 3D gate chain: tools/list through static-scan, threat-feed, origin, and pinning to a recorded verdict\" width=\"100%\" />\n  </a>\n</p>\n\n\n> 🌐 **English** · [Русский](README-ru.md) · [Español](README-es.md) · [Français](README-fr.md) · [中文](README-zh.md) · [Glossary](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)\n\n**One MCP server. Security firewall for advertised tool definitions. Library included.**\n\nTransport: **stdio** (`npx -y @aimarket/warden` / `node dist/mcp-server.js`). Compatible hosts:\nClaude Desktop, Cursor, Glama, and any MCP client that speaks stdio. No API keys.\n\n| Item | Location |\n|------|----------|\n| MCP entrypoint (stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |\n| Tools | `vet_mcp_server`, `static_scan_tools`, `classify_sensitive_tools`, `check_egress_url`, `canonicalize_json`, `list_scan_rules` |\n| Library | `import { Warden } from \"@aimarket/warden\"` |\n| Glama / Docker (stdio) | [`Dockerfile`](Dockerfile), [`glama.json`](glama.json) |\n| Official MCP Registry | [`server.json`](server.json) → `io.github.alexar76/warden` |\n| Smithery | [`smithery.yaml`](smithery.yaml) |\n\nAn MCP server tells your agent what its tools do. The agent believes it — that sentence is the\nattack surface. A tool description is prompt text delivered by a third party straight into your\nmodel's context, and a schema field named `api_key` is a request for your secrets phrased as an API.\n\nWARDEN vets a server **before any of its tools reach the model**, and returns a verdict you can\nrecord: allow/block, a 0..1 score, the findings that produced it, a per-tool partition, and the\nexact rule table that was in force.\n\n**Zero npm runtime dependencies.** The library's only import is `node:crypto`. The stdio MCP\nserver adds other `node:` builtins (`fs`, `path`, `process`) and still pulls in no packages. It is\nthe firewall out of [ARGUS](https://github.com/alexar76/argus), extracted so you can put it in front\nof your own MCP host without adopting an agent.\n\n## Run as MCP server (stdio)\n\n```bash\nnpx -y @aimarket/warden            # bin: warden-mcp\n# from this repo:\nnpm run build && node dist/mcp-server.js\n```\n\nClaude Desktop / Cursor (`mcpServers` entry):\n\n```json\n{\n  \"mcpServers\": {\n    \"warden\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@aimarket/warden\"]\n    }\n  }\n}\n```\n\nThe process never starts, proxies, or sandboxes another MCP server — you pass a `tools/list` dump\nin, you get a verdict out.\n\n| Tool | When to use |\n|---|---|\n| `vet_mcp_server` | Full gate chain on a server identity + advertised tools |\n| `static_scan_tools` | Injection / exfil scan only (no origin / pinning / threat feed) |\n| `classify_sensitive_tools` | Operator glob split — not an injection scan |\n| `check_egress_url` | Hostname allowlist (empty list denies every host) |\n| `canonicalize_json` | RFC 8785 bytes for feeds and pins |\n| `list_scan_rules` | Published rule table + digest |\n\nGlama TDQS: MCP `annotations` (readOnly / destructive / idempotent / openWorld), when-to-use /\nwhen-not naming siblings, every `inputSchema` property described, `outputSchema` on every tool.\n\n### Publish on Glama\n\nListing: **[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)** ·\nquality score: **[glama.ai/mcp/servers/alexar76/warden/score](https://glama.ai/mcp/servers/alexar76/warden/score)**\n\n\nSame pattern as **[ARGUS](https://github.com/alexar76/argus)** and\n**[aimarket-mcp](https://github.com/alexar76/aimarket-mcp)**: repo-root [`glama.json`](glama.json) +\n[`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`. Admin form values: [`docs/GLAMA.md`](docs/GLAMA.md).\n\n## Library (embed in your host)\n\n```bash\nnpm install @aimarket/warden\n```\n\n```ts\nimport { Warden, ThreatFeed, silentLogger } from \"@aimarket/warden\";\n\nconst threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });\nawait threatFeed.load(process.env.FEED_URL); // omit → built-in deny-list only, no network\n\nconst pins = new Map();\nconst warden = Warden.create({\n  policy: {\n    blockAtSeverity: \"high\",\n    sensitiveToolPatterns: [\"*delete*\", \"*transfer*\", \"*key*\"],\n    allowUnknownServers: false, // fail-closed: only servers you declared\n    pinToolDefs: true,\n  },\n  threatFeed,\n  store: {\n    getPin: async (id) => pins.get(id),\n    putPin: async (p) => void pins.set(p.serverId, p),\n  },\n  log: silentLogger(), // or your own logger\n});\n\nconst verdict = await warden.vet(server, await client.listTools());\n\nif (!verdict.allow) throw new Error(`blocked by ${verdict.decidedBy}`);\nconst usable = verdict.allowedTools; // a poisoned tool can be quarantined alone\nawait warden.approve(server, tools); // pin what the user accepted\n```\n\n`vet()` performs **no network I/O**. The only request WARDEN ever makes is the threat-feed fetch you\nasked for by passing a URL to `load()`.\n\n## The gate chain\n\n```mermaid\nflowchart LR\n  T[\"tool defs<br/>from the server\"] --> S[\"static scan<br/>25 rules\"]\n  S --> F[\"threat feed<br/>11 built-ins + signed\"]\n  F --> O[\"origin<br/>declared vs catalog\"]\n  O --> P[\"pinning<br/>drift vs approval\"]\n  P --> V[\"verdict<br/>allow · score · findings<br/>allowedTools / blockedTools\"]\n```\n\n| Gate | What it decides | Network | Fatal? |\n|---|---|---|---|\n| **static-scan** | Injection, exfiltration, credential requests and hidden-Unicode/base64 tells in the tool `name`, its `description` and its `inputSchema` — 25 rules, v4, of which 15 can block and 10 are advisory-only, 17 also cover the name, and 12 carry a context guard | none | no |\n| **threat-feed** | Known-bad server identity or tool, from 11 built-in records plus an optional signed feed | only the feed fetch | yes, for a server-scoped `critical` |\n| **origin** | Whether the operator declared this server or it arrived from a remote catalog | none | yes, under `allowUnknownServers: false` |\n| **pinning** | Whether the tool defs still match what the user approved | none | yes, under `pinToolDefs: true` |\n\nThe composite score is the **product** of gate contributions, so one bad gate drags the whole server\ndown rather than being averaged away. Severity and blocking are separate axes: an `advisory` finding\nis reported and never blocks and never costs a tool, at any `blockAtSeverity` — because \"how much\nattention does this deserve\" and \"is this a defect at all\" are different questions, and encoding the\nsecond as a low severity made it blocking again for anyone who tightened the threshold.\n\n## The verdict is meant to be recorded\n\n```ts\n{\n  allow: false,\n  score: 0,\n  decidedBy: \"threat-feed\",\n  findings: [{ gate, severity, code: \"THREAT_TOOL_MATCH\", message, tool, advisory? }],\n  allowedTools: [\"add\"],\n  blockedTools: [\"sweeper\"],\n  rulesets: { staticScan: { version: \"4\", digest: \"sha256-klRyTiD3…\" } }\n}\n```\n\n`rulesets` is not decoration. The same server scores differently under a later rule table, and\nwithout the version *and* a digest over the rules there is no way to tell that apart from the server\nhaving changed. A stored scan without them is not reproducible.\n\n## Signed threat feed\n\nWARDEN will not read an unsigned remote feed. The contract is deliberately boring:\n\n```\nGET <your feed url>\n{ \"records\": [ {pattern, severity, code, reason, source, scope}, … ],\n  \"timestamp\": 1786205907380,   // epoch ms, integer — required\n  \"signature\": \"f588d5a4…\"      // Ed25519 (hex) over the RFC 8785 canonical\n}                               // form of {records, timestamp}\n```\n\nThree properties are checked, and **any failure keeps the built-in floor** rather than degrading to\nno protection:\n\n1. **authenticity** — Ed25519 against the key you pinned in advance (`feedPublicKey`);\n2. **freshness** — the *signed* timestamp must be inside `maxAgeMs` (24 h by default), so whoever\n   serves the URL cannot replay a months-old snapshot and silently erase every record added since.\n   A signature says who wrote a document, never when you were handed it;\n3. **determinism** — RFC 8785 canonical bytes, so publisher and verifier agree regardless of JSON\n   key order.\n\n[MOMUS](https://github.com/alexar76/momus) is a reference publisher of this contract\n(`/warden/threat-feed`) if you want something to point `load()` at.\n\n## Also in the box\n\n- **`EgressGuard`** — an outbound allowlist to wrap any request a tool makes. A tool reaching a host\n  you never listed is the classic phone-home tell. `*.example.com` matches subdomains; an empty\n  allowlist blocks everything rather than allowing everything.\n- **`isSensitiveTool` / `classifyTools`** — glob classification of tools that must require per-call\n  approval. Sensitive tools stay *advertised*; they just cannot run unattended.\n- **`canonicalize` / `parseJsonStrict`** — a strict RFC 8785 (JCS) implementation, also exported as\n  `@aimarket/warden/jcs` so another implementation can be byte-checked against it. Integers only\n  beyond `MAX_SAFE_JSON_INTEGER`, refusal (not escaping) on lone surrogates, and a reason code on\n  every refusal.\n\n## Documentation\n\n| | |\n|---|---|\n| [The gate chain](docs/gates.md) | Every rule tier, every finding code, how the composite score is built, and how to add a gate |\n| [The signed threat feed](docs/threat-feed.md) | The wire contract, the three checks, and how to publish a feed WARDEN will accept |\n| [Integration guide](docs/integration.md) | Wiring WARDEN into your own MCP host, policy choices, and what to record |\n| [Field survey: 1 108 public MCP servers](docs/mcp-survey.md) | What WARDEN decided on real third-party tool definitions — 50 servers blocked, 4 substantiated, and the six ways the rest were wrong |\n| [Glama / Docker](docs/GLAMA.md) | stdio MCP server, health check, admin Build steps / CMD |\n| [MCP registries](docs/REGISTRIES.md) | Official Registry, Smithery, mcp.so / Pulse |\n| [Security](SECURITY.md) | How to report a firewall bypass |\n| [Contributing](CONTRIBUTING.md) | Zero-dep rule, ruleset PRs |\n\n## What this is not\n\n- **Not a sandbox.** These are in-process JS decisions. OS-level confinement of the MCP child\n  process (seccomp/Landlock, `sandbox-exec`) is not here.\n- **Not a model.** No LLM is called anywhere in the chain. That is why `vet()` is fast, offline and\n  deterministic — and why the static scan is regex-shaped and will miss a paraphrase no rule covers.\n- **Not a reputation service.** An earlier version had a gate that asked a trust oracle for a score\n  it had no data to compute, then reported the oracle as unreachable without having sent a request.\n  It was removed, and `test/no-phantom-gate.test.ts` fails if any gate ever claims unreachability\n  again.\n- **Not a substitute for reading the tool defs.** 11 built-in threat records is a floor, not a\n  catalog.\n- **Not a proxy.** The stdio MCP entry inspects advertised definitions you pass it. It does not\n  connect to, fetch, or execute the server under scan.\n\n## Development\n\n```bash\nnpm install && npm run build && npm test   # 166 tests\n```\n\n`test/packaging.test.ts` is what keeps the headline honest: it fails if an npm runtime dependency\nappears, if any source file imports outside the package (except `node:` builtins), or if the entry\npoint stops exporting the enforcement surface. `test/mcp-server.test.ts` is the Glama health\ncheck: `initialize` + `tools/list` + a `tools/call`.\n\nUsed by [ARGUS](https://github.com/alexar76/argus) (the reference host), [MOMUS](https://github.com/alexar76/momus)\n(the publisher side), and the AICOM MCP-security course.\n\nMIT © AICOM (alexar76)\n",
  "bytes": 13269,
  "sha": "c06c0ba9b9c780e90afa29a7f94c0813b30dae5936102ffb0ef1da9a239c3d35",
  "repo_slug": "alexar76/warden",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_alexar76_warden_c95dccbc/readme"
}