Configuration
CLI flags
Section titled “CLI flags”| Flag | Default | Description |
|---|---|---|
-db | $HOME/.local/share/agent-receipts/audit.db | SQLite 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.db | SQLite 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-taxonomies | true | Include 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 |
-issuer | did:agent:mcp-proxy | Issuer 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 |
-principal | did:user:unknown | Principal DID for receipts |
-chain | (auto UUID) | Chain ID for receipt chaining |
-http | none | HTTP 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-timeout | 1m0s | Maximum time to wait for HTTP approval before a paused call is auto-denied |
Policy rules
Section titled “Policy rules”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: pauseRule fields
Section titled “Rule fields”| Field | Required | Description |
|---|---|---|
name | yes | Unique rule identifier |
description | no | Human-readable description |
enabled | yes | Whether the rule is active |
tool_pattern | no | Glob pattern matching tool name (case-insensitive) |
server_pattern | no | Glob pattern matching server name |
operation_types | no | Filter by operation type: read, write, delete, execute |
min_risk_score | no | Minimum risk score (0-100) to match |
action | yes | One of pass, flag, pause, block |
Actions
Section titled “Actions”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.
| Action | Behavior |
|---|---|
pass | Log only, forward normally |
flag | Log with highlight, forward normally |
pause | Hold for HTTP approval (configurable timeout, auto-denied on timeout) |
block | Reject immediately with error |
When multiple rules match, the most restrictive action wins (block > pause > flag > pass).
Risk scoring
Section titled “Risk scoring”Risk scores range from 0 to 100, computed from:
| Factor | Score | Condition |
|---|---|---|
| Operation type | 0—40 | read=0, write=20, execute=30, delete=40 |
| Sensitive keywords | +30 | Tool name contains: auth, credential, password, token, secret, key |
| SQL without WHERE | +30 | Arguments contain UPDATE/DELETE/TRUNCATE without WHERE |
| Config modification | +20 | Tool name contains: config, setting |
| External messaging | +15 | Tool name starts with: send_, post_ |
| Unknown operation | +10 | Fallback if classification fails |
Operation classification
Section titled “Operation classification”Tool names are classified by prefix (case-insensitive):
| Type | Prefixes |
|---|---|
| delete | delete_, remove_, drop_, destroy_, purge_ |
| execute | run_, exec_, invoke_, call_, trigger_ |
| write | create_, update_, set_, add_, put_, edit_, modify_, write_ |
| read | get_, 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.
Taxonomy mappings
Section titled “Taxonomy mappings”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.
Approval workflow
Section titled “Approval workflow”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}/approveandPOST /api/tool-calls/{id}/deny, both requireAuthorization: Bearer <token>. - The URL and token are printed on stderr at startup, plus a machine-readable JSON line (
{"event":"approval_endpoint",...}). - No
/pendingor index route — hittingGET /returns 404. - The HTTP server only starts when you pass
-http <addr>explicitly. Passingnone(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: -32002and adataobject includingstatus,rule_name,risk_score,approval_id, andapproval_url.
Data redaction
Section titled “Data redaction”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
Encryption at rest
Section titled “Encryption at rest”Set the BEACON_ENCRYPTION_KEY environment variable to enable AES-256-GCM encryption of all stored audit data:
BEACON_ENCRYPTION_KEY="my-passphrase" mcp-proxy node server.jsKey derivation uses Argon2id (t=1, m=64MB, p=4). Encrypted fields are stored with an enc: prefix and transparently decrypted on retrieval.
Troubleshooting
Section titled “Troubleshooting”Tool call errors (-32002 / -32003)
Section titled “Tool call errors (-32002 / -32003)”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:
- 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.
- Proxy: no approver configured (
-32003) — the call reached the proxy, matched apauserule, 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 withpolicy_action = 'rejected',approved_byNULL, andapproval_wait_usNULL or near zero. This is the common case when running the default configuration. The JSON-RPC error response carriesdata.status = "no_approver". - Proxy: approval denied or timed out (
-32002) — the call reached the proxy, matched apauserule, 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-timeoutelapsed. The audit DB row signature is the same as case 2 (policy_action = 'rejected',approved_byNULL). The JSON-RPC error response disambiguates viadata.status:"denied"for an explicit deny,"timed_out"for a timeout. A timeout will haveapproval_wait_usclose to-approval-timeout(default 60s = 60000000μs); a fast deny may have a smallapproval_wait_usindistinguishable from case 2.
Diagnose — which one is it?
# 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'andapproved_byNULL → proxy pause rejection (case 2 or 3). Distinguish via the JSON-RPC error response when you have it: code-32003is case 2; code-32002is case 3, and itsdata.statusfield ("denied"vs"timed_out") tells you which sub-case. If the error response is gone,approval_wait_usonly 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.
# Check which process flags are in effect.ps aux | grep mcp-proxy | grep -v grepIf 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.
# 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.
404 at http://127.0.0.1:8081/
Section titled “404 at http://127.0.0.1:8081/”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:
# Claude Desktopmcp-proxy -name github -http 127.0.0.1:8080 ... /path/to/server
# Claude Codemcp-proxy -name github -http 127.0.0.1:8081 ... /path/to/server
# Codexmcp-proxy -name github -http 127.0.0.1:8082 ... /path/to/server127.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.