Audit logging
The audit middleware emits an event for tools whose metadata declares
idempotent: false. This is not the same as a complete write audit: some
Discord writes are idempotent and are skipped by the current predicate. Use
the trail as an operational aid, not proof that every mutation was captured.
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=stderrdiscord-mcp serve 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.jsonldiscord-mcp serveThe 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=otlpdiscord-mcp serveToday 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=falsediscord-mcp serveNo 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.
Coverage and compliance limits
Section titled “Coverage and compliance limits”Idempotent tools are NOT audited
Section titled “Idempotent tools are NOT audited”The audit middleware short-circuits when tool.idempotent === true. This
covers read-only operations, but it also excludes idempotent writes. The
current catalog contains write tools that therefore do not emit an event.
messages_read,messages_get,channels_list,channels_get,members_search, etc. - anything that returns data without mutation.
Read patterns remain visible through telemetry when it is enabled, but spans and audit events have different retention and integrity properties. There is currently no flag to include reads or idempotent writes. If your controls require complete change or access logging, add an external boundary and verify coverage against the exact tool set before production use.
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: 17 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.
Redaction is a best-effort allowlist, not proof that every sensitive value has been removed. Apply field-level access controls and retention limits in the log platform, and review new tool arguments before forwarding events to a SIEM.
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.