Skip to content

Commit db31884

Browse files
committed
Honor explicit audit.output value in mcp serve
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
1 parent e7da410 commit db31884

9 files changed

Lines changed: 257 additions & 142 deletions

File tree

docs/guides/audit-logging.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Add an `audit` section to `~/.config/mcp/servers.json`:
157157
| Field | Default | Description |
158158
|---|---|---|
159159
| `enabled` | `true` | Enable/disable audit logging |
160-
| `output` | `file` (global), auto-promoted in `mcp serve` to `file+stdout` (HTTP) or `file+stderr` (stdio) | Output destination: `file` (ChronDB, queryable), `stdout`, `stderr` (JSON lines), `file+stdout`, `file+stderr` (ChronDB **and** JSON lines), or `none`. The global default stays `file` so CLI subcommands (`mcp roam ...`, `mcp gh ...`) keep stdout clean for their results. `mcp serve` upgrades the default to a dual-sink mode so audit is visible in `docker logs`/`kubectl logs` without an extra command, while still persisting to ChronDB for `mcp logs` queries. Explicit user values are preserved as-is. |
160+
| `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`. |
161161
| `log_arguments` | `false` | Log tool call arguments (may contain PII) |
162162
| `path` | `~/.config/mcp/audit/data` | ChronDB data directory |
163163
| `index_path` | `~/.config/mcp/audit/index` | ChronDB index directory |
@@ -221,14 +221,16 @@ When disabled, the logger is a no-op and the database is not initialized — zer
221221

222222
The `output` field controls where audit entries go.
223223

224-
The **global default is `file`** — entries are persisted to ChronDB and the CLI stays silent on stdout (so `mcp roam ... | jq` and similar pipelines aren't corrupted by audit JSON interleaved with command output).
224+
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.
225225

226-
In **`mcp serve` only**, the default is auto-promoted:
226+
**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).
227227

228-
- **HTTP transport** (`mcp serve --http ...`): `file``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.
229-
- **Stdio transport** (`mcp serve`): `file``file+stderr`. Stdout in stdio mode is the JSON-RPC channel, so the mirror goes to stderr instead.
228+
**`mcp serve`** resolves `None` to a dual-sink mode:
230229

231-
Any explicit value in the config file or `MCP_AUDIT_OUTPUT` env var **bypasses** the auto-promotion — your choice is respected.
230+
- **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.
231+
- **Stdio transport** (`mcp serve`): `None``file+stderr`. Stdout in stdio mode is the JSON-RPC channel, so the mirror goes to stderr instead.
232+
233+
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.
232234

233235
Each stdout/stderr line is a complete `AuditEntry` JSON object:
234236

@@ -242,14 +244,15 @@ The mirror is emitted **before** the ChronDB write, so entries stay visible even
242244

243245
| `output` | ChronDB | stdout | stderr | `mcp logs` |
244246
|---|---|---|---|---|
245-
| `file` (**global default**) |||||
246-
| `file+stdout` (auto in `mcp serve --http`) |||||
247-
| `file+stderr` (auto in `mcp serve` stdio) |||||
247+
| _unset_`file` (CLI default) / `file+stdout` (serve http) / `file+stderr` (serve stdio) | varies | varies | varies | when ChronDB is active |
248+
| `file` (explicit) |||||
249+
| `file+stdout` |||||
250+
| `file+stderr` |||||
248251
| `stdout` |||||
249252
| `stderr` |||||
250253
| `none` |||||
251254

252-
Pick `file` explicitly if you want chrondb-only **even in `mcp serve`** (an explicit value skips 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.
255+
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.
253256

254257
```bash
255258
MCP_AUDIT_OUTPUT=file mcp serve --http 0.0.0.0:8080
@@ -261,7 +264,7 @@ MCP_AUDIT_OUTPUT=file mcp serve --http 0.0.0.0:8080
261264

262265
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.
263266

264-
> **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 user explicitly picks `stdout` or `file+stdout`, it's rewritten to the stderr variant with a warning.
267+
> **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.
265268
266269
Set `output` to `none` to disable audit entirely without touching the `enabled` flag.
267270

docs/howto/docker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ These variables are especially useful for container deployments. See the full li
178178
| `MCP_LOG_LEVEL` | `info` | Log verbosity: `trace`, `debug`, `info`, `warn`, `error` |
179179
| `MCP_LOG_FORMAT` | `text` | Log format: `text` or `json` (structured, for log drivers) |
180180
| `MCP_AUDIT_ENABLED` | `false` (in Docker image) | Disable audit for read-only fs |
181-
| `MCP_AUDIT_OUTPUT` | `file` (auto-promoted to `file+stdout` in `mcp serve --http`) | `stdout`/`stderr` for log driver only, `file` for ChronDB only (skips auto-promotion in serve), `file+stdout`/`file+stderr` for both, `none` to disable |
181+
| `MCP_AUDIT_OUTPUT` | unset (→ `file+stdout` in `mcp serve --http`) | `stdout`/`stderr` for log driver only, `file` for ChronDB only (setting this env var is treated as explicit and skips auto-promotion in serve), `file+stdout`/`file+stderr` for both, `none` to disable |
182182
| `MCP_AUDIT_PATH` | `~/.config/mcp/db/data` | Override audit data path |
183183
| `MCP_AUDIT_INDEX_PATH` | `~/.config/mcp/db/index` | Override audit index path |
184184
| `MCP_AUTH_CONFIG` || Inline `auth.json` content (read-only, writes are no-ops). Same idea as `MCP_SERVERS_CONFIG`. |

docs/howto/kubernetes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ When Kubernetes sends `SIGTERM` (during rolling updates or scale-down):
282282
| `MCP_LOG_LEVEL` | `info` | `tracing` `EnvFilter` (e.g. `mcp=debug,hyper=warn,reqwest=warn,h2=warn`) |
283283
| `MCP_LOG_FORMAT` | `text` | `json` for newline-delimited JSON to stderr (log drivers) |
284284
| `MCP_AUDIT_ENABLED` | `false` | Enable audit logging |
285-
| `MCP_AUDIT_OUTPUT` | `file` (auto-promoted to `file+stdout` in `mcp serve --http`) | `stdout` for cluster log pipeline only, `file` for PVC only (skips auto-promotion), `file+stdout` for both PVC and pipeline (the auto-promoted default in serve), `none` to disable |
285+
| `MCP_AUDIT_OUTPUT` | unset (→ `file+stdout` in `mcp serve --http`) | `stdout` for cluster log pipeline only, `file` for PVC only (setting this env var is treated as explicit and skips auto-promotion), `file+stdout` for both PVC and pipeline (the auto-promoted default in serve), `none` to disable |
286286
| `MCP_AUDIT_PATH` | `/data/audit/data` | Audit data directory (app default: `~/.config/mcp/db/data`) |
287287
| `MCP_AUDIT_INDEX_PATH` | `/data/audit/index` | Audit index directory (app default: `~/.config/mcp/db/index`) |
288288
| `MCP_CLASSIFIER_CACHE` | `/tmp/tool-classification.json` | Tool classification cache (app default: `~/.config/mcp/tool-classification.json`) |

docs/reference/environment-variables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ These variables configure `mcp` behavior:
1414
| `MCP_PROXY_REQUEST_TIMEOUT` | `120` | (proxy mode) Hard upper bound, in seconds, that any single client request can spend inside `mcp serve` before the proxy returns a JSON-RPC error. Acts as a belt-and-suspenders boundary on top of the per-transport `MCP_TIMEOUT`. |
1515
| `MCP_CLASSIFIER_CACHE` | `~/.config/mcp/tool-classification.json` | Path to the persistent tool read/write classification cache (see [`mcp acl classify`](./cli.md#mcp-acl-classify)). Override this in CI/containers that cannot write to `$HOME`. |
1616
| `MCP_DISCOVERY_CONCURRENCY` | `10` | Max parallel `--help` calls during CLI subcommand discovery (see [CLI as MCP](../guides/cli-as-mcp.md)) |
17-
| `MCP_AUDIT_OUTPUT` | `file` (global), auto-promoted in `mcp serve` to `file+stdout` (HTTP) / `file+stderr` (stdio) | Audit output destination: `file` (ChronDB, queryable via `mcp logs`), `stdout`, `stderr` (JSON lines for container log drivers), `file+stdout`, `file+stderr` (ChronDB **and** JSON lines for both `mcp logs` and a log driver), or `none` (disable). Global default is `file` so CLI subcommands keep stdout clean for command output. `mcp serve` upgrades to a dual-sink default so audit is visible in `docker logs`/`kubectl logs` without an extra command. Any explicit value (config or env) bypasses the auto-promotion. |
17+
| `MCP_AUDIT_OUTPUT` | unset (→ `file` for CLI, `file+stdout` for `serve --http`, `file+stderr` for `serve` stdio) | Audit output destination: `file` (ChronDB, queryable via `mcp logs`), `stdout`, `stderr` (JSON lines for container log drivers), `file+stdout`, `file+stderr` (ChronDB **and** JSON lines for both `mcp logs` and a log driver), or `none` (disable). Setting this env var marks the value as **explicit** and skips the per-context auto-promotion (so `MCP_AUDIT_OUTPUT=file` reliably forces chrondb-only output, including in `mcp serve`). Leaving it unset (and the config field absent) lets each context pick its safe default `file` for CLI to keep stdout clean for command output, dual-sink for `mcp serve` so audit shows up in `docker logs`/`kubectl logs`. |
1818
| `MCP_AUDIT_ENABLED` | `true` | Set to `false` or `0` to disable audit logging and database initialization. Overrides `audit.enabled` in the config file. |
1919
| `MCP_AUDIT_PATH` | `~/.config/mcp/db/data` | Override the ChronDB data directory. Overrides `audit.path` in the config file. |
2020
| `MCP_AUDIT_INDEX_PATH` | `~/.config/mcp/db/index` | Override the ChronDB index directory. Overrides `audit.index_path` in the config file. |

0 commit comments

Comments
 (0)