Middleware and Logging
How tool call logging middleware wraps every MCP tool invocation, the metadata capture strategy, and the audit log requirement.
The MCP server has two logging systems that serve different purposes: tool call logging captures every MCP tool invocation for operational observability, and audit logging captures plan/step mutations for the immutable business event trail.
Tool Call Logging Middleware
The withToolCallLogging middleware in packages/mcp/src/middleware/tool-call-logger.ts transparently logs every tool invocation (except ping) to the mcp_tool_call_log table. It is applied before any tools are registered in server.ts, so all subsequently registered tools are automatically wrapped.
How It Works
The middleware monkey-patches server.registerTool to wrap each tool's callback function:
Original flow: registerTool(name, config, callback)
Patched flow: registerTool(name, config, wrappedCallback)
|
|- Record start time
|- Call original callback
|- Record duration
|- Fire-and-forget: write log row
|- Return original result
Key design decisions:
- Fire-and-forget -- the log write is wrapped in a
voidasync IIFE. If logging fails, the tool response is unaffected. Alogger.warnis emitted on failure but the error is swallowed. - Ping excluded --
pingis a health check that would generate noise. The middleware skips it by checking the tool name. - Error detection -- the middleware catches both thrown exceptions and MCP-style error responses (
{ isError: true }).
Metadata Capture
The middleware captures both request and response metadata, with intelligent truncation:
Request metadata -- the tool's input arguments, with long text fields truncated:
- Fields in the
LONG_TEXT_FIELDSset (e.g.,content,instructions,sourceText,result,report) are replaced with[N chars]if they exceed 500 characters - Nested objects are traversed up to 5 levels deep
- Arrays longer than 10 items are truncated with a
...[N more]suffix
Response metadata -- parsed from the JSON text content of the tool's response:
- Successful responses have their fields extracted (with the same truncation rules)
- Error responses include
isError: true - Non-JSON responses store truncated text
Reference extraction -- planId, stepId, sessionId, and skillExecutionId are extracted from the tool arguments into dedicated columns for efficient querying.
Log Row Schema
Each row in mcp_tool_call_log contains:
| Column | Type | Purpose |
|---|---|---|
id | TEXT (PK) | UUID |
organizationId | TEXT | Tenant isolation |
userId | TEXT | Authenticated user |
toolName | TEXT | MCP tool name (e.g., submit_step_result) |
planId | TEXT | Extracted from args (nullable) |
stepId | TEXT | Extracted from args (nullable) |
skillExecutionId | TEXT | Extracted from executionId arg (nullable) |
claudeCodeSessionId | TEXT | Extracted from sessionId arg (nullable) |
requestMetadata | JSONB | Truncated tool arguments |
responseMetadata | JSONB | Truncated tool response |
isError | BOOLEAN | Whether the call failed |
durationMs | INTEGER | Wall-clock execution time |
createdAt | TIMESTAMP | When the call occurred |
Plan Audit Log
The audit log in packages/mcp/src/tools/helpers/audit-log.ts serves a different purpose from tool call logging. It records business events -- plan created, step started, step completed, user reviewed -- as an immutable append-only trail in the plan_audit_log table.
When to Write Audit Entries
Every tool that mutates plan or step state must call writeAuditLog() inside the same database transaction, before commit. This ensures the audit entry and the state change are atomic -- either both succeed or both roll back.
Tools that write audit entries:
| Tool | Event Type |
|---|---|
create_research_plan | plan_modified (action: created) |
modify_plan | plan_modified (action varies by action type) |
get_next_step | step_started |
submit_step_result | step_completed |
request_user_review | user_reviewed (action: review_requested) |
submit_user_decision | user_reviewed (decision details) |
get_research_context | session_resumed (when sessionId provided) |
Tools that do not write audit entries (read-only or non-plan operations):
ping,get_plan_status,get_step_context,list_active_plansstore_research,search_sources,extract_data,store_research_outputsubmit_subagent_report,log_skill_execution,submit_research_feedback
Audit Event Types
| Event Type | Meaning |
|---|---|
step_started | A step transitioned to in_progress |
step_completed | A step's result was submitted |
step_failed | A step was explicitly failed |
plan_modified | Plan structure changed (created, steps added/removed/reordered/updated) |
user_reviewed | A review was requested or a user decision was recorded |
session_resumed | A new Claude Code session loaded an existing plan's context |
skill_started | A skill execution began |
skill_completed | A skill execution finished |
The writeAuditLog Function
interface AuditLogEntry {
planId: string;
organizationId: string;
stepId?: string | null;
eventType: AuditEventType;
eventData?: Record<string, unknown>;
sessionId?: string | null;
}
async function writeAuditLog(tx: Database, entry: AuditLogEntry): Promise<void> {
await tx.insert(planAuditLog).values({
id: randomUUID(),
planId: entry.planId,
organizationId: entry.organizationId,
stepId: entry.stepId ?? null,
eventType: entry.eventType,
eventData: entry.eventData ?? null,
claudeCodeSessionId: entry.sessionId ?? null,
});
}
The function receives the transaction (tx) rather than using the global db client, guaranteeing it runs within the caller's tenant-scoped transaction.
Two Logging Systems Compared
| Aspect | Tool Call Log | Plan Audit Log |
|---|---|---|
| Table | mcp_tool_call_log | plan_audit_log |
| Scope | Every tool invocation | Plan/step mutations only |
| Timing | Fire-and-forget (async, after response) | Inside transaction (sync, before commit) |
| Failure mode | Silent -- tool response unaffected | Transaction rolls back -- mutation undone |
| Purpose | Operational observability, debugging | Business event trail, compliance |
| Includes | Request/response metadata, duration | Event type, event data, session |
Related Pages
- Tool Protocol Overview -- server architecture and the audit log requirement
- Tool Catalog -- which tools emit which audit events
- Data & Storage -- Schema Design --
mcp_tool_call_logandplan_audit_logtable definitions - Infrastructure -- Structured Logging -- Pino log output from the middleware