---
title: "Audit Every Tool Call Your Agents Make to MCP Servers"
description: "A direct connection to a third-party MCP server leaves no record of what your agents called. Route it through a gateway and every tool call is logged, attributed to an identity, and ready to ship to your SIEM."
canonicalUrl: "https://zuplo.com/blog/2026/06/30/audit-agent-mcp-tool-calls"
pageType: "blog"
date: "2026-06-30"
authors: "martyn"
tags: "Model Context Protocol, API Security"
image: "https://zuplo.com/og?text=Audit%20Every%20Tool%20Call%20Your%20Agents%20Make%20to%20MCP%20Servers"
---
Your agents talk to the Stripe MCP server, Linear, GitHub, an internal server a
teammate stood up last week. Ask which tools they called yesterday, under which
identity, and whether any of those calls failed, and on a direct connection the
honest answer is nothing. The agent talks straight to the upstream, and the only
record is whatever that upstream chooses to keep, in a console you do not
control.

For a security review, "the Stripe integration did it" is not an answer. You
need the tool, the caller, the time, and the result, for every call.

<CalloutAudience
  variant="useIf"
  items={[
    `Running agents against third-party MCP servers like Stripe, Linear, or GitHub`,
    `Asked by security or compliance to show what your agents actually called`,
    `Trying to attribute a tool call to a real identity, not a shared service credential`,
  ]}
/>

## Direct connections leave no trail

When an agent connects straight to an MCP server, the tool calls cross a
boundary you have no view into. The server returns its tools through
`tools/list`, the agent picks one, the call runs, and none of it is attributed
to a caller you can name on your side. If the upstream logs anything, it logs
the shared credential the integration authenticates with, not the person or
agent that triggered the call. The trail stops at the server's edge, which
belongs to someone else.

## Route through a gateway

The [Zuplo MCP Gateway](/blog/introducing-zuplo-mcp-gateway) fronts an upstream
MCP server with a virtual server of your own: a gateway-side endpoint that maps
to one upstream and is what your agents connect to instead of reaching the
upstream directly. Every request the agent makes crosses the gateway, so the
gateway sees and records each one.

This is the same choke point that lets you
[allowlist the tools you trust](/blog/expose-only-mcp-tools-you-choose) and
[bind tokens to one server](/blog/bind-mcp-tokens-to-one-server). Once traffic
flows through a point you own, that point can write down what happened.

The gateway emits structured events for the moments that matter, grouped into
three families:

| Event family            | What it captures                                                       |
| ----------------------- | ---------------------------------------------------------------------- |
| `mcp_request`           | The route boundary: whether the gateway accepted or rejected a request |
| `capability_invocation` | The actual tool, prompt, and resource calls and their results          |
| `auth_event`            | The OAuth lifecycle, from token issued to token validated              |

Each tool call your agent makes is a `capability_invocation`. That is the record
the rest of this post is about.

## Every tool call in one dashboard

In the portal, open **Observability**, then **Analytics**, then the **MCP** tab.
At the account level it aggregates across every project with MCP routes; inside
a single project it scopes to that project. The MCP section appears once the
first MCP request is recorded, so there is nothing to switch on.

![The Zuplo MCP Gateway analytics dashboard showing the Capabilities panel, with tool calls ranked by volume alongside error rate and p95 latency, above the Consumers panel, where calls are attributed to individual identities by email.](/blog-images/2026-06-30-audit-agent-mcp-tool-calls/mcp-analytics-dashboard.png)

The dashboard reads as a set of panels, each answering one question:

| Panel                               | Question it answers                                                                         |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
| Capabilities                        | Which tools, prompts, and resources get called the most, fail the most, and run the slowest |
| Consumers                           | Which identities are driving the calls                                                      |
| MCP Methods                         | Which MCP methods are in play: `tools/call`, `tools/list`, `resources/read`                 |
| Clients                             | Which client apps are connecting, by name and kind                                          |
| Failure Origins                     | Whether a failure came from the gateway, the upstream, or the client                        |
| JSON-RPC Error Codes / Reason Codes | The exact error and reason codes behind the failed calls                                    |

Capabilities is the per-tool view, sortable by any column, with a Type column
separating tools from prompts and resources. It is the direct answer to "which
tools did our agents call, and how often."

## Who made the call

The dashboard is useful because the underlying records carry identity. Three
fields land on every entry:

- `operationId`, the route the call hit.
- `upstreamServerId`, the upstream the call was bound for, shown in the Server
  column of the Capabilities panel.
- `subjectId`, the authenticated caller, listed in the Consumers panel as an
  email where one exists.

So "which identity tried to call `create_refund`" is a question with an answer.
On a direct connection the upstream sees one shared credential for all of it;
through the gateway each call is tied to the subject that
[authenticated through the OAuth flow](/blog/bind-mcp-tokens-to-one-server).
That is the difference between a log and an audit trail.

## Failures and denials

A call that never succeeds is often the one you most want to see. The gateway
sorts every result into one of seven color-coded outcome classes. The four you
reach for first (the other three cover `partial`, `cancelled`, and
`connect_required` states):

| Outcome             | What happened                                                                                                           |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `success`           | The call completed                                                                                                      |
| `denied`            | The gateway rejected it at the boundary, before the upstream saw it                                                     |
| `application_error` | The upstream returned an MCP-level error inside a 200 response                                                          |
| `failure`           | The call never got a clean response, a transport or operational fault rather than an error the upstream chose to return |

A `failureOrigin` field then tells you whether the gateway, the upstream, or the
client was the source. So when an agent reaches for a tool you
[filtered off the route](/blog/expose-only-mcp-tools-you-choose), the blocked
call is not silence, it is a `denied` event with the subject attached, and you
can ask who tried it.

## Send the logs to your SIEM

The dashboard is the fast read. For retention and compliance you want the events
in the system you already audit against, and the same MCP events feed Zuplo's
standard logging pipeline: Datadog, Splunk, AWS CloudWatch, Google Cloud
Logging, New Relic, Sumo Logic, Loki, Dynatrace, and VMware Log Insight, plus
[OpenTelemetry](https://zuplo.com/docs/articles/opentelemetry) for traces and
logs in OTLP format.

The OAuth lifecycle events are recorded as audit entries, at `info` severity,
for compliance review. The gateway is deliberate about what it leaves out:
bearer tokens, authorization codes, client secrets, and customer request bodies
are never written to a log entry, so shipping the audit trail to a third party
does not ship your secrets with it.

<CalloutDoc
  title="MCP Gateway analytics"
  description="The full panel reference: every event family, the dimensions you can group by, and the outcome classes the dashboard reports."
  href="https://zuplo.com/docs/mcp-gateway/observability/analytics"
  icon="book"
/>

## Decide visibility up front

We run our own agents' access to third-party servers through virtual servers.
The first thing the dashboard told us was which tools the agents actually lean
on, which is rarely the list you would guess. The audit trail is a byproduct of
routing through a point you own, the same point that
[governs shadow MCP connections](/blog/shadow-mcp-governance) and scopes what
each agent can reach. You do not bolt logging on afterward. You get it the
moment the traffic stops going direct.

<CalloutSignup
  badge="Public beta"
  title="Get an audit trail for your agents' MCP tool calls"
  description="The Zuplo MCP Gateway fronts any upstream MCP server and records every tool call, attributed to an identity, in the analytics dashboard and your existing log sinks."
  features={[
    "Per-tool-call analytics in the portal",
    "Every call attributed to a subject",
    "Ships to Datadog, Splunk, OpenTelemetry, and more",
  ]}
  signupButtonText="Spin up a project"
  signupUrl="https://portal.zuplo.com/signup?utm_source=zuplo-blog&utm_medium=web&utm_campaign=mcp-gateway"
  secondaryAction={{
    text: "Read the MCP analytics docs",
    href: "https://zuplo.com/docs/mcp-gateway/observability/analytics",
  }}
/>