Skip to content

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 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.
Terminal window
export MCP_AUDIT_ENABLED=true
export MCP_AUDIT_SINK=stderr
discord-mcp serve 2> /var/log/discord-mcp/audit-$(date +%F).jsonl

stderr 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.

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:

/etc/logrotate.d/discord-mcp
/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.

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.

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, secret redacted 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 the SENSITIVE_KEYS_BY_TOOL map.
  • 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.

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 N controls file count; pair with scheduled cleanup if you need calendar-based retention.

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.

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.