Audit logging
Every operation that passes through mcp is logged — CLI commands, proxy requests, tool calls, registry searches. The audit log gives you full visibility into what happened, when, how long it took, and whether it succeeded.
How it works
mcp writes audit entries to an embedded ChronDB database stored locally. Logging happens in a background thread via an async channel, so it never blocks your commands.
mcp <any command> --> AuditLogger (mpsc channel) --> ChronDB (background writer)
|
~/.config/mcp/audit/Every entry records:
timestamp
ISO 8601 timestamp
source
Where it came from: cli, serve:http, serve:stdio
method
What was called: tools/call, tools/list, registry/search, etc.
tool_name
Tool name (for tools/call)
server_name
Backend server name
identity
Who called it: local for CLI, user subject for proxy
duration_ms
How long it took
success
Whether it worked
error_message
Error details when it failed
acl_decision
allow or deny when ACL evaluation is performed
acl_matched_rule
Which rule decided: dev[1], alice.extra[0], default, legacy[3], legacy:default, no-acl
acl_access_kind
Effective access evaluated: read, write, or *
classification_kind
Tool classification: read, write, or ambiguous
classification_source
How it was classified: override, annotation, classifier, or fallback
classification_confidence
Classifier confidence (0.00–1.00)
ACL/classification fields are present on entries that perform ACL checks: proxy tools/call, tools/list:filtered, and CLI acl/check. Other entries omit them (the fields are absent, not null).
What gets logged
Everything:
mcp --list
servers/list
mcp search <query>
registry/search
mcp add <name>
config/add
mcp remove <name>
config/remove
mcp update <name>
config/update
mcp <server> --list
tools/list
mcp <server> --info
tools/info
mcp <server> <tool>
tools/call
Proxy: any JSON-RPC request
initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get
The only command that doesn't log itself is mcp logs (that would be recursive).
Querying logs
Output formats
Terminal (interactive) — colored table:
JSON (piped or --json) — composable with jq:
Follow mode
Stream new entries in real-time, like tail -f:
Follow mode uses polling (1s interval) on the ChronDB database, so it works even when mcp serve runs in a separate process.
Configuration
Add an audit section to ~/.config/mcp/servers.json:
enabled
true
Enable/disable audit logging
output
unset (→ file for CLI, file+stdout for serve --http, file+stderr for serve stdio)
Output destination: file (ChronDB, queryable), stdout, stderr (JSON lines), file+stdout, file+stderr (ChronDB and JSON lines), or none. Internally Option<AuditOutput> — leaving it unset means "use the per-context default" (CLI safety vs serve visibility). Any explicit value (including "file") bypasses the auto-promotion in mcp serve.
log_arguments
false
Log tool call arguments (may contain PII)
path
~/.config/mcp/audit/data
ChronDB data directory
index_path
~/.config/mcp/audit/index
ChronDB index directory
Logging arguments
By default, tool call arguments are not logged to avoid capturing sensitive data (API keys, personal info, query contents). Enable log_arguments only if you need it:
With this enabled, mcp logs --json will include the full arguments:
Storage
Audit data lives in ~/.config/mcp/audit/ by default:
Each entry is stored as a JSON document with key audit:{timestamp_millis}-{uuid}, which gives natural chronological ordering via prefix listing.
Disabling audit logging
Via config file:
Via environment variable (takes priority over config file):
When disabled, the logger is a no-op and the database is not initialized — zero overhead, no files created, no filesystem writes. This is the default in the Docker image.
Output destinations
The output field controls where audit entries go.
The configuration distinguishes explicit values from absent ones. Internally output is Option<AuditOutput>: missing from the config and unset in MCP_AUDIT_OUTPUT means None (default); a present value means Some(...) (explicit). The distinction is what lets mcp serve auto-promote the default without ever overwriting a deliberate operator choice.
CLI subcommands (mcp roam ..., mcp gh ..., etc.) resolve None to file — entries are persisted to ChronDB and stdout stays clean (so pipelines like mcp ... | jq aren't corrupted by audit JSON interleaved with command output).
mcp serve resolves None to a dual-sink mode:
HTTP transport (
mcp serve --http ...):None→file+stdout. Audit is mirrored on stdout so it's visible indocker logs/kubectl logswithout an extra command, while still persisting to ChronDB formcp logsqueries.Stdio transport (
mcp serve):None→file+stderr. Stdout in stdio mode is the JSON-RPC channel, so the mirror goes to stderr instead.
Any explicit value in the config file or MCP_AUDIT_OUTPUT env var (including "file") bypasses the auto-promotion. To force chrondb-only output in mcp serve, set "output": "file" explicitly — it survives the resolution untouched.
Each stdout/stderr line is a complete AuditEntry JSON object:
The mirror is emitted before the ChronDB write, so entries stay visible even if the persistence layer fails.
Choosing a mode
output
ChronDB
stdout
stderr
mcp logs
unset → file (CLI default) / file+stdout (serve http) / file+stderr (serve stdio)
varies
varies
varies
when ChronDB is active
file (explicit)
✅
—
—
✅
file+stdout
✅
✅
—
✅
file+stderr
✅
—
✅
✅
stdout
—
✅
—
—
stderr
—
—
✅
—
none
—
—
—
—
Pick file explicitly if you want chrondb-only even in mcp serve — explicit values skip the auto-promotion. Pick stdout/stderr only when you can't persist (read-only filesystem, ephemeral containers without a volume). Pick file+stderr if your transport is stdio or you want to keep stdout reserved for application output.
When using stdout or stderr alone (without file), mcp logs queries are not available — there's no database to query. Use your log aggregation pipeline instead.
stdio transport caveat: in
mcp servewithout--http(stdio mode), stdout is the JSON-RPC channel. The default auto-promotion picksfile+stderr. If the operator explicitly picksstdoutorfile+stdout, it's rewritten to the stderr variant with a warning so the JSON-RPC channel stays clean.
Set output to none to disable audit entirely without touching the enabled flag.
Environment variable overrides
All audit settings can be overridden via environment variables, which take priority over the config file. This is useful for container deployments where editing the config JSON is impractical.
MCP_AUDIT_ENABLED
audit.enabled
Set to false or 0 to disable
MCP_AUDIT_OUTPUT
audit.output
file, stdout, stderr, file+stdout, file+stderr, or none
MCP_AUDIT_PATH
audit.path
ChronDB data directory
MCP_AUDIT_INDEX_PATH
audit.index_path
ChronDB index directory
Example: redirect audit to a mounted volume in Docker:
Example: stream audit to container stdout (no volume needed):
See the full list of variables in the environment variables reference.
Last updated
Was this helpful?