Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(openapi): collapse nullable unions so Optional[List[X]] tools keep valid schemas - #343

Open
K4bain wants to merge 1 commit into
tadata-org:mainfrom
K4bain:fix/304-preserve-items-optional-list
Open

fix(openapi): collapse nullable unions so Optional[List[X]] tools keep valid schemas#343
K4bain wants to merge 1 commit into
tadata-org:mainfrom
K4bain:fix/304-preserve-items-optional-list

Conversation

@K4bain

@K4bain K4bain commented Sep 2, 2026

Copy link
Copy Markdown

Fixes #304clean_schema_for_display() strips anyOf and loses items for Optional[List[X]] parameters.

Root cause

Pydantic v2 renders Optional[List[X]] as:

{ "anyOf": [{ "type": "array", "items": {...} }, { "type": "null" }] }

Both symptom paths in the issue reproduce from that shape:

  1. inputSchema path (the client-facing breakage): convert.py bolts a bare type: "array" onto the property (via get_single_param_type_from_schema) while leaving anyOf (with the items inside it) in place — the projected property had anyOf: [...] AND type: "array" but no items. This is the exact broken schema I observed from the current code.
  2. display path: clean_schema_for_display() strips anyOf without hoisting items, emitting {type: "array"} alone.

Both are invalid JSON Schema, and MCP clients reject the tool outright (tool parameters array type must have items).

The fix

  • New collapse_nullable_union() in openapi/utils.py: collapses a single-non-null-variant anyOf union to that variant (copying it and carrying over title/description/default). Non-union and multi-variant schemas pass through unchanged. convert.py routes path/query/body parameter schemas through it, so the projected inputSchema becomes {type: "array", items: {...}} — the correct collapse of Optional[List[X]].
  • clean_schema_for_display() now hoists type/items/enum/format from the single non-null variant before stripping anyOf, so the response-schema text never loses items either.

Verification

  • Reproduced on main first: Optional[List[str]] and Optional[List[Model]] properties projected as type='array' items=False anyOf=True; after the fix: type='array' items=True anyOf=False with the full item schema preserved ({name, value} for the model case).
  • 2 new regression tests (12 total in the file, all passing): one end-to-end through convert_openapi_to_mcp_tools covering Optional[List[str]], Optional[List[Model]], Optional[int], plus an invariant sweep asserting no array property anywhere lacks items; one for clean_schema_for_display() hoisting behavior including the multi-variant (non-nullable) pass-through case.
  • Full non-transport test suite green (the real-transport SSE/HTTP suites can't fork subprocess contexts on this Windows checkout — they error identically on clean main, unrelated to this change); ruff check + format clean; mypy clean.

Cross-references #246/#165/#218 as related symptoms of the same root family; this fix addresses the mechanism for all of them, though their individual repros deserve their own assertions if maintainers want them folded in.

…p valid schemas (tadata-org#304)

Pydantic v2 renders Optional[List[X]] as anyOf: [{type: array, items: ...},
{type: null}]. Projecting that shape into a tool inputSchema by bolting on
a bare type: 'array' (or stripping anyOf without hoisting items) yields
{type: 'array'} with no items — invalid JSON Schema that MCP clients reject
with 'tool parameters array type must have items'.

- add collapse_nullable_union(): collapses a single-non-null-variant anyOf
  to that variant, carrying over title/description/default
- convert.py routes path/query/body parameter schemas through it so the
  projected inputSchema is {type: array, items: {...}} (or the plain type)
- clean_schema_for_display() hoists type/items/enum/format from the
  non-null variant before stripping anyOf, for the response-schema text

Regression tests cover Optional[List[str]], Optional[List[Model]],
Optional scalars, and the display-side schema.
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.

clean_schema_for_display() strips anyOf and loses items for Optional[List[X]] parameters

1 participant