@@ -2486,6 +2486,92 @@ def spend() -> str:
24862486 )
24872487
24882488
2489+ async def test_tool_error_with_custom_content_returns_rich_is_error_result ():
2490+ """ToolError with custom content returns that content with is_error=True
2491+ instead of wrapping the message string."""
2492+ mcp = MCPServer ()
2493+
2494+ @mcp .tool ()
2495+ def render (url : str ) -> str :
2496+ raise ToolError (
2497+ "rendering failed" ,
2498+ content = [
2499+ TextContent (type = "text" , text = "could not render the page" ),
2500+ ImageContent (type = "image" , data = "iVBORw0KGgo=" , mime_type = "image/png" ),
2501+ ],
2502+ )
2503+
2504+ async with Client (mcp ) as client :
2505+ result = await client .call_tool ("render" , {"url" : "https://example.com" })
2506+
2507+ assert result .is_error is True
2508+ assert len (result .content ) == 2
2509+ assert result .content [0 ] == TextContent (type = "text" , text = "could not render the page" )
2510+ assert isinstance (result .content [1 ], ImageContent )
2511+ assert result .content [1 ].mime_type == "image/png"
2512+
2513+
2514+ async def test_tool_error_without_content_falls_back_to_text (caplog : pytest .LogCaptureFixture ):
2515+ """A plain ToolError (no content kwarg) still wraps str(exc) in TextContent,
2516+ preserving backward compatibility."""
2517+ mcp = MCPServer ()
2518+
2519+ @mcp .tool ()
2520+ def fail () -> str :
2521+ raise ToolError ("something broke" )
2522+
2523+ async with Client (mcp ) as client :
2524+ result = await client .call_tool ("fail" , {})
2525+
2526+ assert result .is_error is True
2527+ assert result .content == [TextContent (type = "text" , text = "Error executing tool fail: something broke" )]
2528+
2529+
2530+ async def test_tool_error_with_content_propagates_through_programmatic_call ():
2531+ """When call_tool is called programmatically, the re-raised ToolError
2532+ preserves the custom content attribute."""
2533+ mcp = MCPServer ()
2534+
2535+ error_content = [
2536+ TextContent (type = "text" , text = "structured error info" ),
2537+ ImageContent (type = "image" , data = "iVBORw0KGgo=" , mime_type = "image/png" ),
2538+ ]
2539+
2540+ @mcp .tool ()
2541+ def analyze () -> str :
2542+ raise ToolError ("analysis failed" , content = error_content )
2543+
2544+ with pytest .raises (ToolError ) as exc :
2545+ await mcp .call_tool ("analyze" , {})
2546+
2547+ assert exc .value .content is not None
2548+ assert len (exc .value .content ) == 2
2549+ assert exc .value .content [0 ] == TextContent (type = "text" , text = "structured error info" )
2550+
2551+
2552+ async def test_tool_error_with_content_is_logged_at_info (caplog : pytest .LogCaptureFixture ):
2553+ """A ToolError with custom content is still logged at INFO, same as a plain ToolError."""
2554+ mcp = MCPServer ()
2555+
2556+ @mcp .tool ()
2557+ def render () -> str :
2558+ raise ToolError (
2559+ "rendering failed" ,
2560+ content = [TextContent (type = "text" , text = "detailed failure info" )],
2561+ )
2562+
2563+ caplog .set_level (logging .INFO )
2564+ async with Client (mcp ) as client :
2565+ result = await client .call_tool ("render" , {})
2566+
2567+ assert result .is_error is True
2568+ assert result .content == [TextContent (type = "text" , text = "detailed failure info" )]
2569+ assert _server_records (caplog ) == snapshot (
2570+ [("INFO" , "Tool 'render' failed: 'Error executing tool render: rendering failed'" , False )]
2571+ )
2572+ assert not [r for r in caplog .records if r .levelno >= logging .WARNING ]
2573+
2574+
24892575async def test_tool_argument_validation_failure_is_logged_at_info_without_traceback (
24902576 caplog : pytest .LogCaptureFixture ,
24912577):
0 commit comments