[BUG] Union types of two real types (non-null) rejected at MCP-server jsonschema validator with misleading "is not of type 'X'" · Issue #307 · tadata-org/fastapi_mcp · GitHub
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
[BUG] Union types of two real types (non-null) rejected at MCP-server jsonschema validator with misleading "is not of type 'X'" #307
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:
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:
defget_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"inparam_schema:
types= {schema.get("type") forschemainparam_schema["anyOf"] ifschema.get("type")}
if"null"intypes: types.remove("null")
iftypes:
returnnext(iter(types)) # ← arbitrary set pick, not deterministicreturn"string"returnparam_schema.get("type", "string")
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
fromtypingimportAnnotatedfromfastapiimportFastAPIfrompydanticimportBaseModel, Fieldfromfastapi_mcpimportFastApiMCPapp=FastAPI()
classSaveRequest(BaseModel):
tags: dict[str, list[str]] |list[str] =Field(default_factory=dict)
@app.post("/save", operation_id="save", tags=["mcp"])defsave(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:
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.
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.
# Suggested patch in convert.py around L248:if"type"notinproperties[param_name] and"anyOf"notinproperties[param_name]:
properties[param_name]["type"] =get_single_param_type_from_schema(param_schema)
Union-of-two-real-types rejected at the MCP jsonschema validator (with a misleading type error) is another “declared shape vs what the stack will actually accept” gap — painful when the tool still appears listed.
Activity
fix(openapi): also preserve oneOf for union schemas without anyOf
fix(openapi): also preserve anyOf/oneOf for union path params
cid2230 commentedon Sep 23, 2026
@AlexisMarasigan — technical note (not a paid offer).
Union-of-two-real-types rejected at the MCP jsonschema validator (with a misleading type error) is another “declared shape vs what the stack will actually accept” gap — painful when the tool still appears listed.
Exactlane Preflight: static snapshot × requirements fit under Preview — https://exactlane.dev/ · https://exactlane.dev/llms.txt. Not certification, not a live probe.
Questions / pilot interest: @Exactlane on X. Decline = stop.
— Exactlane