Skip to content

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

Closed

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

Activity

  1. added
    mcp[Component] This issues is related to MCP support
    on Nov 6, 2025
  2. added
    tools[Component] This issue is related to tools
    on Nov 6, 2025
  3. added theissue type on Nov 6, 2025
  4. luutuankiet commented on Dec 3, 2025

    @luutuankiet
    Author

    hi team @seanzhou1023 , understand from @boyangsvl that adk don't support Union types tool definitions

    #55 (comment)

    Automatic parsing of argument and docstring is supported by ADK. You can use Pydantic models as arguments in function tools. We just don't support union types as it's a limitation of the model. I'm closing this issue for now.

    The recommended approach is to simplify the function definition. However my scenario is a bit nuanced as I am not the maintainer of the workspace-mcp tool thus cannot alter the source code of the mcp server.

    Could you advise if adk allows manually intercepting the mcp function to gemini api converter and overriding the function declaration ? My hunch is I'd need to start from _gemini_schema_utils.py where the conversion happens. Psuedo code below is my starting point.

    Many thanks in advance.

    agent.py
    from google.adk.agents.llm_agent import Agent
    from google.adk.tools.mcp_tool import McpToolset
    from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
    from mcp import StdioServerParameters
    import copy  
      
    class InterceptJSONSchemaMCPToolset(McpToolset):  
        """MCPToolset that uses native JSON Schema support."""  
          
        async def _get_tools_internal(self, readonly_context):  
            """Get tools and use native JSON Schema."""  
            tools = await super()._get_tools_internal(readonly_context)  
              
            for tool in tools:  
                if hasattr(tool, 'mcpTool') and hasattr(tool.mcpTool, 'inputSchema'):  
                    # Use the raw MCP schema directly as JSON Schema  
                    tool.input_schema = tool.mcpTool.inputSchema  
              
            return tools      
    
    google_workspace_tool = InterceptJSONSchemaMCPToolset(
        connection_params=StdioConnectionParams(
            server_params=StdioServerParameters(
            command="uvx",
            args=[
                "workspace-mcp"
            ],
        ),
        timeout=30
        ),
        tool_filter=["create_event"]
    )
    
    
    
    root_agent = Agent(
        model='gemini-2.5-flash',
        name='root_agent',
        description='A helpful assistant for user questions.',
        instruction='Answer user questions to the best of your knowledge',
        tools=[google_workspace_tool]
    )
    
  5. luutuankiet commented on Dec 3, 2025

    @luutuankiet
    Author

    After much thought, closing this issue as this is a google-genai library issue and not google-adk.

    • Issue only happens using VertexAI backend and not Gemini API.
    • Adk pass through the MCP tool schema and both backends calls the same function generate_content_async(...) which sends the requests to google-genai.
    • In google-genai Vertex backend's implementation
      • Simple Optional types (Optional[List[str]]): Get simplified to proper schema with top-level type
      • Complex Union types (Optional[Union[str, List[Dict]]]): Incorrectly get type: "OBJECT" which fails the validation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

mcp[Component] This issues is related to 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