diff --git a/src/agents/mcp/util.py b/src/agents/mcp/util.py index 18cf4440a..9430719cb 100644 --- a/src/agents/mcp/util.py +++ b/src/agents/mcp/util.py @@ -201,8 +201,8 @@ async def invoke_mcp_tool( elif len(result.content) > 1: tool_output = json.dumps([item.model_dump(mode="json") for item in result.content]) else: - logger.error(f"Errored MCP tool result: {result}") - tool_output = "Error running tool." + # Empty content is a valid result (e.g., "no results found") + tool_output = "[]" current_span = get_current_span() if current_span: diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 31d43c228..954b41a34 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -4,7 +4,14 @@ from typing import Any from mcp import Tool as MCPTool -from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult, PromptMessage, TextContent +from mcp.types import ( + CallToolResult, + Content, + GetPromptResult, + ListPromptsResult, + PromptMessage, + TextContent, +) from agents.mcp import MCPServer from agents.mcp.server import _MCPServerWithClientSession @@ -66,6 +73,7 @@ def __init__( self.tool_results: list[str] = [] self.tool_filter = tool_filter self._server_name = server_name + self._custom_content: list[Content] | None = None def add_tool(self, name: str, input_schema: dict[str, Any]): self.tools.append(MCPTool(name=name, inputSchema=input_schema)) @@ -90,6 +98,11 @@ async def list_tools(self, run_context=None, agent=None): async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult: self.tool_calls.append(tool_name) self.tool_results.append(f"result_{tool_name}_{json.dumps(arguments)}") + + # Allow testing custom content scenarios + if self._custom_content is not None: + return CallToolResult(content=self._custom_content) + return CallToolResult( content=[TextContent(text=self.tool_results[-1], type="text")], ) diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index 3230e63dd..62b786708 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -234,6 +234,60 @@ async def test_agent_convert_schemas_false(): assert baz_tool.strict_json_schema is False, "Shouldn't be converted unless specified" +@pytest.mark.asyncio +async def test_mcp_fastmcp_behavior_verification(): + """Test that verifies the exact FastMCP _convert_to_content behavior we observed. + + Based on our testing, FastMCP's _convert_to_content function behaves as follows: + - None → content=[] → MCPUtil returns "[]" + - [] → content=[] → MCPUtil returns "[]" + - {} → content=[TextContent(text="{}")] → MCPUtil returns full JSON + - [{}] → content=[TextContent(text="{}")] → MCPUtil returns full JSON (flattened) + - [[]] → content=[] → MCPUtil returns "[]" (recursive empty) + """ + + from mcp.types import TextContent + + server = FakeMCPServer() + server.add_tool("test_tool", {}) + + ctx = RunContextWrapper(context=None) + tool = MCPTool(name="test_tool", inputSchema={}) + + # Case 1: None -> "[]". + server._custom_content = [] + result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") + assert result == "[]", f"None should return '[]', got {result}" + + # Case 2: [] -> "[]". + server._custom_content = [] + result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") + assert result == "[]", f"[] should return '[]', got {result}" + + # Case 3: {} -> {"type":"text","text":"{}","annotations":null}. + server._custom_content = [TextContent(text="{}", type="text")] + result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") + expected = '{"type":"text","text":"{}","annotations":null}' + assert result == expected, f"{{}} should return {expected}, got {result}" + + # Case 4: [{}] -> {"type":"text","text":"{}","annotations":null}. + server._custom_content = [TextContent(text="{}", type="text")] + result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") + expected = '{"type":"text","text":"{}","annotations":null}' + assert result == expected, f"[{{}}] should return {expected}, got {result}" + + # Case 5: [[]] -> "[]". + server._custom_content = [] + result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") + assert result == "[]", f"[[]] should return '[]', got {result}" + + # Case 6: String values work normally. + server._custom_content = [TextContent(text="hello", type="text")] + result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") + expected = '{"type":"text","text":"hello","annotations":null}' + assert result == expected, f"String should return {expected}, got {result}" + + @pytest.mark.asyncio async def test_agent_convert_schemas_unset(): """Test that leaving convert_schemas_to_strict unset (defaulting to False) leaves tool schemas