From 4419e661d07029d61d71ca083e5d504f7e6eaf8d Mon Sep 17 00:00:00 2001 From: rain <1504569896@qq.com> Date: Sat, 26 Sep 2026 18:29:46 +0800 Subject: [PATCH] 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 --- .../mcpserver/utilities/func_metadata.py | 11 ++++- tests/server/mcpserver/test_func_metadata.py | 45 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 0ffac07c4e..0ccbb58666 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -16,6 +16,7 @@ ConfigDict, Field, PrivateAttr, + PydanticSchemaGenerationError, PydanticUserError, TypeAdapter, WithJsonSchema, @@ -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: diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index dba0637ded..62b8a328a2 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -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 @@ -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"""