MCP proxy
The proxy checks only the calls sent through it. A tool your agent can reach another way is not protected.
noa-mcp-proxy sits between an AI app (the MCP host) and a tool server it already uses; the tool server’s code does not change. MCP means Model Context Protocol, an open protocol AI software uses to call tools.
Your policy file decides each tool call: allowed calls go through and blocked calls never reach the tool. Calls that match a rule in approval-rules.json are held: the proxy refuses them with an error carrying a receipt id, a person approves, and the agent retries the identical call. Each decision becomes a signed receipt that anyone with the public keys can check offline.
It fails closed, which means it refuses instead of guessing: an unusable policy file stops it from starting, and an unexpected error before forwarding never lets a call through; if the connection breaks after a call was forwarded, the proxy returns an error, and the call may still have run.
You need Node.js 20 or newer.
noa-approve:npm install -g noa-mcp-proxy noa-mcp-adapter-corenoa folder, and prints the start command and an MCP config to paste, both with absolute paths:noa-mcp-proxy init --dir noa --allow-tool search_docs --approval-tool refund --block-tool delete_allinit printed, which has full paths. Everything after -- is your server’s own command. With the paths shortened, it reads:noa-mcp-proxy --policy noa/policy.json --approval-rules noa/approval-rules.json --pending-store noa/pending-store.jsonl --approver-keyring noa/approver-keyring.json --key-file noa/proxy-key.json --keyring-file noa/keyring.json --receipt-log noa/decisions.jsonl --outcome-log noa/outcomes.jsonl -- node your-server.jsrefund call returns an error carrying a receipt id. A person approves it from a separate terminal, then the agent retries the identical call; approving does not run anything by itself:noa-approve approve --id <receiptId> --by you@example.com --pending-store noa/pending-store.jsonl --key-file noa/approver-key.json0 means VALID, 2 TAMPERED, 3 MALFORMED and 4 a usage error:noa-mcp-proxy verify-outcome noa/outcomes.jsonl --keyring noa/keyring.jsoninit only writes starter files; it does not switch protection on. Nothing is protected until your MCP host starts the proxy with these files, the tool names are your own, a person runs noa-approve for each held call, and the agent retries the identical call.
--policy file. Without it the proxy runs a built-in demo policy written for its demo server, and prints a warning at every start.policy.json and approval-rules.json. The proxy refuses to start when an approval rule names a tool the policy does not (POLICY_APPROVAL_MISMATCH).--approval-rules, --pending-store and --approver-keyring. The proxy refuses to start when either of the first two is given without trusted approver keys.init prints. The proxy refuses a policy or approval file that is a symlink or that others can write, but it cannot see a swapped parent folder.--policy takes a noa.policy/0.2 file. The first rule that matches decides, and a call that no rule matches is denied.
{
"spec": "noa.policy/0.2",
"id": "my-tools-v1",
"requiredPaths": ["action"],
"rules": [
{ "id": "allow-search_docs", "when": { "op": "eq", "path": "action", "value": "search_docs" }, "then": "ALLOW" },
{ "id": "small-refunds-only", "when": { "op": "and", "clauses": [
{ "op": "eq", "path": "action", "value": "refund" },
{ "op": "lt", "path": "args.amountMinor", "value": 10000 } ] }, "then": "ALLOW" }
]
}action is the tool name; args.<name> is a tool argument (nested: args.a.b). Numbers must be integers.eq ne lt le gt ge (with value), in (values), exists absent, and or (clauses) and not (clause). A verdict is ALLOW or DENY.1 and one error line that names the file, the field and the fix. The line starts with one of four stable codes: POLICY_UNREADABLE, POLICY_UNPARSABLE, POLICY_INVALID or POLICY_APPROVAL_MISMATCH.There are two kinds of receipt, and each has its own checker.
Outcome receipts (noa.mcp.outcome/0.1, written with --outcome-log) are checked with noa-mcp-proxy verify-outcome, as in step 5.
Decision receipts (noa.receipt/0.1, written with --receipt-log) are checked with the noa command from the noa-receipt package:
noa verify <receipts.json> --keyring <keyring.json>That command reads one JSON array, not one receipt per line, so put the log’s lines inside one [ ... ] array first. Its keyring must hold every key that signed the chain: the proxy’s key from noa/keyring.json and, when a call was approved, the approver’s key from noa/approver-keyring.json, merged into one JSON object.
VALID means every line is a genuine, signed outcome receipt, each for a different decision. It does not prove the log is complete: a deleted line leaves no trace. The same decision’s outcome appearing twice is TAMPERED.
By default the proxy holds its private signing key inside its own process: a new key at every start, or a saved one with --key-file.
noa-signer-sidecar moves the key into a separate process. The proxy asks it for signatures over a local Unix socket meant only for this machine; exposing that socket beyond it, for example through a container mount or an SSH forward, is outside the sidecar’s threat model: do not do it. Start it like this:
npm install -g noa-signer-sidecar
mkdir -p -m 700 /path/to/private-dir
noa-signer-sidecar --key-file /path/to/private-dir/key.json --socket /path/to/private-dir/signer.sockThen start the proxy with this flag instead of --key-file:
--signer-socket /path/to/private-dir/signer.sock0700, open only to you; the sidecar refuses to start otherwise. It checks this at startup, not continuously: loosening the folder later is not noticed.--signer-socket and --key-file cannot be used together.It is not the NOA Mandate guarantee on its own. That guarantee needs all four no-back-door rules, including a vault that checks Noa’s approval at its own lock.
Read NON-CLAIMS.md before you rely on the proxy for anything that matters.
Status, checked 2026-10-06: noa-mcp-proxy 0.5.0 and noa-mcp-adapter-core 0.5.0 are published on npm as open source (Apache-2.0), and noa-signer-sidecar 0.1.0 is an optional add-on.
The source links point to the exact public commit this guide was written from.