Skip to content

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.

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
npx discord-mcp 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.

Compliance considerations (SOC2 / SOX / GDPR / HIPAA)

Section titled “Compliance considerations (SOC2 / SOX / GDPR / HIPAA)”

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

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: 15 mutating 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.

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.

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.