You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
FastMCP crashes when tool return type uses Python 3.10+ A | B | C union syntax #2591
FastMCP crashes on startup when a tool function's return type annotation uses Python 3.10+ union syntax (A | B | C, i.e. types.UnionType). The error originates from _create_wrapped_model in func_metadata.py, which passes the union annotation directly to Pydantic's create_model() as a bare keyword value, causing PydanticUserError.
While developing a local MCP tool server with FastMCP, the server immediately crashed on startup with a PydanticUserError. The error message pointed to the result = dict | list | str field in a dynamically generated Pydantic model. Tracing the call stack backward:
The error came from pydantic's create_model() in _create_wrapped_model.
_create_wrapped_model was called from _try_create_model_and_schema in func_metadata.py.
It was triggered because the scripts() tool's return type dict | list | str (a types.UnionType) fell outside both the GenericAlias and plain type branches, landing in the generic else branch that wraps the annotation as-is.
Comparing with Union[dict, list, str] (which is a GenericAlias and works fine) confirmed that only the Python 3.10+ | syntax triggers this path. The fix was then implemented and verified locally.
Error Trace
pydantic.errors.PydanticUserError: A non-annotated attribute was detected:
`result = dict | list | str`.
All model fields require a type annotation; if `result` is not meant to be a field,
you may be able to resolve this error by annotating it as a `ClassVar` or
updating `model_config['ignored_types']`.
Root Cause
The call chain is:
inspect.signature(func, eval_str=True) resolves dict | list | str into a types.UnionType object (Python 3.10+).
_create_wrapped_model(model_name, result=annotation) is called.
Inside _create_wrapped_model, create_model(model_name, result=annotation) is called — Pydantic's create_model interprets result=dict | list | str as a bare default value (not a type annotation), because UnionType is not recognized as a field annotation in this context.
Proposed Fix
In _create_wrapped_model (file: src/mcp/server/fastmcp/utilities/func_metadata.py), change the create_model call to use the (type, default) tuple form, which explicitly tells Pydantic that the first element is a type annotation:
# Line ~483- return create_model(model_name, result=annotation)+ return create_model(model_name, result=(annotation, ...))
... (Ellipsis) means the field is required. This syntax works correctly with types.UnionType, GenericAlias, and all other annotation forms.
Workaround
If you cannot patch the library, avoid using Python 3.10+ union syntax in tool return types:
Union[dict, list, str] is a GenericAlias and is handled correctly by the existing code path.
Reported by: Ianwusb — discovered the bug while using FastMCP in a personal project, traced the error through the call stack, and identified the root cause with assistance from DeepSeek-V4-Pro analysis.
Why it's fixed:dict | list | str is a types.UnionType, which is neither a types.GenericAlias nor a type, so in _try_create_model_and_schema (func_metadata.py) it now falls through to the catch-all branch that calls _create_wrapped_model(...) with wrap_output=True. The bare union is never passed to create_model() as a field value, so the PydanticUserError from the original report no longer occurs. The schema-generation try/except (added in #2434, 5cbd259, "catch PydanticUserError when generating output schema") additionally guards the StrictJsonSchema path.
The three prior fix attempts (#2592, #2599, #2669) were all closed during backlog cleanup rather than for an approach problem, and the underlying behavior is now correct on main. Suggest this can be closed as resolved (or the needs repro label dropped) — happy to open a focused regression test PR asserting the union output_schema shape if maintainers want a guard against regressions, since I don't see an existing test covering the A | B | C return-annotation case.
The create_model failure is specific to pydantic 2.10.x, below the SDK's supported floor (pydantic>=2.11.0 on the 1.x line, >=2.12.0 on main); on supported pydantic a -> dict | list | str return annotation registers normally, so the fix is upgrading pydantic in the environment rather than an SDK change. Closing this as part of a wider backlog cleanup following the v2 launch. Feel free to reopen if this is still relevant.
Activity
Swcmb commentedon May 14, 2026
Discovery Process
While developing a local MCP tool server with
FastMCP, the server immediately crashed on startup with aPydanticUserError. The error message pointed to theresult = dict | list | strfield in a dynamically generated Pydantic model. Tracing the call stack backward:pydantic'screate_model()in_create_wrapped_model._create_wrapped_modelwas called from_try_create_model_and_schemainfunc_metadata.py.scripts()tool's return typedict | list | str(atypes.UnionType) fell outside both theGenericAliasand plaintypebranches, landing in the genericelsebranch that wraps the annotation as-is.Comparing with
Union[dict, list, str](which is aGenericAliasand works fine) confirmed that only the Python 3.10+|syntax triggers this path. The fix was then implemented and verified locally.Error Trace
Root Cause
The call chain is:
inspect.signature(func, eval_str=True)resolvesdict | list | strinto atypes.UnionTypeobject (Python 3.10+).func_metadata.py:_try_create_model_and_schema, theUnionTypeis not aGenericAliasnor a plaintype, so it falls into theelsebranch at line ~406._create_wrapped_model(model_name, result=annotation)is called._create_wrapped_model,create_model(model_name, result=annotation)is called — Pydantic'screate_modelinterpretsresult=dict | list | stras a bare default value (not a type annotation), becauseUnionTypeis not recognized as a field annotation in this context.Proposed Fix
In
_create_wrapped_model(file:src/mcp/server/fastmcp/utilities/func_metadata.py), change thecreate_modelcall to use the(type, default)tuple form, which explicitly tells Pydantic that the first element is a type annotation:...(Ellipsis) means the field is required. This syntax works correctly withtypes.UnionType,GenericAlias, and all other annotation forms.Workaround
If you cannot patch the library, avoid using Python 3.10+ union syntax in tool return types:
Union[dict, list, str]is aGenericAliasand is handled correctly by the existing code path.jshaofa-ui commentedon May 14, 2026
jshaofa-ui commentedon May 14, 2026
Bartok9 commentedon Jun 18, 2026
Verification on current
main(734746a): this no longer reproduces. The original repro registers cleanly and produces valid structured output.Direct check of
func_metadata(my_tool, structured_output=True).output_schema:{'properties': {'result': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'items': {}, 'type': 'array'}, {'type': 'string'}], 'title': 'Result'}}, 'required': ['result'], 'title': 'my_toolOutput', 'type': 'object'}Why it's fixed:
dict | list | stris atypes.UnionType, which is neither atypes.GenericAliasnor atype, so in_try_create_model_and_schema(func_metadata.py) it now falls through to the catch-all branch that calls_create_wrapped_model(...)withwrap_output=True. The bare union is never passed tocreate_model()as a field value, so thePydanticUserErrorfrom the original report no longer occurs. The schema-generationtry/except(added in #2434,5cbd259, "catch PydanticUserError when generating output schema") additionally guards the StrictJsonSchema path.The three prior fix attempts (#2592, #2599, #2669) were all closed during backlog cleanup rather than for an approach problem, and the underlying behavior is now correct on
main. Suggest this can be closed as resolved (or theneeds reprolabel dropped) — happy to open a focused regression test PR asserting the unionoutput_schemashape if maintainers want a guard against regressions, since I don't see an existing test covering theA | B | Creturn-annotation case.maxisbey commentedon Aug 10, 2026
The
create_modelfailure is specific to pydantic 2.10.x, below the SDK's supported floor (pydantic>=2.11.0on the 1.x line,>=2.12.0onmain); on supported pydantic a-> dict | list | strreturn annotation registers normally, so the fix is upgrading pydantic in the environment rather than an SDK change. Closing this as part of a wider backlog cleanup following the v2 launch. Feel free to reopen if this is still relevant.AI Disclaimer