MCP observability: how to trace and monitor MCP servers
MCP observability means tracing each tool call from agent to server using the spec's trace context in _meta and OpenTelemetry's MCP conventions.
MCP observability is the practice of tracing and measuring every request between an MCP client and an MCP server, so a slow or failed tool call can be tied to the agent run that made it. The pieces are standard: the MCP specification defines how trace context travels in a request, and OpenTelemetry defines the span and metric names.
How trace context travels in MCP
The MCP specification reserves three _meta keys, traceparent, tracestate and baggage, for OpenTelemetry trace context. When present, the values must follow the W3C Trace Context and W3C Baggage formats. The spec’s example is a tools/call request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"location": "New York"},
"_meta": {
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
}
}
}
The changelog lists this convention among the minor changes in the 2026-07-28 revision. For the transport, the OpenTelemetry conventions say the client span is the parent of the server span whether the messages travel over stdio or HTTP. That matters because an HTTP header alone would not cover stdio. For the general mechanism, see distributed tracing.
The spec’s statelessness rules shape what you can group by. A server must not rely on earlier requests on the same connection, and the spec notes that an open stdio process is not a conversation or session, since a client may interleave unrelated requests on it. So the trace ID, not the connection, is how you tie a tool call to the run that caused it.
What to put on each span
The OpenTelemetry conventions for MCP have Development status. As of November 2026, they say:
| Item | Convention |
|---|---|
| Span name | {mcp.method.name} {target}, where the target is gen_ai.tool.name or gen_ai.prompt.name; just the method if there is no low-cardinality target |
| Span kind | CLIENT for the calling side, SERVER for the receiving side |
| Required attribute | mcp.method.name |
| Set when relevant | gen_ai.tool.name, gen_ai.prompt.name, mcp.resource.uri, jsonrpc.request.id, and error.type if the operation fails |
| Recommended | gen_ai.operation.name (execute_tool for tool calls), mcp.protocol.version, mcp.session.id (when the request is part of a session; the 2026-07-28 revision removed protocol-level sessions and Mcp-Session-Id), network.transport (pipe for stdio, tcp or quic for HTTP) |
| Opt-in, possibly sensitive | gen_ai.tool.call.arguments, gen_ai.tool.call.result, gen_ai.prompt.variable |
Two details save trouble. First, a tool result with isError set to true should use the error.type value tool_error, so a tool that failed inside a successful JSON-RPC response still counts as an error. Second, the conventions advise against putting the resource URI in span names by default, because it makes names high cardinality.
The same document defines four histograms in seconds: mcp.client.operation.duration, mcp.server.operation.duration, mcp.client.session.duration and mcp.server.session.duration. Operation duration gives you latency per method and tool without sampling traces. The session histograms have little to measure on servers that speak only the 2026-07-28 revision, which dropped protocol-level sessions.
What the protocol gives you for debugging
- The spec says
io.modelcontextprotocol/clientInfoandio.modelcontextprotocol/serverInfoin_metaare intended for display, logging and debugging. They are self-reported and unverified, so use them as span or log fields, never for security decisions. - The per-request
io.modelcontextprotocol/logLevelkey sets the minimum log level a server should emit for that request. The protocol-level Logging feature itself is now deprecated, and the changelog points tostderron stdio or OpenTelemetry instead. - Protocol errors use JSON-RPC codes, and the revision defines
-32020(HeaderMismatch),-32021(MissingRequiredClientCapability) and-32022(UnsupportedProtocolVersion). Group these separately from tool errors, which arrive as results. - On Streamable HTTP, the 2026-07-28 revision requires
Mcp-MethodandMcp-Nameheaders on POST requests, which a proxy or gateway can log without parsing the body. See MCP gateway.
Where the gaps are
Instrumentation libraries may not follow these conventions yet, since they are still in development. Check what your SDK or instrumentation emits before you build dashboards on attribute names. Also decide up front whether to record tool arguments and results: the conventions leave them off by default for a reason, and a tool that reads files or tickets can return anything. Sensitive tool inputs are covered in MCP security.
Tracing MCP in HoneyHive
HoneyHive’s MCP tracing guide covers Python servers and clients. It offers experimental OpenInference auto-instrumentation and recommends @trace decorators with enrich_span for production, recording tool name, arguments, result status and timing. The guide does not describe client-to-server propagation; its distributed tracing guide covers linking traces across services so calls from each service appear in one session. For the surrounding concepts, see agent tracing, OpenTelemetry and the Model Context Protocol.
FAQ
How do I trace across an MCP client and server?
Put the W3C traceparent (and optionally tracestate and baggage) in the request's params._meta. The MCP specification reserves those keys for OpenTelemetry trace context, and the OpenTelemetry MCP conventions say the receiver extracts them and uses them as the remote parent.
Does MCP have built-in logging I can use for monitoring?
Logging is deprecated in the 2026-07-28 revision. The changelog suggests logging to stderr on stdio or using OpenTelemetry instead.
Are the OpenTelemetry MCP conventions stable?
No. The conventions have Development status, so attribute and metric names can still change.
Should I record tool arguments and results in spans?
Only if you can handle the data. The OpenTelemetry conventions make gen_ai.tool.call.arguments and gen_ai.tool.call.result opt-in and flag them as possibly containing sensitive information.