Add output schema to tool decorator

This commit is contained in:
Jeremiah Lowin 2025-06-20 18:58:23 -04:00
commit cc69e56758
3 changed files with 120 additions and 0 deletions

View file

@ -768,6 +768,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None = None,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
enabled: bool | None = None,
@ -781,6 +782,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None = None,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
enabled: bool | None = None,
@ -793,6 +795,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None = None,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
enabled: bool | None = None,
@ -815,6 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
name: Optional name for the tool (keyword-only, alternative to name_or_fn)
description: Optional description of what the tool does
tags: Optional set of tags for categorizing the tool
output_schema: Optional JSON schema for the tool's output
annotations: Optional annotations about the tool's behavior (e.g. {"is_async": True})
exclude_args: Optional list of argument names to exclude from the tool schema
enabled: Optional boolean to enable or disable the tool
@ -867,6 +871,7 @@ class FastMCP(Generic[LifespanResultT]):
name=tool_name,
description=description,
tags=tags,
output_schema=output_schema,
annotations=annotations,
exclude_args=exclude_args,
serializer=self._tool_serializer,
@ -897,6 +902,7 @@ class FastMCP(Generic[LifespanResultT]):
name=tool_name,
description=description,
tags=tags,
output_schema=output_schema,
annotations=annotations,
exclude_args=exclude_args,
enabled=enabled,

View file

@ -390,6 +390,15 @@ class TestToolDecorator:
def my_function(x: int) -> str:
return f"Result: {x}"
async def test_tool_decorator_with_output_schema(self):
mcp = FastMCP()
@mcp.tool(output_schema={"type": "integer"})
def my_function(x: int) -> str:
return f"Result: {x}"
assert my_function.output_schema == {"type": "integer"}
class TestResourceDecorator:
async def test_no_resources_before_decorator(self):

View file

@ -369,6 +369,111 @@ class TestToolFromFunctionOutputSchema:
tool = Tool.from_function(func)
assert tool.output_schema is None
async def test_provided_output_schema_takes_precedence_over_json_compatible_annotation(
self,
):
"""Test that provided output_schema takes precedence over inferred schema from JSON-compatible annotation."""
def func() -> dict[str, int]:
return {"a": 1, "b": 2}
# Provide a custom output schema that differs from the inferred one
custom_schema = {"type": "string", "description": "Custom schema"}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
async def test_provided_output_schema_takes_precedence_over_complex_annotation(
self,
):
"""Test that provided output_schema takes precedence over inferred schema from complex annotation."""
def func() -> list[dict[str, int | float]]:
return [{"a": 1, "b": 2.5}]
# Provide a custom output schema that differs from the inferred one
custom_schema = {"type": "object", "properties": {"custom": {"type": "string"}}}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
async def test_provided_output_schema_takes_precedence_over_unserializable_annotation(
self,
):
"""Test that provided output_schema takes precedence over None schema from unserializable annotation."""
class Unserializable:
def __init__(self, data: Any):
self.data = data
def func() -> Unserializable:
return Unserializable(data="test")
# Provide a custom output schema even though the annotation is unserializable
custom_schema = {"type": "array", "items": {"type": "string"}}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
async def test_provided_output_schema_takes_precedence_over_no_annotation(self):
"""Test that provided output_schema takes precedence over None schema from no annotation."""
def func():
return "hello"
# Provide a custom output schema even though there's no return annotation
custom_schema = {"type": "number", "minimum": 0}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
async def test_provided_output_schema_takes_precedence_over_converted_annotation(
self,
):
"""Test that provided output_schema takes precedence over converted schema from Image/Audio/File annotations."""
def func() -> Image:
return Image(data=b"test")
# Provide a custom output schema that differs from the converted ImageContent schema
custom_schema = {
"type": "object",
"properties": {"custom_image": {"type": "string"}},
}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
async def test_provided_output_schema_takes_precedence_over_union_annotation(self):
"""Test that provided output_schema takes precedence over inferred schema from union annotation."""
def func() -> str | int | None:
return "hello"
# Provide a custom output schema that differs from the inferred union schema
custom_schema = {"type": "boolean"}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
async def test_provided_output_schema_takes_precedence_over_pydantic_annotation(
self,
):
"""Test that provided output_schema takes precedence over inferred schema from Pydantic model annotation."""
class Person(BaseModel):
name: str
age: int
def func() -> Person:
return Person(name="John", age=30)
# Provide a custom output schema that differs from the inferred Person schema
custom_schema = {"type": "array", "items": {"type": "number"}}
tool = Tool.from_function(func, output_schema=custom_schema)
assert tool.output_schema == custom_schema
class TestLegacyToolJsonParsing:
"""Tests for Tool's JSON pre-parsing functionality."""