Fix: W3C trace context propagation on outbound MCP HTTP/SSE requests#28299
Fix: W3C trace context propagation on outbound MCP HTTP/SSE requests#28299HrdlickaJakub wants to merge 1 commit into
Conversation
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: Potential Related PR FoundPR #27085: feat(observability): propagate trace context to spawned subprocesses Why related: This PR also addresses trace context propagation in the observability system. While it focuses on subprocess spawning rather than MCP HTTP/SSE transports, both PRs are working on the same foundational issue of propagating W3C trace context across different execution boundaries. They may share or inform similar implementation patterns for handling No other duplicate PRs found for the MCP W3C trace propagation fix itself. |
Remote MCP servers received no traceparent header, so their spans appeared as separate roots in Jaeger instead of children of the opencode session span. Two issues blocked propagation: 1. @effect/opentelemetry creates a NodeTracerProvider but never calls register(), so the @opentelemetry/api globals (context manager, propagator) stayed no-op. propagation.inject() was a no-op. 2. The MCP transports use the default global fetch, which has no hook for injecting headers. Fix: - Install the W3C propagator and AsyncLocalStorage context manager on the @opentelemetry/api globals from observability.ts, idempotently, via setupOtelApiGlobals(). - Wrap fetch for StreamableHTTP/SSE transports so each MCP HTTP call injects traceparent/tracestate from the active context, preserving any headers already present on the Request or init. - Gate the wrapper on Observability.enabled && experimental.openTelemetry so behavior is unchanged when tracing is off. Verified end-to-end against a Jaeger + MCP server stack: trace now contains opencode + mcp-echo-server spans with mcp-echo-server's echo.handler as a child of opencode's SessionPrompt.runner.
f9d6229 to
f91e618
Compare
Issue for this PR
Closes #28295
Type of change
What does this PR do?
When opencode talks to a remote MCP server over HTTP or SSE, the outbound
requests do not carry the W3C
traceparentheader, so the MCP server'sspans land in a separate trace and you can't follow a tool call end-to-end.
Two things blocked it:
StreamableHTTPClientTransport,SSEClientTransport)use the default global
fetchwith no hook to inject headers.@effect/opentelemetrybuilds aNodeTracerProviderbut never callsregister(), so the@opentelemetry/apiglobal propagator and contextmanager stay no-op. Even if the transport had called
propagation.inject(...)it would have done nothing.
This PR fixes both, gated on tracing being enabled so the default code path
is unchanged:
setupOtelApiGlobals()inpackages/core/src/effect/observability.tsthat installs
AsyncLocalStorageContextManagerandW3CTraceContextPropagatoron the
@opentelemetry/apiglobals once, idempotently. Called from theexisting
traces()setup and from the MCP fetch wrapper.buildTracingFetch()inpackages/opencode/src/mcp/index.tsandpasses it as the
fetchoption to both MCP HTTP/SSE transports whenObservability.enabled && experimental.openTelemetry === true. It mergesany caller-supplied headers and injects
traceparent/tracestatefromthe active context.
Scope is limited to the two MCP HTTP/SSE transports — no other outbound HTTP
traffic (provider calls, auth, plugins) is touched.
How did you verify your code works?
Unit tests — added 3 tests in
packages/opencode/test/mcp/headers.test.tscovering:
traceparentis injected when tracing is enabled, no injectionwhen tracing is disabled, and caller-supplied headers are preserved alongside
the injected ones. The 3 pre-existing header tests still pass.
End-to-end check — ran opencode against a local Jaeger 1.60 + echo MCP
server, with
OTEL_EXPORTER_OTLP_ENDPOINTset andexperimental.openTelemetry: true.The echo server logs whatever
traceparentheader it receives.In Jaeger the opencode-side and
mcp-echo-server-side spans now share onetrace ID (236 spans across both services) instead of being two unrelated
traces.
Screenshots / recordings
N/A — not a UI change.
Checklist