Skip to content

Commit 96bf22e

Browse files
authored
Stop flagging snake_case is_error results as tool errors in OTel span (#2971)
1 parent 1b1abf6 commit 96bf22e

2 files changed

Lines changed: 12 additions & 5 deletions

File tree

src/mcp/server/_otel.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@ async def __call__(self, ctx: ServerRequestContext[Any, Any], call_next: CallNex
5959
span.set_status(StatusCode.ERROR, str(e))
6060
raise
6161
if ctx.method == "tools/call":
62+
# Tool errors are detected pre-serialization, so only shapes that reach the wire as an error
63+
# count: the model, or the camelCase alias (`is_error` is dropped by the alias-only wire
64+
# validation). A raw-dict `isError` is matched as a literal bool only - non-bool coercible
65+
# values (1, "true") would serialize to an error but are rare enough to leave undetected.
6266
match result:
63-
case CallToolResult(is_error=True) | {"isError": True} | {"is_error": True}:
67+
case CallToolResult(is_error=True) | {"isError": True}:
6468
span.set_attribute("error.type", "tool_error")
6569
span.set_status(StatusCode.ERROR)
6670
case _:

tests/server/test_otel.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,22 @@ async def err_tool(ctx: Ctx, params: CallToolRequestParams) -> CallToolResult:
9292

9393

9494
@pytest.mark.anyio
95-
async def test_tool_error_snake_case_dict_result_sets_error_type(server: SrvT, spans: SpanCapture):
95+
async def test_snake_case_dict_result_is_not_a_tool_error(server: SrvT, spans: SpanCapture):
96+
# `is_error` is alias-only on the wire, so serialization drops it; the result reaches the
97+
# client as a success and the span must not contradict that.
9698
async def err_tool(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:
9799
return {"content": [], "is_error": True}
98100

99101
server.add_request_handler("tools/call", CallToolRequestParams, err_tool)
100102
server.middleware.append(OpenTelemetryMiddleware())
101103
async with connected_runner(server) as (client, _):
102104
spans.clear()
103-
await client.send_raw_request("tools/call", {"name": "mytool", "arguments": {}})
105+
result = await client.send_raw_request("tools/call", {"name": "mytool", "arguments": {}})
106+
assert result == {"content": []}
104107
[span] = [s for s in spans.finished() if s.kind == SpanKind.SERVER]
105108
assert span.attributes is not None
106-
assert span.attributes["error.type"] == "tool_error"
107-
assert span.status.status_code == StatusCode.ERROR
109+
assert "error.type" not in span.attributes
110+
assert span.status.status_code == StatusCode.UNSET
108111

109112

110113
@pytest.mark.anyio

0 commit comments

Comments
 (0)