# KarmaDue verdicts as MCP Guard policy

[MCP Guard](https://github.com/permission-protocol/mcp-guard) is a stdio proxy. It enforces a YAML policy on every `tools/call` and signs each decision. KarmaDue answers a different question: should this MCP server be run at all? This recipe connects the two. It turns KarmaDue's verdict on the server package into MCP Guard's `default_action`.

Tested: 2026-11-12 with @permission-protocol/mcp-guard 0.3.1 and Node 20. We generated policies from live KarmaDue answers and ran a small stdio server through the guard. Real results for the same `tools/call`:

| Package | KarmaDue enforce | Generated `default_action` | What the agent got |
|---|---|---|---|
| `npm:event-stream@3.3.6` | `block` | `block` | `{"code":-32001,"message":"Blocked: No matching rule; default action \"block\""}` |
| `pypi:requests==2.31.0` | `require_human_approval` | `require_approval` | `{"code":-32002,"message":"Held for approval: ..."}` |
| `npm:left-pad@1.3.0` | `proceed_with_care` | `allow` | the tool ran (`echo ran`) |

(requests and left-pad are not MCP servers. They are just stand-ins with known verdicts.)

## `kd-to-mcp-guard.mjs` (Node 18+, no dependencies)

```js
#!/usr/bin/env node
// Turn a KarmaDue verdict on an MCP server package into an MCP Guard policy (pp.config.yaml).
// Usage: node kd-to-mcp-guard.mjs <target> [server-label] > pp.config.yaml
//   target: npm:<name>@<version> | pypi:<name>==<version> | a repo or server URL
// Mapping (KarmaDue data.enforce -> MCP Guard default_action):
//   block                                   -> block            (every tools/call is refused, -32001)
//   require_human_approval, unknown, error  -> require_approval (every tools/call waits for a person, -32002 without UI)
//   proceed_with_care                       -> allow            (MCP Guard still signs a receipt per call)
// Node 18+, no dependencies, no account. KarmaDue is not a safety guarantee; keep your own rules below the generated ones.
const target = process.argv[2];
const label = (process.argv[3] || "mcp-server").replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 60);
if (!target) { console.error("usage: kd-to-mcp-guard.mjs <target> [server-label]"); process.exit(2); }
const API = process.env.KARMADUE_PREFLIGHT || "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/public-api/v1/preflight";
let d = {}, rid = "", err = "";
try {
  const r = await fetch(API, { method: "POST", headers: { "content-type": "application/json", "x-kd-via": "recipe-mcp-guard" }, body: JSON.stringify({ target, action: "install" }), signal: AbortSignal.timeout(15000) });
  const b = await r.json(); d = b.data || {}; rid = b.request_id || "";
} catch (e) { err = String(e?.message || e); }
const enforce = d.enforce || "unknown";
const action = enforce === "block" ? "block" : enforce === "proceed_with_care" ? "allow" : "require_approval";
const one = (s) => String(s || "").replace(/[\r\n]+/g, " ").slice(0, 300);
process.stdout.write(`# Generated by kd-to-mcp-guard from KarmaDue on ${new Date().toISOString()}
# target: ${one(target)}
# KarmaDue enforce: ${enforce}${err ? ` (check failed: ${one(err)})` : ""}
# KarmaDue headline: ${one(d.headline)}
# KarmaDue request id: ${rid}
# Re-run before each upgrade. Not a safety guarantee.
default_action: ${action}
mode: enforce
server_label: ${label}
rules: []
`);
```

## Use

```bash
node kd-to-mcp-guard.mjs npm:<server-package>@<exact-version> my-server > pp.config.yaml
npx mcp-guard keys init        # once
npx mcp-guard --config pp.config.yaml -- npx -y <server-package>@<exact-version>
```

Re-run the generator whenever you change the version. Add your own per-tool `rules:` after the generated lines. MCP Guard's strict loader rejects unknown keys, so keep its schema. KarmaDue's answer only sets the default for the whole server. MCP Guard still decides each call and signs the receipt.
