fix: correct MCPServer call_tool result type (#2816)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
parent
f253682393
commit
fda4c54362
2 changed files with 63 additions and 45 deletions
|
|
@ -4,9 +4,8 @@ from __future__ import annotations
|
|||
|
||||
import base64
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||
from typing import Any, Generic, Literal, TypeVar, overload
|
||||
|
||||
|
|
@ -53,7 +52,6 @@ from mcp.types import (
|
|||
CompleteRequestParams,
|
||||
CompleteResult,
|
||||
Completion,
|
||||
ContentBlock,
|
||||
GetPromptRequestParams,
|
||||
GetPromptResult,
|
||||
Icon,
|
||||
|
|
@ -311,28 +309,11 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
) -> CallToolResult:
|
||||
context = Context(request_context=ctx, mcp_server=self)
|
||||
try:
|
||||
result = await self.call_tool(params.name, params.arguments or {}, context)
|
||||
return await self.call_tool(params.name, params.arguments or {}, context)
|
||||
except MCPError:
|
||||
raise
|
||||
except Exception as e:
|
||||
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.
|
||||
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(
|
||||
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
|
||||
|
|
@ -406,7 +387,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
|
||||
async def call_tool(
|
||||
self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None
|
||||
) -> Sequence[ContentBlock] | dict[str, Any]:
|
||||
) -> CallToolResult:
|
||||
"""Call a tool by name with arguments."""
|
||||
if context is None:
|
||||
context = Context(mcp_server=self)
|
||||
|
|
|
|||
|
|
@ -88,14 +88,10 @@ class FuncMetadata(BaseModel):
|
|||
else:
|
||||
return await anyio.to_thread.run_sync(functools.partial(fn, **arguments_parsed_dict))
|
||||
|
||||
def convert_result(self, result: Any) -> Any:
|
||||
"""Convert a function call result to the format for the lowlevel tool call handler.
|
||||
def convert_result(self, result: Any) -> CallToolResult:
|
||||
"""Convert a function call result into a `CallToolResult`.
|
||||
|
||||
- If output_model is None, return the unstructured content directly.
|
||||
- If output_model is not None, convert the result to structured output format
|
||||
(dict[str, Any]) and return both unstructured and structured content.
|
||||
|
||||
Note: we return unstructured content here **even though the lowlevel server
|
||||
Note: we build unstructured content here **even though the lowlevel server
|
||||
tool call handler provides generic backwards compatibility serialization of
|
||||
structured content**. This is for MCPServer backwards compatibility: we need to
|
||||
retain MCPServer's ad hoc conversion logic for constructing unstructured output
|
||||
|
|
@ -111,16 +107,16 @@ class FuncMetadata(BaseModel):
|
|||
unstructured_content = _convert_to_content(result)
|
||||
|
||||
if self.output_schema is None:
|
||||
return unstructured_content
|
||||
else:
|
||||
if self.wrap_output:
|
||||
result = {"result": result}
|
||||
return CallToolResult(content=unstructured_content)
|
||||
|
||||
assert self.output_model is not None, "Output model must be set if output schema is defined"
|
||||
validated = self.output_model.model_validate(result)
|
||||
structured_content = validated.model_dump(mode="json", by_alias=True)
|
||||
if self.wrap_output:
|
||||
result = {"result": result}
|
||||
|
||||
return (unstructured_content, structured_content)
|
||||
assert self.output_model is not None, "Output model must be set if output schema is defined"
|
||||
validated = self.output_model.model_validate(result)
|
||||
structured_content = validated.model_dump(mode="json", by_alias=True)
|
||||
|
||||
return CallToolResult(content=unstructured_content, structured_content=structured_content)
|
||||
|
||||
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Pre-parse data from JSON.
|
||||
|
|
@ -496,7 +492,7 @@ def _create_dict_model(func_name: str, dict_annotation: Any) -> type[BaseModel]:
|
|||
return DictModel
|
||||
|
||||
|
||||
def _convert_to_content(result: Any) -> Sequence[ContentBlock]:
|
||||
def _convert_to_content(result: Any) -> list[ContentBlock]:
|
||||
"""Convert a result to a sequence of content objects.
|
||||
|
||||
Note: This conversion logic comes from previous versions of MCPServer and is being
|
||||
|
|
|
|||
Loading…
Reference in a new issue