-
Notifications
You must be signed in to change notification settings - Fork 742
Description
Hi, I found a schema validation issue that only affects Vertex AI API calls when processing MCP tools with complex Union types.
The Schema.from_json_schema() method fails to add the required top-level type field for complex anyOf constructs, causing Vertex AI to reject valid JSON Schema that works fine with the Gemini API.
Steps to reproduce
- Use adk / any library to create this setup (adk-python version 1.19.0, google-genai version 1.16.1)
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
google_workspace_tool = McpToolset(
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]
)
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": []
}
}
}
- Configure adk backend to call google.genai's Vertex AI client with
GOOGLE_GENAI_USE_VERTEXAI=1 - Error below will occur when the agent serves any request
google.genai.errors.ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': "Unable to submit request because `create_event` functionDeclaration `parameters.reminders` schema didn't specify the schema type field. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling", 'status': 'INVALID_ARGUMENT'}}
- Configure adk backend to call google.genai's Gemini API client with
GOOGLE_GENAI_USE_VERTEXAI=0andGEMINI_API_KEY - The agent can now serve requests without validation errors.
Impact
- MCP tools with > 2 Union types will fail for VertexAI client backend, while Gemini API will proceed successfully.
- A workaround for now is to use simpler Union types for VertexAI e.g.
Optional[str]which translate toUnion[T, None]
Root Cause Analysis
Data Flow
- MCP Server → Emits valid JSON Schema with
anyOfconstructs - python-adk → Calls external
google-genai's methodmcp_to_gemini_tool()conversion function. - google-genai →
Schema.from_json_schema()processes schema differently for Vertex AI vs Gemini API
The MCP Tool Conversion Function
In google/genai/_mcp_utils.py, the mcp_to_gemini_tool() function:
def mcp_to_gemini_tool(tool: McpTool) -> types.Tool:
"""Translates an MCP tool to a Google GenAI tool."""
return types.Tool(
function_declarations=[{
"name": tool.name,
"description": tool.description,
"parameters": types.Schema.from_json_schema(
json_schema=types.JSONSchema(
**_filter_to_supported_schema(tool.inputSchema)
)
),
}]
)The Schema Conversion Method
In google/genai/types.py, the Schema.from_json_schema() method:
@classmethod
def from_json_schema(
cls,
*,
json_schema: JSONSchema,
api_option: Literal['VERTEX_AI', 'GEMINI_API'] = 'GEMINI_API',
raise_error_on_unsupported_field: bool = False,
) -> 'Schema':
"""Converts a JSONSchema object to a Schema object.
Note: Conversion of fields that are not included in the JSONSchema class
are ignored.
Json Schema is now supported natively by both Vertex AI and Gemini API.
Users
are recommended to pass/receive Json Schema directly to/from the API. For
example:
1. the counter part of GenerateContentConfig.response_schema is
GenerateContentConfig.response_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
2. the counter part of FunctionDeclaration.parameters is
FunctionDeclaration.parameters_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
3. the counter part of FunctionDeclaration.response is
FunctionDeclaration.response_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
The JSONSchema is compatible with 2020-12 JSON Schema draft, specified by
OpenAPI 3.1.
Args:
json_schema: JSONSchema object to be converted.
api_option: API option to be used. If set to 'VERTEX_AI', the
JSONSchema will be converted to a Schema object that is compatible
with Vertex AI API. If set to 'GEMINI_API', the JSONSchema will be
converted to a Schema object that is compatible with Gemini API.
Default is 'GEMINI_API'.
raise_error_on_unsupported_field: If set to True, an error will be
raised if the JSONSchema contains any unsupported fields. Default is
False.
Returns:
Schema object that is compatible with the specified API option.
Raises:
ValueError: If the JSONSchema contains any unsupported fields and
raise_error_on_unsupported_field is set to True. Or if the JSONSchema
is not compatible with the specified API option.
"""The Problem: Missing Top-Level Type Field
In google/genai/types.py, the Schema class requires a type field:
type: Optional[Type] = Field(
default=None, description="""Optional. The type of the data."""
)When Schema.from_json_schema() encounters complex anyOf schemas like:
{
"anyOf": [
{"type": "string"},
{"type": "array", "items": {"type": "object"}},
{"type": "null"}
],
"default": null
}It doesn't add the required top-level type field that the Vertex AI API demands.
Test Evidence
In google/genai/tests/types/test_schema_from_json_schema.py, the test test_nullable_in_union_like_type_conversion() shows how complex unions are handled:
def test_nullable_in_union_like_type_conversion():
"""Test conversion of JSONSchema.nullable to Schema.nullable"""
json_schema1 = types.JSONSchema(
type=[
types.JSONSchemaType('string'),
types.JSONSchemaType('null'),
types.JSONSchemaType('object'),
types.JSONSchemaType('number'),
types.JSONSchemaType('array'),
types.JSONSchemaType('boolean'),
types.JSONSchemaType('integer'),
],
)
gemini_api_schema1 = types.Schema.from_json_schema(json_schema=json_schema1)
vertex_ai_schema1 = types.Schema.from_json_schema(
json_schema=json_schema1, api_option='VERTEX_AI'
)
expected_schema = types.Schema(
nullable=True,
any_of=[
types.Schema(type='STRING'),
types.Schema(type='OBJECT'),
types.Schema(type='NUMBER'),
types.Schema(type='ARRAY'),
types.Schema(type='BOOLEAN'),
types.Schema(type='INTEGER'),
],
)
assert gemini_api_schema1 == expected_schema
assert vertex_ai_schema1 == expected_schemaNotice that the expected schema has nullable=True and any_of=[...] but no top-level type field. This works for Gemini API but fails for Vertex AI.
Expected Fix
The fix should be in Schema.from_json_schema() to add an appropriate top-level type for complex union schemas when api_option='VERTEX_AI':
if api_option == 'VERTEX_AI' and is_union_like_type and not schema.type:
# Set appropriate type based on union content
schema.type = Type('OBJECT') # or derive from union types