Skip to content
Open
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
24 changes: 9 additions & 15 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, TypeVar, overload
from typing import Any, Generic, Literal, TypeAlias, TypeVar, cast, overload

import anyio
import pydantic_core
Expand Down Expand Up @@ -75,6 +74,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]):
Expand Down Expand Up @@ -316,19 +318,11 @@ 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
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.
if isinstance(result, tuple):
unstructured_content, structured_content = cast(tuple[Sequence[ContentBlock], dict[str, Any]], result)
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))

Expand Down Expand Up @@ -399,7 +393,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)
Expand Down
17 changes: 17 additions & 0 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
Loading