Skip to content

[Fix] Gemini tasks fail when MCP tools use nullable arrays - #1302

Open
mrubens wants to merge 10 commits into
developfrom
fix/mcp-proxy-gemini-schema-normalization
Open

[Fix] Gemini tasks fail when MCP tools use nullable arrays#1302
mrubens wants to merge 10 commits into
developfrom
fix/mcp-proxy-gemini-schema-normalization

Conversation

@mrubens

@mrubens mrubens commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

​Opened on behalf of @mrubens. View the task or mention @roomote for follow-up asks.

What changed

  • Normalize MCP tool input and output schemas before returning tools/list results so Gemini receives valid array and object union declarations.
  • Read SSE-framed tools/list responses incrementally and return as soon as the matching JSON-RPC response arrives, rather than waiting for a long-lived stream to close.
  • Preserve finite JSON response handling and cancel the unused upstream stream after an SSE result is rebuilt.
  • Add regression coverage for strict-provider schema normalization and never-closing SSE tools/list responses.

Why this change was made

Valid upstream MCP schemas can become invalid Gemini function declarations when composite nullable unions are converted downstream. Applying the normalization to every tools/list response fixes that provider failure, while incremental SSE parsing prevents the proxy from hanging on Streamable HTTP servers that keep their response channel open.

Impact

Gemini-backed tasks can load tools with nullable arrays and objects without poisoning the full tool list, and MCP integrations using long-lived SSE responses continue returning promptly. All 208 MCP handler tests, API type checking, changed-file lint and formatting, and the repository pre-push checks pass. Package-wide API lint still reports an unrelated pre-existing unused-disable warning in apps/api/src/handlers/github/__tests__/notifyPullRequestTerminalStatus.test.ts.

Google AI Studio strictly validates function declarations: every ARRAY
schema must carry items, and the AI SDK's JSON-Schema-to-Gemini conversion
splits type: [T, "null"] unions into anyOf branches while leaving
items/properties at the outer level. An upstream MCP tool schema that is
valid JSON Schema therefore turns into an invalid request, failing every
Gemini task turn for any workspace with that integration connected.

Normalize the offending shapes at the MCP proxy boundary for all
integrations: rewrite array/object type unions into explicit anyOf
branches carrying their own type's keywords, fill missing array items with
a permissive string schema, and rebuild every tools/list reply (not just
restricted ones) so the normalization always applies.
@roomote-community

roomote-community Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

No new code issues found. See task

  • apps/api/src/handlers/mcp/proxy-utils.ts:1352 no longer waits for long-lived SSE tools/list responses to close; the id-matched response is read incrementally and covered by a never-closing stream regression test.
  • apps/api/src/handlers/mcp/proxy-utils.ts:642 preserves non-type-specific sibling constraints by declining splits with sibling combinators, references, and conditionals.
  • apps/api/src/handlers/mcp/proxy-utils.ts:428 preserves additionalItems when splitting nullable tuple-array schemas, retaining the upstream tuple bound.
  • apps/api/src/handlers/mcp/proxy-utils.ts:553 no longer injects items into schemas that use unevaluatedItems, so the array constraint remains effective after the union split.
  • apps/api/src/handlers/mcp/proxy-utils.ts:591 no longer traverses array-valued dependencies and dependentRequired map entries as schemas, preserving dependency property names.
  • apps/api/src/handlers/mcp/proxy-utils.ts:620 no longer recursively normalizes opaque values under unrecognized schema keywords; nested vendor-extension data remains untouched when the outer union is declined.

Reviewed b242080

Comment thread apps/api/src/handlers/mcp/proxy-utils.ts
@roomote-community roomote-community Bot changed the title fix: normalize MCP tool schemas that Gemini function declarations reject [Fix] Gemini tasks fail when MCP tools use nullable arrays Aug 13, 2026
- Never forward an unfiltered tools/list to a connection with tool
  restrictions or schema stripping: uncorrelatable SSE replies get a
  bounded response-frame-matched read, and unfilterable replies return
  a JSON-RPC error instead of the raw upstream stream
- Tolerate upstreams that echo the JSON-RPC id as a different type
- Only inject items into array schemas that declare none; declared
  items of any form (tuple, boolean, empty object) pass through
- Whitelist per-type keywords when splitting type unions into anyOf
  branches; annotations stay at the top level only
- Stop recursing into default/const/enum/examples data values
- Log when a composite union with a sibling combinator is left
  un-normalized so strict-provider rejections stay diagnosable
- Cancel the unread upstream tee branch on every tools/list rebuild
Comment thread apps/api/src/handlers/mcp/proxy-utils.ts
Splitting a composite type union that carries a non-type-specific
sibling constraint ($ref, $dynamicRef, not, if/then/else) would drop
the constraint and widen the accepted arguments. Treat those siblings
like combinators and leave the schema untouched (with the decline log).
dependentRequired/dependentSchemas are object-only keywords and now
travel with the object branch.
Comment thread apps/api/src/handlers/mcp/proxy-utils.ts
additionalItems (draft-04 tuple companion) and unevaluatedItems join
the array-only whitelist, unevaluatedProperties joins the object-only
whitelist, so a nullable tuple union no longer loses its extra-item
bound when split into anyOf branches.
Comment thread apps/api/src/handlers/mcp/proxy-utils.ts
unevaluatedItems is not equivalent to additionalItems: injecting a
permissive items next to unevaluatedItems: false voids the constraint
(every element becomes evaluated), widening an empty-only array to
arbitrary string arrays. Treat items, prefixItems, additionalItems,
unevaluatedItems, and contains as declared item constraints and only
inject when none are present.
Comment thread apps/api/src/handlers/mcp/proxy-utils.ts
Invert the split decision from blacklisting known-dangerous siblings to
allowlisting exactly-understood keywords: a composite type union is
split only when every keyword it carries has a known branch placement.
Combinators, references, conditionals, vendor extensions, and
future-dialect keywords all decline (with a log naming them) instead of
risking a semantic rewrite. Declining costs at most the pre-fix status
quo for one tool; a wrong rewrite would corrupt the contract for every
provider.

Annotation keywords (description, title, default, examples, deprecated,
readOnly, writeOnly) stay on the anyOf wrapper.

R_DISABLE_MCP_SCHEMA_NORMALIZATION=true disables the transform without
a rollback, since it runs on every tools/list for every workspace.
dependencies and dependentRequired are keyed by property names with
array-of-property-name values; recursing into them as schemas rewrote
an entry named 'type' into a type declaration. A dedicated traversal
context leaves array-valued entries as data and normalizes only
object-valued legacy draft-04 dependency schemas. dependencies also
joins the object-only whitelist so it travels with the object branch.
Comment thread apps/api/src/handlers/mcp/proxy-utils.ts Outdated
Values under unrecognized keywords (vendor extensions, future
dialects) are opaque data; recursing into them rewrote schema
lookalikes nested inside even when the outer schema was declined.
Recursion is now allowlist-driven like the split decision: only
SCHEMA_VALUE_KEYWORDS positions, schema maps, and dependency maps are
traversed. This also subsumes the previous default/const/enum/examples
skip list.
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.

1 participant