---
title: Configuration
description: CLI flags, policy rules, redaction, and encryption options for the MCP Proxy.
---

## 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](/mcp-proxy/approval-ui/). |
| `-approval-timeout` | `1m0s` | Maximum time to wait for HTTP approval before a paused call is auto-denied |

## Policy rules

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

```yaml
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
```

### 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

`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

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

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

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.

```json
{
  "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](https://github.com/agent-receipts/ar/blob/main/spec/spec/taxonomy/action-types.json) for the full list.

The proxy ships with bundled mappings — currently [`github_taxonomy.json`](https://github.com/agent-receipts/ar/blob/main/mcp-proxy/configs/github_taxonomy.json) and [`atlassian_taxonomy.json`](https://github.com/agent-receipts/ar/blob/main/mcp-proxy/configs/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

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](/mcp-proxy/approval-ui/) 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`.

## 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

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

```bash
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.

## Troubleshooting

### 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:

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?**

```bash
# 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](https://github.com/agent-receipts/ar/issues/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](/mcp-proxy/approval-ui/) for both paths.

```bash
# 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.

```bash
# 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/`

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](/mcp-proxy/approval-ui/#endpoints).

### 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:

```bash
# 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.
