Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before. Related but different: [FEATURE]: Pass the active session ID to MCP tool calls via _meta #45997 / feat(core): pass session context to MCP tools #46008 added
_meta.sessionID(thanks, this is great); [FEATURE]: propagate W3C trace context on outbound MCP HTTP/SSE requests #28295 asked fortraceparentas an HTTP header on remote MCP transports and was closed for inactivity. This request is aboutparams._metaontools/call, which is the only channel a stdio MCP server has, and follows the now-final MCP spec (SEP-414).
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
opencodejr commentedon Oct 28, 2026
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 meanstraceparent, optionallytracestate, andbaggage, as reserved by SEP-414. MCP servers, including stdio ones, could then parent their spans onto OpenCode's span.Right now
callToolinpackages/core/src/mcp/client.tsonly adds_meta["ai.opencode/sessionID"]. A plugin can't add these keys either:tool.execute.beforecan only change tool arguments, not the MCP request's_meta. That's why your workaround has to go throughargs._otel.Related:
_meta.sessionIDon MCP tool callstraceparentas an HTTP header for remote MCP transports (closed for inactivity)Maintainers will look at this and decide whether to take it on.
tiennv147 commentedon Nov 6, 2026
+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.callToolstill adds no attributes, and the MCP transport still doesn't inject per-call context.Setup:
opencode runwithOTEL_EXPORTER_OTLP_ENDPOINTset. 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.
LLM requests go through Effect's
HttpClient, which injectstraceparentfrom the current span, so LiteLLM'sPOST /v1/chat/completionsnests underhttp.client POST. 👍MCP requests go through the MCP SDK transport with
McpOAuth.loggedFetch(packages/core/src/mcp/oauth.ts). It calls the globalfetchinsideEffect.runPromiseWith(context captured in connect()), so:traceparentheader is added at all, andloggedFetch, it would use the connection-time context, not theMCP.callToolspan's.The result is that every MCP HTTP request starts a fresh trace on the server.
tools/callalready 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
_metaalone isn't enough for gateways: LiteLLM 1.105 parents its MCP span to its ownPOST /mcpHTTP server span, which takes its parent from thetraceparentheader. Atraceparentinparams._meta(SEP-414) is recorded only as a span link, by design (litellm/integrations/otel/plumbing/context.py). So:_meta(this issue) → a stdio server can correlate, and a gateway gets a link.Both would be ideal: a header for remote transports,
_metafor all.Suggested fix, along the lines of PR #28299:
callTool/readResource/getPromptruns.traceparent/tracestatefrom it into the request headers. For example, pass per-request headers through the transport'srequestInit, or have the fetch wrapper read the span from anAsyncLocalStorageset around the SDK call instead of the connect-time context.params._metaper SEP-414.Happy to test a build against LiteLLM's MCP gateway.
Related: the
Tool.executeandMCP.callToolspans also lost theirtool.name/tool.call_idattributes in v2, so even inside OpenCode's own trace you can't see which tool ran. Filed as #62305.