feat: Add structured output support for tool functions (#993)
This commit is contained in:
parent
c8bbfc034d
commit
43bb24f62b
1 changed files with 1774 additions and 61 deletions
|
|
@ -9,7 +9,6 @@ from contextlib import (
|
|||
AbstractAsyncContextManager,
|
||||
asynccontextmanager,
|
||||
)
|
||||
from itertools import chain
|
||||
from typing import Any, Generic, Literal
|
||||
|
||||
import anyio
|
||||
|
|
@ -38,7 +37,6 @@ from mcp.server.fastmcp.prompts import Prompt, PromptManager
|
|||
from mcp.server.fastmcp.resources import FunctionResource, Resource, ResourceManager
|
||||
from mcp.server.fastmcp.tools import Tool, ToolManager
|
||||
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
|
||||
from mcp.server.fastmcp.utilities.types import Image
|
||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||
from mcp.server.lowlevel.server import LifespanResultT
|
||||
from mcp.server.lowlevel.server import Server as MCPServer
|
||||
|
|
@ -54,7 +52,6 @@ from mcp.types import (
|
|||
AnyFunction,
|
||||
ContentBlock,
|
||||
GetPromptResult,
|
||||
TextContent,
|
||||
ToolAnnotations,
|
||||
)
|
||||
from mcp.types import Prompt as MCPPrompt
|
||||
|
|
@ -235,7 +232,10 @@ class FastMCP:
|
|||
def _setup_handlers(self) -> None:
|
||||
"""Set up core MCP protocol handlers."""
|
||||
self._mcp_server.list_tools()(self.list_tools)
|
||||
self._mcp_server.call_tool()(self.call_tool)
|
||||
# Note: we disable the lowlevel server's input validation.
|
||||
# FastMCP does ad hoc conversion of incoming data before validating -
|
||||
# for now we preserve this for backwards compatibility.
|
||||
self._mcp_server.call_tool(validate_input=False)(self.call_tool)
|
||||
self._mcp_server.list_resources()(self.list_resources)
|
||||
self._mcp_server.read_resource()(self.read_resource)
|
||||
self._mcp_server.list_prompts()(self.list_prompts)
|
||||
|
|
@ -251,6 +251,7 @@ class FastMCP:
|
|||
title=info.title,
|
||||
description=info.description,
|
||||
inputSchema=info.parameters,
|
||||
outputSchema=info.output_schema,
|
||||
annotations=info.annotations,
|
||||
)
|
||||
for info in tools
|
||||
|
|
@ -267,12 +268,10 @@ class FastMCP:
|
|||
request_context = None
|
||||
return Context(request_context=request_context, fastmcp=self)
|
||||
|
||||
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock]:
|
||||
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock] | dict[str, Any]:
|
||||
"""Call a tool by name with arguments."""
|
||||
context = self.get_context()
|
||||
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
||||
converted_result = _convert_to_content(result)
|
||||
return converted_result
|
||||
return await self._tool_manager.call_tool(name, arguments, context=context, convert_result=True)
|
||||
|
||||
async def list_resources(self) -> list[MCPResource]:
|
||||
"""List all available resources."""
|
||||
|
|
@ -322,6 +321,7 @@ class FastMCP:
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
structured_output: bool | None = None,
|
||||
) -> None:
|
||||
"""Add a tool to the server.
|
||||
|
||||
|
|
@ -334,8 +334,19 @@ class FastMCP:
|
|||
title: Optional human-readable title for the tool
|
||||
description: Optional description of what the tool does
|
||||
annotations: Optional ToolAnnotations providing additional tool information
|
||||
structured_output: Controls whether the tool's output is structured or unstructured
|
||||
- If None, auto-detects based on the function's return type annotation
|
||||
- If True, unconditionally creates a structured tool (return type annotation permitting)
|
||||
- If False, unconditionally creates an unstructured tool
|
||||
"""
|
||||
self._tool_manager.add_tool(fn, name=name, title=title, description=description, annotations=annotations)
|
||||
self._tool_manager.add_tool(
|
||||
fn,
|
||||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
annotations=annotations,
|
||||
structured_output=structured_output,
|
||||
)
|
||||
|
||||
def tool(
|
||||
self,
|
||||
|
|
@ -343,6 +354,7 @@ class FastMCP:
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
structured_output: bool | None = None,
|
||||
) -> Callable[[AnyFunction], AnyFunction]:
|
||||
"""Decorator to register a tool.
|
||||
|
||||
|
|
@ -355,6 +367,10 @@ class FastMCP:
|
|||
title: Optional human-readable title for the tool
|
||||
description: Optional description of what the tool does
|
||||
annotations: Optional ToolAnnotations providing additional tool information
|
||||
structured_output: Controls whether the tool's output is structured or unstructured
|
||||
- If None, auto-detects based on the function's return type annotation
|
||||
- If True, unconditionally creates a structured tool (return type annotation permitting)
|
||||
- If False, unconditionally creates an unstructured tool
|
||||
|
||||
Example:
|
||||
@server.tool()
|
||||
|
|
@ -378,7 +394,14 @@ class FastMCP:
|
|||
)
|
||||
|
||||
def decorator(fn: AnyFunction) -> AnyFunction:
|
||||
self.add_tool(fn, name=name, title=title, description=description, annotations=annotations)
|
||||
self.add_tool(
|
||||
fn,
|
||||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
annotations=annotations,
|
||||
structured_output=structured_output,
|
||||
)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
|
@ -942,28 +965,6 @@ class FastMCP:
|
|||
raise ValueError(str(e))
|
||||
|
||||
|
||||
def _convert_to_content(
|
||||
result: Any,
|
||||
) -> Sequence[ContentBlock]:
|
||||
"""Convert a result to a sequence of content objects."""
|
||||
if result is None:
|
||||
return []
|
||||
|
||||
if isinstance(result, ContentBlock):
|
||||
return [result]
|
||||
|
||||
if isinstance(result, Image):
|
||||
return [result.to_image_content()]
|
||||
|
||||
if isinstance(result, list | tuple):
|
||||
return list(chain.from_iterable(_convert_to_content(item) for item in result)) # type: ignore[reportUnknownVariableType]
|
||||
|
||||
if not isinstance(result, str):
|
||||
result = pydantic_core.to_json(result, fallback=str, indent=2).decode()
|
||||
|
||||
return [TextContent(type="text", text=result)]
|
||||
|
||||
|
||||
class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
||||
"""Context object providing access to MCP capabilities.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue