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 148 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
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

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.