Skip to content

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.

SinkSelectorUse when
stderr (default)MCP_AUDIT_SINK=stderrstdio MCP transports (the default), or any container that ships stderr to a log aggregator (Loki, CloudWatch, Datadog Logs).
fileMCP_AUDIT_SINK=file + optional MCP_AUDIT_FILE=/path/to/audit.jsonlLong-running daemon deployments where stderr is owned by another process.
otlp (stub)MCP_AUDIT_SINK=otlpReserved 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.
noneMCP_AUDIT_SINK=none or MCP_AUDIT_ENABLED=falseWhen 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 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.

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:

FieldTypeRequiredDescription
timestampISO-8601 stringyesServer-side wall clock at audit emission.
request_idstringyesUUID v4 from the MCP request. Empty string if not yet set (rare; should not happen in practice).
toolstringyesTool name, e.g. messages_send.
categorystringyesTool category (e.g. messages, webhooks).
idempotentbooleanyesAlways false for emitted events (idempotent tools are skipped).
args_redactedobjectyesRedacted arg payload - see redaction section above.
statussuccess / tool_error / thrownyesOutcome: handler succeeded, returned isError: true, or threw.
duration_msnumberyesWall-clock duration of the tool call.
transportstdio / (future)yesMCP transport that received the call.
result_codestringoptionalMachine-readable code from structuredContent.code on tool_error, or error.name on thrown.
trace_idhex32optionalOTel trace ID if a span was active. Absent (NOT empty string) when telemetry is off.
span_idhex16optionalOTel 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.

VarDefaultDescription
MCP_AUDIT_ENABLEDtrueMaster switch (set literal false to disable).
MCP_AUDIT_SINKstderrOne of stderr, file, otlp, none.
MCP_AUDIT_FILE./discord-mcp-audit.jsonlPath used by the file sink only.