Skip to content

Configuration

FlagDefaultDescription
-db$HOME/.local/share/agent-receipts/audit.dbSQLite audit database path. Parent directory is created on first run (mode 0700 on Unix). Respects $XDG_DATA_HOME.
-receipt-db$HOME/.local/share/agent-receipts/receipts.dbSQLite receipt store path. Parent directory is created on first run (mode 0700 on Unix). Respects $XDG_DATA_HOME.
-key(ephemeral)Ed25519 private key PEM file for signing receipts
-taxonomy(none)Taxonomy mappings JSON file for action classification. Merged with bundled taxonomies; user mappings win on conflict.
-bundled-taxonomiestrueInclude bundled taxonomies (e.g. GitHub, Atlassian) embedded in the binary. Set to false to use only -taxonomy.
-rules(built-in defaults)Policy rules YAML file
-name(inferred from command)Server name for the audit trail
-issuerdid:agent:mcp-proxyIssuer DID for receipts
-issuer-name(none)Issuer name, e.g. Claude Code or Codex
-issuer-model(none)AI model identifier, e.g. claude-sonnet-4-6. Static per session — omit if the client can switch models mid-session
-operator-id(none)Operator DID (organisation running the agent), e.g. did:web:anthropic.com
-operator-name(none)Operator name, e.g. Anthropic
-principaldid:user:unknownPrincipal DID for receipts
-chain(auto UUID)Chain ID for receipt chaining
-httpnoneHTTP address for the approval listener. Off by default — no listener starts unless you opt in. Pass 127.0.0.1:0 for a random free port or 127.0.0.1:<port> to pin a port. See Approval Server.
-approval-timeout1m0sMaximum time to wait for HTTP approval before a paused call is auto-denied

Rules are defined in YAML and control what happens when a tool call matches:

rules:
- name: block_destructive_ops
description: Block delete operations on sensitive tools
enabled: true
tool_pattern: "delete_*"
server_pattern: "*postgres*"
operation_types: [delete]
min_risk_score: 70
action: block
- name: pause_high_risk
description: Require approval for high-risk operations
enabled: true
min_risk_score: 50
action: pause
FieldRequiredDescription
nameyesUnique rule identifier
descriptionnoHuman-readable description
enabledyesWhether the rule is active
tool_patternnoGlob pattern matching tool name (case-insensitive)
server_patternnoGlob pattern matching server name
operation_typesnoFilter by operation type: read, write, delete, execute
min_risk_scorenoMinimum risk score (0-100) to match
actionyesOne of pass, flag, pause, block

pass and flag are audit-only — they record the call and forward it. pause and block are proxy-layer enforcement features; the Agent Receipts protocol itself is audit-only and does not block or modify tool calls.

ActionBehavior
passLog only, forward normally
flagLog with highlight, forward normally
pauseHold for HTTP approval (configurable timeout, auto-denied on timeout)
blockReject immediately with error

When multiple rules match, the most restrictive action wins (block > pause > flag > pass).

Risk scores range from 0 to 100, computed from:

FactorScoreCondition
Operation type0—40read=0, write=20, execute=30, delete=40
Sensitive keywords+30Tool name contains: auth, credential, password, token, secret, key
SQL without WHERE+30Arguments contain UPDATE/DELETE/TRUNCATE without WHERE
Config modification+20Tool name contains: config, setting
External messaging+15Tool name starts with: send_, post_
Unknown operation+10Fallback if classification fails

Tool names are classified by prefix (case-insensitive):

TypePrefixes
deletedelete_, remove_, drop_, destroy_, purge_
executerun_, exec_, invoke_, call_, trigger_
writecreate_, update_, set_, add_, put_, edit_, modify_, write_
readget_, read_, list_, search_, describe_, show_
unknown(fallback)

MCP tool names are automatically stripped of their mcp__<server>__ prefix before classification. For example, mcp__github-audited__create_branch is classified as create_branch (write). This means taxonomy mappings and policy rules use bare tool names.

For more precise classification than prefix-based inference, provide a -taxonomy JSON file mapping tool names to action types. Bundled taxonomies (see below) are loaded automatically; the -taxonomy file is merged on top and wins on tool-name conflicts.

{
"mappings": [
{"tool_name": "merge_pull_request", "action_type": "data.api.write"},
{"tool_name": "list_issues", "action_type": "data.api.read"},
{"tool_name": "delete_file", "action_type": "data.api.delete"}
]
}

Available action types include filesystem.file.*, system.*, and data.api.* (read, write, delete). See the taxonomy spec for the full list.

The proxy ships with bundled mappings — currently github_taxonomy.json and atlassian_taxonomy.json — embedded into the binary. They are loaded automatically; no -taxonomy flag is needed for those servers. A user-supplied -taxonomy is merged on top and wins on tool-name conflicts. Pass -bundled-taxonomies=false to disable the embedded set.

Approvals are off by default — the HTTP listener does not start unless you pass -http <addr>. A pause rule that fires without a listener fails fast with JSON-RPC code -32003 (no approver configured…) so the failure is immediate and obvious rather than a silent 60-second timeout.

The Approval Server page has the full treatment — endpoints, auth, running an approver, opting out. Quick reference:

  • Endpoints: POST /api/tool-calls/{id}/approve and POST /api/tool-calls/{id}/deny, both require Authorization: Bearer <token>.
  • The URL and token are printed on stderr at startup, plus a machine-readable JSON line ({"event":"approval_endpoint",...}).
  • No /pending or index route — hitting GET / returns 404.
  • The HTTP server only starts when you pass -http <addr> explicitly. Passing none (or omitting -http) keeps the listener off.
  • Pin a port (127.0.0.1:8081) if you’re running an external approver that can’t parse the stderr discovery event.
  • On deny or timeout the client receives a JSON-RPC error with code: -32002 and a data object including status, rule_name, risk_score, approval_id, and approval_url.

The proxy redacts sensitive data before storage using two passes:

JSON-aware redaction replaces values of sensitive keys including: password, token, api_key, secret, authorization, private_key, access_token, jwt, database_url, ssh_key, connection_string, and others (42 keys total).

Pattern-based redaction matches known secret formats:

  • GitHub PATs and OAuth tokens (ghp_*, gho_*)
  • OpenAI/Anthropic API keys (sk-*)
  • AWS access keys (AKIA*)
  • Bearer tokens
  • Slack tokens (xox*)
  • PEM private key blocks

Set the BEACON_ENCRYPTION_KEY environment variable to enable AES-256-GCM encryption of all stored audit data:

Terminal window
BEACON_ENCRYPTION_KEY="my-passphrase" mcp-proxy node server.js

Key derivation uses Argon2id (t=1, m=64MB, p=4). Encrypted fields are stored with an enc: prefix and transparently decrypted on retrieval.

Three different things produce these error codes. Distinguish them before reaching for a fix, because they need different remedies and only two of them involve the proxy:

  1. Client-side denial — the MCP client (e.g. Claude Code) rejected the tool use before it reached the proxy. The call never hits the audit DB.
  2. Proxy: no approver configured (-32003) — the call reached the proxy, matched a pause rule, but the approval HTTP listener is not running (operator did not pass -http <addr>). The proxy fails fast rather than waiting out the timeout. The call appears in the audit DB with policy_action = 'rejected', approved_by NULL, and approval_wait_us NULL or near zero. This is the common case when running the default configuration. The JSON-RPC error response carries data.status = "no_approver".
  3. Proxy: approval denied or timed out (-32002) — the call reached the proxy, matched a pause rule, and an approver listener was wired up (-http <addr> was passed). The approver either explicitly denied the call, or no approver POSTed a decision before -approval-timeout elapsed. The audit DB row signature is the same as case 2 (policy_action = 'rejected', approved_by NULL). The JSON-RPC error response disambiguates via data.status: "denied" for an explicit deny, "timed_out" for a timeout. A timeout will have approval_wait_us close to -approval-timeout (default 60s = 60000000μs); a fast deny may have a small approval_wait_us indistinguishable from case 2.

Diagnose — which one is it?

Terminal window
# Did the failing call reach the proxy at all?
# Substitute the tool name you saw fail.
sqlite3 ~/.local/share/agent-receipts/audit.db \
"SELECT tool_name, policy_action, risk_score, approved_by, approval_wait_us, requested_at
FROM tool_calls
WHERE tool_name = 'create_pull_request'
AND requested_at > datetime('now', '-1 hour')
ORDER BY id DESC LIMIT 10;"
  • No rows at all → client-side denial (case 1). The proxy never saw it. See ar#157 for the ongoing work to make these visible.
  • Rows with policy_action = 'pass' → the proxy forwarded it; the error came from somewhere downstream (the MCP server, the remote API). Check the proxy’s stderr log and the server’s own logs.
  • Rows with policy_action = 'rejected' and approved_by NULL → proxy pause rejection (case 2 or 3). Distinguish via the JSON-RPC error response when you have it: code -32003 is case 2; code -32002 is case 3, and its data.status field ("denied" vs "timed_out") tells you which sub-case. If the error response is gone, approval_wait_us only reliably identifies the timeout sub-case (it’ll be close to -approval-timeout); a small or NULL value can be either case 2 (no approver, fail-fast) or a fast deny under case 3 — they’re indistinguishable from the audit DB alone.

If the error is -32003 (no approver configured):

The proxy is pausing high-risk calls but has no approval listener running. Either start an approver by passing -http <addr> when launching the proxy, or relax the policy so the tool is not paused. See Approval Server for both paths.

Terminal window
# Check which process flags are in effect.
ps aux | grep mcp-proxy | grep -v grep

If the error is -32002 (approval denied or timed out):

An approver listener is running and either denied the call or didn’t respond in time. Check data.status in the JSON-RPC error: "denied" means the approver said no — that’s the workflow working as intended, no proxy fix needed. "timed_out" means no decision arrived; verify your approver can reach the endpoint and that -approval-timeout is long enough.

Terminal window
# Confirm the -http address and timeout flags.
ps aux | grep mcp-proxy | grep -v grep
# Check overall breakdown.
sqlite3 ~/.local/share/agent-receipts/audit.db \
"SELECT policy_action, COUNT(*) FROM tool_calls GROUP BY policy_action;"

The scorer stacks a base (read 0, write 20, execute 30, delete 40, unknown 10) with modifiers — sensitive keywords in the tool name (+30), SQL mutations without WHERE (+30), config/setting substrings (+20), send_*/post_* prefixes (+15) — so what actually crosses 50 is combinations: create_token (write 20 + sensitive 30 = 50), update_auth_config (70), delete_credential (70), delete_config (60), exec_sql with a DELETE and no WHERE (60). Ordinary writes like create_pull_request (20), unknown-prefix calls like merge_pull_request (10), plain deletes like delete_branch (40), update_config with no sensitive keyword (40), and sensitive-keyword reads like get_token (30) all stay below the threshold — if one of those is failing, you’re in path #1, not #2 or #3.

The approval server is not a web UI. GET / returns 404 by design. The only routes are POST /api/tool-calls/{id}/approve and POST /api/tool-calls/{id}/deny, both requiring a bearer token — see Approval Server.

Multiple MCP clients running the proxy simultaneously

Section titled “Multiple MCP clients running the proxy simultaneously”

By default, mcp-proxy starts no HTTP listener (-http none), so multiple concurrent sessions work without any port configuration — they simply don’t collide.

If you do opt in to the approval workflow, each instance that passes -http <addr> binds its own server. Give each a distinct port so an approver can route to the right one:

Terminal window
# Claude Desktop
mcp-proxy -name github -http 127.0.0.1:8080 ... /path/to/server
# Claude Code
mcp-proxy -name github -http 127.0.0.1:8081 ... /path/to/server
# Codex
mcp-proxy -name github -http 127.0.0.1:8082 ... /path/to/server

127.0.0.1:0 picks a random free port automatically — fine when the approver parses the stderr discovery event at startup.

Each instance can also use separate -db and -receipt-db paths if you want isolated audit trails, or share the same databases if you want a unified log. By default both point at $HOME/.local/share/agent-receipts/ (respecting $XDG_DATA_HOME), so all clients share one audit log unless you override them.