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+ INVALID_PARAMS ,
18+ CallToolRequestParams ,
19+ CallToolResult ,
20+ GetPromptRequestParams ,
21+ GetPromptResult ,
22+ ListToolsResult ,
23+ NotificationParams ,
24+ PaginatedRequestParams ,
25+ Tool ,
26+ )
1727
1828from .conftest import SpanCapture
1929from .test_runner import Ctx , SrvT , connected_runner
@@ -40,13 +50,98 @@ async def test_emits_server_span_with_method_and_target(server: SrvT, spans: Spa
4050 result = await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
4151 assert result == {"content" : [], "isError" : False }
4252 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
43- assert span .name == "MCP handle tools/call mytool"
53+ assert span .name == "tools/call mytool"
4454 assert span .attributes is not None
4555 assert span .attributes ["mcp.method.name" ] == "tools/call"
56+ assert span .attributes ["gen_ai.operation.name" ] == "execute_tool"
57+ assert span .attributes ["gen_ai.tool.name" ] == "mytool"
4658 assert isinstance (span .attributes ["jsonrpc.request.id" ], str )
4759 assert span .status .status_code == StatusCode .UNSET
4860
4961
62+ @pytest .mark .anyio
63+ async def test_tool_error_dict_result_sets_error_type (server : SrvT , spans : SpanCapture ):
64+ async def err_tool (ctx : Ctx , params : CallToolRequestParams ) -> dict [str , Any ]:
65+ return {"content" : [], "isError" : True }
66+
67+ server .add_request_handler ("tools/call" , CallToolRequestParams , err_tool )
68+ server .middleware .append (OpenTelemetryMiddleware ())
69+ async with connected_runner (server ) as (client , _ ):
70+ spans .clear ()
71+ await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
72+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
73+ assert span .attributes is not None
74+ assert span .attributes ["error.type" ] == "tool_error"
75+ assert span .status .status_code == StatusCode .ERROR
76+
77+
78+ @pytest .mark .anyio
79+ async def test_tool_error_model_result_sets_error_type (server : SrvT , spans : SpanCapture ):
80+ async def err_tool (ctx : Ctx , params : CallToolRequestParams ) -> CallToolResult :
81+ return CallToolResult (content = [], is_error = True )
82+
83+ server .add_request_handler ("tools/call" , CallToolRequestParams , err_tool )
84+ server .middleware .append (OpenTelemetryMiddleware ())
85+ async with connected_runner (server ) as (client , _ ):
86+ spans .clear ()
87+ await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
88+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
89+ assert span .attributes is not None
90+ assert span .attributes ["error.type" ] == "tool_error"
91+ assert span .status .status_code == StatusCode .ERROR
92+
93+
94+ @pytest .mark .anyio
95+ async def test_tool_error_snake_case_dict_result_sets_error_type (server : SrvT , spans : SpanCapture ):
96+ async def err_tool (ctx : Ctx , params : CallToolRequestParams ) -> dict [str , Any ]:
97+ return {"content" : [], "is_error" : True }
98+
99+ server .add_request_handler ("tools/call" , CallToolRequestParams , err_tool )
100+ server .middleware .append (OpenTelemetryMiddleware ())
101+ async with connected_runner (server ) as (client , _ ):
102+ spans .clear ()
103+ await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {}})
104+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
105+ assert span .attributes is not None
106+ assert span .attributes ["error.type" ] == "tool_error"
107+ assert span .status .status_code == StatusCode .ERROR
108+
109+
110+ @pytest .mark .anyio
111+ async def test_named_non_tool_prompt_method_omits_gen_ai_attrs (server : SrvT , spans : SpanCapture ):
112+ async def custom (ctx : Ctx , params : CallToolRequestParams ) -> dict [str , Any ]:
113+ return {"content" : [], "isError" : False }
114+
115+ server .add_request_handler ("custom/op" , CallToolRequestParams , custom )
116+ server .middleware .append (OpenTelemetryMiddleware ())
117+ async with connected_runner (server ) as (client , _ ):
118+ spans .clear ()
119+ await client .send_raw_request ("custom/op" , {"name" : "thing" , "arguments" : {}})
120+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
121+ assert span .name == "custom/op thing"
122+ assert span .attributes is not None
123+ assert "gen_ai.operation.name" not in span .attributes
124+ assert "gen_ai.tool.name" not in span .attributes
125+ assert "gen_ai.prompt.name" not in span .attributes
126+
127+
128+ @pytest .mark .anyio
129+ async def test_prompt_get_sets_prompt_name (server : SrvT , spans : SpanCapture ):
130+ async def get_prompt (ctx : Ctx , params : GetPromptRequestParams ) -> GetPromptResult :
131+ return GetPromptResult (messages = [])
132+
133+ server .add_request_handler ("prompts/get" , GetPromptRequestParams , get_prompt )
134+ server .middleware .append (OpenTelemetryMiddleware ())
135+ async with connected_runner (server ) as (client , _ ):
136+ spans .clear ()
137+ await client .send_raw_request ("prompts/get" , {"name" : "myprompt" })
138+ [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
139+ assert span .name == "prompts/get myprompt"
140+ assert span .attributes is not None
141+ assert span .attributes ["gen_ai.prompt.name" ] == "myprompt"
142+ assert "gen_ai.operation.name" not in span .attributes
143+
144+
50145@pytest .mark .anyio
51146async def test_notification_span_omits_request_id (server : SrvT , spans : SpanCapture ):
52147 async def on_roots (ctx : Ctx , params : NotificationParams | None ) -> None :
@@ -59,7 +154,7 @@ async def on_roots(ctx: Ctx, params: NotificationParams | None) -> None:
59154 await client .notify ("notifications/roots/list_changed" , None )
60155 await anyio .wait_all_tasks_blocked ()
61156 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
62- assert span .name == "MCP handle notifications/roots/list_changed"
157+ assert span .name == "notifications/roots/list_changed"
63158 assert span .attributes is not None
64159 assert span .attributes ["mcp.method.name" ] == "notifications/roots/list_changed"
65160 assert "jsonrpc.request.id" not in span .attributes
@@ -146,6 +241,9 @@ async def test_records_error_status_on_mcp_error(server: SrvT, spans: SpanCaptur
146241 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
147242 assert span .status .status_code == StatusCode .ERROR
148243 assert span .status .description == "Method not found"
244+ assert span .attributes is not None
245+ assert span .attributes ["error.type" ] == str (exc .value .error .code )
246+ assert span .attributes ["rpc.response.status_code" ] == str (exc .value .error .code )
149247 assert not [e for e in span .events if e .name == "exception" ]
150248
151249
@@ -160,6 +258,11 @@ async def test_validation_failure_sets_sanitized_status(server: SrvT, spans: Spa
160258 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
161259 assert span .status .status_code == StatusCode .ERROR
162260 assert span .status .description == "Invalid request parameters"
261+ assert span .attributes is not None
262+ assert span .attributes ["error.type" ] == str (INVALID_PARAMS )
263+ assert span .attributes ["rpc.response.status_code" ] == str (INVALID_PARAMS )
264+ assert span .attributes ["gen_ai.operation.name" ] == "execute_tool"
265+ assert "gen_ai.tool.name" not in span .attributes
163266 assert not span .events
164267
165268
@@ -177,6 +280,8 @@ async def failing(ctx: Ctx, params: PaginatedRequestParams | None) -> Any:
177280 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
178281 assert span .status .status_code == StatusCode .ERROR
179282 assert span .status .description == "handler blew up"
283+ assert span .attributes is not None
284+ assert span .attributes ["error.type" ] == "ValueError"
180285 [event ] = [e for e in span .events if e .name == "exception" ]
181286 assert event .attributes is not None
182287 assert event .attributes ["exception.type" ] == "ValueError"
@@ -202,4 +307,4 @@ async def inject_arg(ctx: Ctx, call_next: CallNext) -> Any:
202307 await client .send_raw_request ("tools/call" , {"name" : "mytool" , "arguments" : {"x" : 1 }})
203308 assert seen_arguments == {"x" : 1 , "injected" : True }
204309 [span ] = [s for s in spans .finished () if s .kind == SpanKind .SERVER ]
205- assert span .name == "MCP handle tools/call mytool"
310+ assert span .name == "tools/call mytool"
0 commit comments