> For the complete documentation index, see [llms.txt](https://mcp.avelino.run/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mcp.avelino.run/guides/audit-logging.md).

# 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](https://chrondb.avelino.run/) 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:

| Field                       | Description                                                                                        |
| --------------------------- | -------------------------------------------------------------------------------------------------- |
| `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:

| Command                     | Method                                                                                                      |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `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

```bash
# Recent entries (default: last 50)
mcp logs

# Last 100 entries
mcp logs --limit 100

# Filter by backend server
mcp logs --server sentry

# Filter by tool name prefix
mcp logs --tool sentry__search

# Filter by JSON-RPC method
mcp logs --method tools/call

# Filter by caller identity (proxy mode)
mcp logs --identity alice

# Only failures
mcp logs --errors

# Time-based filter
mcp logs --since 5m       # last 5 minutes
mcp logs --since 1h       # last hour
mcp logs --since 24h      # last 24 hours
mcp logs --since 7d       # last 7 days

# Combine filters
mcp logs --server sentry --errors --since 24h
```

### Output formats

**Terminal** (interactive) — colored table:

```
Timestamp                         Source       Method           Tool                     Server   Identity  Duration  Status  Detail
2026-03-16T18:30:00+00:00         serve:http   tools/call       sentry__search_issues    sentry   alice     142ms     ok      -
2026-03-16T18:30:02+00:00         cli          registry/search  -                        -        local     630ms     ok      query=filesystem
2026-03-16T18:30:05+00:00         cli          tools/call       search_issues            sentry   local     27ms      error   MCP error -32602: Invalid arguments for tool search_issues:

3 entry(ies)
```

**JSON** (piped or `--json`) — composable with `jq`:

```bash
# Slow calls (>500ms)
mcp logs --json | jq '.[] | select(.duration_ms > 500)'

# Error messages only
mcp logs --errors --json | jq '.[].error_message'

# Count calls per server
mcp logs --json | jq 'group_by(.server_name) | map({server: .[0].server_name, count: length})'

# Denied write requests
mcp logs --json | jq '.[] | select(.acl_decision=="deny" and .acl_access_kind=="write")'

# Low-confidence classifications that were allowed
mcp logs --json | jq '.[] | select(.classification_confidence < 0.5 and .acl_decision=="allow")'

# Which rules are denying requests
mcp logs --json | jq '[.[] | select(.acl_decision=="deny")] | group_by(.acl_matched_rule) | map({rule: .[0].acl_matched_rule, count: length})'
```

## Follow mode

Stream new entries in real-time, like `tail -f`:

```bash
# Follow all entries
mcp logs -f

# Follow only errors
mcp logs -f --errors

# Follow filtered by server
mcp logs -f --server sentry
```

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

```json
{
  "mcpServers": { ... },
  "audit": {
    "enabled": true,
    "log_arguments": false
  }
}
```

| Field           | Default                                                                                     | Description                                                                                                                                                                                                                                                                                                                                                           |
| --------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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:

```json
{
  "audit": {
    "log_arguments": true
  }
}
```

With this enabled, `mcp logs --json` will include the full arguments:

```json
{
  "method": "tools/call",
  "tool_name": "search_issues",
  "arguments": {"query": "is:unresolved", "organizationSlug": "my-org"},
  ...
}
```

## Storage

Audit data lives in `~/.config/mcp/audit/` by default:

```
~/.config/mcp/audit/
  data/     # ChronDB git-based document store
  index/    # Lucene search index
```

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:

```json
{
  "audit": {
    "enabled": false
  }
}
```

Via environment variable (takes priority over config file):

```bash
MCP_AUDIT_ENABLED=false mcp serve --http 0.0.0.0:8080
```

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 in `docker logs`/`kubectl logs` without an extra command, while still persisting to ChronDB for `mcp logs` queries.
* **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:

```json
{"timestamp":"2026-04-14T12:00:00-03:00","source":"serve:http","method":"tools/call","tool_name":"search_issues","server_name":"sentry","identity":"alice","duration_ms":142,"success":true}
```

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.

```bash
MCP_AUDIT_OUTPUT=file mcp serve --http 0.0.0.0:8080
```

```json
{ "audit": { "output": "file" } }
```

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 serve` without `--http` (stdio mode), stdout is the JSON-RPC channel. The default auto-promotion picks `file+stderr`. If the operator explicitly picks `stdout` or `file+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.

| Variable               | Overrides          | Description                                                         |
| ---------------------- | ------------------ | ------------------------------------------------------------------- |
| `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:

```bash
docker run -d \
  -e MCP_AUDIT_ENABLED=true \
  -e MCP_AUDIT_PATH=/data/audit/data \
  -e MCP_AUDIT_INDEX_PATH=/data/audit/index \
  -v audit-vol:/data/audit \
  ghcr.io/avelino/mcp serve --http 0.0.0.0:8080
```

Example: stream audit to container stdout (no volume needed):

```bash
docker run -d \
  -e MCP_AUDIT_OUTPUT=stdout \
  ghcr.io/avelino/mcp serve --http 0.0.0.0:8080
```

See the full list of variables in the [environment variables reference](/reference/environment-variables.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mcp.avelino.run/guides/audit-logging.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
