From bbac5ca5473a3ba5d39e67b960ff1a0c4ea04313 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sun, 31 May 2026 16:01:52 +0800 Subject: [PATCH] fix: preserve FastMCP tool error results --- .../server/fastmcp/utilities/func_metadata.py | 2 +- tests/server/fastmcp/test_func_metadata.py | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index 241100d317..2438a2a544 100644 --- a/src/mcp/server/fastmcp/utilities/func_metadata.py +++ b/src/mcp/server/fastmcp/utilities/func_metadata.py @@ -112,7 +112,7 @@ def convert_result(self, result: Any) -> Any: the structured output. """ if isinstance(result, CallToolResult): - if self.output_schema is not None: + if self.output_schema is not None and not result.isError: assert self.output_model is not None, "Output model must be set if output schema is defined" self.output_model.model_validate(result.structuredContent) return result diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index 61e524290e..ada5cbb38f 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, Field from mcp.server.fastmcp.utilities.func_metadata import func_metadata -from mcp.types import CallToolResult +from mcp.types import CallToolResult, TextContent class SomeInputModelA(BaseModel): @@ -878,6 +878,24 @@ def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, Per meta.convert_result(func_returning_annotated_tool_call_result()) +def test_tool_call_result_annotated_error_skips_structured_validation(): + class PersonClass(BaseModel): + name: str + + def func_returning_tool_error() -> Annotated[CallToolResult, PersonClass]: # pragma: no cover + return CallToolResult(content=[TextContent(type="text", text="Division by zero")], isError=True) + + meta = func_metadata(func_returning_tool_error) + result = meta.convert_result(func_returning_tool_error()) + + assert isinstance(result, CallToolResult) + assert result.isError is True + assert result.structuredContent is None + content = result.content[0] + assert isinstance(content, TextContent) + assert content.text == "Division by zero" + + def test_tool_call_result_in_optional_is_rejected(): """Test that Optional[CallToolResult] raises InvalidSignature"""