Launching on Product Hunt today
Blog

Guide5 min read

MCP audit logging: how to record every tool call

Record who made each MCP tool call, from which client, with what arguments and result. Capture it in one place every call passes through, with secrets masked.

Eldad FuxFounder and CEO, Appwrite

Audit log

  • Matejgithub__merge_pull_requestOK
  • CI keysentry__search_issuesOK
  • Harshstripe__create_refundBlocked

Audit log

  • Matejgithub__merge_pull_requestOK
  • CI keysentry__search_issuesOK
  • Harshstripe__create_refundBlocked

An MCP audit log records every tool call an agent makes: the person behind it, the client, the server and tool, the arguments, the result and the time. Capture it at a point every call passes through, such as a gateway in front of your servers, rather than on each laptop. Store arguments encrypted, mask secrets, and record refused calls as well as successful ones.

Does MCP have audit logging built in?

No. The protocol defines how clients and servers exchange tool calls, not who keeps a record of them. Its logging utility (notifications/message) is for a server to send diagnostic messages to the client, which is useful for debugging and no help to an auditor. The record has to come from your client, your server or something in between.

What should an MCP audit log record?

Enough to answer “who did what, through which agent, and was it allowed” without asking anyone:

FieldWhy
Person
The human the credential belongs to, from the OAuth token or API key. Never a name the agent passes in the arguments.
Client
Claude Code, Cursor, a CI key. Tells you whether a person typed the request or a pipeline ran it.
Server and tool
Namespaced, like github__merge_pull_request, so two servers’ search tools stay apart.
Arguments
What the call asked for: the repo, the query, the record ID. Encrypted at rest, with secrets masked.
Result
OK, failed or blocked, with the reason for a block or the error for a failure.
Time and latency
When it ran and how long it took. Latency also shows which servers slow your agents down.

Log sign-ins and permission changes in the same place. When someone asks why an agent could merge a pull request yesterday, the answer is often a role change, not a call.

Why aren’t the service’s own logs enough?

Because they see the credential, not the person or the agent. GitHub’s audit log, for example, records the account the token belongs to. That breaks down in common setups:

  • A shared token, pasted into everyone’s config, makes every agent call look like one person.
  • The service can’t tell a call from Claude Code from one from a CI job or a script using the same token.
  • Calls you refused never reach the service, so its log can’t show attempts. Refusals are often the most useful entries.
  • Each service keeps its own log in its own format, with its own retention. Ten servers means ten places to look.

Where should you capture MCP tool calls?

There are three places, and they differ in what they can see and who can change them:

WhereTrade-off
In the client
Easy to start and sees the full conversation. Lives on each person’s machine, covers one client, and the person can edit or delete it.
In the server
Complete for that server if you wrote it. Covers only the servers you own, not the vendor servers your team connects to.
In a gateway
One record for every server and client, outside anyone’s laptop, including refused calls. Requires every client to connect through it.

For one person, a client hook is enough. Claude Code runs PostToolUse hooks after each tool call with the tool’s name and input on stdin, and MCP tools are named mcp__<server>__<tool>, so a matcher on mcp__.* catches them all:

~/.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__.*",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{at: (now | todate), session: .session_id, tool: .tool_name, input: .tool_input}' >> ~/.claude/mcp-calls.jsonl"
          }
        ]
      }
    ]
  }
}

That gives you a local JSON Lines file to grep. It isn’t an audit log for a team: it covers one client on one machine, it only sees calls that ran, and its owner can change it. For a team, capture calls where people can’t switch it off.

What should never go in an MCP audit log?

Secrets and bulk data. Arguments often carry tokens, passwords or connection strings, and answers often carry customer records. Mask secret-looking keys before you write anything:

mask.ts
const SECRET = /pass(word)?|secret|token|api[_-]?key|authorization|cookie/i

function mask(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(mask)
  if (value && typeof value === 'object')
    return Object.fromEntries(
      Object.entries(value).map(([k, v]) => [k, SECRET.test(k) ? '[masked]' : mask(v)]),
    )
  return value
}

Masking by key name catches most secrets but not a token pasted into a free-text field, which is why arguments should also be encrypted at rest. Record the answer’s status and size rather than its contents; the service already holds the data, and your log shouldn’t become a second copy of it.

Who can read it matters as much as what’s in it

Give read access to the people who investigate, through a role that can read the log but not call tools or change settings. The people whose calls are logged shouldn’t be able to edit or delete entries.

What questions should the log answer?

Test your log against the questions you’ll actually be asked. A useful one answers each in a single filter:

  • Who called github__merge_pull_request last week, and from which client?
  • Which calls were blocked today, and what rule blocked them?
  • What did someone who just left call in their last 30 days?
  • Which API key made these calls, and which pipeline uses it?

MCP audit logging in MCP1

MCP1 is a gateway, so every call through it is recorded in one place, whichever client or server it involves:

  • Every call, sign-in and change. Who, from which client and gateway, which tool, how long it took, and the result: OK, Failed or Blocked, with the reason.
  • Arguments encrypted, secrets masked. Arguments are stored with AES-256-GCM. An admin can turn off Record arguments to keep only the metadata.
  • Settings → Audit log. Owners, admins and auditors filter by kind, result, server or person, follow it live, and export it as CSV.
  • An auditor role that reads everything, including the audit log, and can’t call tools.

Admins can also ask their own agent. With admin tools on, this finds every refused merge in the last week:

Tool call
{
  "name": "mcp1__search_audit",
  "arguments": { "tool": "merge_pull_request", "status": "denied", "hours": 168 }
}

Everyone else can see their own calls with mcp1__recent_calls. Workspace data and the audit log are hosted in Frankfurt. The docs cover the log and the rules that produce its Blocked entries, and 10 steps for rolling out MCP across your company covers using it to tune them.

Common questions

Should I log MCP tool call arguments?

Yes. Arguments are what make an entry useful in an investigation. Encrypt them at rest, mask keys that look like secrets, and leave tool answers out.

Can Claude Code log MCP tool calls?

Yes, with a PostToolUse hook whose matcher is mcp__.*. It records calls on that machine only, and the person using it can change or delete the file.

Who should be able to read an MCP audit log?

The people who investigate, through a role that can read the log but can’t call tools or change settings. Nobody whose calls are logged should be able to edit it.

Should blocked MCP calls be logged?

Yes. A refused call never reaches the service, so only your gateway or client can record it, and refusals show both misconfigured rules and attempts worth looking at.

Give your team one URL

Create a workspace, install your servers and share a gateway with a group. Free for individuals.