|
1 | 1 | import base64 |
2 | 2 | from pathlib import Path |
3 | | -from typing import Any |
| 3 | +from typing import Any, get_type_hints |
4 | 4 | from unittest.mock import AsyncMock, MagicMock, patch |
5 | 5 |
|
6 | 6 | import pytest |
|
15 | 15 | from mcp.server.mcpserver.exceptions import ToolError |
16 | 16 | from mcp.server.mcpserver.prompts.base import Message, UserMessage |
17 | 17 | from mcp.server.mcpserver.resources import FileResource, FunctionResource |
| 18 | +from mcp.server.mcpserver.server import ToolResult |
18 | 19 | from mcp.server.mcpserver.utilities.types import Audio, Image |
19 | 20 | from mcp.server.transport_security import TransportSecuritySettings |
20 | 21 | from mcp.shared.exceptions import MCPError |
21 | 22 | from mcp.types import ( |
22 | 23 | AudioContent, |
23 | 24 | BlobResourceContents, |
| 25 | + CallToolResult, |
24 | 26 | Completion, |
25 | 27 | CompletionArgument, |
26 | 28 | CompletionContext, |
@@ -304,6 +306,64 @@ async def test_tool_return_value_conversion(self): |
304 | 306 | assert result.structured_content is not None |
305 | 307 | assert result.structured_content == {"result": 3} |
306 | 308 |
|
| 309 | + def test_call_tool_return_annotation_matches_reachable_shapes(self): |
| 310 | + hints = get_type_hints(MCPServer.call_tool) |
| 311 | + assert hints["return"] == ToolResult |
| 312 | + |
| 313 | + async def test_call_tool_preserves_direct_call_tool_result(self): |
| 314 | + mcp = MCPServer() |
| 315 | + |
| 316 | + @mcp.tool() |
| 317 | + def direct_result() -> CallToolResult: |
| 318 | + return CallToolResult(content=[TextContent(type="text", text="direct")]) |
| 319 | + |
| 320 | + result = await mcp.call_tool("direct_result", {}) |
| 321 | + assert isinstance(result, CallToolResult) |
| 322 | + assert result.content == [TextContent(type="text", text="direct")] |
| 323 | + |
| 324 | + async with Client(mcp) as client: |
| 325 | + handled = await client.call_tool("direct_result", {}) |
| 326 | + assert handled.content == result.content |
| 327 | + assert handled.structured_content is None |
| 328 | + |
| 329 | + async def test_call_tool_wraps_bare_content_sequence(self): |
| 330 | + mcp = MCPServer() |
| 331 | + |
| 332 | + def raw_blocks() -> list[ContentBlock]: |
| 333 | + return [TextContent(type="text", text="raw")] |
| 334 | + |
| 335 | + mcp.add_tool(raw_blocks, structured_output=False) |
| 336 | + |
| 337 | + result = await mcp.call_tool("raw_blocks", {}) |
| 338 | + assert isinstance(result, list) |
| 339 | + assert all(isinstance(item, ContentBlock) for item in result) |
| 340 | + |
| 341 | + async with Client(mcp) as client: |
| 342 | + handled = await client.call_tool("raw_blocks", {}) |
| 343 | + assert handled.content == result |
| 344 | + assert handled.structured_content is None |
| 345 | + |
| 346 | + async def test_call_tool_wraps_structured_tuple_result(self): |
| 347 | + class UserOutput(BaseModel): |
| 348 | + name: str |
| 349 | + age: int |
| 350 | + |
| 351 | + def get_user() -> UserOutput: |
| 352 | + return UserOutput(name="John Doe", age=30) |
| 353 | + |
| 354 | + mcp = MCPServer() |
| 355 | + mcp.add_tool(get_user) |
| 356 | + |
| 357 | + result = await mcp.call_tool("get_user", {}) |
| 358 | + assert isinstance(result, tuple) |
| 359 | + unstructured_content, structured_content = result |
| 360 | + assert structured_content == {"name": "John Doe", "age": 30} |
| 361 | + |
| 362 | + async with Client(mcp) as client: |
| 363 | + handled = await client.call_tool("get_user", {}) |
| 364 | + assert handled.content == list(unstructured_content) |
| 365 | + assert handled.structured_content == structured_content |
| 366 | + |
307 | 367 | async def test_tool_image_helper(self, tmp_path: Path): |
308 | 368 | # Create a test image |
309 | 369 | image_path = tmp_path / "test.png" |
|
0 commit comments