---
title: "MCP Observability: What to Record When an Agent Calls Your Tools"
description: "What to record when an agent calls your MCP tools: protocol version, client label, outcome, and redacted arguments, aligned to the 2026-07-28 spec."
canonical: "https://www.agentrail.co/blog/mcp-observability"
author: "Harsh Raj"
published: "2026-10-17T00:00:00Z"
last_updated: "2026-10-22T18:13:14.588Z"
---

# MCP Observability: What to Record When an Agent Calls Your Tools

What to record when an agent calls your MCP tools: protocol version, client label, outcome, and redacted arguments, aligned to the 2026-07-28 spec.

![Pulse icon in a rounded square on a blue gradient](https://images.ctfassets.net/1htfyfnn1o5y/3IUKjudWyV8NcBxYYStqvV/144f8a6d4a47eb159ba5f2851aa26e0e/hero_image.png?w=1600&fm=webp&q=72)

An agent that calls your MCP server is already using the product. The call is a JSON-RPC `tools/call`. A reverse-proxy line tells you a request arrived. It leaves out the tool name, the outcome, and the client that sent it.

MCP observability is the record you persist for that call. [Anthropic reported](https://www.anthropic.com/news/donating-the-model-context-protocol-and-establishing-of-the-agentic-ai-foundation) more than 10,000 active public MCP servers in December 2025, and more than 97 million monthly SDK downloads across Python and TypeScript. The current protocol revision is `2026-07-28`. Keep a slot for clients that still speak the older handshake.

[Agent analytics](https://www.agentrail.co/blog/agent-analytics) asks what the agent was trying to do across the product. This page owns the MCP call record only. How those records turn into call counts and abandoned tools is [MCP analytics](https://www.agentrail.co/blog/mcp-analytics).

> **Key Takeaways**
>
> Persist one record per `tools/call`, paired with its result or JSON-RPC error. The logging utility is a different channel.
>
> On revision `2026-07-28`, protocol version and client capabilities ride on every request. `clientInfo` is a self-reported label, not an authentication decision.
>
> Store the outcome as a protocol error, a tool execution error (`isError: true`), a completed result, `input_required`, or a local timeout.
>
> Argument values and result bodies are opt-in. Default to names, types, sizes, and a redacted payload.
>
> Revision `2026-07-28` removed protocol sessions. Record a session id only for a legacy handshake. Correlate modern calls with an explicit handle or a trace id.

## Record the tool call, not a log notification

The unit of MCP observability is one `tools/call` and the response that closes it. The [tools specification](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) says clients should log tool usage for audit. That duty belongs to the team that runs the server. Meet it by storing a row that names the tool, the outcome, and the client label.

The [2026-07-28 changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog) deprecates the Logging feature and removes `logging/setLevel`. Log level is now optional request metadata, `io.modelcontextprotocol/logLevel`. If the client omits it, the server must not emit `notifications/message`. New servers should write stderr on stdio, or export OpenTelemetry.

The audit row is a separate channel. Store it even when the client never opts into logs. Rate-limit it, and strip secrets. The tools page already requires validation, access control, rate limits, and sanitized outputs.

Also store `tools/list` as its own record: the tool names returned, `ttlMs`, `cacheScope`, and `nextCursor` when the page has one. Full input schemas can live in version control. The list row exists so a later query can see a name you offered. Counting names that never show up on `tools/call` belongs in [MCP analytics](https://www.agentrail.co/blog/mcp-analytics).

## Write identity onto every call

Revision `2026-07-28` is stateless. The [base protocol](https://modelcontextprotocol.io/specification/2026-07-28/basic) says a server must not use an earlier request on the same connection to decide protocol version, capabilities, or client identity. Every request carries that metadata in `_meta`.

Two fields are required on each request:

- `io.modelcontextprotocol/protocolVersion`, for example `2026-07-28`
- `io.modelcontextprotocol/clientCapabilities`

Clients should also send `io.modelcontextprotocol/clientInfo` unless they are configured not to. Servers should send `io.modelcontextprotocol/serverInfo` on results. Both values are self-reported. The specification says they are for display, logging, and debugging. Implementations should not use them for security decisions, and should not change behavior because of them.

Record the name and version when they arrive. Record that they were absent when they don't. A call with no `clientInfo` is unattributed. That is a different fact from a call that names a client.

On HTTP, the same version is carried in the `MCP-Protocol-Version` header. If the header and `_meta` disagree, the server returns `HeaderMismatch` (`-32020`). Store the version you actually served. Store the error code when you reject the call.

Legacy clients still open with `initialize`. That path is revision `2025-11-25` and earlier. The [versioning rules](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning) say how a dual-era server tells the eras apart. Copy handshake `clientInfo` onto later calls for that process or HTTP session, and set `era` to `legacy`.

The [versioning guide](https://modelcontextprotocol.io/docs/2026-07-28/learn/versioning) states that the current protocol version is `2026-07-28`. Store the string from the request. Older callers still arrive.

## Separate protocol errors from tool errors

The tools specification defines two failure channels. Your record has to keep them apart, because the model is expected to react to them differently.

Protocol errors are JSON-RPC errors. The documented cases are an unknown tool, a malformed request, and a server error. The unknown-tool example uses code `-32602`. A missing client capability uses `-32021` (`MissingRequiredClientCapability`). An unsupported protocol version uses `-32022` (`UnsupportedProtocolVersion`), and the body lists versions the server supports. The client should retry with a version from that list.

Tool execution errors are successful JSON-RPC responses with `isError: true`. The documented cases are API failures, input validation, and business-logic failures. The specification says clients should pass those results to the model so it can correct the arguments. Clients may pass protocol errors through as well. Those are less likely to recover.

Give every call one `outcome`.

- `protocol_error` stores the integer code and the error message.
- `tool_error` means `isError` is true.
- `complete` means `resultType` is `complete` and `isError` is not true.
- `input_required` means `resultType` is `input_required`.
- `timeout` means your server stopped waiting and sent no response.

A timeout is local. The base protocol says purely local failures, such as an SDK timeout, have no specification error code. Write `timeout` only when you gave up. Leave `error_code` empty unless the peer returned one.

Older servers omit `resultType`. Clients must treat a missing `resultType` as `complete`. Store `absent` in `result_type` so a 2025 response is not rewritten as a 2026 vocabulary you did not see.

## Treat arguments and results as opt-in

The [OpenTelemetry MCP conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/mcp.md) are marked Development. They recommend a span named `{mcp.method.name} {tool name}`. For a tool call, `gen_ai.operation.name` should be `execute_tool`. `mcp.method.name` is required. `gen_ai.tool.name` is required when the operation is about one tool. `jsonrpc.request.id` is required when the client sent a request.

`gen_ai.tool.call.arguments` and `gen_ai.tool.call.result` are opt-in. The conventions warn that both may contain sensitive information. A full argument object belongs in a trace you turned on for a reason. It is a poor default for every production call. Arguments carry queries, account ids, and tokens.

On every call, store argument names, JSON types, and serialized size. Store result content types, whether `structuredContent` was present, and the size. Leave raw image and audio bytes out. Opt in to redacted values for a named tool or a short window.

If you export OpenTelemetry, map the opt-in payload to those attributes. When another GenAI span already traces the tool, add the MCP attributes there instead of emitting a second span.

The tools page tells server authors not to mark sensitive parameters with `x-mcp-header`. Those values are copied into HTTP headers and are visible to intermediaries. If a parameter is mirrored, record the header name. Skip a second copy of the secret.

## Record resultType, and give the retry its own id

A finished tool result uses `resultType: "complete"`. A server that needs more input returns `resultType: "input_required"`, with `inputRequests` and an optional `requestState`. The client retries `tools/call` with `inputResponses` and that same `requestState`. The specification says the JSON-RPC id on the retry must be a new id.

On the first response, store `result_type`, the method name inside each input request (for example `elicitation/create`), and whether `requestState` was present. Apply the same redaction rule to `requestState` that you apply to arguments. The specification's example encodes call context into that string.

Skip the user's filled form. The method name shows that the call stopped for elicitation. The form is user data.

The retry's new id means the two rows join on tool name plus `requestState`, not on the first request id. Building that join, and deciding the call was abandoned, is usage analysis. The fields live here. The counts live in [MCP analytics](https://www.agentrail.co/blog/mcp-analytics).

## Correlate modern calls without a protocol session

The changelog removes protocol-level sessions and the `Mcp-Session-Id` header from Streamable HTTP. The base protocol states what follows. An open connection is not a conversation. Clients may interleave unrelated requests on one transport. State that spans calls has to be an identifier the client passes each time.

The tools page says to return an explicit handle and accept it on the next call. A handle is a name, not a capability. Record the argument name and a hash. Leave the raw value opt-in with the other arguments. Authenticated servers still check authorization on every call.

The OpenTelemetry conventions still list `mcp.session.id` as recommended when the request is part of a session. The attribute text points at the June 2025 transport chapter. Those conventions remain in Development. Copy `mcp.session.id` only onto records whose protocol version is `2025-11-25` or earlier and whose transport actually sent a session id.

For a `2026-07-28` call, honest correlation keys are `traceparent` from `_meta`, a handle hash your tool minted, and the JSON-RPC id. The base protocol reserves `traceparent`, `tracestate`, and `baggage` without a DNS prefix. The request id identifies this call alone.

`server/discover` is a different method. Servers must implement it. It returns supported versions, capabilities, and server identity. Store it under its own method name if you keep it. It is not a `tools/call`.

## Fields to persist

| Field | Persist | Bound |
| --- | --- | --- |
| `timestamp`, `duration_ms` | Start, and wall time until result or local timeout | Always |
| `protocol_version` | Version you served | Always |
| `era` | `modern` or `legacy` | Always |
| `jsonrpc_id` | Request id | Always. A request id is never null. |
| `tool_name` | `params.name`, case-sensitive | Always on `tools/call` |
| `listed_tool_names`, `ttl_ms`, `cache_scope` | Page of `tools/list` | On list records |
| `client_name`, `client_version` | `clientInfo` when present | Label only |
| `server_name`, `server_version` | `serverInfo` when present | Label only |
| `argument_names`, `arguments_bytes` | Names, types, size | Always |
| `arguments_redacted` | Values with secrets removed | Opt-in |
| `outcome` | One of the five outcomes above | Always |
| `error_code`, `error_message` | The peer's error, when it sent one | Protocol errors |
| `result_type` | `complete`, `input_required`, or `absent` | Always |
| `content_types`, `result_bytes` | Types and size, not raw media | Always |
| `structured_content` | Present or absent, plus size | Always |
| `input_methods` | Method names inside `inputRequests` | On `input_required` |
| `request_state` | Presence. Value only if redacted. | On `input_required` |
| `traceparent` | W3C value from `_meta` | When the client sent one |
| `legacy_session_id` | Transport session id | Legacy era only |
| `handle_name`, `handle_hash` | Explicit tool handle | When the tool uses one |

A minimal completed call looks like this. The object is a schema example, not a measured customer call.

`{
  "protocol_version": "2026-07-28",
  "era": "modern",
  "jsonrpc_id": "2",
  "tool_name": "get_weather",
  "client_name": "ExampleClient",
  "client_version": "1.0.0",
  "outcome": "complete",
  "result_type": "complete",
  "argument_names": ["location"],
  "arguments_bytes": 24,
  "content_types": ["text"],
  "result_bytes": 86,
  "duration_ms": 40
}`

## What this record is for

The row says an agent called this tool, from this self-reported client, on this protocol version, and the call ended in this outcome. Counts of unused tools and retries need many rows. That work is [MCP analytics](https://www.agentrail.co/blog/mcp-analytics).

When the same agent also reads your docs or calls your HTTP API, the MCP row is one step. The whole task is [agent analytics](https://www.agentrail.co/blog/agent-analytics).

## FAQ

### What is MCP observability?

MCP observability is the record of each `tools/call` an agent makes against your server. The record includes protocol version, tool name, a self-reported client label, duration, and an outcome. The outcome is a protocol error, a tool error, a completed result, a request for more input, or a local timeout.

### Should tool arguments be stored in full?

Store argument names, types, and size on every call. Store redacted values only when you opt in for a tool or a short window. The OpenTelemetry MCP conventions mark argument and result attributes as opt-in because they may contain sensitive information.

### Where does the audit log live?

The audit log is the row you persist for `tools/call`. `notifications/message` is optional server-to-client traffic. Revision `2026-07-28` deprecates the Logging feature and moves log level onto the request. Clients should still log tool usage for audit, which is this row.

### Do current clients still have a session id?

Record a session id for a legacy handshake, revision `2025-11-25` and earlier, when the transport sent one. Revision `2026-07-28` removes protocol sessions and `Mcp-Session-Id`. Correlate a modern call with `traceparent` or with an explicit handle the tool returned. The JSON-RPC id identifies that single call.

## Close the record before you count it

Write the row at the server boundary, one call at a time, with secrets stripped and the protocol era labeled. Which tools agents call, and which they never reach, is [MCP analytics](https://www.agentrail.co/blog/mcp-analytics).

We built Agentrail to reconstruct the agent's task across website, docs, APIs, SDKs, and MCPs. The call record is one input to that path. Public pricing is on the [pricing page](https://www.agentrail.co/pricing): 100 journeys free per organization each month, then $0.10 per journey.
