diff --git a/docs/migration.md b/docs/migration.md index 8b70885e8..f281f856a 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1136,3 +1136,12 @@ If you encounter issues during migration: 1. Check the [API Reference](api/mcp/index.md) for updated method signatures 2. Review the [examples](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples) for updated usage patterns 3. Open an issue on [GitHub](https://github.com/modelcontextprotocol/python-sdk/issues) if you find a bug or need further assistance + +## Tool result: marking non-text results as errors +Tools may now return a 3-tuple: (unstructured_content, structured_content, is_error). +If the third element is True, the resulting CallToolResult sent to clients will have +is_error=True. This allows returning non-text content (images, audio) while still +indicating the tool execution failed or produced an error state. + +Alternatively, tools may return a full `CallToolResult` instance directly to control +is_error and other fields explicitly. diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index b3471163b..6758a0e75 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -316,12 +316,24 @@ async def _handle_call_tool( return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True) if isinstance(result, CallToolResult): return result - if isinstance(result, tuple) and len(result) == 2: - unstructured_content, structured_content = result + if isinstance(result, tuple): + # Support either (unstructured_content, structured_content) or + # (unstructured_content, structured_content, is_error). The third element, + # if present, controls the CallToolResult.is_error flag. + if len(result) == 2: + unstructured_content, structured_content = result + is_error = False + elif len(result) == 3: + unstructured_content, structured_content, is_error = result + else: + # Fallback: treat as a sequence of content blocks + return CallToolResult(content=list(result)) return CallToolResult( content=list(unstructured_content), # type: ignore[arg-type] structured_content=structured_content, # type: ignore[arg-type] + is_error=bool(is_error), ) + if isinstance(result, dict): # pragma: no cover # TODO: this code path is unreachable — convert_result never returns a raw dict. # The call_tool return type (Sequence[ContentBlock] | dict[str, Any]) is wrong