From c2e1d5533bbbf57c8d326e35c7a8f138b9d36b1a Mon Sep 17 00:00:00 2001 From: cognis-digital Date: Thu, 11 Jun 2026 23:56:23 -0400 Subject: [PATCH] test: lock in CallToolResult(is_error=True) with non-text content (#348) A FastMCP tool can already report failure while returning rich (non-text) content by returning a CallToolResult with is_error=True directly -- MCPServer passes it through unchanged. This was untested and undocumented, which is what issue #348 reports ("no way to set isError=True for arbitrary tool result content"). Add a regression test proving an image content block plus is_error=True round-trips to the caller over all transports, and a matching requirements manifest entry (source issue:#348). Test-only; no library change. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/interaction/_requirements.py | 8 ++++++++ tests/interaction/mcpserver/test_tools.py | 25 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/interaction/_requirements.py b/tests/interaction/_requirements.py index caed8905d..4bc4fe440 100644 --- a/tests/interaction/_requirements.py +++ b/tests/interaction/_requirements.py @@ -528,6 +528,14 @@ def __post_init__(self) -> None: "in content, not as a JSON-RPC error." ), ), + "tools:call:is-error-with-content": Requirement( + source="issue:#348", + behavior=( + "A tool can return a hand-built CallToolResult with isError true that carries arbitrary " + "content (e.g. an image), not just text; the content blocks and the isError flag reach the " + "caller intact." + ), + ), "tools:call:logging-mid-execution": Requirement( source=f"{SPEC_BASE_URL}/server/utilities/logging#log-message-notifications", behavior=( diff --git a/tests/interaction/mcpserver/test_tools.py b/tests/interaction/mcpserver/test_tools.py index 05135c128..9f33e303d 100644 --- a/tests/interaction/mcpserver/test_tools.py +++ b/tests/interaction/mcpserver/test_tools.py @@ -16,6 +16,7 @@ CallToolResult, ElicitRequestURLParams, ErrorData, + ImageContent, LoggingMessageNotification, LoggingMessageNotificationParams, TextContent, @@ -133,6 +134,30 @@ def add() -> None: assert result == snapshot(CallToolResult(content=[TextContent(text="Unknown tool: nope")], is_error=True)) +@requirement("tools:call:is-error-with-content") +async def test_tool_returning_call_tool_result_can_flag_is_error_on_non_text_content(connect: Connect) -> None: + """A tool may hand back a CallToolResult with is_error=True carrying non-text content. + + Raising an exception is the usual way to produce an is_error result, but that only yields a + text message. A tool that wants to report failure while returning richer content (here, an + image) can return a CallToolResult directly; MCPServer passes it through unchanged, so both the + image block and the is_error flag reach the caller. Regression lock-in for issue #348. + """ + mcp = MCPServer("imager") + + @mcp.tool() + def render() -> CallToolResult: + return CallToolResult( + content=[ImageContent(data="aW1n", mime_type="image/png")], + is_error=True, + ) + + async with connect(mcp) as client: + result = await client.call_tool("render", {}) + + assert result == snapshot(CallToolResult(content=[ImageContent(data="aW1n", mime_type="image/png")], is_error=True)) + + @requirement("mcpserver:tool:output-schema:model") @requirement("tools:call:structured-content:text-mirror") async def test_call_tool_model_return_becomes_structured_content(connect: Connect) -> None: