Tail and parse the audit log

Problem: you need to answer “what did the gateway decide and why?” — for an incident, a compliance question, or to tune a policy.

Solution: the audit log is JSONL at the path you set in audit.path. One line per request. Read it with jq.

Recipe — common queries

Stream all decisions in real time:

tail -F /var/log/mcpgw/audit.jsonl | jq .

Show only non-allow decisions (deny, redact, rate_limit_blocked, parse_error, body_too_large):

jq -c 'select(.decision != "allow")' /var/log/mcpgw/audit.jsonl

Top denied tools in the last 100k events:

tail -100000 /var/log/mcpgw/audit.jsonl \
  | jq -r 'select(.decision == "deny") | .tool' \
  | sort | uniq -c | sort -rn | head

All actions taken by a specific session:

jq -c 'select(.session_id == "alice-laptop-2024")' /var/log/mcpgw/audit.jsonl

Bulk redact rate over time (per-minute):

jq -r 'select(.decision == "redact") | .ts[:16]' /var/log/mcpgw/audit.jsonl \
  | sort | uniq -c

Slowest requests (p99 you missed in Datadog):

jq -c 'select(.duration_ms > 1000)' /var/log/mcpgw/audit.jsonl

Schema cheatsheet

{
  "ts": "2024-01-15T14:23:01.234Z",
  "session_id": "claude-desktop-3f2a",
  "method": "tools/call",
  "tool": "fs_read",
  "upstream": "filesystem",
  "decision": "allow",
  "rule_id": "",
  "client_ip": "203.0.113.42",
  "bytes_in": 312,
  "bytes_out": 1024,
  "status": 200,
  "duration_ms": 47,
  "request_id": "req_01HXYZ..."
}

Full field reference at Reference: audit log schema.

Rotation and retention

mcpgw rotates the file when it reaches audit.max_size_mb. Rotated files are renamed to audit.jsonl.<timestamp> and, if audit.compress_rotated: true, gzipped asynchronously. mcpgw does not delete rotated files — that is your retention layer’s job. Common patterns:

  • logrotate with size-based rotation disabled (mcpgw owns rotation), delaycompress off (mcpgw owns compression), and a maxage / rotate count for retention
  • filebeat / fluentbit / vector to ship to GCS, S3, Elastic, Loki, etc., with retention enforced downstream
  • systemd-tmpfiles with q - rules for time-based deletion of *.jsonl.gz

Audit log integrity

  • Append-only by default. The mcpgw process opens the file with O_APPEND and never seeks. Concurrent writers from a single mcpgw instance are safe.
  • No HMAC, no chain. v1 does not sign or chain audit lines. If you need tamper-evidence, ship the log to a write-once destination (GCS Bucket Lock, S3 Object Lock, or a logging SIEM with WORM mode).
  • Ordering is per-process. With multiple mcpgw replicas, each writes its own file. Aggregate downstream and sort by ts if you need a global stream.

Pitfalls

  • ts is RFC3339 with millisecond precision in UTC. Always UTC. If your dashboards default to local time, set them to UTC for audit work.
  • rule_id is empty for decision: "allow" from no-rule paths. It is populated when an explicit rule fired (allow rules included).
  • duration_ms includes upstream latency for forwarded requests; for deny decisions where mcpgw never contacted the upstream, it is purely gateway processing time.
  • bytes_out is 0 on errors that produce a JSON-RPC error before any upstream bytes were observed (e.g. parse_error, policy_denied).