Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/agents/mcp/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 14 additions & 1 deletion tests/mcp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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")],
)
Expand Down
54 changes: 54 additions & 0 deletions tests/mcp/test_mcp_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down