feat(runtime-mcp): propagate W3C traceparent on outbound MCP tool calls (#6128)#6153
Merged
Conversation
…ls (#6128) The MCP client did not propagate W3C trace context to the servers it calls, so spans from an OTel-instrumented MCP server started their own disconnected trace instead of joining the LibreFang agent turn that triggered the tool call. LibreFang already does this on LLM HTTP egress (`librefang-llm-drivers::drivers::trace_headers::inject_w3c_trace_context`); this mirrors it on the MCP transport. New `trace_context` module reads the active trace from `opentelemetry::Context::current()` (not `tracing::Span::current().context()`, which is silently invalid behind the reload layer — see the trace_headers.rs root-cause notes) and injects via the globally-registered text-map propagator, exposing one context as two views: an `http::HeaderMap` for the per-request HttpCompat path, and a `(name, value)` list for the MCP `_meta` path. Wired into all three transports of `call_tool_with_caller`, alongside (never replacing) the existing `io.librefang/caller` entry: - HttpCompat builds the reqwest request per call, so the context goes on as real per-request `traceparent`/`tracestate` headers that standard `opentelemetry-http` middleware extracts directly. - Rmcp/streamable-HTTP sets `custom_headers` once at connect time and rmcp 1.7 exposes no per-request header hook, so the context rides in the request `_meta` under `io.librefang/trace`. - SSE goes through a shared `sse_send_request` helper that takes no per-request header argument, so it uses the same `_meta` carrier (merged with any caller `_meta`). Unconditional and self-disabling, matching the LLM-egress precedent: when telemetry is off the global propagator is the Noop propagator (writes nothing); when no recording span is active the context is invalid and the propagator writes nothing. Either way the carrier is empty and the call is unchanged. Known limitation: for the Rmcp and SSE transports the context is in `_meta`, not wire headers, so a server that only auto-extracts `traceparent` from HTTP headers will not stitch on those transports until rmcp exposes a per-request header hook (the upstream gap noted in the issue). HttpCompat is unaffected. Acceptance criterion #1 (outbound call carries a traceparent matching the active span) holds on all three. Adds `opentelemetry` + `opentelemetry-http` deps (already in the workspace via telemetry) and otel/tracing dev-deps for the test. Verification: - cargo test -p librefang-runtime-mcp — 169 lib + 3 integration tests pass, including 3 new trace_context tests (recording span yields a non-all-zero traceparent matching the active span; no active span yields nothing; _meta and header views are consistent), reusing the reload-layer test harness from trace_headers.rs. - cargo clippy -p librefang-runtime-mcp --all-targets — zero warnings. - cargo fmt clean.
CLAUDE.md: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." Trimmed three inline comment blocks in lib.rs and the module doc, const doc, function docs, and test helper doc in trace_context.rs. No logic changes; cargo clippy -p librefang-runtime-mcp --all-targets passes clean.
This was referenced Jun 17, 2026
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.
The MCP client did not propagate W3C trace context to the servers it calls, so spans from an OTel-instrumented MCP server started their own disconnected trace instead of joining the LibreFang agent turn that triggered the tool call. LibreFang already does this on LLM HTTP egress (
librefang-llm-drivers::drivers::trace_headers::inject_w3c_trace_context); this mirrors it on the MCP transport.New
trace_contextmodule reads the active trace fromopentelemetry::Context::current()(nottracing::Span::current().context(), which is silently invalid behind the reload layer — see the trace_headers.rs root-cause notes) and injects via the globally-registered text-map propagator, exposing one context as two views: anhttp::HeaderMapfor the per-request HttpCompat path, and a(name, value)list for the MCP_metapath.Wired into all three transports of
call_tool_with_caller, alongside (never replacing) the existingio.librefang/callerentry:traceparent/tracestateheaders that standardopentelemetry-httpmiddleware extracts directly.custom_headersonce at connect time and rmcp 1.7 exposes no per-request header hook, so the context rides in the request_metaunderio.librefang/trace.sse_send_requesthelper that takes no per-request header argument, so it uses the same_metacarrier (merged with any caller_meta).Unconditional and self-disabling, matching the LLM-egress precedent: when telemetry is off the global propagator is the Noop propagator (writes nothing); when no recording span is active the context is invalid and the propagator writes nothing. Either way the carrier is empty and the call is unchanged.
Known limitation: for the Rmcp and SSE transports the context is in
_meta, not wire headers, so a server that only auto-extractstraceparentfrom HTTP headers will not stitch on those transports until rmcp exposes a per-request header hook (the upstream gap noted in the issue). HttpCompat is unaffected. Acceptance criterion #1 (outbound call carries a traceparent matching the active span) holds on all three.Adds
opentelemetry+opentelemetry-httpdeps (already in the workspace via telemetry) and otel/tracing dev-deps for the test.Verification: