[BUG] Union types of two real types (non-null) rejected at MCP-server jsonschema validator with misleading "is not of type 'X'"

Author: AlexisMarasiganCreated May 26, 2026Updated May 26, 2026

Describe the bug

For a request body field typed as T | U where both T and U are real types (neither is None), fastapi-mcp projects an MCP tool schema with a single arbitrary "type" field picked from a Python set. The MCP server's own jsonschema validator then rejects every call that uses the type that lost the toss, with the misleading error Input validation error: ... is not of type '<picked>'.

The corresponding OpenAPI schema is correct (FastAPI's generator preserves anyOf); only the MCP-projected schema is broken.

Related but distinct symptoms of the same root cause:

  • #246 — same convert_openapi_to_mcp_tools adds-a-"type"-field bug, but triggered by T | None (nullable rejection)
  • #165 — union with arrays loses items property (OpenAI-side schema rejection)
  • #218 — meta: MCP tool inputSchema not valid JSON Schema 2020-12

This issue covers the MCP-server-side jsonschema rejection for non-null unions, which the others don't.

Root cause (current main of 0.4.x)

fastapi_mcp/openapi/utils.py:

python
def get_single_param_type_from_schema(param_schema):
    """Get the type of a parameter from the schema.
    If the schema is a union type, return the first type."""
    if "anyOf" in param_schema:
        types = {schema.get("type") for schema in param_schema["anyOf"] if schema.get("type")}
        if "null" in types: types.remove("null")
        if types:
            return next(iter(types))   # ← arbitrary set pick, not deterministic
        return "string"
    return param_schema.get("type", "string")

Then in fastapi_mcp/openapi/convert.py:

python
properties[param_name] = param_schema.copy()                 # carries anyOf
properties[param_name]["title"] = param_name
if "type" not in properties[param_name]:
    properties[param_name]["type"] = get_single_param_type_from_schema(param_schema)

For value: dict | list Pydantic produces {"anyOf": [{"type":"object",...}, {"type":"array",...}]} with no top-level type — so the if "type" not in properties[param_name] branch fires and a single "type" gets injected. The MCP server's jsonschema validator then enforces "type" strictly, rejecting whichever side wasn't picked.

T | None is special-cased (the null strip) and works. T | U with both real types is the bug surface.

To reproduce

python
from typing import Annotated
from fastapi import FastAPI
from pydantic import BaseModel, Field
from fastapi_mcp import FastApiMCP

app = FastAPI()

class SaveRequest(BaseModel):
    tags: dict[str, list[str]] | list[str] = Field(default_factory=dict)

@app.post("/save", operation_id="save", tags=["mcp"])
def save(body: SaveRequest):
    return {"ok": True}

mcp = FastApiMCP(app, include_tags=["mcp"])
mcp.mount_http()

# Call save with tags as a dict (e.g. {"Skills": ["python"]}) over MCP →
# fails at the MCP layer's jsonschema validator with
#   Input validation error: {'Skills': [...]} is not of type 'array'
# (or 'object', depending on which side won the non-deterministic set pick).

Expected behaviour

Either:

  1. Preserve anyOf verbatim in the projected MCP schema — the JSON Schema 2020-12 validator at the MCP layer already handles anyOf correctly. The injection of a single "type" is what breaks things.
  2. If a single "type" must be injected for downstream tooling, emit a discriminated-union-style projection (e.g. oneOf with explicit per-variant subschemas) rather than picking one arbitrarily.

The minimal fix is don't inject "type" when anyOf is already present — let the anyOf constraint stand alone. That covers this case, #246's nullable case, and probably most of #218's spec-conformance gripes.

python
# Suggested patch in convert.py around L248:
if "type" not in properties[param_name] and "anyOf" not in properties[param_name]:
    properties[param_name]["type"] = get_single_param_type_from_schema(param_schema)

System info

  • fastapi-mcp version: 0.4.0
  • Python: 3.12.8
  • FastAPI: latest 0.121.x
  • Pydantic: latest 2.x