Skip to content

Commit 42562fc

Browse files
committed
we can use Annotated to still support output schemas
1 parent 37df670 commit 42562fc

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

examples/fastmcp/direct_call_tool_result_return.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22
FastMCP Echo Server with direct CallToolResult return
33
"""
44

5+
from typing import Annotated
6+
7+
from pydantic import BaseModel
8+
59
from mcp.server.fastmcp import FastMCP
610
from mcp.types import CallToolResult, TextContent
711

812
mcp = FastMCP("Echo Server")
913

1014

15+
class EchoResponse(BaseModel):
16+
text: str
17+
18+
1119
@mcp.tool()
12-
def echo(text: str) -> CallToolResult:
20+
def echo(text: str) -> Annotated[CallToolResult, EchoResponse]:
1321
"""Echo the input text with structure and metadata"""
1422
return CallToolResult(
1523
content=[TextContent(type="text", text=text)], structuredContent={"text": text}, _meta={"some": "metadata"}

src/mcp/server/fastmcp/utilities/func_metadata.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ def convert_result(self, result: Any) -> Any:
105105
the structured output.
106106
"""
107107
if isinstance(result, CallToolResult):
108+
if self.output_schema is not None:
109+
assert self.output_model is not None, "Output model must be set if output schema is defined"
110+
self.output_model.model_validate(result.structuredContent)
108111
return result
109112

110113
unstructured_content = _convert_to_content(result)
@@ -271,9 +274,13 @@ def func_metadata(
271274
output_info = FieldInfo.from_annotation(_get_typed_annotation(sig.return_annotation, globalns))
272275
annotation = output_info.annotation
273276

274-
# if the typehint is CallToolResult, the user intends to return it directly without validation
277+
# if the typehint is CallToolResult, the user either intends to return without validation
278+
# or they provided validation as Annotated metadata
275279
if isinstance(annotation, type) and issubclass(annotation, CallToolResult):
276-
return FuncMetadata(arg_model=arguments_model)
280+
if output_info.metadata:
281+
annotation = output_info.metadata[0]
282+
else:
283+
return FuncMetadata(arg_model=arguments_model)
277284

278285
output_model, output_schema, wrap_output = _try_create_model_and_schema(annotation, func.__name__, output_info)
279286

tests/server/fastmcp/test_func_metadata.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,39 @@ def func_returning_call_tool_result() -> CallToolResult:
845845
assert isinstance(meta.convert_result(func_returning_call_tool_result()), CallToolResult)
846846

847847

848+
def test_tool_call_result_annotated_is_structured_and_converted():
849+
class PersonClass(BaseModel):
850+
name: str
851+
852+
def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]:
853+
return CallToolResult(content=[], structuredContent={"name": "Brandon"})
854+
855+
meta = func_metadata(func_returning_annotated_tool_call_result)
856+
857+
assert meta.output_schema == {
858+
"type": "object",
859+
"properties": {
860+
"name": {"title": "Name", "type": "string"},
861+
},
862+
"required": ["name"],
863+
"title": "PersonClass",
864+
}
865+
assert isinstance(meta.convert_result(func_returning_annotated_tool_call_result()), CallToolResult)
866+
867+
868+
def test_tool_call_result_annotated_is_structured_and_invalid():
869+
class PersonClass(BaseModel):
870+
name: str
871+
872+
def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]:
873+
return CallToolResult(content=[], structuredContent={"person": "Brandon"})
874+
875+
meta = func_metadata(func_returning_annotated_tool_call_result)
876+
877+
with pytest.raises(ValueError):
878+
meta.convert_result(func_returning_annotated_tool_call_result())
879+
880+
848881
def test_structured_output_with_field_descriptions():
849882
"""Test that Field descriptions are preserved in structured output"""
850883

0 commit comments

Comments
 (0)