Audit logging
Audit logging
Section titled “Audit logging”Every mutating tool call (i.e. idempotent: false) emits a single audit
event that captures who/what/when/result. Read-only tools (e.g.
messages_read, channels_get) do NOT generate audit noise — their read
patterns are already visible in telemetry.
This page covers sink configuration, log forwarding, retention, and the on-disk JSONL schema.
Sink options
Section titled “Sink options”| Sink | Selector | Use when |
|---|---|---|
stderr (default) | MCP_AUDIT_SINK=stderr | stdio MCP transports (the default), or any container that ships stderr to a log aggregator (Loki, CloudWatch, Datadog Logs). |
file | MCP_AUDIT_SINK=file + optional MCP_AUDIT_FILE=/path/to/audit.jsonl | Long-running daemon deployments where stderr is owned by another process. |
otlp (stub) | MCP_AUDIT_SINK=otlp | Reserved for the OTel Logs pipeline. Currently a stub — falls back to stderr with a [FALLBACK:logs-pipeline-not-wired] prefix so misconfiguration is visible. Will become real once @opentelemetry/api-logs LoggerProvider lands in mcp-server/otel.ts. |
none | MCP_AUDIT_SINK=none or MCP_AUDIT_ENABLED=false | When the deployment is in a regulated context that explicitly bans logging the body of mutating ops, or for unit-test isolation. |
Setup per sink
Section titled “Setup per sink”export MCP_AUDIT_ENABLED=trueexport MCP_AUDIT_SINK=stderrnpx discord-mcp 2> /var/log/discord-mcp/audit-$(date +%F).jsonlstderr is the only safe stream in stdio MCP — stdout is reserved for
JSON-RPC frames. App logs and audit events both go to stderr, but audit
events carry "level":"audit" so log routers can filter them into a
separate destination.
export MCP_AUDIT_ENABLED=trueexport MCP_AUDIT_SINK=fileexport MCP_AUDIT_FILE=/var/log/discord-mcp/audit.jsonlnpx discord-mcpThe file is opened with flags: 'a' (append-only) and stays open for the
process lifetime. On SIGTERM the transport flushes pending writes before
exiting (auditSink.shutdown() is called from
mcp-server/src/transports/stdio.ts).
export MCP_AUDIT_ENABLED=trueexport MCP_AUDIT_SINK=otlpnpx discord-mcpToday this falls back to stderr with a [FALLBACK:logs-pipeline-not-wired]
prefix. It is intentionally not silent — enabling otlp should not drop
events when the LoggerProvider is not yet wired.
The full OTLP path requires:
@opentelemetry/api-logsLoggerProvider boot inmcp-server/src/otel.ts.- A logs pipeline (BatchLogRecordProcessor → OTLPLogExporter) wired to the same OTLP endpoint as traces.
OtlpAuditSinkswapped from stderr-fallback tologger.emit(...).
Tracked but out of scope for v0.8.x.
export MCP_AUDIT_ENABLED=falsenpx discord-mcpNo audit events emitted at all. Use only when a regulator explicitly bans logging mutating-call bodies, or in unit tests where audit noise muddies expectations.
File rotation
Section titled “File rotation”discord-mcp does not ship in-process log rotation. This is deliberate — rotation is an OS-level concern with established solutions; embedding logrotate-equivalent logic in-process duplicates work and adds failure modes (partial writes during rotation, lock contention).
Use logrotate(8) (Linux) or your container platform’s native log driver:
/var/log/discord-mcp/audit.jsonl { daily rotate 30 compress delaycompress missingok notifempty copytruncate}For containerised deployments, prefer the platform’s log driver
(json-file, awslogs, gelf, etc.) and have it consume stderr — i.e. use
the stderr sink, not the file sink, in a container.
Compliance considerations (SOC2 / SOX / GDPR / HIPAA)
Section titled “Compliance considerations (SOC2 / SOX / GDPR / HIPAA)”Idempotent tools are NOT audited
Section titled “Idempotent tools are NOT audited”By design (Plan 8 §10 critical rule 2), the audit middleware short-circuits
when tool.idempotent === true. This covers all read-only operations:
messages_read,messages_get,channels_list,channels_get,members_search, etc. — anything that returns data without mutation.
The reasoning: SOC2 audit-trail requirements focus on changes, not reads.
Read patterns are still observable via telemetry (every tool call has a span
with mcp.tool.name); duplicating that into audit records would balloon log
volume without adding compliance value.
If your compliance regime requires read-trail logging (rare — usually for
HIPAA-scope deployments), there is currently no override flag. File an issue
and we’ll consider a MCP_AUDIT_INCLUDE_READS toggle for Plan 9+.
PII redaction
Section titled “PII redaction”Sensitive arg fields are redacted in args_redacted before hitting the sink.
See redact.ts
for the full policy:
- Global keys:
token,bearer_token,auth,password,secretredacted at any depth (case-insensitive). - Per-tool keys: 15 mutating tools have explicit content-bearing fields
(
content,embeds,components,messages, etc.) marked sensitive. New tools must opt in via theSENSITIVE_KEYS_BY_TOOLmap. - Length-aware marker: redacted strings become
[REDACTED:${length}ch]so the log preserves a size signal without leaking the value. - Truncation: any non-redacted string > 200 chars is truncated to
100ch + "...[TRUNCATED]"to bound record size.
The audit JSONL is therefore safe to ingest into a SIEM that does not have field-level access controls — the bytes themselves are already redacted.
Retention
Section titled “Retention”discord-mcp does not enforce retention. Use your log platform’s retention policy:
- CloudWatch Logs: per-log-group retention (default infinite — set to 90/180/365 days per your policy).
- Loki: retention via
compactor.retention_period. - Datadog: per-index retention.
- File sink + logrotate:
rotate Ncontrols file count; pair with scheduled cleanup if you need calendar-based retention.
Schema reference (AuditEvent)
Section titled “Schema reference (AuditEvent)”Every event written to the sink is a single JSON object on one line. Fields:
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | ISO-8601 string | yes | Server-side wall clock at audit emission. |
request_id | string | yes | UUID v4 from the MCP request. Empty string if not yet set (rare; should not happen in practice). |
tool | string | yes | Tool name, e.g. messages_send. |
category | string | yes | Tool category (e.g. messages, webhooks). |
idempotent | boolean | yes | Always false for emitted events (idempotent tools are skipped). |
args_redacted | object | yes | Redacted arg payload — see redaction section above. |
status | success / tool_error / thrown | yes | Outcome: handler succeeded, returned isError: true, or threw. |
duration_ms | number | yes | Wall-clock duration of the tool call. |
transport | stdio / (future) | yes | MCP transport that received the call. |
result_code | string | optional | Machine-readable code from structuredContent.code on tool_error, or error.name on thrown. |
trace_id | hex32 | optional | OTel trace ID if a span was active. Absent (NOT empty string) when telemetry is off. |
span_id | hex16 | optional | OTel span ID if a span was active. Same absence rule. |
Stderr-sink emissions also wrap the event with {"level":"audit",...} so
log routers can filter by level. File-sink emissions are the bare event (no
wrapper) — the file path itself is the routing.
Reference: env vars
Section titled “Reference: env vars”| Var | Default | Description |
|---|---|---|
MCP_AUDIT_ENABLED | true | Master switch (set literal false to disable). |
MCP_AUDIT_SINK | stderr | One of stderr, file, otlp, none. |
MCP_AUDIT_FILE | ./discord-mcp-audit.jsonl | Path used by the file sink only. |
Related
Section titled “Related”- Telemetry — read patterns are observable here even when audit is off.
- Architecture → Middleware chain — where the audit middleware sits in the call path.
- Architecture → Confirmation — the
__confirmcontract for destructive mutations. - Source:
packages/mcp-core/src/middleware/audit.ts.