@@ -63,7 +63,12 @@ async def __call__(self, *args, **kwargs):
6363 TextResourceContents ,
6464 )
6565else :
66- from mcp .types import GetPromptResult , PromptMessage , TextContent
66+ from mcp .types import (
67+ CallToolResult ,
68+ GetPromptResult ,
69+ PromptMessage ,
70+ TextContent ,
71+ )
6772
6873from mcp .server .sse import SseServerTransport
6974from mcp .server .streamable_http_manager import StreamableHTTPSessionManager
@@ -749,6 +754,7 @@ def failing_tool(tool_name, arguments):
749754 assert not error_payload ["exception" ]["values" ][0 ]["mechanism" ]["handled" ]
750755
751756 assert span ["status" ] == "error"
757+ assert SPANDATA .ERROR_TYPE not in span ["attributes" ]
752758 else :
753759 events = capture_events ()
754760 with start_transaction (name = "mcp tx" ):
@@ -786,6 +792,145 @@ def failing_tool(tool_name, arguments):
786792
787793 assert span ["status" ] == "internal_error"
788794 assert span ["tags" ]["status" ] == "internal_error"
795+ assert SPANDATA .ERROR_TYPE not in span ["data" ]
796+
797+
798+ @pytest .mark .asyncio
799+ @pytest .mark .skipif (
800+ MCP_PACKAGE_VERSION < (1 , 19 , 0 ),
801+ reason = "Handlers can only return a CallToolResult from mcp 1.19" ,
802+ )
803+ @pytest .mark .parametrize ("span_streaming" , [True , False ])
804+ @pytest .mark .parametrize ("is_error" , [True , False ])
805+ async def test_tool_result_is_error (
806+ sentry_init , capture_events , capture_items , span_streaming , is_error , stdio
807+ ):
808+ """A tool result with isError=true sets error.type, but not the span status"""
809+ sentry_init (
810+ integrations = [MCPIntegration ()],
811+ traces_sample_rate = 1.0 ,
812+ trace_lifecycle = "stream" if span_streaming else "static" ,
813+ )
814+
815+ server = Server ("test-server" )
816+
817+ if IS_MCP_V2 :
818+
819+ async def test_tool (ctx , params ):
820+ return CallToolResult (
821+ content = [TextContent (type = "text" , text = "warehouse DB timeout" )],
822+ is_error = is_error ,
823+ )
824+
825+ server .add_request_handler ("tools/call" , CallToolRequestParams , test_tool )
826+ else :
827+
828+ @server .call_tool ()
829+ async def test_tool (tool_name , arguments ):
830+ return CallToolResult (
831+ content = [TextContent (type = "text" , text = "warehouse DB timeout" )],
832+ isError = is_error ,
833+ )
834+
835+ if span_streaming :
836+ items = capture_items ("event" , "span" )
837+ with sentry_sdk .traces .start_span (name = "mcp tx" ):
838+ result = await stdio (
839+ server ,
840+ method = "tools/call" ,
841+ params = {"name" : "lookup" , "arguments" : {}},
842+ request_id = "req-is-error" ,
843+ )
844+ sentry_sdk .flush ()
845+
846+ assert not [item for item in items if item .type == "event" ]
847+ span = _find_mcp_span (items , method_name = "tools/call" )
848+ assert span is not None
849+ data = span ["attributes" ]
850+ else :
851+ events = capture_events ()
852+ with start_transaction (name = "mcp tx" ):
853+ result = await stdio (
854+ server ,
855+ method = "tools/call" ,
856+ params = {"name" : "lookup" , "arguments" : {}},
857+ request_id = "req-is-error" ,
858+ )
859+
860+ (tx ,) = events
861+ assert len (tx ["spans" ]) == 1
862+ span = tx ["spans" ][0 ]
863+ data = span ["data" ]
864+
865+ assert _get_response (result ).result ["isError" ] is is_error
866+ # The span status is left as it is for a successful call
867+ if span_streaming :
868+ assert span ["status" ] == "ok"
869+ else :
870+ assert "status" not in span
871+
872+ if is_error :
873+ assert data [SPANDATA .ERROR_TYPE ] == "tool_error"
874+ else :
875+ assert SPANDATA .ERROR_TYPE not in data
876+
877+
878+ @pytest .mark .asyncio
879+ @pytest .mark .skipif (not IS_MCP_V2 , reason = "MCPServer is MCP v2 only" )
880+ @pytest .mark .parametrize ("span_streaming" , [True , False ])
881+ async def test_mcpserver_raising_tool_sets_error_type (
882+ sentry_init , capture_events , capture_items , span_streaming , stdio
883+ ):
884+ """MCPServer turns an exception raised by a tool into an isError result
885+ before the middleware sees it, so the span gets error.type=tool_error."""
886+ from mcp .server import MCPServer
887+
888+ sentry_init (
889+ integrations = [MCPIntegration ()],
890+ traces_sample_rate = 1.0 ,
891+ trace_lifecycle = "stream" if span_streaming else "static" ,
892+ )
893+
894+ mcp_server = MCPServer ("test-server" )
895+
896+ @mcp_server .tool ()
897+ def failing (x : int ) -> int :
898+ raise ValueError ("Tool execution failed" )
899+
900+ if span_streaming :
901+ items = capture_items ("span" )
902+ with sentry_sdk .traces .start_span (name = "mcp tx" ):
903+ result = await stdio (
904+ mcp_server ._lowlevel_server ,
905+ method = "tools/call" ,
906+ params = {"name" : "failing" , "arguments" : {"x" : 1 }},
907+ request_id = "req-mcpserver-error" ,
908+ )
909+ sentry_sdk .flush ()
910+ span = _find_mcp_span (items , method_name = "tools/call" )
911+ assert span is not None
912+ data = span ["attributes" ]
913+ else :
914+ events = capture_events ()
915+ with start_transaction (name = "mcp tx" ):
916+ result = await stdio (
917+ mcp_server ._lowlevel_server ,
918+ method = "tools/call" ,
919+ params = {"name" : "failing" , "arguments" : {"x" : 1 }},
920+ request_id = "req-mcpserver-error" ,
921+ )
922+ # MCPServer also logs the unexpected exception, which is captured as an event
923+ (tx ,) = (event for event in events if event .get ("type" ) == "transaction" )
924+ assert len (tx ["spans" ]) == 1
925+ span = tx ["spans" ][0 ]
926+ data = span ["data" ]
927+
928+ assert _get_response (result ).result ["isError" ] is True
929+ if span_streaming :
930+ assert span ["status" ] == "ok"
931+ else :
932+ assert "status" not in span
933+ assert data [SPANDATA .ERROR_TYPE ] == "tool_error"
789934
790935
791936@pytest .mark .asyncio
0 commit comments