49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
108 lines
4.6 KiB
Markdown
108 lines
4.6 KiB
Markdown
# 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"
|
|
--8<-- "docs_src/opentelemetry/tutorial001.py"
|
|
```
|
|
|
|
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`](../advanced/middleware.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](authorization.md)**.
|