Skip to content

Inject W3C Trace Context into vMCP backend HTTP requests#3814

Open
ChrisJBurns wants to merge 2 commits intomainfrom
fix/vmcp-trace-context-propagation
Open

Inject W3C Trace Context into vMCP backend HTTP requests#3814
ChrisJBurns wants to merge 2 commits intomainfrom
fix/vmcp-trace-context-propagation

Conversation

@ChrisJBurns
Copy link
Collaborator

@ChrisJBurns ChrisJBurns commented Feb 12, 2026

Summary

  • Add tracePropagatingRoundTripper to the vMCP HTTP transport chain that injects traceparent/tracestate headers into outgoing backend requests, linking vMCP and backend spans in the same distributed trace
  • Propagator is injected as a struct field (not global lookup per request) for testability and consistency with telemetryBackendClient's tracer injection pattern
  • Uses otel.GetTextMapPropagator().Inject() instead of otelhttp.NewTransport() to avoid creating duplicate client spans (telemetry.go already creates them)
  • Clones requests before header injection, consistent with authRoundTripper and the http.RoundTripper contract
  • Update transport chain comment to accurately describe all layers

Context

When vMCP calls backend MCP servers, traces appeared as separate, unlinked traces in Tempo. The vMCP telemetry middleware correctly creates client spans, but outgoing HTTP requests never received W3C Trace Context headers, so backends created new root spans instead of continuing the trace.

Confirmed via Tempo: tools/call yardstick-traced_echo and tools/call echo had different trace IDs despite being the same request flow.

How trace context flows

This PR closes the gap on the outgoing side of vMCP → backend communication. The full trace chain now works:

Client span (from HTTP headers or _meta)
  └── vMCP server span
        └── vMCP client span
              └── Backend server span

Incoming (already implemented): The telemetry middleware (pkg/telemetry/middleware.go:167-177) extracts trace context from both HTTP headers and the MCP _meta field, with _meta taking priority per the MCP OTEL spec.

Outgoing (this PR): The tracePropagatingRoundTripper injects trace context into HTTP headers on all outgoing requests to backends. This works for every MCP method (tools/call, tools/list, resources/read, prompts/get, initialize, etc.), unlike _meta injection which would only cover methods that carry _meta in their params.

Callers providing _meta

If a caller includes traceparent in the MCP _meta field:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "_meta": {
      "traceparent": "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01"
    },
    "name": "yardstick-traced_echo",
    "arguments": {"input": "hello"}
  },
  "id": 2
}

The vMCP spans will appear as children within that trace, and the backend spans will chain from there — giving a complete distributed trace from client through vMCP to backend.

Design decisions

  • HTTP headers, not _meta injection: HTTP header propagation is the primary mechanism because it works universally for all MCP methods. The InjectMetaTraceContext infrastructure exists in pkg/telemetry/propagation.go but is not used for outgoing requests — this could be a follow-up for methods that support _meta.
  • Not otelhttp.NewTransport(): That would create duplicate client spans since telemetryBackendClient in pkg/vmcp/server/telemetry.go already creates SpanKindClient spans for each backend operation.
  • Propagator as struct field: Instead of calling otel.GetTextMapPropagator() on every request, the propagator is captured at client creation time. This makes tests parallel-safe (no global state mutation) and is consistent with how the tracer is injected in telemetryBackendClient.
image

Test plan

  • 4 table-driven unit tests: header injection, no-span context, error propagation, no-op propagator
  • 1 standalone unit test: parent-child span ID propagation
  • go test ./pkg/vmcp/client/... -race passes (all tests run in parallel)
  • task lint-fix reports 0 issues
  • Deploy to Kind cluster with OTEL stack, verify in Tempo that backend spans share trace ID with vMCP spans

🤖 Generated with Claude Code

@github-actions github-actions bot added the size/S Small PR: 100-299 lines changed label Feb 12, 2026
@ChrisJBurns ChrisJBurns force-pushed the fix/vmcp-trace-context-propagation branch from c1b9f09 to 048a9e4 Compare February 12, 2026 21:53
@github-actions github-actions bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Feb 12, 2026
When vMCP calls backend MCP servers, traces appeared as separate,
unlinked traces in Tempo instead of being part of the same distributed
trace. Add a tracePropagatingRoundTripper to the HTTP transport chain
that injects traceparent/tracestate headers into outgoing requests,
linking vMCP client spans with backend server spans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ChrisJBurns ChrisJBurns force-pushed the fix/vmcp-trace-context-propagation branch from 048a9e4 to 90ef6c4 Compare February 12, 2026 21:57
@github-actions github-actions bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Feb 12, 2026
@github-actions github-actions bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants