From 73ec962bd9ac6e343eb9b6718faf1d3fd2e68820 Mon Sep 17 00:00:00 2001 From: Armaan Sandhu Date: Wed, 27 May 2026 16:16:22 +0530 Subject: [PATCH] fix: correct MCPServer.call_tool return type and remove dead code --- docs/migration.md | 20 ++++++++++++++++++++ src/mcp/server/mcpserver/server.py | 20 +++++++++----------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/docs/migration.md b/docs/migration.md index 8b70885e8d..f074813349 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -428,6 +428,26 @@ async def my_tool(x: int, ctx: Context) -> str: The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument. +### `MCPServer.call_tool()` return type corrected + +`MCPServer.call_tool()`'s return type signature has been corrected from `Sequence[ContentBlock] | dict[str, Any]` to `CallToolResult | Sequence[ContentBlock] | tuple[Sequence[ContentBlock], dict[str, Any]]` to match what the internal tool manager actually returns when converting tool results. + +**Before (v1):** + +```python +async def call_tool( + self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None +) -> Sequence[ContentBlock] | dict[str, Any]: +``` + +**After (v2):** + +```python +async def call_tool( + self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None +) -> CallToolResult | Sequence[ContentBlock] | tuple[Sequence[ContentBlock], dict[str, Any]]: +``` + ### Registering lowlevel handlers on `MCPServer` (workaround) `MCPServer` does not expose public APIs for `subscribe_resource`, `unsubscribe_resource`, or `set_logging_level` handlers. In v1, the workaround was to reach into the private lowlevel server and use its decorator methods: diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index b3471163b7..e850ac7914 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -4,7 +4,6 @@ import base64 import inspect -import json import re from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence from contextlib import AbstractAsyncContextManager, asynccontextmanager @@ -322,14 +321,6 @@ async def _handle_call_tool( 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, - ) return CallToolResult(content=list(result)) async def _handle_list_resources( @@ -399,8 +390,15 @@ 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]: - """Call a tool by name with arguments.""" + ) -> CallToolResult | Sequence[ContentBlock] | tuple[Sequence[ContentBlock], dict[str, Any]]: + """Call a tool by name with arguments. + + Returns: + CallToolResult: If the tool returned a CallToolResult directly. + Sequence[ContentBlock]: If the tool returned unstructured content and has no output schema. + tuple[Sequence[ContentBlock], dict[str, Any]]: If the tool has an output schema, + returning both unstructured content and structured content. + """ if context is None: context = Context(mcp_server=self) return await self._tool_manager.call_tool(name, arguments, context, convert_result=True)