feat: Add structured output support for tool functions (#993)

This commit is contained in:
bhosmer-ant 2025-06-26 04:36:17 -04:00 • committed by GitHub
commit 43bb24f62b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1774 additions and 61 deletions

View file

@ -3,6 +3,7 @@ from __future__ import annotations as _annotations
import functools
import inspect
from collections.abc import Callable
from functools import cached_property
from typing import TYPE_CHECKING, Any, get_origin
from pydantic import BaseModel, Field
@ -32,6 +33,10 @@ class Tool(BaseModel):
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
annotations: ToolAnnotations | None = Field(None, description="Optional annotations for the tool")
@cached_property
def output_schema(self) -> dict[str, Any] | None:
return self.fn_metadata.output_schema
@classmethod
def from_function(
cls,
@ -41,6 +46,7 @@ class Tool(BaseModel):
description: str | None = None,
context_kwarg: str | None = None,
annotations: ToolAnnotations | None = None,
structured_output: bool | None = None,
) -> Tool:
"""Create a Tool from a function."""
from mcp.server.fastmcp.server import Context
@ -65,6 +71,7 @@ class Tool(BaseModel):
func_arg_metadata = func_metadata(
fn,
skip_names=[context_kwarg] if context_kwarg is not None else [],
structured_output=structured_output,
)
parameters = func_arg_metadata.arg_model.model_json_schema()
@ -84,15 +91,21 @@ class Tool(BaseModel):
self,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
convert_result: bool = False,
) -> Any:
"""Run the tool with arguments."""
try:
return await self.fn_metadata.call_fn_with_arg_validation(
result = await self.fn_metadata.call_fn_with_arg_validation(
self.fn,
self.is_async,
arguments,
{self.context_kwarg: context} if self.context_kwarg is not None else None,
)
if convert_result:
result = self.fn_metadata.convert_result(result)
return result
except Exception as e:
raise ToolError(f"Error executing tool {self.name}: {e}") from e

View file

@ -49,9 +49,17 @@ class ToolManager:
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
structured_output: bool | None = None,
) -> Tool:
"""Add a tool to the server."""
tool = Tool.from_function(fn, name=name, title=title, description=description, annotations=annotations)
tool = Tool.from_function(
fn,
name=name,
title=title,
description=description,
annotations=annotations,
structured_output=structured_output,
)
existing = self._tools.get(tool.name)
if existing:
if self.warn_on_duplicate_tools:
@ -65,10 +73,11 @@ class ToolManager:
name: str,
arguments: dict[str, Any],
context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
convert_result: bool = False,
) -> Any:
"""Call a tool by name with arguments."""
tool = self.get_tool(name)
if not tool:
raise ToolError(f"Unknown tool: {name}")
return await tool.run(arguments, context=context)
return await tool.run(arguments, context=context, convert_result=convert_result)