# OpenTelemetry

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

Every server you create emits an [OpenTelemetry](https://opentelemetry.io/) 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(...)`.

```python title="server.py"
# docs_src/opentelemetry/tutorial001.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](https://opentelemetry.io/docs/specs/semconv/gen-ai/):

* `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:

```console
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](https://logfire.pydantic.dev/) 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](https://www.w3.org/TR/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](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/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:

```python
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`](https://py.sdk.modelcontextprotocol.io/advanced/middleware/index.md) 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.

The thing that decides whether a request runs at all is **[Authorization](https://py.sdk.modelcontextprotocol.io/run/authorization/index.md)**.
