Skip to content

All posts

AI

How to audit MCP tool calls

Ritesh Patel

AI: How to audit MCP tool calls

To audit MCP tool calls from Claude Code, turn on the OpenTelemetry logs exporter and set OTEL_LOG_TOOL_DETAILS=1. Each call then produces a tool_decision event, and each call that runs also produces a tool_result event, both naming the server and tool. Add hooks for a local record of each server’s source.

Checked against the Claude Code monitoring and hooks docs on code.claude.com, fetched 2026-10-26. Those pages give no single Claude Code version for the whole document. Where a feature needs one, this post says which.

What an MCP audit record has to answer

An MCP audit trail has to answer five questions: who made the call, which server and tool it reached, what arguments it sent, whether it was allowed and by which decision source, and what came back. Claude Code’s telemetry can answer all five, but no single event does it alone. The table shows which source covers which case.

Source Records Denied calls Failed calls Setup
tool_decision event Accept or reject, decision source, server and tool Yes, decision is reject Not applicable Logs exporter, plus OTEL_LOG_TOOL_DETAILS=1 for names
tool_result event Success, duration, server scope, arguments No, not emitted for rejected calls Yes, success is "false" Same as above
mcp_server_connection event Connect, disconnect, connection failure Not applicable Not applicable Logs exporter
PostToolUse hook Input and response, server source No, fires after success only No Hook in a settings file
PostToolUseFailure hook Input for failed calls No Yes Hook in a settings file
PermissionDenied hook Input and reason Auto mode denials only No Hook in a settings file

Two rules in the table shape the setup. A rejected call produces no tool_result, so denials come from tool_decision. A failed call still produces a tool_result, with success set to "false".

Step 1: turn on telemetry and the logs exporter

Telemetry needs CLAUDE_CODE_ENABLE_TELEMETRY set, and the monitoring docs list it as required. Set the variables in managed settings, or in each developer’s shell. Claude Code ignores the OpenTelemetry exporter variables in a repository’s .claude/settings.json and .claude/settings.local.json, so a repo cannot turn audit on or send it somewhere else.

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_LOG_TOOL_DETAILS=1
  1. Put the block above in managed settings, or in each developer’s shell profile.
  2. Start Claude Code and run a session that calls an MCP tool.

When managed settings set OTEL_EXPORTER_OTLP_ENDPOINT, Claude Code removes every developer-set per-signal endpoint. The exporter selectors, such as OTEL_LOGS_EXPORTER, follow normal precedence instead. A developer’s own setting can still disable the logs, so set the selectors in managed settings too if you need them locked.

Step 2: read the tool_decision and tool_result events

Both events carry tool_use_id, the same ID hook input carries, so you can join a call’s decision, its result and its hook record. The fields you need are below.

Question tool_decision tool_result
Was the call allowed? decision is accept or reject decision_type is always accept
Where did the decision come from? source: config, hook, user_permanent, user_temporary, user_abort or user_reject decision_source: config, hook, user_permanent or user_temporary
Which server and tool? tool_parameters: mcp_server_name, mcp_tool_name tool_parameters: the same two fields
Did it run, and did it work? Not listed success, duration_ms, error_type
What arguments? tool_parameters tool_input
Which server scope? Not listed mcp_server_scope

Filter on tool_parameters, not tool_name. For user-configured servers, tool_name is always the literal mcp_tool. The server and tool names appear only in tool_parameters, and only with OTEL_LOG_TOOL_DETAILS=1 set.

Step 3: add a hook for a local record

The events cover most of the audit. Hooks add two things the event attributes above do not list: the server’s source (the docs give plugin, sdk, user and project as examples), and the reason for an auto mode denial. The mcp_server field in hook input requires Claude Code v2.1.274 or later.

Add the hook to ~/.claude/settings.json to log every project on the machine, or to .claude/settings.json to commit it for everyone who clones the repo. Replace the path with your script.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__.*",
        "hooks": [{ "type": "command", "command": "/path/to/audit-mcp.sh" }]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "mcp__.*",
        "hooks": [{ "type": "command", "command": "/path/to/audit-mcp.sh" }]
      }
    ],
    "PermissionDenied": [
      {
        "matcher": "mcp__.*",
        "hooks": [{ "type": "command", "command": "/path/to/audit-mcp.sh" }]
      }
    ]
  }
}

The script appends the whole hook input to a file, one JSON object per line. It needs jq on the machine.

#!/bin/sh
# Appends the hook input as one JSON line, with a UTC timestamp.
LOG="$HOME/.claude/logs/mcp-tool-calls.jsonl"
mkdir -p "$(dirname "$LOG")"
jq -c '. + {logged_at: (now | todateiso8601)}' >> "$LOG"

Make the script executable with chmod +x.

What the events and hooks still miss

  • Argument size. The docs truncate individual tool_input values over 512 characters and bound the full payload to about 4 K characters. A long argument in the collector may not match the call that ran.
  • Subagents. Hooks from settings files run inside subagents too, so the hook log covers their MCP calls.
  • Transcripts. The transcript_path file may lag the current turn. Do not use it as the record of the latest call. Use the events or the hook log.
  • Sensitive data. Arguments and responses can hold secrets or customer data. Decide who can read the collector and the hook log before you turn on OTEL_LOG_TOOL_DETAILS. That advice is ours, not a documented requirement.
  • Local hooks. A developer can edit the hook in their own settings, so the hook log shows what that machine recorded. Keep the collector as the record you rely on.

Review denials and failures

A regular query answers the first questions most teams ask. Start with tool_decision events where decision is reject, and group them by mcp_server_name in tool_parameters. That needs OTEL_LOG_TOOL_DETAILS=1. A server with many rejects is either blocked on purpose or misconfigured, and the source field shows whether a setting, a hook or a person made each decision.

Then filter tool_result events where success is "false", and group them by server and error_type. A spike in one error type that stays with one server is a lead to check that server’s configuration. Keep the same grouping for each review so that the numbers stay comparable from one period to the next.

Check the setup

  1. Start Claude Code with the variables set, and submit a prompt. The monitoring docs say that a logs-only setup shows a claude_code.user_prompt event.
  2. Ask Claude to call an MCP tool.
  3. In the collector, find the tool_decision and tool_result events with the same tool_use_id. Check that tool_parameters names the server.
  4. Check that the hook log has a line with that tool_use_id.

Enforcing this with AIControls

AIControls can run Claude Code through its gateway. Claude Code via the gateway (CLI and VS Code) and per-user tokens are both on the AIControls confirmed list. The steps above work without the gateway. This post makes no claim about what AIControls records for each tool call, so check the AIControls product page for that. For the wider picture, read the MCP security guide, and the post on restricting which MCP servers Claude Code can use.

Frequently asked questions

Does Claude Code log MCP tool calls without extra configuration?

Telemetry needs CLAUDE_CODE_ENABLE_TELEMETRY=1 first, and the monitoring docs list it as required. Even then, MCP detail is off by default. Without OTEL_LOG_TOOL_DETAILS=1, the tool_name of a user-configured server is recorded as the literal mcp_tool, and argument content is left out. With the flag on, the server and tool names appear in tool_parameters.

Are denied MCP tool calls recorded?

Yes, in the tool_decision event, where decision is reject. A rejected call produces no tool_result. The PermissionDenied hook also fires, but only when auto mode denies a call. It does not run when you deny a permission prompt yourself, when a PreToolUse hook blocks a call, or when a deny rule matches. Use tool_decision as the main record of denials.

Why does my MCP audit show mcp_tool instead of the tool name?

That is the redaction the docs describe for user-configured servers. The event's tool_name is always the literal mcp_tool. The server and tool names are in tool_parameters, as mcp_server_name and mcp_tool_name, and only with OTEL_LOG_TOOL_DETAILS=1. Filter on those two fields in your collector, not on tool_name.

Can a developer turn off MCP audit logging on their machine?

They can, unless you lock it in managed settings. A developer's own setting can still disable a signal or switch it to the console exporter. A repository's .claude/settings.json cannot set the exporter variables at all. A hook lives in a settings file the developer can edit, so keep the collector as the record you rely on and treat the hook log as a local copy.

Do hooks see MCP calls made by subagents?

Yes. Hooks from settings files, managed policy settings and plugins also run inside subagents. When a subagent calls a tool, PreToolUse and PostToolUse fire the same configured hooks as in the main conversation. A matcher of mcp__.* covers MCP tools in those subagent calls too.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Govern AI agents with AIControlsControl what AI agents do, and prove it.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.