Audit logging
The audit middleware emits an event for every tool whose metadata is not explicitly read-only and whose handler is actually reached. Idempotent writes are included: idempotency describes retry semantics, not whether Discord changed. Validation, scope, write-mode, and precondition rejections stop before the audit layer. Use the trail as an operational aid and verify coverage against the exact tool set before making a compliance claim.
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 |
Local stdio (the default) or HTTP deployments whose stderr is shipped 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 |
MCP_AUDIT_SINK=otlp + OTEL_ENABLED=true + OTEL_EXPORTER_OTLP_ENDPOINT=... |
Sends redacted audit records through the server-owned OTel Logs pipeline to /v1/logs. If telemetry or the endpoint is not configured, it falls back to stderr with a visible [FALLBACK:logs-pipeline-not-wired] prefix; a runtime collector outage is best-effort and may drop queued records. |
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).
The server boots a LoggerProvider with a bounded
BatchLogRecordProcessor and an OTLP HTTP logs exporter. The exporter posts
JSON log batches to the same base endpoint used by traces and metrics, at
/v1/logs.
export OTEL_ENABLED=trueexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318export OTEL_EXPORTER_OTLP_HEADERS=x-api-key=YOUR_KEYexport MCP_AUDIT_ENABLED=trueexport MCP_AUDIT_SINK=otlpdiscord-mcp serveThe queue is bounded (1,024 records; batches up to 128) and exports have a five-second timeout. Collector failure never turns a Discord mutation into a tool failure; records can be dropped and the SDK reports exporter errors through its diagnostics. Shutdown attempts a bounded flush. This is best-effort delivery, not a durable disk spool or compliance-grade write-once ledger.
If OTEL_ENABLED or the endpoint is missing, the explicit stderr fallback
remains active so selecting otlp cannot silently discard events. Once the
pipeline is running, exporter failure is reported without replaying the
event to stderr; configure a separate durable collector or file sink when
guaranteed retention is required.
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”Read-only tools are NOT audited
Section titled “Read-only tools are NOT audited”The audit middleware short-circuits only when the tool’s
annotations.readOnlyHint is explicitly true. This keeps read patterns out of
the mutation trail while retaining idempotent writes.
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. 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 | Retry-semantics hint copied from the tool; may be true for an audited idempotent write. |
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 / http |
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. OTLP emissions use the same
level: "audit" JSON body and add only bounded routing attributes (tool,
category, status, transport, idempotent, and optional request/result
identifiers); credentials and raw authorization headers are never copied into
those attributes.
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.

