oct.mcp.audit module#
Purpose#
JSONL audit logger for the oct-mcp server. Writes one JSON object per
line to ~/.oct-mcp/audit.log with size-based rotation (5 MB per file)
and count-based pruning (keep the 20 newest files).
Responsibilities#
Define
McpAuditRecord— the on-disk schema (blueprint §5.7).Implement
McpAuditLoggerwith the same rotation/pruning logic asoct.git.audit.AuditLogger, adapted for the global~/.oct-mcp/path and the richer MCP schema.Never log raw
argswhenargs_redactedis True — callers pass the redacted dict; logger serialises whatever it receives.Emit diagnostic events via
_dbg("MCP", ..., level).
Diagnostics#
Domain: MCP Levels:
L1 — errors: IO failures, serialisation failures L2 — lifecycle: file opened/rotated, files pruned L4 — deep trace: every write() call
Contracts#
This module does not import
click, the MCP SDK, or any otheroct.mcp.*module (no cycles).McpAuditLogger.write()must never raise. IO failures are logged at level 1 and swallowed — audit logging is observability, not correctness.Filenames follow
mcp-audit-YYYYMMDD-HHMMSS.jsonlstrictly so lexical ordering matches chronological ordering for pruning.Every
McpAuditRecordfield is JSON-serialisable.
- oct.mcp.audit.MCP_AUDIT_MAX_FILES: int = 20#
Maximum number of
mcp-audit-*.jsonlfiles kept on disk.
- oct.mcp.audit.MCP_AUDIT_MAX_SIZE_BYTES: int = 5242880#
Rotation threshold per audit file.
- class oct.mcp.audit.McpAuditLogger(audit_log_path: str | Path | None = None, max_files: int = 20, max_size_bytes: int = 5242880)[source]#
Bases:
objectAppend-only rotating JSONL logger for
oct-mcptool calls.One instance is created per MCP server session (in
server.py). The constructor does not open a file — the firstwrite()call lazily creates the~/.oct-mcp/directory and the first audit file. This keeps the constructor side-effect-free so the server can instantiate it during startup without touching the filesystem.Storage location#
The audit log lives at
audit_log_path(fromMcpConfig, default~/.oct-mcp/audit.log). Rotated files are placed in the same parent directory with amcp-audit-YYYYMMDD-HHMMSS.jsonlnaming pattern. The initial write goes to theaudit_log_pathitself; once rotation is triggered, subsequent writes go to timestamped files. Pruning removes the oldest timestamped files when the count exceedsmax_files.Rotation strategy#
Size rotation: before each write, if the current file is at or above
max_size_bytes, close it and open a new timestamped file.Count pruning: after rotation, if the count of
mcp-audit-*.jsonlfiles in the directory exceedsmax_files, delete the oldest until the count is within the limit.
Error handling#
Every IO path in
write()is wrapped in a try/except. Failures are logged via_dbgat level 1 and the method returns silently. The audit log must never block or crash tool execution.- property audit_dir: Path#
Directory where audit files are stored.
- property current_path: Path | None#
Current audit file path (
Nonebefore first write).
- write(record: McpAuditRecord) None[source]#
Serialise record and append it as one JSONL line.
Never raises. Any failure is caught, logged at level 1, and swallowed so callers are never blocked by audit misbehaviour.
- class oct.mcp.audit.McpAuditRecord(timestamp: str = '', session_id: str = '', request_id: str = '', tool: str = '', args: dict = <factory>, args_redacted: bool = True, decision: str = '', policy_rule: str = '', exit_code: int | None = None, duration_ms: int = 0, output_size_bytes: int = 0, output_truncated: bool = False, output_hash_sha256: str = '', redactions_applied: int = 0, user: str = '', project_root_hash: str = '')[source]#
Bases:
objectSchema for one MCP audit row. Serialised via
dataclasses.asdict().All fields are JSON-primitive or containers of JSON-primitives.
argsshould already be credential-redacted by the caller before being assigned; whenargs_redactedisTruethe logger does not apply further sanitisation (it trusts the caller). Whenargs_redactedisFalse, args contain the raw validated dict — callers MUST set this consciously.project_root_hashis the first 8 hex characters ofsha256(str(project_root)), giving per-project correlation without leaking the absolute path.- args: dict#
Validated (and optionally redacted) args dict.
- args_redacted: bool = True#
True →
argshas had sensitive values replaced.
- decision: str = ''#
"allowed","denied", or"requires_confirmation".- Type:
Policy decision
- duration_ms: int = 0#
Wall-clock time from request receipt to response dispatch, in ms.
- exit_code: int | None = None#
Exit code from the sandboxed subprocess (
Noneif not executed).
- output_hash_sha256: str = ''#
SHA-256 hex digest of the raw (pre-redaction) output for integrity.
- output_size_bytes: int = 0#
Byte length of the (post-redaction) tool output.
- output_truncated: bool = False#
True if the output was truncated to
max_output_bytes.
- policy_rule: str = ''#
Name of the policy rule that determined the decision.
- project_root_hash: str = ''#
First 8 hex chars of sha256(str(project_root)) — correlation, not path.
- redactions_applied: int = 0#
Count of redaction substitutions applied by the Output Redactor.
- request_id: str = ''#
Unique identifier for this tool invocation.
- session_id: str = ''#
Opaque identifier for the MCP session (set at server startup).
- timestamp: str = ''#
ISO 8601 UTC timestamp of the request.
- tool: str = ''#
MCP tool name, e.g.
"oct_lint".
- user: str = ''#
OS username of the process running the MCP server.