Skip to content

OpenTelemetry

Your server is already traced. You don't have to add anything.

Every server you create emits an OpenTelemetry span for every message it handles. You didn't write that, and you don't import it. It is there the moment you call MCPServer(...).

server.py
from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(query: str) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r}."

That is a complete, traced server. Call search_books and a span is created for it. The same is true for the low-level Server: the tracing lives on both.

What you get

Every inbound message becomes a SERVER span named after the method and its target. So a tools/call for search_books is the span tools/call search_books, and a bare tools/list is just tools/list.

Each span carries a few attributes:

  • mcp.method.name and mcp.protocol.version, on every span.
  • jsonrpc.request.id, on a request (a notification has none).
  • A handler that raises sets the span status to error. So does a tool result with is_error=True.

And because tracing a tool call is such a common thing to want, tools/call spans speak OpenTelemetry's GenAI semantic conventions:

  • gen_ai.operation.name, set to "execute_tool".
  • gen_ai.tool.name, set to the tool being called.

A prompts/get span gets gen_ai.prompt.name in the same spirit. The list methods carry no gen_ai.* keys, because there is nothing to name.

Tip

Those GenAI attributes are the reason a tracing UI groups your tool calls the way it groups any other agent's. You get that grouping for free, with no extra code.

It costs nothing until you want it

Here is the part that makes "on by default" a comfortable default.

The SDK depends only on opentelemetry-api, the lightweight half of OpenTelemetry. With no SDK and no exporter installed, creating a span is a no-op. So the spans your server is emitting right now cost you almost nothing, and nobody is collecting them.

The day you want to see them, you install the other half and point it somewhere:

uv add opentelemetry-sdk opentelemetry-exporter-otlp

Configure an exporter the usual OpenTelemetry way, and every span the SDK has been quietly creating lights up. Your server code does not change. Not one line.

Info

Pydantic Logfire is one such backend, and it does the configuration for you: pip install logfire, logfire.configure(), and your MCP spans show up in the live view. It is built on OpenTelemetry, so anything below applies to it too.

Traces that cross the wire

A trace is most useful when it follows a request from the client into the server, in one connected picture.

When the client and the server both run the SDK, that connection is automatic. The client injects the W3C trace context into the request, and the server reads it back out, so the server span nests under the client span in the same trace. This is SEP-414, and you get it without asking.

If the inbound message carries no trace context, for example a request from a client that is not the SDK, the server span simply parents to whatever span is already current on the server, rather than starting a brand-new orphan trace.

Turning it off

Tracing is a middleware, the first one on your server's list. If you really want a server that emits no spans, take it off:

from mcp.server._otel import OpenTelemetryMiddleware

mcp._lowlevel_server.middleware[:] = [
    m for m in mcp._lowlevel_server.middleware if not isinstance(m, OpenTelemetryMiddleware)
]

Warning

That import has a leading underscore, and that is on purpose. The class is provisional, the same way Server.middleware is provisional, so the import path is something you should expect to change. You almost never need this: with no exporter installed the spans are free, so the usual answer is to leave them on and not install an exporter.

Recap

  • Every MCPServer and every low-level Server emits one SERVER span per inbound message, out of the box. You write nothing.
  • Spans carry mcp.method.name and mcp.protocol.version; tools/call and prompts/get also carry GenAI attributes so your tool calls group like any other agent's.
  • It costs nothing until you install an OpenTelemetry SDK and an exporter, and then it lights up with no change to your server.
  • Client-to-server trace context propagates automatically when both sides run the SDK.

Next, the thing that decides whether a request runs at all: Authorization.