Skip to content

test(mcpserver): cover PEP 604 union return annotations (#2591)#2906

Open
Bartok9 wants to merge 1 commit into
modelcontextprotocol:mainfrom
Bartok9:test/2591-pep604-union-return-regression
Open

test(mcpserver): cover PEP 604 union return annotations (#2591)#2906
Bartok9 wants to merge 1 commit into
modelcontextprotocol:mainfrom
Bartok9:test/2591-pep604-union-return-regression

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jun 18, 2026

Copy link
Copy Markdown

Summary

  • Adds a regression test for tool functions whose return annotation is a multi-member PEP 604 union mixing container and scalar types (dict | list | str).
  • Guards an already-fixed-but-untested case so it can't silently regress.

Motivation

Closes #2591.

FastMCP/MCPServer used to crash at registration with PydanticUserError when a tool's return type was a bare PEP 604 union like dict | list | str, because the types.UnionType was passed to create_model() as a field value.

This already works on current main (734746a): such a union is neither a types.GenericAlias nor a type, so _try_create_model_and_schema (func_metadata.py) falls through to the catch-all branch that calls _create_wrapped_model(...) with wrap_output=True; #2434 (5cbd259) additionally guards the schema-generation path. But there is no test covering the reported signature — the existing test_structured_output_generic_types only exercises a 2-member, all-scalar union (func_union() -> str | int), not a container+scalar mix with bare (unparametrized) generics, which is a different branch interaction (the dict member touches the RootModel dict special-casing before the union wraps).

The three prior fix attempts (#2592, #2599, #2669) were all closed during backlog cleanup rather than for an approach problem, and one of them switched the repro to typed generics to satisfy pyright — which masked the exact reported case. This PR keeps the bare-generic signature (with scoped # type: ignore) so the test reproduces the issue faithfully.

Verification

  • uv run pytest tests/server/mcpserver/test_func_metadata.py -q — 34 passed
  • uv run ruff check tests/server/mcpserver/test_func_metadata.py — clean
  • uv run ruff format --check tests/server/mcpserver/test_func_metadata.py — already formatted
  • uv run pyright tests/server/mcpserver/test_func_metadata.py — 0 errors
  • Did NOT change: any source file. This is a test-only addition; the behavior it asserts already ships on main.

Real behavior proof

  • Behavior addressed: @mcp.tool() async def my_tool(flag: bool) -> dict | list | str registering without PydanticUserError, and producing a wrapped anyOf output_schema.
  • Real environment: Python 3.12, uv run, branch off upstream/main 734746a.
  • Command run: uv run python -c "..." invoking func_metadata(func, structured_output=True).output_schema for () -> dict | list | str.
  • Captured output (the schema the new test asserts):
    {
      "properties": {
        "result": {
          "anyOf": [
            {"additionalProperties": true, "type": "object"},
            {"items": {}, "type": "array"},
            {"type": "string"}
          ],
          "title": "Result"
        }
      },
      "required": ["result"],
      "title": "func_pep604_unionOutput",
      "type": "object"
    }
  • Regression test: tests/server/mcpserver/test_func_metadata.py::test_structured_output_pep604_union_return — asserts the exact wrapped anyOf shape above for the issue's signature.
  • What was NOT tested: runtime conversion of an actual returned value through this model (the existing structured-output round-trip tests already cover the wrap path generically); this test is scoped to the schema-generation path that previously raised.

…protocol#2591)

Closes modelcontextprotocol#2591.

A tool whose return type uses a multi-member Python 3.10+ union mixing
container and scalar types (dict | list | str) previously crashed at
registration with PydanticUserError, because the bare types.UnionType was
passed to create_model() as a field value.

This is already fixed on main: such a union is neither a types.GenericAlias
nor a type, so _try_create_model_and_schema falls through to the catch-all
branch and wraps the result under {"result": ...}; modelcontextprotocol#2434 additionally guards
the schema-generation path. But there was no regression test for the exact
reported signature -- the existing func_union test only covers a 2-member
all-scalar union (str | int), not a container+scalar mix with bare generics.

Adds test_structured_output_pep604_union_return asserting the wrapped anyOf
output_schema for the issue's exact signature, so the fix can't silently
regress.

Verification: uv run pytest tests/server/mcpserver/test_func_metadata.py -q
-> 34 passed; ruff check/format clean; pyright 0 errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FastMCP crashes when tool return type uses Python 3.10+ A | B | C union syntax

1 participant