OpenTelemetry - MCP Python SDK
https://py.sdk.modelcontextprotocol.io/run/opentelemetry/ • 55 KB fetched Open original page
OpenTelemetry - MCP Python SDK
Skip to content
MCP Python SDK
OpenTelemetry
*
en - English
*
de - Deutsch
*
es - español
*
fr - français
*
hi - हिन्दी
*
ja - 日本語
*
ko - 한국어
*
pt - português (Brasil)
*
ru - русский язык
*
tr - Türkçe
*
uk - українська мова
*
zh - 简体中文
*
zh-hant - 繁體中文
Search
modelcontextprotocol/python-sdk
MCP Python SDK
modelcontextprotocol/python-sdk
*
MCP Python SDK
*
What's new in v2
*
Get started
Get started
*
Installation
*
First steps
*
Connect to a real host
*
Testing
*
Servers
Servers
*
Tools
*
Structured Output
*
Resources
*
URI templates
*
Prompts
*
Completions
*
Images, audio & icons
*
Handling errors
*
Inside your handler
Inside your handler
*
The Context
*
Dependencies
*
Lifespan
*
Elicitation
*
Multi-round-trip requests
*
Sampling and roots
*
Progress
*
Logging
*
Subscriptions
*
Running your server
Running your server
*
Add to an existing app
*
Deploy & scale
*
Authorization
*
OpenTelemetry
OpenTelemetry
On this page
*
What you get
*
It costs nothing until you want it
*
Traces that cross the wire
*
Turning it off
*
Recap
*
Serving legacy clients
*
Clients
Clients
*
Callbacks
*
Transports
*
OAuth
*
Identity assertion
*
Multiple servers
*
Subscriptions
*
Caching
*
Protocol versions
*
Deprecated features
*
Advanced
Advanced
*
The low-level Server
*
Pagination
*
Middleware
*
Extensions
*
MCP Apps
*
Troubleshooting
*
Translations
*
Migration Guide
*
API Reference
API Reference
*
mcp
*
mcp_types
On this page
*
What you get
*
It costs nothing until you want it
*
Traces that cross the wire
*
Turning it off
*
Recap
*
MCP Python SDK
*
Running your server
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.
The thing that decides whether a request runs at all is Authorization .
Back to top
Previous
Authorization
Next
Serving legacy clients
Made with
Zensical
Links found on this page
- Skip to content [direct]
- en - English [direct]
- de - Deutsch [direct]
- es - español [direct]
- fr - français [direct]
- hi - हिन्दी [direct]
- ja - 日本語 [direct]
- ko - 한국어 [direct]
- pt - português (Brasil) [direct]
- ru - русский язык [direct]
- tr - Türkçe [direct]
- uk - українська мова [direct]
- zh - 简体中文 [direct]
- zh-hant - 繁體中文 [direct]
- modelcontextprotocol/python-sdk [direct]
- What's new in v2 [direct]
- Get started [direct]
- Installation [direct]
- First steps [direct]
- Connect to a real host [direct]
- Testing [direct]
- Servers [direct]
- Tools [direct]
- Structured Output [direct]
- Resources [direct]
- URI templates [direct]
- Prompts [direct]
- Completions [direct]
- Images, audio & icons [direct]
- Handling errors [direct]
- Inside your handler [direct]
- The Context [direct]
- Dependencies [direct]
- Lifespan [direct]
- Elicitation [direct]
- Multi-round-trip requests [direct]
- Sampling and roots [direct]
- Progress [direct]
- Logging [direct]
- Subscriptions [direct]
- Running your server [direct]
- Add to an existing app [direct]
- Deploy & scale [direct]
- Authorization [direct]
- Serving legacy clients [direct]
- Clients [direct]
- Callbacks [direct]
- Transports [direct]
- OAuth [direct]
- Identity assertion [direct]
- Multiple servers [direct]
- Subscriptions [direct]
- Caching [direct]
- Protocol versions [direct]
- Deprecated features [direct]
- Advanced [direct]
- The low-level Server [direct]
- Pagination [direct]
- Middleware [direct]
- Extensions [direct]
- MCP Apps [direct]
- Troubleshooting [direct]
- Translations [direct]
- Migration Guide [direct]
- mcp [direct]
- mcp_types [direct]
- OpenTelemetry [direct]
- GenAI semantic conventions [direct]
- Pydantic Logfire [direct]
- W3C trace context [direct]
- SEP-414 [direct]
- Zensical [direct]
|
|