Skip to content

[FEATURE]: propagate W3C trace context (traceparent/baggage) in MCP tools/call _meta per SEP-414 #46856

Open

Description

@Yagi-Michael

Feature hasn't been suggested before.

Describe the enhancement you want to request

What: when OpenCode invokes an MCP tool, include the W3C trace context of the active tool-call span in CallToolRequest.params._meta, using the keys the MCP spec reserves for this in SEP-414: traceparent, optionally tracestate, and baggage (unprefixed, by spec exception to the _meta prefix rule). For example:

{
  "method": "tools/call",
  "params": {
    "name": "engram_search",
    "arguments": { "...": "..." },
    "_meta": {
      "sessionID": "ses_…",
      "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
      "baggage": "session.id=ses_…,opencode.tool.call_id=call_…"
    }
  }
}

_meta.sessionID from #46008 can stay as is; baggage just carries the same value under the spec-conventional key so servers built against the OTel GenAI MCP semantic conventions pick it up without OpenCode-specific code.

Why: OpenCode already opens an OpenTelemetry span per tool call (session/tools.ts has session.id and tool.call_id in scope at the call site; with experimental.openTelemetry those spans export). An MCP server that traces its own work today has no way to parent its spans onto that span, so the client-side and server-side halves of one tool call land in the backend as two unrelated traces. With traceparent in _meta the server span becomes a child of OpenCode's, and a trace view shows: model turn → tool call → what the server actually did (retrieval stages, cache hits, lock waits) in one waterfall. The OTel GenAI MCP semantic conventions (Development) say clients SHOULD inject propagators into params._meta and servers SHOULD use it as the parent; the Rust SDK (rmcp ≥ 3.1) already exposes RequestMetaObject::get_traceparent/get_baggage for exactly this.

Where: the same place #46008 adds _meta.sessionID (packages/core/src/mcp/client.ts), reading the current span context from the OTel API (propagation.inject into a carrier and copying the three keys) when tracing is enabled; when it is not, traceparent can simply be omitted.

Workaround today: a plugin's tool.execute.before hook can inject a reserved argument (we use args._otel = { traceparent, baggage }) and the server lifts it into _meta. It works, but it couples the server to an OpenCode-specific argument and leaks into the stored tool input; native _meta propagation makes that unnecessary.

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed request, @Yagi-Michael!

    To restate it: when OpenCode calls an MCP tool, it would also send the W3C trace context of the active tool-call span in CallToolRequest.params._meta. That means traceparent, optionally tracestate, and baggage, as reserved by SEP-414. MCP servers, including stdio ones, could then parent their spans onto OpenCode's span.

    Right now callTool in packages/core/src/mcp/client.ts only adds _meta["ai.opencode/sessionID"]. A plugin can't add these keys either: tool.execute.before can only change tool arguments, not the MCP request's _meta. That's why your workaround has to go through args._otel.

    Related:

    Maintainers will look at this and decide whether to take it on.

  2. tiennv147 commented on Nov 6, 2026

    @tiennv147

    +1. Here is a reproduction on 2.1.2 with an MCP gateway (LiteLLM 1.105.0), and why gateways also need the HTTP header, not only _meta. Checked again on v2.2.5: MCP.callTool still adds no attributes, and the MCP transport still doesn't inject per-call context.

    Setup: opencode run with OTEL_EXPORTER_OTLP_ENDPOINT set. Remote MCP server = LiteLLM's MCP gateway (/mcp/, streamable HTTP), Code Mode on. LiteLLM exports its own OTel v2 spans to the same backend.

    What happens: the model call links up, but the MCP call doesn't.

    trace A (opencode)                                  trace B (litellm), new root, no parent
    SessionStep.attempt                                 POST /mcp                              10.48 s
    ├─ http.client POST ──► POST /v1/chat/completions   └─ tools/call deepwiki-ask_wiki_question 10.47 s
    │                       └─ chat claude-haiku-4-5        └─ MCP send tools/call ask_wiki_question
    └─ Tool.execute (execute)
       └─ Tool.execute
          └─ MCP.callTool                     10.50 s   ◄── same call, separate trace
    
    • LLM requests go through Effect's HttpClient, which injects traceparent from the current span, so LiteLLM's POST /v1/chat/completions nests under http.client POST. 👍

    • MCP requests go through the MCP SDK transport with McpOAuth.loggedFetch (packages/core/src/mcp/oauth.ts). It calls the global fetch inside Effect.runPromiseWith(context captured in connect()), so:

      1. no traceparent header is added at all, and
      2. even if propagation were added inside loggedFetch, it would use the connection-time context, not the MCP.callTool span's.

      The result is that every MCP HTTP request starts a fresh trace on the server.

    • tools/call already carries _meta: {"ai.opencode/sessionID": …} (thanks for feat(core): pass session context to MCP tools #46008). The trace context could ride along in the same place.

    Why _meta alone isn't enough for gateways: LiteLLM 1.105 parents its MCP span to its own POST /mcp HTTP server span, which takes its parent from the traceparent header. A traceparent in params._meta (SEP-414) is recorded only as a span link, by design (litellm/integrations/otel/plumbing/context.py). So:

    Both would be ideal: a header for remote transports, _meta for all.

    Suggested fix, along the lines of PR #28299:

    • Capture the current span when callTool / readResource / getPrompt runs.
    • Inject W3C traceparent / tracestate from it into the request headers. For example, pass per-request headers through the transport's requestInit, or have the fetch wrapper read the span from an AsyncLocalStorage set around the SDK call instead of the connect-time context.
    • Add the same values under params._meta per SEP-414.

    Happy to test a build against LiteLLM's MCP gateway.

    Related: the Tool.execute and MCP.callTool spans also lost their tool.name / tool.call_id attributes in v2, so even inside OpenCode's own trace you can't see which tool ran. Filed as #62305.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions