1313from mcp .server .runner import otel_middleware
1414from mcp .shared ._otel import inject_trace_context
1515from mcp .shared .exceptions import MCPError
16- from mcp .types import CallToolRequestParams , ListToolsResult , NotificationParams , PaginatedRequestParams , Tool
16+ from mcp .types import (
17+ CallToolRequestParams ,
18+ CallToolResult ,
19+ GetPromptRequestParams ,
20+ GetPromptResult ,
21+ ListToolsResult ,
22+ NotificationParams ,
23+ PaginatedRequestParams ,
24+ Tool ,
25+ )
1726
1827from .conftest import SpanCapture
1928from .test_runner import Ctx , SrvT , connected_runner
@@ -40,13 +49,98 @@ async def test_emits_server_span_with_method_and_target(server: SrvT, spans: Spa
4049 result = await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
4150 assert result == {"content" : [], "isError" : False }
4251 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
43- assert span .name == "MCP handle tools/call mytool"
52+ assert span .name == "tools/call mytool"
4453 assert span .attributes is not None
4554 assert span .attributes ["mcp.method.name" ] == "tools/call"
55+ assert span .attributes ["gen_ai.operation.name" ] == "execute_tool"
56+ assert span .attributes ["gen_ai.tool.name" ] == "mytool"
4657 assert isinstance (span .attributes ["jsonrpc.request.id" ], str )
4758 assert span .status .status_code == StatusCode .UNSET
4859
4960
61+ @pytest .mark .anyio
62+ async def test_tool_error_dict_result_sets_error_type (server : SrvT , spans : SpanCapture ):
63+ async def err_tool (ctx : Ctx , params : CallToolRequestParams ) -> dict [str , Any ]:
64+ return {"content" : [], "isError" : True }
65+
66+ server .add_request_handler ("tools/call" , CallToolRequestParams , err_tool )
67+ server .middleware .append (OpenTelemetryMiddleware ())
68+ async with connected_runner (server ) as (client , _ ):
69+ spans .clear ()
70+ await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
71+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
72+ assert span .attributes is not None
73+ assert span .attributes ["error.type" ] == "tool_error"
74+ assert span .status .status_code == StatusCode .ERROR
75+
76+
77+ @pytest .mark .anyio
78+ async def test_tool_error_model_result_sets_error_type (server : SrvT , spans : SpanCapture ):
79+ async def err_tool (ctx : Ctx , params : CallToolRequestParams ) -> CallToolResult :
80+ return CallToolResult (content = [], is_error = True )
81+
82+ server .add_request_handler ("tools/call" , CallToolRequestParams , err_tool )
83+ server .middleware .append (OpenTelemetryMiddleware ())
84+ async with connected_runner (server ) as (client , _ ):
85+ spans .clear ()
86+ await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
87+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
88+ assert span .attributes is not None
89+ assert span .attributes ["error.type" ] == "tool_error"
90+ assert span .status .status_code == StatusCode .ERROR
91+
92+
93+ @pytest .mark .anyio
94+ async def test_tool_error_snake_case_dict_result_sets_error_type (server : SrvT , spans : SpanCapture ):
95+ async def err_tool (ctx : Ctx , params : CallToolRequestParams ) -> dict [str , Any ]:
96+ return {"content" : [], "is_error" : True }
97+
98+ server .add_request_handler ("tools/call" , CallToolRequestParams , err_tool )
99+ server .middleware .append (OpenTelemetryMiddleware ())
100+ async with connected_runner (server ) as (client , _ ):
101+ spans .clear ()
102+ await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
103+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
104+ assert span .attributes is not None
105+ assert span .attributes ["error.type" ] == "tool_error"
106+ assert span .status .status_code == StatusCode .ERROR
107+
108+
109+ @pytest .mark .anyio
110+ async def test_named_non_tool_prompt_method_omits_gen_ai_attrs (server : SrvT , spans : SpanCapture ):
111+ async def custom (ctx : Ctx , params : CallToolRequestParams ) -> dict [str , Any ]:
112+ return {"content" : [], "isError" : False }
113+
114+ server .add_request_handler ("custom/op" , CallToolRequestParams , custom )
115+ server .middleware .append (OpenTelemetryMiddleware ())
116+ async with connected_runner (server ) as (client , _ ):
117+ spans .clear ()
118+ await client .send_raw_request ("custom/op" , {"name" : "thing" , "arguments" : {}})
119+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
120+ assert span .name == "custom/op thing"
121+ assert span .attributes is not None
122+ assert "gen_ai.operation.name" not in span .attributes
123+ assert "gen_ai.tool.name" not in span .attributes
124+ assert "gen_ai.prompt.name" not in span .attributes
125+
126+
127+ @pytest .mark .anyio
128+ async def test_prompt_get_sets_prompt_name (server : SrvT , spans : SpanCapture ):
129+ async def get_prompt (ctx : Ctx , params : GetPromptRequestParams ) -> GetPromptResult :
130+ return GetPromptResult (messages = [])
131+
132+ server .add_request_handler ("prompts/get" , GetPromptRequestParams , get_prompt )
133+ server .middleware .append (OpenTelemetryMiddleware ())
134+ async with connected_runner (server ) as (client , _ ):
135+ spans .clear ()
136+ await client .send_raw_request ("prompts/get" , {"name" : "myprompt" })
137+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
138+ assert span .name == "prompts/get myprompt"
139+ assert span .attributes is not None
140+ assert span .attributes ["gen_ai.prompt.name" ] == "myprompt"
141+ assert "gen_ai.operation.name" not in span .attributes
142+
143+
50144@pytest .mark .anyio
51145async def test_notification_span_omits_request_id (server : SrvT , spans : SpanCapture ):
52146 async def on_roots (ctx : Ctx , params : NotificationParams | None ) -> None :
@@ -59,7 +153,7 @@ async def on_roots(ctx: Ctx, params: NotificationParams | None) -> None:
59153 await client .notify ("notifications/roots/list_changed" , None )
60154 await anyio .wait_all_tasks_blocked ()
61155 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
62- assert span .name == "MCP handle notifications/roots/list_changed"
156+ assert span .name == "notifications/roots/list_changed"
63157 assert span .attributes is not None
64158 assert span .attributes ["mcp.method.name" ] == "notifications/roots/list_changed"
65159 assert "jsonrpc.request.id" not in span .attributes
@@ -146,6 +240,9 @@ async def test_records_error_status_on_mcp_error(server: SrvT, spans: SpanCaptur
146240 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
147241 assert span .status .status_code == StatusCode .ERROR
148242 assert span .status .description == "Method not found"
243+ assert span .attributes is not None
244+ assert span .attributes ["error.type" ] == str (exc .value .error .code )
245+ assert span .attributes ["rpc.response.status_code" ] == str (exc .value .error .code )
149246 assert not [e for e in span .events if e .name == "exception" ]
150247
151248
@@ -160,6 +257,8 @@ async def test_validation_failure_sets_sanitized_status(server: SrvT, spans: Spa
160257 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
161258 assert span .status .status_code == StatusCode .ERROR
162259 assert span .status .description == "Invalid request parameters"
260+ assert span .attributes is not None
261+ assert span .attributes ["error.type" ] == "ValidationError"
163262 assert not span .events
164263
165264
@@ -177,6 +276,8 @@ async def failing(ctx: Ctx, params: PaginatedRequestParams | None) -> Any:
177276 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
178277 assert span .status .status_code == StatusCode .ERROR
179278 assert span .status .description == "handler blew up"
279+ assert span .attributes is not None
280+ assert span .attributes ["error.type" ] == "ValueError"
180281 [event ] = [e for e in span .events if e .name == "exception" ]
181282 assert event .attributes is not None
182283 assert event .attributes ["exception.type" ] == "ValueError"
@@ -202,4 +303,4 @@ async def inject_arg(ctx: Ctx, call_next: CallNext) -> Any:
202303 await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {"x" : 1 }})
203304 assert seen_arguments == {"x" : 1 , "injected" : True }
204305 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
205- assert span .name == "MCP handle tools/call mytool"
306+ assert span .name == "tools/call mytool"
0 commit comments