Skip to content

Fix: W3C trace context propagation on outbound MCP HTTP/SSE requests#28299

Open
HrdlickaJakub wants to merge 1 commit into
anomalyco:devfrom
HrdlickaJakub:fix/mcp-w3c-trace-propagation
Open

Fix: W3C trace context propagation on outbound MCP HTTP/SSE requests#28299
HrdlickaJakub wants to merge 1 commit into
anomalyco:devfrom
HrdlickaJakub:fix/mcp-w3c-trace-propagation

Conversation

@HrdlickaJakub
Copy link
Copy Markdown

Issue for this PR

Closes #28295

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

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 traceparent header, so the MCP server's
spans land in a separate trace and you can't follow a tool call end-to-end.

Two things blocked it:

  1. The MCP SDK transports (StreamableHTTPClientTransport, SSEClientTransport)
    use the default global fetch with no hook to inject headers.
  2. @effect/opentelemetry builds a NodeTracerProvider but never calls
    register(), so the @opentelemetry/api global propagator and context
    manager 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:

  • Adds setupOtelApiGlobals() in packages/core/src/effect/observability.ts
    that installs AsyncLocalStorageContextManager and W3CTraceContextPropagator
    on the @opentelemetry/api globals once, idempotently. Called from the
    existing traces() setup and from the MCP fetch wrapper.
  • Adds buildTracingFetch() in packages/opencode/src/mcp/index.ts and
    passes it as the fetch option to both MCP HTTP/SSE transports when
    Observability.enabled && experimental.openTelemetry === true. It merges
    any caller-supplied headers and injects traceparent / tracestate from
    the 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.ts
covering: traceparent is injected when tracing is enabled, no injection
when 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_ENDPOINT set and experimental.openTelemetry: true.
The echo server logs whatever traceparent header it receives.

In Jaeger the opencode-side and mcp-echo-server-side spans now share one
trace ID (236 spans across both services) instead of being two unrelated
traces.

Screenshots / recordings

N/A — not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions
Copy link
Copy Markdown
Contributor

Hey! Your PR title Fix/mcp w3c trace propagation doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Related PR Found

PR #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 setupOtelApiGlobals() or trace context propagation.


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.
@HrdlickaJakub HrdlickaJakub force-pushed the fix/mcp-w3c-trace-propagation branch from f9d6229 to f91e618 Compare May 19, 2026 06:33
@HrdlickaJakub HrdlickaJakub changed the title Fix/mcp w3c trace propagation Fix(mcp): w3c trace propagation May 19, 2026
@HrdlickaJakub HrdlickaJakub changed the title Fix(mcp): w3c trace propagation Fix: W3C trace context propagation on outbound MCP HTTP/SSE requests May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: propagate W3C trace context on outbound MCP HTTP/SSE requests

1 participant