feat(mcp): opt-in W3C trace-context propagation for MCP tool calls - #78965
Open
jy-cimcloud wants to merge 1 commit into
Open
feat(mcp): opt-in W3C trace-context propagation for MCP tool calls#78965jy-cimcloud wants to merge 1 commit into
jy-cimcloud wants to merge 1 commit into
Conversation
Joins the agent's OpenTelemetry span to the spans instrumented MCP servers create for themselves, by injecting a traceparent header on the HTTP transport around each tool-call RPC. Off by default; enabled with mcp.trace_propagation: true in config.yaml. Two constraints shape the design (both from the review of NousResearch#60466): * Capture on the agent thread, before the thread boundary. MCP RPCs run on the daemon loop where the caller's context is invisible (contextvars do not cross run_coroutine_threadsafe), so the tool handler captures its own context and hands the formatted header across. * Inject per call, not per connection. One ClientSession serves many calls; a connection-time header pins them all to one stale span. The header exists on the shared httpx client exactly for the duration of one RPC, under the per-server RPC lock that already serializes calls. Trace context is read via the standard propagation API, so any instrumentation that sets ambient context works unmodified; tracing plugins that keep their own span registry can register a provider callback (hermes-otel's get_current_traceparent slots straight in). Provider output is validated against the W3C grammar. No path raises into a tool call, no OpenTelemetry import is required when disabled, and stdio transports are untouched. Fixes NousResearch#52211 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
When an OpenTelemetry-instrumented agent calls a tool on an instrumented MCP server, the two sides today emit disconnected traces: the agent has a span for the tool call, the server has spans for serving it, and nothing joins them — you cannot answer "which MCP call made this turn slow" in a trace viewer. This PR injects a W3C
traceparentheader on the HTTP transport around each tool-call RPC, making the server's spans children of the agent's span.Off by default, opt-in via config:
Relationship to #60466
#60466 attempts the same feature and stalled on review. This PR is built around that review rather than against it — credit to #60466 for opening the problem, and to @teknium1 for pinpointing why connection-time injection can't work:
ClientSessionserves many calls; a header set when the transport is established pins every later call to one stale span. Here the header exists on the shared httpx client for exactly one RPC (set and restored under the per-server RPC lock that already serializes calls on a session).trace.get_current_span()there observes nothing because contextvars don't crossrun_coroutine_threadsafe. The tool handler captures the caller's context on the agent thread, before the boundary, and hands the formatted header across. There's a test demonstrating exactly this thread-visibility property.config.yamlterms (no env vars). To be clear about scope: this propagates the user's own trace ids to the user's own MCP servers on requests the agent already makes — no analytics, no third-party attribution. Happy to wire a setup-wizard toggle if maintainers want one.If maintainers would rather see this folded into #60466, I'm glad to coordinate with its author instead — flagging per the CONTRIBUTING duplicate-PR guidance.
Attribution
The trace context is read via the standard OTel propagation API, so any instrumentation that sets ambient context works unmodified. For tracing plugins that keep their own span registry, there's a provider hook — designed so briancaffey/hermes-otel's
get_current_traceparent(their issue #25) slots straight in:This design has been running in production at CIMcloud since July as a maintained override of
tools/mcp_tool.py(rebased across v2026.7.20 → v2026.7.30 → v2026.8.3), producing joined agent→MCP-server traces for a fleet of internal MCP servers. This PR is the native, plugin-agnostic version of that patch, and would let us delete the override.Related Issue
Fixes #52211
Type of Change
Changes Made
tools/mcp_trace_propagation.py(new) — the gate, capture (standard propagation API + validated provider hook), andinjected_headers()per-RPC scoping. Stdlib-only imports; OpenTelemetry imported lazily and optionally; no path raises into a tool call.tools/mcp_tool.py— minimal diff: expose the HTTP transport's shared httpx client on the server object (_http_client, cleared when the transport winds down; stdio staysNone), capturetraceparentin the call_tool handler on the agent thread, wrap thesession.call_tool()await withinjected_headers().tests/test_mcp_trace_propagation.py(new) — 25 tests, stdlib + pytest + monkeypatch only, no network, passes with or without the OTel SDK installed.website/docs/user-guide/features/mcp.md— "Trace propagation (OpenTelemetry)" section.How to Test
pytest tests/test_mcp_trace_propagation.py -q→ 25 passed.mcp.trace_propagation: true, run an OTel-instrumented agent against an instrumented HTTP MCP server (e.g. FastMCP + OTLP exporter), call any tool: the server's spans now carry the agent's trace id — one joined trace in your viewer.🤖 Generated with Claude Code