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),
delaycompressoff (mcpgw owns compression), and amaxage/rotatecount 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_APPENDand 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
tsif you need a global stream.
Pitfalls
tsis RFC3339 with millisecond precision in UTC. Always UTC. If your dashboards default to local time, set them to UTC for audit work.rule_idis empty fordecision: "allow"from no-rule paths. It is populated when an explicit rule fired (allow rules included).duration_msincludes upstream latency for forwarded requests; fordenydecisions where mcpgw never contacted the upstream, it is purely gateway processing time.bytes_outis0on errors that produce a JSON-RPC error before any upstream bytes were observed (e.g.parse_error,policy_denied).
Related
- Reference: audit log schema
- Reference: error codes —
decisionvalues and HTTP/JSON-RPC code mappings