From c25679bf364ecacf67228111c679189eed6b6053 Mon Sep 17 00:00:00 2001 From: FU-max-boop Date: Sun, 10 May 2026 14:34:35 +0800 Subject: [PATCH 1/2] Fix MCPServer.call_tool return type --- src/mcp/server/mcpserver/server.py | 19 +++++++------------ tests/server/mcpserver/test_server.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index b3471163b..da73cbf4f 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -8,7 +8,7 @@ import re from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence from contextlib import AbstractAsyncContextManager, asynccontextmanager -from typing import Any, Generic, Literal, TypeVar, overload +from typing import Any, Generic, Literal, TypeAlias, TypeVar, overload import anyio import pydantic_core @@ -75,6 +75,9 @@ logger = get_logger(__name__) _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) +_MCPServerToolResult: TypeAlias = ( + Sequence[ContentBlock] | tuple[Sequence[ContentBlock], dict[str, Any]] | CallToolResult +) class Settings(BaseSettings, Generic[LifespanResultT]): @@ -319,16 +322,8 @@ async def _handle_call_tool( if isinstance(result, tuple) and len(result) == 2: unstructured_content, structured_content = result return CallToolResult( - content=list(unstructured_content), # type: ignore[arg-type] - structured_content=structured_content, # type: ignore[arg-type] - ) - if isinstance(result, dict): # pragma: no cover - # TODO: this code path is unreachable — convert_result never returns a raw dict. - # The call_tool return type (Sequence[ContentBlock] | dict[str, Any]) is wrong - # and needs to be cleaned up. - return CallToolResult( - content=[TextContent(type="text", text=json.dumps(result, indent=2))], - structured_content=result, + content=list(unstructured_content), + structured_content=structured_content, ) return CallToolResult(content=list(result)) @@ -399,7 +394,7 @@ async def list_tools(self) -> list[MCPTool]: async def call_tool( self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None - ) -> Sequence[ContentBlock] | dict[str, Any]: + ) -> _MCPServerToolResult: """Call a tool by name with arguments.""" if context is None: context = Context(mcp_server=self) diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 3457ec944..e10be891f 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -519,6 +519,23 @@ def calculate_sum(a: int, b: int) -> int: assert result.structured_content is not None assert result.structured_content == {"result": 12} + async def test_call_tool_direct_returns_structured_tuple(self): + """Test direct call_tool returns both content and structured output.""" + + def calculate_sum(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + mcp = MCPServer() + mcp.add_tool(calculate_sum) + + result = await mcp.call_tool("calculate_sum", {"a": 5, "b": 7}) + + assert isinstance(result, tuple) + content, structured_content = result + assert list(content) == [TextContent(text="12")] + assert structured_content == {"result": 12} + async def test_tool_structured_output_list(self): """Test tool with structured output returning list""" From ab4b4819fe10f23801af28836126309cbcf8763b Mon Sep 17 00:00:00 2001 From: FU-max-boop Date: Sun, 10 May 2026 16:23:48 +0800 Subject: [PATCH 2/2] Fix call_tool result type narrowing --- src/mcp/server/mcpserver/server.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index da73cbf4f..7f68e3b9c 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -4,11 +4,10 @@ import base64 import inspect -import json import re from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence from contextlib import AbstractAsyncContextManager, asynccontextmanager -from typing import Any, Generic, Literal, TypeAlias, TypeVar, overload +from typing import Any, Generic, Literal, TypeAlias, TypeVar, cast, overload import anyio import pydantic_core @@ -319,8 +318,8 @@ async def _handle_call_tool( return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True) if isinstance(result, CallToolResult): return result - if isinstance(result, tuple) and len(result) == 2: - unstructured_content, structured_content = result + if isinstance(result, tuple): + unstructured_content, structured_content = cast(tuple[Sequence[ContentBlock], dict[str, Any]], result) return CallToolResult( content=list(unstructured_content), structured_content=structured_content,