Skip to content

Commit d40d00e

Browse files
committed
fix: tighten mcpserver tool result typing
1 parent ac36a39 commit d40d00e

2 files changed

Lines changed: 23 additions & 12 deletions

File tree

src/mcp/server/mcpserver/server.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import base64
66
import inspect
7-
import json
87
import re
98
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
109
from contextlib import AbstractAsyncContextManager, asynccontextmanager
@@ -322,14 +321,6 @@ async def _handle_call_tool(
322321
content=list(unstructured_content), # type: ignore[arg-type]
323322
structured_content=structured_content, # type: ignore[arg-type]
324323
)
325-
if isinstance(result, dict): # pragma: no cover
326-
# TODO: this code path is unreachable — convert_result never returns a raw dict.
327-
# The call_tool return type (Sequence[ContentBlock] | dict[str, Any]) is wrong
328-
# and needs to be cleaned up.
329-
return CallToolResult(
330-
content=[TextContent(type="text", text=json.dumps(result, indent=2))],
331-
structured_content=result,
332-
)
333324
return CallToolResult(content=list(result))
334325

335326
async def _handle_list_resources(
@@ -399,7 +390,7 @@ async def list_tools(self) -> list[MCPTool]:
399390

400391
async def call_tool(
401392
self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None
402-
) -> Sequence[ContentBlock] | dict[str, Any]:
393+
) -> CallToolResult | Sequence[ContentBlock] | tuple[Sequence[ContentBlock], dict[str, Any]]:
403394
"""Call a tool by name with arguments."""
404395
if context is None:
405396
context = Context(mcp_server=self)

tests/server/mcpserver/test_func_metadata.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from pydantic import BaseModel, Field
1414

1515
from mcp.server.mcpserver.exceptions import InvalidSignature
16-
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
17-
from mcp.types import CallToolResult
16+
from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, func_metadata
17+
from mcp.types import CallToolResult, TextContent
1818

1919

2020
class SomeInputModelA(BaseModel):
@@ -203,6 +203,26 @@ def func_with_many_params(keep_this: int, skip_this: str, also_keep: float, also
203203
assert model.also_keep == 2.5 # type: ignore
204204

205205

206+
def test_convert_result_serializes_unstructured_dict_to_text_content():
207+
"""Unstructured dict returns are content blocks, not raw dict values."""
208+
209+
def func_dict(): # pragma: no cover
210+
return {"ok": True, "count": 2}
211+
212+
meta: FuncMetadata = func_metadata(func_dict)
213+
214+
assert meta.output_schema is None
215+
assert meta.convert_result({"ok": True, "count": 2}) == [
216+
TextContent(
217+
type="text",
218+
text="""{
219+
"ok": true,
220+
"count": 2
221+
}""",
222+
)
223+
]
224+
225+
206226
def test_structured_output_dict_str_types():
207227
"""Test that dict[str, T] types are handled without wrapping."""
208228

0 commit comments

Comments
 (0)