Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/mcp/server/mcpserver/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ConfigDict,
Field,
PrivateAttr,
PydanticSchemaGenerationError,
PydanticUserError,
TypeAdapter,
WithJsonSchema,
Expand Down Expand Up @@ -441,7 +442,15 @@ def func_metadata(
# structured_output=True still forces one.
return FuncMetadata(arg_model=arguments_model)

output_model, wrap_output = _create_output_model(original_annotation, return_type_expr, func.__name__)
try:
output_model, wrap_output = _create_output_model(original_annotation, return_type_expr, func.__name__)
except PydanticSchemaGenerationError as e:
# Picking the output model builds throwaway pydantic models for unsupported shapes (e.g.
# Iterator[str] wrapped in a {"result": ...} model), which can fail the same way the
# FuncMetadata construction below can. Degrade to the unstructured fallback instead of
# letting the error escape registration.
logger.info(f"Cannot create schema for type {return_type_expr} in {func.__name__}: {type(e).__name__}: {e}")
output_model, wrap_output = None, False

if output_model is not None:
try:
Expand Down
45 changes: 44 additions & 1 deletion tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
# pyright: reportMissingParameterType=false
# pyright: reportUnknownArgumentType=false
# pyright: reportUnknownLambdaType=false
from collections.abc import Callable
import typing
from collections.abc import AsyncIterator, Callable, Generator, Iterator
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Any, Final, NamedTuple, TypedDict

Expand Down Expand Up @@ -1230,6 +1231,48 @@ def tree() -> Node:
assert result.structured_content == {"name": "root", "children": [{"name": "leaf", "children": []}]}


def test_iterator_return_annotations_register_as_unstructured():
"""Iterator/AsyncIterator return annotations take the unstructured fallback instead of crashing.

pydantic cannot build a schema for these annotations, which used to escape func_metadata as an
uncaught PydanticSchemaGenerationError at registration time (both for the default and for
structured_output=True, which must raise InvalidSignature instead).
"""

def gen_typing(n: int) -> typing.Iterator[str]:
yield from ["a"] * n

def gen_collections(n: int) -> Iterator[str]:
yield from ["a"] * n

def async_gen_typing(n: int) -> typing.AsyncIterator[str]:
yield "a"

def async_gen_collections(n: int) -> AsyncIterator[str]:
yield "a"

for fn in (gen_typing, gen_collections, async_gen_typing, async_gen_collections):
meta = func_metadata(fn)
assert meta.output_schema is None
assert meta.output_model is None

with pytest.raises(InvalidSignature) as exc_info:
func_metadata(fn, structured_output=True)
assert "is not serializable for structured output" in str(exc_info.value)
assert fn.__name__ in str(exc_info.value)


def test_generator_return_annotation_keeps_structured_output():
"""Generator return annotations are sequence-like for pydantic and must keep their structured schema."""

def gen(n: int) -> Generator[str, None, None]:
yield from ["a"] * n

meta = func_metadata(gen)
assert meta.output_schema is not None
assert meta.output_schema["properties"]["result"]["type"] == "array"


def test_structured_output_unserializable_type_error():
"""Test error when structured_output=True is used with unserializable types"""

Expand Down
Loading