Skip to content

Commit 076eb72

Browse files
thoothein
andauthored
Empty mcp tool result error (#1142)
### Summary Fixed `MCPUtil.invoke_mcp_tool()` to return `"[]"` instead of logging an error when MCP tools return empty content. Empty results (`None, []`) are valid outputs and should be treated consistently with function tools. The `None -> "[]"` and `[] -> "[]"` behavior comes from the MCP Python SDK's[ FastMCP implementation]( https://github.com/modelcontextprotocol/python-sdk/blob/6566c08446ad37b66f1457532c7f5a14f243ae10/src/mcp/server/fastmcp/utilities/func_metadata.py#L500). ### Test plan - Added comprehensive test coverage in `test_mcp_fastmcp_behavior_verification()` that verifies all FastMCP edge cases - Enhanced FakeMCPServer to support custom content testing scenarios - Ran full test suite - all tests pass - Verified the fix handles `None, [], {}, [{}], [[]]` cases correctly ### Issue number Closes #1035 ### Checks - [x] I've added new tests (if relevant) - [ ] I've added/updated the relevant documentation - [x] I've run `make lint` and `make format` - [x] I've made sure tests pass Co-authored-by: thein <[email protected]>
1 parent 00c87ea commit 076eb72

File tree

3 files changed

+70
-3
lines changed

3 files changed

+70
-3
lines changed

src/agents/mcp/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ async def invoke_mcp_tool(
209209
elif server.use_structured_content and result.structuredContent:
210210
tool_output = json.dumps(result.structuredContent)
211211
else:
212-
logger.error(f"Errored MCP tool result: {result}")
213-
tool_output = "Error running tool."
212+
# Empty content is a valid result (e.g., "no results found")
213+
tool_output = "[]"
214214

215215
current_span = get_current_span()
216216
if current_span:

tests/mcp/helpers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
from typing import Any
55

66
from mcp import Tool as MCPTool
7-
from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult, PromptMessage, TextContent
7+
from mcp.types import (
8+
CallToolResult,
9+
Content,
10+
GetPromptResult,
11+
ListPromptsResult,
12+
PromptMessage,
13+
TextContent,
14+
)
815

916
from agents.mcp import MCPServer
1017
from agents.mcp.server import _MCPServerWithClientSession
@@ -67,6 +74,7 @@ def __init__(
6774
self.tool_results: list[str] = []
6875
self.tool_filter = tool_filter
6976
self._server_name = server_name
77+
self._custom_content: list[Content] | None = None
7078

7179
def add_tool(self, name: str, input_schema: dict[str, Any]):
7280
self.tools.append(MCPTool(name=name, inputSchema=input_schema))
@@ -91,6 +99,11 @@ async def list_tools(self, run_context=None, agent=None):
9199
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
92100
self.tool_calls.append(tool_name)
93101
self.tool_results.append(f"result_{tool_name}_{json.dumps(arguments)}")
102+
103+
# Allow testing custom content scenarios
104+
if self._custom_content is not None:
105+
return CallToolResult(content=self._custom_content)
106+
94107
return CallToolResult(
95108
content=[TextContent(text=self.tool_results[-1], type="text")],
96109
)

tests/mcp/test_mcp_util.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,60 @@ async def test_agent_convert_schemas_false():
234234
assert baz_tool.strict_json_schema is False, "Shouldn't be converted unless specified"
235235

236236

237+
@pytest.mark.asyncio
238+
async def test_mcp_fastmcp_behavior_verification():
239+
"""Test that verifies the exact FastMCP _convert_to_content behavior we observed.
240+
241+
Based on our testing, FastMCP's _convert_to_content function behaves as follows:
242+
- None → content=[] → MCPUtil returns "[]"
243+
- [] → content=[] → MCPUtil returns "[]"
244+
- {} → content=[TextContent(text="{}")] → MCPUtil returns full JSON
245+
- [{}] → content=[TextContent(text="{}")] → MCPUtil returns full JSON (flattened)
246+
- [[]] → content=[] → MCPUtil returns "[]" (recursive empty)
247+
"""
248+
249+
from mcp.types import TextContent
250+
251+
server = FakeMCPServer()
252+
server.add_tool("test_tool", {})
253+
254+
ctx = RunContextWrapper(context=None)
255+
tool = MCPTool(name="test_tool", inputSchema={})
256+
257+
# Case 1: None -> "[]".
258+
server._custom_content = []
259+
result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "")
260+
assert result == "[]", f"None should return '[]', got {result}"
261+
262+
# Case 2: [] -> "[]".
263+
server._custom_content = []
264+
result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "")
265+
assert result == "[]", f"[] should return '[]', got {result}"
266+
267+
# Case 3: {} -> {"type":"text","text":"{}","annotations":null}.
268+
server._custom_content = [TextContent(text="{}", type="text")]
269+
result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "")
270+
expected = '{"type":"text","text":"{}","annotations":null}'
271+
assert result == expected, f"{{}} should return {expected}, got {result}"
272+
273+
# Case 4: [{}] -> {"type":"text","text":"{}","annotations":null}.
274+
server._custom_content = [TextContent(text="{}", type="text")]
275+
result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "")
276+
expected = '{"type":"text","text":"{}","annotations":null}'
277+
assert result == expected, f"[{{}}] should return {expected}, got {result}"
278+
279+
# Case 5: [[]] -> "[]".
280+
server._custom_content = []
281+
result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "")
282+
assert result == "[]", f"[[]] should return '[]', got {result}"
283+
284+
# Case 6: String values work normally.
285+
server._custom_content = [TextContent(text="hello", type="text")]
286+
result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "")
287+
expected = '{"type":"text","text":"hello","annotations":null}'
288+
assert result == expected, f"String should return {expected}, got {result}"
289+
290+
237291
@pytest.mark.asyncio
238292
async def test_agent_convert_schemas_unset():
239293
"""Test that leaving convert_schemas_to_strict unset (defaulting to False) leaves tool schemas

0 commit comments

Comments
 (0)