Skip to content

MCP tools with anyOf schemas fail Gemini validation - missing top-level type field #3424

@luutuankiet

Description

@luutuankiet

Problem

When using MCPToolset to load remote MCP tools, tools with Optional[Union[...]] type hints fail with Gemini API validation errors:

google.genai.errors.ClientError: 400 Bad Request
Unable to submit request because `create_event` functionDeclaration `parameters.reminders` 
schema didn't specify the schema type field.

The mcp server in use is workspace-mcp - link to function create_event

async def create_event(
    service,
    user_google_email: str,
    summary: str,
    start_time: str,
    end_time: str,
    calendar_id: str = "primary",
    description: Optional[str] = None,
    location: Optional[str] = None,
    attendees: Optional[List[str]] = None,
    timezone: Optional[str] = None,
    attachments: Optional[List[str]] = None,
    add_google_meet: bool = False,
    reminders: Optional[Union[str, List[Dict[str, Any]]]] = None,
    use_default_reminders: bool = True,
    transparency: Optional[str] = None,
) -> str:

... which emits the following json schema to clients

json schema
{
      "name": "create_event",
      "description": "Creates a new event.\n\nArgs:\n    user_google_email (str): The user's Google email address. Required.\n    summary (str): Event title.\n    start_time (str): Start time (RFC3339, e.g., \"2023-10-27T10:00:00-07:00\" or \"2023-10-27\" for all-day).\n    end_time (str): End time (RFC3339, e.g., \"2023-10-27T11:00:00-07:00\" or \"2023-10-28\" for all-day).\n    calendar_id (str): Calendar ID (default: 'primary').\n    description (Optional[str]): Event description.\n    location (Optional[str]): Event location.\n    attendees (Optional[List[str]]): Attendee email addresses.\n    timezone (Optional[str]): Timezone (e.g., \"America/New_York\").\n    attachments (Optional[List[str]]): List of Google Drive file URLs or IDs to attach to the event.\n    add_google_meet (bool): Whether to add a Google Meet video conference to the event. Defaults to False.\n    reminders (Optional[Union[str, List[Dict[str, Any]]]]): JSON string or list of reminder objects. Each should have 'method' (\"popup\" or \"email\") and 'minutes' (0-40320). Max 5 reminders. Example: '[{\"method\": \"popup\", \"minutes\": 15}]' or [{\"method\": \"popup\", \"minutes\": 15}]\n    use_default_reminders (bool): Whether to use calendar's default reminders. If False, uses custom reminders. Defaults to True.\n    transparency (Optional[str]): Event transparency for busy/free status. \"opaque\" shows as Busy (default), \"transparent\" shows as Available/Free. Defaults to None (uses Google Calendar default).\n\nReturns:\n    str: Confirmation message of the successful event creation with event link.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "user_google_email": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "start_time": {
            "type": "string"
          },
          "end_time": {
            "type": "string"
          },
          "calendar_id": {
            "default": "primary",
            "type": "string"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "location": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "attendees": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "timezone": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "attachments": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "add_google_meet": {
            "default": false,
            "type": "boolean"
          },
          "reminders": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "items": {
                  "additionalProperties": true,
                  "type": "object"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "use_default_reminders": {
            "default": true,
            "type": "boolean"
          },
          "transparency": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "user_google_email",
          "summary",
          "start_time",
          "end_time"
        ]
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "result": {
            "type": "string"
          }
        },
        "required": [
          "result"
        ],
        "x-fastmcp-wrap-result": true
      },
      "_meta": {
        "_fastmcp": {
          "tags": []
        }
      }
}

Root Cause

The MCP server (FastMCP) generates JSON schemas with anyOf constructs that lack a top-level type field :

"reminders": {
  "anyOf": [
    {"type": "string"},
    {"type": "array", "items": {"type": "object"}},
    {"type": "null"}
  ],
  "default": null
}

This is valid JSON Schema but fails Gemini's stricter validation requirements.

Expected Behavior

ADK should sanitize MCP tool schemas to add fallback type fields to anyOf constructs before passing them to Gemini, similar to how it handles schemas from direct Python function declarations in src/google/adk/tools/_automatic_function_calling_util.py , with a TODO comment acknowledging: "Unclear why a Type is needed with 'anyOf' to avoid google.genai.errors.ClientError: 400 INVALID_ARGUMENT".

def _map_pydantic_type_to_property_schema(property_schema: Dict):
  if 'type' in property_schema:
    property_schema['type'] = _py_type_2_schema_type.get(
        property_schema['type'], 'TYPE_UNSPECIFIED'
    )
    if property_schema['type'] == 'ARRAY':
      _map_pydantic_type_to_property_schema(property_schema['items'])
  for type_ in property_schema.get('anyOf', []):
    if 'type' in type_:
      type_['type'] = _py_type_2_schema_type.get(
          type_['type'], 'TYPE_UNSPECIFIED'
      )
      # TODO: To investigate. Unclear why a Type is needed with 'anyOf' to
      # avoid google.genai.errors.ClientError: 400 INVALID_ARGUMENT.
      property_schema['type'] = type_['type']

Proposed Fix

Apply the same anyOf handling logic to _sanitize_schema_formats_for_gemini() in src/google/adk/tools/_gemini_schema_util.py:139-186. The function should recursively add fallback type fields to any anyOf construct that lacks one, using the first non-null type as the fallback.

Reproduction

  1. Create a FastMCP server with a tool using Optional[Union[str, List[Dict[str, Any]]]] type hint
  2. Connect to it via MCPToolset with StreamableHTTPConnectionParams
  3. Attempt to use the tool - Gemini will reject the schema

Impact

This affects any MCP tool with union types, making it impossible to use many real-world MCP servers with ADK without modifying the remote server's source code

Additional info

If this helps I find that when downgrading to adk to v1.17.0 and pinning google-genai==1.44.0 , the issue did not occur. Please advise if this is better to post under the python-genai repo instead.

May be related with this issue : googleapis/python-genai#625

Metadata

Metadata

Assignees

Labels

mcp[Component] Issues about MCP supporttools[Component] This issue is related to tools

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions