Seeing What an Agent Did Through an MCP Server
Seeing what an agent did through an MCP server means keeping four things together: the tool-call arguments (intent), the tool results (effect), the vendor request ID that links each call to a vendor-side record, and a trace ID carried from the triggering task. Retain them for weeks, because the question always arrives days after the change.
What the record has to contain
Seeing what an agent did through an MCP server means being able to reconstruct, after the fact, which tool the agent called, with what arguments, what came back, and which task caused the call in the first place. None of that is written down by default. The client negotiates a session with the server, the model emits tool calls, the server executes them against a vendor API, and unless something in that chain persists the exchange, the only durable trace is whatever the vendor keeps — which, as the points below cover, is not enough on its own.
Cloudinary is the worked example because its agent surface is unusually broad. Cloudinary’s MCP server documentation lists five servers — Asset Management, Environment Config, Structured Metadata, Analysis and MediaFlows — with the remote ones authenticating over OAuth and available on every plan, Free included (checked 2026-08-18). An agent with those servers connected can upload, tag, transform, delete and reconfigure a product environment without a human seeing any single call. The same reasoning applies to any MCP server that mutates state you pay for.
Arguments are intent, results are effect
A tool call has two halves and they answer different questions. The arguments — the JSON the model constructed when it decided to call upload or update_metadata or delete_resources — are the record of intent: what the agent was trying to do, expressed in its own terms. The result — the JSON the server returned — is the record of effect: what actually happened, including the public ID that was assigned, the version that was created, or the error that came back.
Logging only one leaves an unanswerable question later. Arguments without results tell you the agent asked for a deletion but not whether it succeeded, was rate-limited, or hit a resource that no longer existed. Results without arguments tell you a resource was overwritten but not what the agent thought it was overwriting, or with what. When an asset looks wrong a week later, the investigation is a comparison between the two halves, and a comparison needs both sides.
The practical consequence is that the logging point has to sit where both halves pass through: the client’s tool-call hook if it exposes one, or a thin proxy in front of the server. Logging inside the vendor’s API client sees the effect and a translated version of the intent; logging in the model’s transcript sees the intent and a truncated version of the effect. Neither is the pair.
The vendor request ID is the join key
Every call the server makes ends up as an HTTP request against the vendor, and vendor APIs return a request identifier in the response headers. That identifier is what ties a local log line to a vendor-side record. Without it, matching your log against the vendor’s is a timestamp-and-guesswork exercise across two clocks and two time zones; with it, the match is exact. It is also the first thing vendor support asks for, and a ticket opened without one is a ticket that starts with a round trip.
The difficulty is that an MCP tool result is a document, not an HTTP response, and headers do not survive the translation unless the server puts them there. Whether a given server surfaces them in its results is covered separately in surfacing rate-limit and request-ID headers in MCP tool results; the short version is that if the tool result does not carry the ID, the only place it exists is inside the server process, and a wrapper there is where to capture it.
Cloudinary’s Admin API reference is the read side of this: it is how you fetch the current state of a resource — its version, its tags, its metadata — to compare against what a logged call claims to have done. Note that on the Free plan the Admin API is limited to 500 requests per hour (Cloudinary pricing, checked 2026-08-18), which rules out reconciling a large agent run by polling every touched asset; sample, or reconcile from the logged results rather than by re-reading.
The vendor’s log records the operation, not the reasoning
Cloudinary, like most vendors, keeps its own account-level record of operations, and it is tempting to treat that as the audit trail. It records that a resource was deleted, by which credential, at what time. It does not record why: which task the agent was executing, what it had just read, or what it inferred that made deletion seem correct. That is because the reasoning never reached the vendor — it lived in the model’s context on your side.
So neither side alone explains an asset change. The vendor’s log confirms an operation happened and pins the credential; your log carries the arguments and the surrounding calls that show what the agent was doing at the time. An investigation that has only the vendor’s log knows what changed and can prove who held the key, but cannot say whether the change was the agent doing its job or a misread instruction. An investigation with only your log knows what was attempted but has to trust its own record of what took effect. This is the reason to keep both, and the request ID from the previous point is what lets you read them as one.
Attribution is a tracing problem
The question that follows what changed is which task caused it. An agent working through an MCP server makes many calls per task, and a long-running or multi-agent setup makes many tasks concurrently, so the calls interleave. A flat log of tool calls does not group them; the grouping needs an identifier minted when the task starts and carried through every downstream call — from the client, into the server, and out to the vendor as a header if the vendor will accept one.
That is a tracing problem rather than a logging one, and it is worth using the vocabulary that already exists for it. OpenTelemetry’s description of traces covers the model: a trace is the whole task, each tool call is a span inside it, and context propagation is what carries the trace ID across the process boundary between client and server. You do not need the full toolchain to benefit; you need the discipline that every log line written on the way to the vendor carries the same task identifier, and that the identifier is not one the model can forget to include. In practice that means the wrapper or proxy attaches it from the session, not the model from its prompt.
The cost of skipping this shows up in exactly one situation: a change nobody can attribute. With per-task identifiers, the log answers which task did it in one query. Without them, the answer is a manual reconstruction from timestamps that gets less reliable the busier the system was.
Retention matters more than volume
Tool-call logs are small. Even a run that touches thousands of assets produces a few megabytes of JSON, and the temptation is to treat them as ephemeral debug output. The problem is not the size; it is that the question about a surprising change is always asked days later — when a customer notices a missing image, when a moderation decision is queried, when a transformation started rendering differently. A log with a seven-day rotation is a log that has been deleted by the time it is needed.
The retention window should match how long it takes for a change to be noticed and questioned in your context, and for content that reaches the public that is weeks, not days. Retention also has to cover the join key: keeping your log for ninety days but losing the request IDs to a field-length cap, or keeping request IDs but letting the trace ID column go, breaks the chain that the previous points built. Store the arguments, the result, the request ID and the trace ID together, as one record, and expire them together.
What to look up next
When a logged call is present but its result is an error or the wrong shape, diagnosing an MCP tool call that fails picks up where this page stops. If the investigation reveals the agent had access to a tool it should never have called, restricting which tools an MCP server exposes to a client is the corrective — a smaller tool surface is also a smaller log to search. And where the change under review was a moderation action, driving content moderation from an agent through an MCP server covers what a defensible record of that specific decision needs to hold.
Sources
- Cloudinary's MCP server documentationcloudinary.com
- Admin API referencecloudinary.com
- Cloudinary pricingcloudinary.com
- OpenTelemetry's description of tracesopentelemetry.io
See also
How the cloudinary-region header selects the API region an MCP server talks to, why a wrong value fails silently, and what it does not do for data residency.
How to move an MCP-driven step from a developer machine into CI: header credentials, secret store, tool allowlist, outcome assertions and shared rate limits.
Remote and local MCP servers expose the same tools. Compare runtime cost, credential handling, update ownership, and the one condition that settles the choice.
Launch a local MCP server as a subprocess, pass credentials through its environment, and keep stdout clean for JSON-RPC messages.