Skip to content

feat(runtime-mcp): propagate W3C traceparent on outbound MCP tool calls #6128

Description

@neo-wanderer

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/runtimeAgent loop, LLM drivers, WASM sandboxenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions