MCP OpenTelemetry tracing vs Spanly: what each one actually captures
The MCP Python SDK now ships OpenTelemetry tracing per SEP-414. It answers a different question than packet-level MCP observability does. Here is what the spans capture, what they leave out, and how traceparent ties the two together.
- mcp
- opentelemetry
- observability
In June 2026, the MCP Python SDK shipped built-in OpenTelemetry tracing, following SEP-414. If your team already runs OTel, the obvious question is whether that covers MCP observability now. It covers something real, but something narrow, and it is worth being precise about which question each tool answers.
TL;DR
They answer different questions. The SDK’s OpenTelemetry tracing creates lightweight spans around each MCP request and propagates W3C trace context, so the MCP hop joins your application’s existing trace in Datadog, Jaeger, or any OTel backend. It captures almost no MCP-specific data, and it only produces output if you run an OTel stack on both sides.
Spanly records the wire itself: every JSON-RPC packet with full payloads, paired into requests, grouped into sessions, with no OTel setup and no code in your handlers. The two link up through traceparent, which Spanly preserves on every captured packet so you can deep-link into your APM.
Side by side
| Dimension | MCP SDK OTel tracing | Spanly |
|---|---|---|
| Language coverage | Python SDK only (TypeScript: none) | TS SDK, Python SDK, CLI for any language |
| Integration | OTel provider, exporter, and backend | One-line .monitor() or CLI wrap, nothing in handlers |
| Payloads | None (method name and request id) | Full params, results, and errors, up to 16 MiB |
| Notifications and progress | Not modeled (request spans only) | Captured, with a dedicated notifications view |
| Sessions and client analytics | No (one trace per request) | Yes (sessions, client identity, per-client views) |
| Cross-service continuity | Yes, joins the app’s whole trace | No, observes the MCP hop only |
| Inside-the-handler detail | Yes, via app-added child spans | No, the handler is a black box |
| Server crash mid-request | Span likely lost (in-process) | Request packet already captured, missing response flagged -32002 |
| Backend | Generic APM | Purpose-built MCP dashboard |
How the SDK’s OTel tracing works
The Python SDK ships tracing in-process. On the client side, each outgoing request is wrapped in a CLIENT span, and the SDK injects traceparent and tracestate into params._meta. That is the core of SEP-414: because the carrier is a JSON-RPC field rather than an HTTP header, trace context survives stdio pipes the same way it crosses service meshes. On the server side, an OTel middleware extracts that context and opens a child SERVER span around your handler.
The spans are deliberately minimal. Each one carries exactly two attributes, mcp.method.name and jsonrpc.request.id, plus error status. No params, no results, no payloads. That is by design, and it matches the OpenTelemetry semantic conventions for MCP, which keep request content out of spans.
Export is the host application’s job. The SDK calls the OTel API, and without a configured provider and exporter, those calls are a no-op. You bring the collector, the backend, and the sampling configuration.
The TypeScript SDK has none of this today. It passes _meta through untouched, so a TypeScript server team that wants spans must hand-roll the instrumentation.
How Spanly works
Spanly’s instrumentation is observation, not span creation. The TypeScript SDK taps the transport streams, the Python SDK wraps the read and write streams, and the CLI sits outside the process entirely, as a stdio wrapper or an HTTP reverse proxy. Nothing runs inside your handlers, and no OTel provider is involved.
Raw packets ship to Spanly’s ingest, and the heavy lifting happens server-side: requests and responses are paired by JSON-RPC id, durations are computed from packet timestamps, sessions are reconstructed, and client identity is read from the initialize handshake. The result is a queryable record of every exchange: tool calls with their arguments and results, notifications, errors with their payloads, and per-client analytics.
Where OTel tracing wins
The SDK’s tracing is genuinely better at two things, and Spanly does not attempt either.
Trace continuity. If the MCP client also runs an OTel-enabled stack, the server span parents onto the client span, and the server’s own downstream calls, database queries, HTTP requests, all parent onto the MCP span. You get one trace across the whole system, from the host application through the MCP hop and into your infrastructure.
Inside-the-handler detail. Because it composes with everything else your team already instruments, “why was this tool call slow” can resolve to a specific query in your APM’s waterfall view. Spanly sees the handler as a black box between request and response.
Two practical caveats temper this. The big one: real-world MCP clients, including Claude Desktop, Cursor, and ChatGPT, do not inject traceparent today. In production, the “distributed” part usually degrades to standalone server-side spans, and only if your server happens to be Python with OTel wired up. The second: because spans deliberately exclude payloads, an OTel trace tells you a tools/call errored in 240ms, but not what arguments triggered the error or what came back.
Where Spanly wins
Spanly wins on everything protocol-shaped: what actually crossed the wire. That includes full request and response payloads, malformed traffic, notifications and progress updates, server-initiated requests like sampling and elicitation, and orphaned requests that never got a response, which are flagged with error code -32002. Because capture is independent of your handler completing, a server crash mid-request still leaves the request packet in your dashboard, with the missing response called out.
It also wins on adoption cost. There is no OTel stack to deploy, the CLI covers any language without code changes, and the experience is uniform across an SDK ecosystem where tracing support is anything but. We go deeper on the protocol-vs-platform split in MCP observability vs APM.
Use both: traceparent is the join key
Spanly intentionally does not emit OTel spans. Instead, it preserves the inbound traceparent verbatim on every captured packet, and that powers the APM integrations: from any request in Spanly, you can jump to the matching trace in Datadog, Sentry, or New Relic.
For a SaaS team with an existing APM, the coherent setup is both layers. Your APM, optionally fed by the SDK’s OTel tracing if you are on Python, explains what happened inside your code. Spanly explains what happened on the MCP surface: payloads, sessions, clients, protocol errors. The traceparent is the join key between the two, so neither layer is a silo.
Where this is heading
SEP-414 is new, and it will likely spread beyond the Python SDK over time. If MCP clients start injecting trace context, the linking gets more valuable, because more requests arrive with a join key to your APM. What does not change is the data each layer holds: spans will still carry method names and ids, not payloads, sessions, or client analytics, and in-process tracing will still not cover the languages and servers you cannot instrument. Pick the tracing layer for continuity, pick the packet layer for the protocol, and let traceparent connect them.
Keep reading
- MCP observability vs APM: the same question one level up, including what APM vendors’ own MCP support covers.
- How to monitor your MCP server in production: the practical setup guide.
- What is MCP observability?: the category, defined.
- Live demo dashboard: see the packet-level view on real data.
Tim