Closed as not planned
Closed as not planned
Copy link
Labels
bugSomething isn't workingSomething isn't workingneeds reproneeds additional information to be able to reproduce bugneeds additional information to be able to reproduce bug
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
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.
Example Code
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("test")
@mcp.tool()
async def my_tool(flag: bool) -> dict | list | str: # <-- triggers the bug
if flag:
return {"key": "value"}
return ["item"]Python & MCP Python SDK
- mcp (Python): latest (>=1.0.0,<2.0.0)
- pydantic: >=2.10 (observed on 2.10.x)
- Python: >=3.10
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingneeds reproneeds additional information to be able to reproduce bugneeds additional information to be able to reproduce bug
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