Skip to content

fix(telemetry): emit Tool.execute span for MCP and plugin tools#25452

Merged
kitlangton merged 1 commit intodevfrom
kit/tool-execute-span-mcp-plugin
May 2, 2026
Merged

fix(telemetry): emit Tool.execute span for MCP and plugin tools#25452
kitlangton merged 1 commit intodevfrom
kit/tool-execute-span-mcp-plugin

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

Summary

  • MCP-served tools (sentry_*, exa_*, etc.) and plugin-defined tools were not emitting a Tool.execute span. Only native tools built via Tool.define() got one, because the span lives inside wrap() in tool/tool.ts.
  • Add Effect.withSpan("Tool.execute", ...) to both bypassed paths, mirroring the attribute shape from wrap(): tool.name, tool.call_id, session.id, message.id.

Evidence

Trace 33c8941c89094d50650c2b91e244c778 had 28 ai.toolCall spans and only 24 Tool.execute children. The 4 missing executions were exactly the MCP tools used in the session:

  • sentry_search_issues
  • sentry_find_organizations
  • sentry_whoami
  • exa_web_search_exa

Native tools (bash, webfetch, glob, grep) all had matching pairs with the same tool.call_id. Permission.ask fired 28 times (matching ai.toolCall count), so MCP tools went through permission gating but skipped the execution wrapper specifically.

Why it matters

  • Lose visibility into MCP/plugin tool timing and errors at the runner level — only the AI SDK's end-to-end ai.toolCall is observable.
  • Any cross-cutting telemetry filtering on operation=Tool.execute silently excludes MCP and plugin traffic.

Changes

  • packages/opencode/src/session/prompt.ts: wrap the MCP ctx.ask + execute(args, opts) body with Effect.withSpan("Tool.execute", ...). Plugin tool.execute.before/after hooks remain outside the span, matching the native scope (wrap() covers decode + execute + truncate; the before/after triggers happen in the registry consumer).
  • packages/opencode/src/tool/registry.ts: pipe fromPlugin()'s Effect.gen body through the same withSpan so user-defined tool files and plugin-supplied tools also emit the span.

Verification

  • bun typecheck (packages/opencode): passes
  • bun run test test/tool/registry.test.ts test/tool/tool-define.test.ts: 7/7 pass

MCP-served tools and plugin-defined tools were bypassing the Tool.execute
span that wraps native tool runs. Native tools get the span via wrap() in
tool/tool.ts, called from Tool.define(); MCP tools are constructed as AI
SDK dynamicTools in mcp/index.ts and inserted via a parallel path in
session/prompt.ts, and plugin tools are built in registry.ts fromPlugin()
without going through Tool.define().

Verified against trace 33c8941c89094d50650c2b91e244c778: 28 ai.toolCall
spans had only 24 Tool.execute children; the missing 4 were all MCP tools
(sentry_*, exa_*). Cross-cutting telemetry filtering on
operation=Tool.execute was silently dropping MCP and plugin traffic.
@kitlangton kitlangton merged commit 6cd02c0 into dev May 2, 2026
13 checks passed
@kitlangton kitlangton deleted the kit/tool-execute-span-mcp-plugin branch May 2, 2026 18:49
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.

1 participant