From 559fc00a89016afc5c46ed21f29d6be2450674fc Mon Sep 17 00:00:00 2001 From: Alex Vazquez Date: Mon, 27 Jul 2026 13:33:38 +0200 Subject: [PATCH] fix(telemetry): expose tool call success/failure in tool_call span metadata 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. --- lib/anubis/server/session.ex | 9 +++- test/anubis/server/session_test.exs | 69 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/lib/anubis/server/session.ex b/lib/anubis/server/session.ex index 47bf8ab0..7db21245 100644 --- a/lib/anubis/server/session.ex +++ b/lib/anubis/server/session.ex @@ -802,7 +802,10 @@ defmodule Anubis.Server.Session do :telemetry.span( Telemetry.event_server_tool_call(), %{tool: tool_name}, - fn -> {module.handle_request(request, frame), %{tool: tool_name}} end + fn -> + result = module.handle_request(request, frame) + {result, %{tool: tool_name, is_error: tool_call_error?(result)}} + end ) end @@ -810,6 +813,10 @@ defmodule Anubis.Server.Session do module.handle_request(request, frame) end + defp tool_call_error?({:error, _reason, _frame}), do: true + defp tool_call_error?({:reply, %{"isError" => true}, _frame}), do: true + defp tool_call_error?(_result), do: false + # Async dispatch helpers defp decode_task_result({:reply, response, %Frame{} = frame}, inflight, state) do diff --git a/test/anubis/server/session_test.exs b/test/anubis/server/session_test.exs index c20bcaa0..750bf196 100644 --- a/test/anubis/server/session_test.exs +++ b/test/anubis/server/session_test.exs @@ -633,6 +633,75 @@ defmodule Anubis.Server.SessionTest do end end + describe "tool_call telemetry :is_error metadata" do + setup do + test_pid = self() + handler_id = "test-tool-call-#{System.unique_integer([:positive])}" + + :telemetry.attach( + handler_id, + # NOTE: currently unprefixed (missing the :anubis_mcp namespace) — see #243/#244. + [:server, :tool_call, :stop], + fn _e, _m, meta, _c -> send(test_pid, {:tool_call_stop, meta}) end, + nil + ) + + on_exit(fn -> :telemetry.detach(handler_id) end) + + transport_name = Registry.transport_name(TasksStubServer, StubTransport) + start_supervised!({StubTransport, name: transport_name}) + task_sup = Registry.task_supervisor_name(TasksStubServer) + start_supervised!({Task.Supervisor, name: task_sup}) + + session_id = "tool-call-telemetry-#{System.unique_integer([:positive])}" + session_name = Registry.session_name(TasksStubServer, session_id) + + session = + start_supervised!( + {Session, + session_id: session_id, + server_module: TasksStubServer, + name: session_name, + transport: [layer: StubTransport, name: transport_name], + task_supervisor: task_sup} + ) + + init_msg = init_request("2025-03-26", %{"name" => "TestClient", "version" => "1.0.0"}) + assert {:ok, _} = GenServer.call(session, {:mcp_request, init_msg, %{}}) + + init_notification = build_notification("notifications/initialized", %{}) + assert :ok = GenServer.cast(session, {:mcp_notification, init_notification, %{}}) + + %{session: session} + end + + test "is_error is false for a successful tool call", %{session: session} do + request = build_request("tools/call", %{"name" => "no_tasks", "arguments" => %{}}, 1) + + assert {:ok, _} = GenServer.call(session, {:mcp_request, request, %{}}) + assert_receive {:tool_call_stop, %{tool: "no_tasks", is_error: false}}, 500 + end + + test "is_error is true when the tool itself reports a CallToolResult error", %{session: session} do + request = + build_request( + "tools/call", + %{"name" => "always_fails", "arguments" => %{"reason" => "boom"}}, + 2 + ) + + assert {:ok, _} = GenServer.call(session, {:mcp_request, request, %{}}) + assert_receive {:tool_call_stop, %{tool: "always_fails", is_error: true}}, 500 + end + + test "is_error is true when the tool is not found (protocol-level error)", %{session: session} do + request = build_request("tools/call", %{"name" => "nonexistent_tool"}, 3) + + assert {:ok, _} = GenServer.call(session, {:mcp_request, request, %{}}) + assert_receive {:tool_call_stop, %{tool: "nonexistent_tool", is_error: true}}, 500 + end + end + describe "terminate/2 on supervisor-initiated stop" do defp start_supervised_session(server_module, session_id) do transport_name = Registry.transport_name(server_module, StubTransport)