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 McpAuditLogger with the same rotation/pruning logic as oct.git.audit.AuditLogger, adapted for the global ~/.oct-mcp/ path and the richer MCP schema.

  • Never log raw args when args_redacted is 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 other oct.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.jsonl strictly so lexical ordering matches chronological ordering for pruning.

  • Every McpAuditRecord field is JSON-serialisable.

oct.mcp.audit.MCP_AUDIT_MAX_FILES: int = 20#

Maximum number of mcp-audit-*.jsonl files 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: object

Append-only rotating JSONL logger for oct-mcp tool calls.

One instance is created per MCP server session (in server.py). The constructor does not open a file — the first write() 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 (from McpConfig, default ~/.oct-mcp/audit.log). Rotated files are placed in the same parent directory with a mcp-audit-YYYYMMDD-HHMMSS.jsonl naming pattern. The initial write goes to the audit_log_path itself; once rotation is triggered, subsequent writes go to timestamped files. Pruning removes the oldest timestamped files when the count exceeds max_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-*.jsonl files in the directory exceeds max_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 _dbg at 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 (None before 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: object

Schema for one MCP audit row. Serialised via dataclasses.asdict().

All fields are JSON-primitive or containers of JSON-primitives. args should already be credential-redacted by the caller before being assigned; when args_redacted is True the logger does not apply further sanitisation (it trusts the caller). When args_redacted is False, args contain the raw validated dict — callers MUST set this consciously.

project_root_hash is the first 8 hex characters of sha256(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 → args has 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 (None if 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.