Summary
LibreFang's MCP client does not propagate W3C trace context (traceparent) to MCP servers it calls.
As a result, spans emitted by an OTel-instrumented MCP server cannot be joined to the LibreFang agent turn that triggered the tool call — they start their own disconnected trace.
LibreFang already does the equivalent propagation on its LLM HTTP egress; this asks to mirror it on the MCP transport.
Current behavior
When an agent invokes an MCP tool, McpConnection::call_tool_with_caller (crates/librefang-runtime-mcp/src/lib.rs) builds the outbound request and attaches:
- the kernel-attested caller context in the request
_meta under io.librefang/caller (rmcp transport) / a top-level _meta (SSE),
Authorization (cached OAuth token) and any operator-configured extra headers.
It does not attach traceparent / tracestate.
So a downstream MCP server (e.g. an rmcp-based sidecar that exports spans to a collector) has no incoming parent context; its per-request / per-connection spans become a fresh root and never stitch to the agent's trace.
Existing precedent (LLM egress already does this)
inject_w3c_trace_context() in crates/librefang-llm-drivers/src/drivers/trace_headers.rs:177 already injects traceparent (and tracestate) into outbound LLM HTTP requests using opentelemetry_http::HeaderInjector and the globally-registered TraceContextPropagator (registered at crates/librefang-api/src/telemetry.rs:153).
A downstream proxy that auto-extracts traceparent stitches cleanly.
The MCP client should use the same propagator the same way.
Proposed fix
Inject the active span's W3C context on every outbound MCP tool call, behind the existing telemetry gate (no-op when telemetry is disabled / the context is all-zero, exactly as inject_w3c_trace_context already handles).
Two viable placements:
- HTTP header on the transport (preferred).
Add traceparent / tracestate to the streamable-HTTP / SSE request headers next to where Authorization and extra headers are attached.
This mirrors trace_headers.rs exactly and lets any MCP server extract it with standard opentelemetry-http middleware — no MCP-protocol-level convention required.
Implementation note: the rmcp streamable-HTTP client transport needs a per-request header hook for this; if one isn't exposed, that's the small upstream-rmcp gap to resolve or work around.
- MCP
_meta field.
Carry traceparent in the request _meta alongside the existing caller context.
Transport-agnostic, but requires both ends to agree on the _meta key and the server to map it to a remote parent manually.
Acceptance criteria
- An outbound MCP tool call made under a recording span carries a
traceparent whose trace id matches the active span (mirror the existing test build_trace_header_map_injects_traceparent_behind_reload_layer in trace_headers.rs).
- With telemetry disabled (or no active span), no
traceparent is emitted / it is all-zero — the call still succeeds.
- An OTel-instrumented MCP server can extract the context and parent its tool span under the agent's trace.
Out of scope
- Server-side extraction (each MCP server owns that).
- Changes to the existing
_meta caller-context payload.
Summary
LibreFang's MCP client does not propagate W3C trace context (
traceparent) to MCP servers it calls.As a result, spans emitted by an OTel-instrumented MCP server cannot be joined to the LibreFang agent turn that triggered the tool call — they start their own disconnected trace.
LibreFang already does the equivalent propagation on its LLM HTTP egress; this asks to mirror it on the MCP transport.
Current behavior
When an agent invokes an MCP tool,
McpConnection::call_tool_with_caller(crates/librefang-runtime-mcp/src/lib.rs) builds the outbound request and attaches:_metaunderio.librefang/caller(rmcp transport) / a top-level_meta(SSE),Authorization(cached OAuth token) and any operator-configured extra headers.It does not attach
traceparent/tracestate.So a downstream MCP server (e.g. an rmcp-based sidecar that exports spans to a collector) has no incoming parent context; its per-request / per-connection spans become a fresh root and never stitch to the agent's trace.
Existing precedent (LLM egress already does this)
inject_w3c_trace_context()incrates/librefang-llm-drivers/src/drivers/trace_headers.rs:177already injectstraceparent(andtracestate) into outbound LLM HTTP requests usingopentelemetry_http::HeaderInjectorand the globally-registeredTraceContextPropagator(registered atcrates/librefang-api/src/telemetry.rs:153).A downstream proxy that auto-extracts
traceparentstitches cleanly.The MCP client should use the same propagator the same way.
Proposed fix
Inject the active span's W3C context on every outbound MCP tool call, behind the existing telemetry gate (no-op when telemetry is disabled / the context is all-zero, exactly as
inject_w3c_trace_contextalready handles).Two viable placements:
Add
traceparent/tracestateto the streamable-HTTP / SSE request headers next to whereAuthorizationand extra headers are attached.This mirrors
trace_headers.rsexactly and lets any MCP server extract it with standardopentelemetry-httpmiddleware — no MCP-protocol-level convention required.Implementation note: the rmcp streamable-HTTP client transport needs a per-request header hook for this; if one isn't exposed, that's the small upstream-rmcp gap to resolve or work around.
_metafield.Carry
traceparentin the request_metaalongside the existing caller context.Transport-agnostic, but requires both ends to agree on the
_metakey and the server to map it to a remote parent manually.Acceptance criteria
traceparentwhose trace id matches the active span (mirror the existing testbuild_trace_header_map_injects_traceparent_behind_reload_layerintrace_headers.rs).traceparentis emitted / it is all-zero — the call still succeeds.Out of scope
_metacaller-context payload.