fix: give a failed MCP tool call error status and a named reason #912

Merged
coilysiren merged 1 commit from issue-873-mcp-tool-span-error into main 2026-08-17 17:53:27 +00:00
Member

Gives a failed MCP tool call the span status every generic observability surface keys on, and a reason where there was none.

What changed

  • result.IsError now marks the span under a new sirens_echo.mcp.tool_reported_error, which fills error.type, error.stage, error.outcome, and the status description.
  • Transport failure splits: sirens_echo.mcp.tool_call_timed_out when the error wraps context.DeadlineExceeded, sirens_echo.mcp.tool_call_failed otherwise.
  • Exception catalog cardinality 38 to 40. Stage cardinality is unchanged, since both live at mcp.

The reason, and what it deliberately is not

The issue asks why a call failed. The tool's own failure text does not go on the span. A span here carries byte counts and identifiers and no bodies, which is the boundary docs/sirens-echo-observability.md states, and the tool's text already reaches the model where it is acted on. So the recorded reason is the class: a tool reporting failure reads apart from a transport error, and a deadline reads apart from every other transport error.

That last one is the third bullet. The 30.45s turn could not be confirmed as a 30s MCP timeout from telemetry; a deadline is now named rather than inferred from duration.

What this does not close

The missing child spans on trace_id c64e75d46fb352930a248cfa374641af. Every span in that path is started and ended in-process, so a trace holding only its root points at export rather than instrumentation, and I have no evidence to distinguish those from source. Worth a separate issue if it recurs on a build carrying this change.

Tests

tooloutcomespan_test.go drives a real tool round through Complete and reads the recorded span: a reporting tool carries codes.Error and the catalog type, a succeeding one carries neither, and the deadline classifier is asserted directly.

just gate PASS.

closes #873

Gives a failed MCP tool call the span status every generic observability surface keys on, and a reason where there was none. ## What changed * `result.IsError` now marks the span under a new `sirens_echo.mcp.tool_reported_error`, which fills `error.type`, `error.stage`, `error.outcome`, and the status description. * Transport failure splits: `sirens_echo.mcp.tool_call_timed_out` when the error wraps `context.DeadlineExceeded`, `sirens_echo.mcp.tool_call_failed` otherwise. * Exception catalog cardinality 38 to 40. Stage cardinality is unchanged, since both live at `mcp`. ## The reason, and what it deliberately is not The issue asks why a call failed. **The tool's own failure text does not go on the span.** A span here carries byte counts and identifiers and no bodies, which is the boundary `docs/sirens-echo-observability.md` states, and the tool's text already reaches the model where it is acted on. So the recorded reason is the class: a tool reporting failure reads apart from a transport error, and a deadline reads apart from every other transport error. That last one is the third bullet. The 30.45s turn could not be confirmed as a 30s MCP timeout from telemetry; a deadline is now named rather than inferred from duration. ## What this does not close The missing child spans on `trace_id c64e75d46fb352930a248cfa374641af`. Every span in that path is started and ended in-process, so a trace holding only its root points at export rather than instrumentation, and I have no evidence to distinguish those from source. Worth a separate issue if it recurs on a build carrying this change. ## Tests `tooloutcomespan_test.go` drives a real tool round through `Complete` and reads the recorded span: a reporting tool carries `codes.Error` and the catalog type, a succeeding one carries neither, and the deadline classifier is asserted directly. `just gate` PASS. closes #873
fix: give a failed MCP tool call error status and a named reason
All checks were successful
ci / image-build (pull_request) Successful in 34s
ci / test (pull_request) Successful in 58s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
9220b258c5
mcp.tool.call recorded mcp.tool.outcome = failed and left the span status
unset, so 51 failures over twelve hours showed as one error span of 158 and
every dashboard, alert, and error query sailed past them. A 100% failure rate
on playwright and 47% on demo-discord sat unnoticed behind that.

A tool that reports its own failure now marks the span under
sirens_echo.mcp.tool_reported_error, which also fills error.type and
error.outcome. The tool's own text stays off the span: the span carries no
bodies, so the recorded reason is the class.

Transport failure splits a deadline out as sirens_echo.mcp.tool_call_timed_out,
so a trace confirms a timeout rather than leaving the duration to imply one.
That is the gap behind the third bullet, whose 30.45s turn could not be
confirmed from telemetry.

The turn is unaffected either way. A tool reporting failure is still a result
the model self-corrects from, and the root span stays clean.

closes #873

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!912
No description provided.