Skip to content

Commit 4419e66

Browse files
committed
Fall back to unstructured output when output model selection fails
Picking the output model builds throwaway pydantic models for the return type, which raises PydanticSchemaGenerationError for annotations pydantic cannot model, such as Iterator[str] or AsyncIterator[str]. That call sat outside the try/except that routes expected schema failures to the unstructured fallback, so the error escaped func_metadata and a properly annotated generator tool could not register at all. With structured_output=True the existing flow raises InvalidSignature instead. Fixes #3573
1 parent f1b6589 commit 4419e66

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

‎src/mcp/server/mcpserver/utilities/func_metadata.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
ConfigDict,
1717
Field,
1818
PrivateAttr,
19+
PydanticSchemaGenerationError,
1920
PydanticUserError,
2021
TypeAdapter,
2122
WithJsonSchema,
@@ -441,7 +442,15 @@ def func_metadata(
441442
# structured_output=True still forces one.
442443
return FuncMetadata(arg_model=arguments_model)
443444

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

446455
if output_model is not None:
447456
try:

‎tests/server/mcpserver/test_func_metadata.py‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
# pyright: reportMissingParameterType=false
44
# pyright: reportUnknownArgumentType=false
55
# pyright: reportUnknownLambdaType=false
6-
from collections.abc import Callable
6+
import typing
7+
from collections.abc import AsyncIterator, Callable, Generator, Iterator
78
from dataclasses import dataclass
89
from typing import TYPE_CHECKING, Annotated, Any, Final, NamedTuple, TypedDict
910

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

12321233

1234+
def test_iterator_return_annotations_register_as_unstructured():
1235+
"""Iterator/AsyncIterator return annotations take the unstructured fallback instead of crashing.
1236+
1237+
pydantic cannot build a schema for these annotations, which used to escape func_metadata as an
1238+
uncaught PydanticSchemaGenerationError at registration time (both for the default and for
1239+
structured_output=True, which must raise InvalidSignature instead).
1240+
"""
1241+
1242+
def gen_typing(n: int) -> typing.Iterator[str]:
1243+
yield from ["a"] * n
1244+
1245+
def gen_collections(n: int) -> Iterator[str]:
1246+
yield from ["a"] * n
1247+
1248+
def async_gen_typing(n: int) -> typing.AsyncIterator[str]:
1249+
yield "a"
1250+
1251+
def async_gen_collections(n: int) -> AsyncIterator[str]:
1252+
yield "a"
1253+
1254+
for fn in (gen_typing, gen_collections, async_gen_typing, async_gen_collections):
1255+
meta = func_metadata(fn)
1256+
assert meta.output_schema is None
1257+
assert meta.output_model is None
1258+
1259+
with pytest.raises(InvalidSignature) as exc_info:
1260+
func_metadata(fn, structured_output=True)
1261+
assert "is not serializable for structured output" in str(exc_info.value)
1262+
assert fn.__name__ in str(exc_info.value)
1263+
1264+
1265+
def test_generator_return_annotation_keeps_structured_output():
1266+
"""Generator return annotations are sequence-like for pydantic and must keep their structured schema."""
1267+
1268+
def gen(n: int) -> Generator[str, None, None]:
1269+
yield from ["a"] * n
1270+
1271+
meta = func_metadata(gen)
1272+
assert meta.output_schema is not None
1273+
assert meta.output_schema["properties"]["result"]["type"] == "array"
1274+
1275+
12331276
def test_structured_output_unserializable_type_error():
12341277
"""Test error when structured_output=True is used with unserializable types"""
12351278

0 commit comments

Comments
 (0)