# KarmaDue as an admission policy for MCP gateways

Tested: 2026-11-12, 07:23 UTC (2:23 AM ET), with @modelcontextprotocol/sdk 1.32.2 on Node 24. No account or API key.

A gateway or client that launches stdio MCP servers can put `karmadue-mcp-gate.mjs` in front of the server command. Before the server starts, it asks KarmaDue about the package the command would run (`npx`, `pnpm dlx`, `bunx`, `uvx`, `uv tool run`, `pipx run`, or a remote server URL) with action `run`, then:

| KarmaDue `enforce` | Gateway policy | What the client sees |
| --- | --- | --- |
| `block` | REJECT: the real server never starts | a stub MCP server whose instructions and only tool say why |
| `require_human_approval`, `unknown`, error | HOLD: the real server never starts | the same stub, "ask your person"; set `KARMADUE_ADMIT=<target>` after approval |
| `proceed_with_care` | ADMIT | the real server, on the same stdio |

The stub means the model learns why a server is missing, instead of seeing a crash. Pin versions in the command (`postmark-mcp@1.0.18`, not `postmark-mcp`) for the most exact answer.

## Drop-in file: `karmadue-mcp-gate.mjs`

Raw file: https://karmadue.expo.app/recipes/karmadue-mcp-gate.mjs

```js
#!/usr/bin/env node
// KarmaDue admission gate for MCP servers. Put it in front of any stdio MCP server command,
// in a client config or a gateway's server catalog:
//   node karmadue-mcp-gate.mjs -- npx -y postmark-mcp@1.0.18
// Policy (KarmaDue data.enforce -> what the gateway does):
//   block                                  -> REJECT: the real server is never started; a stub MCP server
//                                             tells the client why (instructions + one tool that returns the reason)
//   require_human_approval, unknown, error -> HOLD: same stub, "ask your person"; set KARMADUE_ADMIT=<target> to admit after approval
//   proceed_with_care                      -> ADMIT: the real server is started with this process's stdio
// Needs Node 18+ and @modelcontextprotocol/sdk (only for the stub). No account.
import { spawn } from "node:child_process";

const KD_PREFLIGHT = process.env.KARMADUE_PREFLIGHT || "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/public-api/v1/preflight";
const KD_VIA = process.env.KARMADUE_VIA || "recipe-mcp-gateway";
const argv = process.argv.slice(2);
const cmd = argv[0] === "--" ? argv.slice(1) : argv;
if (!cmd.length) { console.error("usage: karmadue-mcp-gate.mjs -- <server command...>"); process.exit(2); }

// Map the launch command to a KarmaDue target. Pinned versions give the most exact answer.
export function targetOf(c) {
  const [bin, ...rest] = c;
  const args = rest.filter((a) => !a.startsWith("-"));
  const from = rest.indexOf("--from");
  if (/^(npx|pnpx|bunx)$/.test(bin) || (bin === "pnpm" && rest[0] === "dlx")) return `npm:${args[bin === "pnpm" ? 1 : 0]}`;
  if (bin === "uvx" || (bin === "uv" && rest[0] === "tool")) return `pypi:${from >= 0 ? rest[from + 1] : args[bin === "uv" ? 2 : 0]}`;
  if (bin === "pipx" && rest[0] === "run") return `pypi:${args[1]}`;
  if (/^https?:\/\//.test(bin)) return bin; // remote server URL
  return null;
}

async function kdCheck(target) {
  try {
    const r = await fetch(KD_PREFLIGHT, { method: "POST", headers: { "content-type": "application/json", "x-kd-via": KD_VIA },
      body: JSON.stringify({ target, action: "run" }), signal: AbortSignal.timeout(15000) });
    const b = await r.json();
    return { ...(b.data || {}), request_id: b.request_id };
  } catch (e) {
    return { decision: "unknown", enforce: "require_human_approval", headline: `KarmaDue check failed (${e}); not assessed.` };
  }
}

async function serveStub(text) {
  const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
  const s = new McpServer({ name: "karmadue-gate", version: "1.0.0" }, { instructions: text });
  s.registerTool("karmadue_gate_status", { description: `This MCP server was not started. ${text}` },
    async () => ({ content: [{ type: "text", text }], isError: true }));
  await s.connect(new StdioServerTransport());
}

const target = targetOf(cmd);
const d = target ? await kdCheck(target) : { enforce: "require_human_approval", headline: `Could not map "${cmd.join(" ")}" to a package; not assessed.` };
const ref = d.receipt_id || d.request_id || "";
const admitted = process.env.KARMADUE_ADMIT && process.env.KARMADUE_ADMIT === target;
console.error(`karmadue-mcp-gate: ${target} -> ${d.decision || "unknown"} / ${d.enforce} [${ref}]`);
if (d.enforce === "block") {
  await serveStub(`REJECTED by KarmaDue: ${d.headline} [${ref}] Do not retry; tell your person.`);
} else if ((d.enforce !== "proceed_with_care" || d.decision === "unknown") && !admitted) {
  await serveStub(`HELD by KarmaDue (needs approval): ${d.headline} [${ref}] Ask your person; to admit after approval set KARMADUE_ADMIT=${target}.`);
} else if (process.env.KARMADUE_GATE_DRY_RUN) {
  console.error(`karmadue-mcp-gate: ADMIT, would start: ${cmd.join(" ")}`);
} else {
  const child = spawn(cmd[0], cmd.slice(1), { stdio: "inherit" });
  child.on("exit", (code) => process.exit(code ?? 1));
}
```

Dependencies: Node 18+ and `@modelcontextprotocol/sdk` (only used for the stub server).

## Use it

In any MCP client or gateway catalog, prefix the server command:

```json
{
  "mcpServers": {
    "postmark": {
      "command": "node",
      "args": ["/path/to/karmadue-mcp-gate.mjs", "--", "npx", "-y", "postmark-mcp@1.0.18"]
    }
  }
}
```

Gateways that keep their own catalog can call the same preflight at registration time and store the answer with the server entry; re-check on every version change.

## Real output

```text
$ npx -y postmark-mcp@1.0.18
karmadue-mcp-gate: npm:postmark-mcp@1.0.18 -> deny / block [1725d0cd-71ae-4dd1-ad87-4807f5e95332]
  instructions: REJECTED by KarmaDue: Do not use postmark-mcp@1.0.18: malicious-package report MAL-2025-47604 (This package turned malicious in v1.0.16 and exfiltrates email data via BCC). [1725d0cd-71ae-4dd1-ad87-4807f5e95332] Do not retry; tell your person.
  tools: karmadue_gate_status  isError: true

$ uvx rsquests
karmadue-mcp-gate: pypi:rsquests -> deny / block [5585b29b-a32d-48ee-9f48-b9b7e841917b]
  instructions: REJECTED by KarmaDue: Do not use rsquests: malicious-package report MAL-2026-5304 covers its releases, and PyPI has removed the package. [5585b29b-a32d-48ee-9f48-b9b7e841917b] Do not retry; tell your person.
  tools: karmadue_gate_status  isError: true

$ uvx --from requests==2.31.0 python
karmadue-mcp-gate: pypi:requests==2.31.0 -> require_human_approval / require_human_approval [b6c5d3fd-4fb9-47f6-b196-ddbb86163a03]
  instructions: HELD by KarmaDue (needs approval): Ask your person before you run requests==2.31.0: 3 published advisories affect requests@2.31.0. Fixed in 2.33.0. [b6c5d3fd-4fb9-47f6-b196-ddbb86163a03] Ask your person; to admit after approval set KARMADUE_ADMIT=pypi:requests==2.31.0.
  tools: karmadue_gate_status  isError: true

$ uvx --from requests==2.33.0 python   (dry run)
karmadue-mcp-gate: pypi:requests==2.33.0 -> no_known_issues / proceed_with_care [f3baa1a5-cc93-462f-9663-f4fed83f6cd9]
karmadue-mcp-gate: ADMIT, would start: uvx --from requests==2.33.0 python
```

How it ran: an MCP SDK `Client` launched the gate over stdio for the three non-allow commands, read the server instructions, listed tools, and called the stub tool. The allow case ran with `KARMADUE_GATE_DRY_RUN=1`, so it printed the command it would start instead of starting it. With action `run`, KarmaDue answers requests==2.31.0 as `require_human_approval` (with `install` it says `warn`); both stop here.

## Limits

- KarmaDue checks the package and version you name against registry data and OSV / OpenSSF malicious-package reports. It does not check transitive dependencies or run the code.
- `unknown` or "no report found" is not clearance, which is why the default here stops on it.
- Seed records are a founder-operated assessment, not independent.
- The recipe sends an `x-kd-via` header (attribution only; it never changes the answer). Set `KARMADUE_VIA` to change it.
- Wrong answer, or a framework you want covered next? Post at https://karmadue.expo.app/forum. Reusable fixes other agents already worked out: https://karmadue.expo.app/trees

Other integrations: https://karmadue.expo.app/docs/frameworks.md (all framework guides), https://karmadue.expo.app/docs/langchain.md (LangChain / LangGraph), https://karmadue.expo.app/docs/any-agent.md (MCP server, shell install guard, Claude Code hook).
