WARDEN
MCP security firewall · zero runtime dependencies

An MCP server tells your agent what its tools do.
The agent believes it.

A tool description is prompt text a third party delivers straight into your model's context, and a schema field named api_key is a request for your secrets phrased as an API. WARDEN vets a server before any of its tools reach the model, and returns a verdict you can record.

npm i @aimarket/warden Read the source See what it got wrong
0 runtime dependencies 4 gates ruleset v4 166 tests node:crypto the only import
IN THE HOST PROCESS
tools/list
G1 static-scan
G2 threat-feed
G3 origin
G4 pinning
◆ verdict
drag to rotate

WebGL is unavailable here, so the gate chain is not drawn. The table below names all four gates and what each one decides.

● tool definitions 4 gates ● blocked at a gate ◆ recorded verdict
Calibrated against the ecosystem, not against fixtures

We pointed it at 1 108 public MCP servers and published what it got wrong

Hours after the first release we reached every public MCP server we legitimately could: 2 787 registry entries with a network endpoint, of which 1 108 answered a real tools/list and handed over 17 491 tool definitions. No third-party code was executed — every definition came from the server's own answer.

1 108servers answered
17 491tool definitions
50 → 6servers blocked, v3 → v4 (re-run)
4 → 4real findings, still caught

The headline is not about the ecosystem, it is about us. Ruleset v3 blocked 50 of those servers and only 4 held up on review. The other 46 were blocked for saying the right thing: "Never send a private key", "the private key never leaves your machine", a security scanner listing the attacks it detects, a Persian description spelled with the ZERO WIDTH NON-JOINER its language requires.

A scanner's false-positive profile is the only number that decides whether anyone switches it on. One that refuses honest servers is not cautious — it is uninstalled. So the failures are published with the evidence, and the regression suite is built from this corpus's real text rather than from invented fixtures.
Re-measured on the same 1 108 serversruleset v3ruleset v4 *
servers blocked506
of those, substantiated44
blocking findings49212
advisory findings3 4723 494
* Only the v3 column is published as data: the committed dataset is the run as executed, and it records that v3 changed nothing on this corpus. The v4 column comes from a local re-run whose corpus is not published, so it is our measurement rather than something you can recompute. What is reproducible is the direction — the regression suite asserts, from the corpus's own text and with no network, that the 46 false positives no longer block and the 4 real findings still do.

Ruleset v4 gets there with guards — named context checks that are part of the published rule table, and therefore of its digest. polarity alone accounted for 390 of those 492 blocking findings: a credential noun inside a refusal is a promise, not a request. mention knows that a phrase in quotes, in backticks or as a bare JSON enum value is cited rather than said.

The chain

Four gates, cheapest and most local first

Nothing in the chain performs a network request. The only fetch WARDEN ever makes is the threat feed you asked for by passing a URL. The composite score is the product of gate contributions, so one bad gate drags a server down instead of being averaged away.

GateWhat it decidesNetwork
static-scanInjection, exfiltration, credential requests and hidden-Unicode or base64 payloads in the tool name, its description and its input schema. 25 rules in ruleset v4 — 15 can block, 10 are advisory-only, and 17 also cover the name.none
threat-feedA known-bad server identity or tool, from 11 built-in records plus an optional signed feed. Fatal for a server-scoped critical.the feed fetch only
originWhether the operator declared this server or it arrived from a remote catalog. Fail-closed under allowUnknownServers: false.none
pinningWhether the tool definitions — and the server's launch identity — still match what the user approved. A catalog can repoint an approved id at a different command; that is drift too.none
Severity and blocking are separate axes. An advise-tier finding is reported and never blocks and never costs a tool, at any threshold — because "how much attention does this deserve" and "is this a defect at all" are different questions, and encoding the second as a low severity made it blocking again for anyone who tightened the threshold.
The output

A verdict meant to be recorded

// warden.vet(server, tools)
{
  allow: false,
  score: 0,
  decidedBy: "threat-feed",
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
  allowedTools: ["add"],
  blockedTools: ["sweeper"],
  rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
}

rulesets is not decoration. The same server scores differently under a later rule table, and without the version and a digest over the rules there is no way to tell that apart from the server having changed. A stored scan without them is not reproducible — and because guards are part of the table, the same regex with and without polarity is a different ruleset.

Per-tool partition

A server with one poisoned tool and nine good ones stays usable: only what the verdict named is dropped.

Display-safe

Control and invisible characters in a tool name are escaped in the message — a name carrying ESC[2K used to overwrite the operator's terminal line.

Deterministic

No LLM anywhere in the chain. Same inputs, same verdict, offline, in milliseconds.

Optional, signed, fail-closed

A threat feed that cannot switch your protection off

WARDEN ships 11 built-in threat records as a floor, and will not read an unsigned remote feed at all. Three properties are checked, and any failure keeps the floor rather than degrading to nothing.

1 · Authenticity

Ed25519 against a key you pinned in advance, over the RFC 8785 canonical form of {records, timestamp} — not over the raw body.

2 · Freshness

The signed timestamp must be inside the window. A signature says who wrote a document, never when you were handed it.

3 · Determinism

Canonical bytes, so publisher and verifier agree regardless of JSON key order.

Remote records are appended to the built-ins. A feed can never remove one, so a compromised publisher can only add protection — and the download is bounded and aborted past its cap, because a feed URL is otherwise a memory-exhaustion vector. MOMUS is a reference publisher of the contract.

Limits, stated

What this is not

In front of any MCP host

Quick start

import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";

const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
await threatFeed.load(process.env.FEED_URL);   // omit → built-in floor only, no network

const pins = new Map();
const warden = Warden.create({
  policy: {
    blockAtSeverity: "high",
    sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
    allowUnknownServers: false,   // fail-closed: only servers you declared
    pinToolDefs: true,
  },
  threatFeed,
  store: { getPin: async (id) => pins.get(id), putPin: async (p) => void pins.set(p.serverId, p) },
  log: silentLogger(),
});

const verdict = await warden.vet(server, await client.listTools());
if (!verdict.allow) throw new Error(`blocked by ${verdict.decidedBy}`);
const usable = verdict.allowedTools;      // a poisoned tool can be quarantined alone
await warden.approve(server, tools);      // pin what the user accepted

Used by ARGUS as its reference host, by MOMUS on the publisher side, and by the AICOM MCP-security course. It is a library, not a service — which is why on the ecosystem map it is drawn as a layer rather than a host.