fix: correct MCPServer call_tool result type (#2816)

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
冯基魁 2026-06-20 22:56:16 +08:00 • committed by GitHub
commit fda4c54362
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 45 deletions

View file

@ -1038,7 +1038,8 @@ def test_structured_output_aliases():
# Check that the actual output uses aliases too
result = ModelWithAliases(**{"first": "hello", "second": "world"})
_, structured_content = meta.convert_result(result)
structured_content = meta.convert_result(result).structured_content
assert structured_content is not None
# The structured content should use aliases to match the schema
assert "first" in structured_content
@ -1050,7 +1051,8 @@ def test_structured_output_aliases():
# Also test the case where we have a model with defaults to ensure aliases work in all cases
result_with_defaults = ModelWithAliases() # Uses default None values
_, structured_content_defaults = meta.convert_result(result_with_defaults)
structured_content_defaults = meta.convert_result(result_with_defaults).structured_content
assert structured_content_defaults is not None
# Even with defaults, should use aliases in output
assert "first" in structured_content_defaults

View file

@ -23,6 +23,7 @@ from mcp.types import (
INVALID_PARAMS,
AudioContent,
BlobResourceContents,
CallToolResult,
Completion,
CompletionArgument,
CompletionContext,
@ -306,6 +307,29 @@ class TestServerTools:
assert result.structured_content is not None
assert result.structured_content == {"result": 3}
async def test_call_tool_always_returns_call_tool_result(self):
mcp = MCPServer()
@mcp.tool()
def direct() -> CallToolResult:
return CallToolResult(content=[TextContent(type="text", text="direct")])
@mcp.tool(structured_output=False)
def unstructured() -> str:
return "plain"
@mcp.tool()
def structured() -> int:
return 3
assert await mcp.call_tool("direct", {}) == CallToolResult(content=[TextContent(type="text", text="direct")])
assert await mcp.call_tool("unstructured", {}) == CallToolResult(
content=[TextContent(type="text", text="plain")]
)
assert await mcp.call_tool("structured", {}) == CallToolResult(
content=[TextContent(type="text", text="3")], structured_content={"result": 3}
)
async def test_tool_image_helper(self, tmp_path: Path):
# Create a test image
image_path = tmp_path / "test.png"

View file

@ -11,7 +11,7 @@ from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.server.mcpserver.tools import Tool, ToolManager
from mcp.server.mcpserver.utilities.func_metadata import ArgModelBase, FuncMetadata
from mcp.types import TextContent, ToolAnnotations
from mcp.types import CallToolResult, TextContent, ToolAnnotations
class TestAddTools:
@ -455,7 +455,8 @@ class TestStructuredOutput:
manager.add_tool(get_user)
result = await manager.call_tool("get_user", {"user_id": 1}, Context(), convert_result=True)
# don't test unstructured output here, just the structured conversion
assert len(result) == 2 and result[1] == {"name": "John", "age": 30}
assert isinstance(result, CallToolResult)
assert result.structured_content == {"name": "John", "age": 30}
@pytest.mark.anyio
async def test_tool_with_primitive_output(self):
@ -470,7 +471,8 @@ class TestStructuredOutput:
result = await manager.call_tool("double_number", {"n": 5}, Context())
assert result == 10
result = await manager.call_tool("double_number", {"n": 5}, Context(), convert_result=True)
assert isinstance(result[0][0], TextContent) and result[1] == {"result": 10}
assert isinstance(result, CallToolResult)
assert isinstance(result.content[0], TextContent) and result.structured_content == {"result": 10}
@pytest.mark.anyio
async def test_tool_with_typeddict_output(self):
@ -510,7 +512,8 @@ class TestStructuredOutput:
manager.add_tool(get_person)
result = await manager.call_tool("get_person", {}, Context(), convert_result=True)
# don't test unstructured output here, just the structured conversion
assert len(result) == 2 and result[1] == expected_output
assert isinstance(result, CallToolResult)
assert result.structured_content == expected_output
@pytest.mark.anyio
async def test_tool_with_list_output(self):
@ -528,7 +531,8 @@ class TestStructuredOutput:
result = await manager.call_tool("get_numbers", {}, Context())
assert result == expected_list
result = await manager.call_tool("get_numbers", {}, Context(), convert_result=True)
assert isinstance(result[0][0], TextContent) and result[1] == expected_output
assert isinstance(result, CallToolResult)
assert isinstance(result.content[0], TextContent) and result.structured_content == expected_output
@pytest.mark.anyio
async def test_tool_without_structured_output(self):