Repository navigation
func_metadata raises uncaught PydanticSchemaGenerationError for Iterator/AsyncIterator tool return annotations instead of the unstructured fallback #3573
Description
Activity
Confirmed on mcp 2.2.0 — all three registration shapes raise the raw
pydantic.errors.PydanticSchemaGenerationErrorforcollections.abc.Iterator[str]/AsyncIterator[str]returns (func_metadata(search),func_metadata(search, structured_output=True), and theTool.from_functionpath).The traceback pins exactly where the intended fallback is defeated. In
mcp/server/mcpserver/utilities/func_metadata.py:line 444, in func_metadata -> output_model, wrap_output = _create_output_model(...) line 518, in _create_output_model -> model = _create_wrapped_model(func_name, original_annotation) line 621, in _create_wrapped_model -> create_model(... result: Iterator[str] ...) # raisesTwo things make this a straightforward fix:
-
The raise happens at L444, outside the guarded region. The
try/exceptat L447-469 exists precisely to turn unsupported return types into a graceful outcome, andPydanticSchemaGenerationError(aPydanticUserErrorsubclass on pydantic 2.13) is already covered conceptually by its caught types — but_create_output_modelruns before thetry, so_create_wrapped_model'screate_modelcall escapes unprotected. Both intended behaviors are defeated by the same escape: the unstructured fallback (return FuncMetadata(arg_model=arguments_model), L477) and, forstructured_output=True, the properInvalidSignature(L471-475). -
Iterator returns aren't recognized as content-iterators. The early-out at L437 (
if structured_output is None and _returns_content(return_type_expr)) returns an unstructuredFuncMetadatafor annotations the model reads as content — but it doesn't match the PEP 484 generator spellingsIterator[...]/AsyncIterator[...], so they fall through into schema construction instead.
Wrapping the
_create_output_modelcall in the existing except handling would restore both documented behaviors with one change; optionally treatingIterator/AsyncIteratororigins in_returns_contentwould give generator tools the same unstructured default as content-block returns.-
- addedv2Affects the v2 line (2.x on main)Affects the v2 line (2.x on main)v1Affects the v1.x maintenance lineAffects the v1.x maintenance line
on Sep 23, 2026 Reproduced this. Root cause: src/mcp/server/mcpserver/utilities/func_metadata.py:444 calls _create_output_model outside the try/except at lines 446-470 that reports unserializable return types at registration, so the PydanticSchemaGenerationError it raises for Iterator/AsyncIterator escapes func_metadata uncaught.
Minimal reproduction:
from typing import Iterator from mcp.server.mcpserver.tools import Tool from mcp.server.mcpserver.utilities.func_metadata import func_metadata def search(n: int) -> Iterator[str]: yield from ["a"] * n for label, call in [ ("func_metadata(search)", lambda: func_metadata(search)), ("func_metadata(search, structured_output=True)", lambda: func_metadata(search, structured_output=True)), ("Tool.from_function(search)", lambda: Tool.from_function(search)), ]: try: call() print(f"{label}: OK") except Exception as e: print(f"{label}: UNCAUGHT {type(e).__module__}.{type(e).__name__}: {str(e)[:80]}") # AsyncIterator[str] return annotations behave identicallyObserved output:
func_metadata(search): UNCAUGHT pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for typing.Iterator[str]. Set `arbitrary func_metadata(search, structured_output=True): UNCAUGHT pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for typing.Iterator[str]. Set `arbitrary Tool.from_function(search): UNCAUGHT pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for typing.Iterator[str]. Set `arbitraryFix is ready on
BlueX888:fix/prep-func-metadata-iterator-return-uncaught-pydantic-error(commit2f4ce1ae). Moves the _create_output_model call inside that existing try/except, so Iterator/AsyncIterator returns follow the documented contract: no output schema by default, InvalidSignature with structured_output=True.
Verified with:uv run --frozen pytest tests/server/mcpserver/test_func_metadata.pyHappy to open the PR once you confirm the approach or assign this to me.
Confirmed on mcp 2.2.0 with the v2 API (
MCPServer): both the default path andstructured_output=Trueraise the rawPydanticSchemaGenerationErrorat registration for a-> Iterator[str]tool — the generator tool simply cannot be registered at all.Minimal repro:
from mcp.server.mcpserver import MCPServer from typing import Iterator server = MCPServer("repro") @server.tool() def search(query: str) -> Iterator[str]: """Search things.""" yield "a"
→
PydanticSchemaGenerationError: Unable to generate pydantic-core schema for typing.Iterator[str]. Same forstructured_output=True.The contract says the default should fall back to an unstructured tool (
structured_output=None), andstructured_output=Trueshould raise the SDK's ownInvalidSignature— a raw pydantic error escapes both. Root-cause candidate:func_metadata's return-type schema generation treatsIterator/AsyncIteratoras a schema-able output type instead of short-circuiting to the generator/unstructured path first.Happy to open a PR with a failing test first (both spellings must register without raising;
structured_output=Truemust surfaceInvalidSignature) plus the narrow fix, if a maintainer assigns the issue — the repo's bot auto-closes non-assignee PRs, so I'm holding the branch until then.Affiliation: we ship a 140-tool MCP server and hit this exact class of annotation edge case in our own tool registry.
Confirmed on Windows 11 / Python 3.13 / pydantic 2.12.5 against
main(f1b6589) — all four spellings crash at registration, with both the default andstructured_output=True:from typing import Iterator from mcp.server.mcpserver.utilities.func_metadata import func_metadata def search(n: int) -> Iterator[str]: yield from ["a"] * n func_metadata(search) # pydantic.errors.PydanticSchemaGenerationError
The escape point matches @BlueX888's analysis: the
_create_output_model()call (func_metadata.py:444on main) sits outside thetryat :446 that routes expected schema failures to the unstructured fallback, so thePydanticSchemaGenerationErrorraised bycreate_model(..., result=Iterator[str])inside_create_wrapped_model()escapesfunc_metadataentirely.The fix that keeps the existing semantics is to route that call through the same failure path: on
PydanticSchemaGenerationError, degrade to the unstructured fallback (output_model=None), which also makesstructured_output=TrueraiseInvalidSignaturevia the existing check. Two notes from testing this locally:- Both spellings need covering:
typing.Iterator[str](typing._GenericAlias) andcollections.abc.Iterator[str](types.GenericAlias) take different branches in_create_output_modelbut end at the samecreate_modelcrash. Generator[str, None, None]is unaffected: pydantic models it as a sequence, so it builds the wrapped{"result": [...]}schema today and keeps doing so after the fix.
I have this working locally with regression tests for all four spellings plus the
Generatorcontrast — a PR is up at #3587 linking this issue.Disclosure: this was developed with AI assistance; I have reviewed the diff and run the tests locally.
- Both spellings need covering:
Initial Checks
Release line
2.x (current stable)
Description
Registering a tool whose function is annotated
-> Iterator[...]or-> AsyncIterator[...]— the PEP 484 spelling for generator functions — raises a rawpydantic.errors.PydanticSchemaGenerationErrorat registration time, instead of either falling back to an unstructured tool (structured_output=None, the default) or raising the SDK'sInvalidSignature(structured_output=True). The same happens via@server.tool()/Tool.from_function(), so a properly typed generator tool cannot be registered at all.Actual output of the script in "Example Code" (error text truncated at 80 chars by the script):
Full traceback (captured with the
collections.abcspelling of the same annotation, so the error names it accordingly):What I expected is what already happens for other unserializable return types, pinned by
test_structured_output_unserializable_type_error(tests/server/mcpserver/test_func_metadata.py:1233, passes on main) and documented indocs/servers/structured-output.md: withstructured_output=None, registration succeeds andoutput_schemaisNone(fallback to text); withstructured_output=True,InvalidSignature: Function search: return type ... is not serializable for structured output. For contrast,Iterable[str]andGenerator[str, None, None]both register successfully through the same wrapped-model path — only the PEP 484-recommended spellings for generators crash.Root cause:
_create_output_model(...)is called atsrc/mcp/server/mcpserver/utilities/func_metadata.py:444, outside thetry/exceptat lines 446–470 whose except tuple (PydanticUserError,pydantic_core.SchemaError, ...) exists exactly so that "an unsupported return type surfaces here, at registration" as a clean failure._create_output_model→_create_wrapped_model→create_model(model_name, result=annotation)(line 621) builds a schema itself, and itsPydanticSchemaGenerationError(aPydanticUserErrorsubclass) escapes uncaught. Moving the line 444 call inside the existing try/except looks like it would restore both documented behaviours; I'd be happy to be assigned and open a PR with that approach.Related: the guard was added in #2434 (for #1131), but it wraps only the
FuncMetadataconstruction, not this call. #1060 reports the same error class for a different type (Image, 1.x fastmcp) and looks unrelated to this code path.AI disclosure: this issue and its reproduction were prepared with AI assistance.
Example Code
Python & MCP Python SDK