From a3e240c0769394cfeb3c6b7426d1b162ff6b63ca Mon Sep 17 00:00:00 2001 From: kimsehwan96 Date: Sat, 18 Apr 2026 18:20:03 +0900 Subject: [PATCH] fix: skip output schema validation when tool returns isError=True Backport of the main branch fix to v1.x. When a tool with an inferred output_schema returns CallToolResult(isError=True), the SDK's convert_result() calls model_validate(None) unconditionally, raising a pydantic error that replaces the intended error message. Skip output schema validation when the result signals an error, matching the TypeScript SDK fix (modelcontextprotocol/typescript-sdk#655). Fixes #2429 --- .../server/fastmcp/utilities/func_metadata.py | 2 +- tests/server/fastmcp/test_func_metadata.py | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/fastmcp/utilities/func_metadata.py b/src/mcp/server/fastmcp/utilities/func_metadata.py index 241100d31..2438a2a54 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 61e524290..86c52d3ca 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,31 @@ 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_is_error_skips_validation(): + """Test that isError=True skips output schema validation. + + Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/2429 + """ + + class DivideOutput(BaseModel): + result: float + + def func_returning_error() -> Annotated[CallToolResult, DivideOutput]: + return CallToolResult( + content=[TextContent(type="text", text="Division by zero")], + isError=True, + ) + + meta = func_metadata(func_returning_error) + assert meta.output_schema is not None + + result = meta.convert_result(func_returning_error()) + assert isinstance(result, CallToolResult) + assert result.isError is True + assert isinstance(result.content[0], TextContent) + assert result.content[0].text == "Division by zero" + + def test_tool_call_result_in_optional_is_rejected(): """Test that Optional[CallToolResult] raises InvalidSignature"""