Repository navigation
fix(telemetry): expose tool call success/failure in tool_call span metadata - #246
Conversation
…tadata
The tool_call telemetry span's :stop event metadata only ever carried
%{tool: tool_name}, regardless of whether the wrapped handle_request/2
call succeeded, returned a protocol-level error, or returned a
CallToolResult with isError: true. Consumers attaching handlers to this
event have no way to distinguish a successful tool call from a failed
one without re-deriving it from the (discarded) result.
Add an is_error boolean to the span's stop metadata, derived from the
actual result: true for {:error, _, _} protocol errors and for
{:reply, %{"isError" => true}, _} tool-level errors, false otherwise.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughProblem
SolutionAdd an RationaleImproves telemetry visibility without changing public APIs or request behavior. Walkthrough
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
QA — verified against a live MCP deploymentIn addition to the unit tests in this PR, I ran a broad manual QA pass against a real, running MCP server deployment fronted by this library (multi-pod, behind a load balancer, real network hops) to make sure nothing about the change's assumptions breaks under real traffic:
All five failure cases returned exactly the response shapes this PR's The deployment I tested against is still on the pre-fix version of this library, so I could not observe |
🚀 Want to release this? --- ## [1.11.0](v1.10.0...v1.11.0) (2026-07-29) ### Features * **session_store:** supervise Redis store subtree to make restarts race-free ([#242](#242)) ([48c8c1a](48c8c1a)) ### Bug Fixes * **prompts:** wrap prompt content objects and map system_message to user role ([#234](#234)) ([2451bb7](2451bb7)) * **server:** make title option compile in use Anubis.Server.Component ([b04feed](b04feed)) * **server:** use restart :temporary for session processes ([#240](#240)) ([30bc4f5](30bc4f5)) * **sse:** buffer partial events across Finch chunks ([#245](#245)) ([beea2f6](beea2f6)) * Stream the client SSE GET instead of buffering it (server push never delivered) ([#231](#231)) ([a722c1b](a722c1b)) * **telemetry:** expose tool call success/failure in tool_call span metadata ([#246](#246)) ([ace5ecb](ace5ecb)) * **telemetry:** include client_info in initialize response event metadata ([#248](#248)) ([74ed457](74ed457)) * **telemetry:** namespace tool_call span under :anubis_mcp ([#244](#244)) ([561a96b](561a96b)) ### Tests * **server:** fix stale tool_call event name and function_exported? loading races ([7247d2c](7247d2c)) * **transport:** synchronize held SSE plug with Bypass teardown ([93c5a34](93c5a34)) ### Continuous Integration * fix dialyzer plt caching ([8424439](8424439)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Problem
The
tool_calltelemetry span (do_handle_request/4for"tools/call"requests) always emits the same:stopmetadata,%{tool: tool_name}, regardless of whether the wrapped call succeeded, returned a protocol-level error (e.g. tool not found, invalid params), or returned aCallToolResultwithisError: true.The actual result of
module.handle_request(request, frame)is available inside the span function, but it is discarded before being turned into span metadata — a telemetry handler attached to this event has no way to tell a successful tool call apart from a failed one without independently re-deriving that information elsewhere (e.g. by separately handling[:anubis_mcp, :server, :error], which only fires for protocol-level errors, not for tool-levelisError: trueresults).Solution
Bind the result of
module.handle_request(request, frame)and derive anis_errorboolean from it before it becomes span metadata:{:error, _reason, _frame}(protocol-level error) →is_error: true{:reply, %{"isError" => true}, _frame}(tool-level error viaResponse.error/2) →is_error: trueis_error: falseAdded three regression tests covering all three branches (successful call, tool-level
isError: true, protocol-level tool-not-found), attached directly to thetool_call:stoptelemetry event.Rationale
This was the smallest possible change that surfaces the missing signal: the result is already computed and in scope inside the span closure, so no additional work is done, only a cheap pattern match on a value that already exists. No public API, return value, or behavior changes — this only adds a new key to telemetry metadata, which is additive and non-breaking for existing handlers.
I kept the derivation as a private function (
tool_call_error?/1) rather than inlining it, since the two failure shapes it distinguishes (protocol error vs. tool-levelisError) are exactly the two failure modes the MCP spec itself defines, and a named predicate makes that mapping explicit rather than embedding it in the span call.Related to #243 (unprefixed event name) but independent — this PR does not touch the event namespace, only the
:stopmetadata shape. Happy to rebase on top of #244 if that merges first.