From 26e77e16947aed1abcfdd7f526532a708f1f073b Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Sat, 27 Dec 2025 15:50:29 -0800 Subject: [PATCH] fix: Fix issue with MCP tools throwing an error Fixes: https://github.com/google/adk-python/issues/3082 PiperOrigin-RevId: 849562638 --- src/google/adk/tools/_gemini_schema_util.py | 43 +++++++++++--- .../tools/test_gemini_schema_util.py | 56 ++++++++++++++++--- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/google/adk/tools/_gemini_schema_util.py b/src/google/adk/tools/_gemini_schema_util.py index d2ed560e..74fdfc02 100644 --- a/src/google/adk/tools/_gemini_schema_util.py +++ b/src/google/adk/tools/_gemini_schema_util.py @@ -74,6 +74,35 @@ def _to_snake_case(text: str) -> str: return text +def _sanitize_schema_type( + schema: dict[str, Any], preserve_null_type: bool = False +) -> dict[str, Any]: + if not schema: + schema["type"] = "object" + if isinstance(schema.get("type"), list): + types_no_null = [t for t in schema["type"] if t != "null"] + nullable = len(types_no_null) != len(schema["type"]) + if "array" in types_no_null: + non_null_type = "array" + else: + non_null_type = types_no_null[0] if types_no_null else "object" + if nullable: + schema["type"] = [non_null_type, "null"] + else: + schema["type"] = non_null_type + elif schema.get("type") == "null" and not preserve_null_type: + schema["type"] = ["object", "null"] + + schema_type = schema.get("type") + is_array = schema_type == "array" or ( + isinstance(schema_type, list) and "array" in schema_type + ) + if is_array: + schema.setdefault("items", {"type": "string"}) + + return schema + + def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]: """Resolves $ref pointers in a JSON schema.""" @@ -113,7 +142,7 @@ def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]: def _sanitize_schema_formats_for_gemini( - schema: dict[str, Any], + schema: dict[str, Any], preserve_null_type: bool = False ) -> dict[str, Any]: """Filters the schema to only include fields that are supported by JSONSchema.""" supported_fields: set[str] = set(_ExtendedJSONSchema.model_fields.keys()) @@ -135,8 +164,12 @@ def _sanitize_schema_formats_for_gemini( field_value ) elif field_name in list_schema_field_names: + should_preserve = field_name in ("any_of", "one_of") snake_case_schema[field_name] = [ - _sanitize_schema_formats_for_gemini(value) for value in field_value + _sanitize_schema_formats_for_gemini( + value, preserve_null_type=should_preserve + ) + for value in field_value ] elif field_name in dict_schema_field_names and field_value is not None: snake_case_schema[field_name] = { @@ -158,11 +191,7 @@ def _sanitize_schema_formats_for_gemini( elif field_name in supported_fields and field_value is not None: snake_case_schema[field_name] = field_value - # If the schema is empty, assume it has the type of object - if not snake_case_schema: - snake_case_schema["type"] = "object" - - return snake_case_schema + return _sanitize_schema_type(snake_case_schema, preserve_null_type) def _to_gemini_schema(openapi_schema: dict[str, Any]) -> Schema: diff --git a/tests/unittests/tools/test_gemini_schema_util.py b/tests/unittests/tools/test_gemini_schema_util.py index ff38f07a..d895199f 100644 --- a/tests/unittests/tools/test_gemini_schema_util.py +++ b/tests/unittests/tools/test_gemini_schema_util.py @@ -66,9 +66,15 @@ class TestToGeminiSchema: "nullable_string": {"type": ["string", "null"]}, "nullable_number": {"type": ["null", "integer"]}, "nullable_object": {"type": ["object", "null"]}, + "object_nullable": {"type": "null"}, "multi_types_nullable": {"type": ["string", "null", "integer"]}, "only_null": {"type": "null"}, "empty_default_object": {}, + "empty_list_type": {"type": []}, + "multi_type_with_array_nullable": { + "type": ["string", "array", "null"] + }, + "multi_type_with_array_nonnullable": {"type": ["integer", "array"]}, }, } gemini_schema = _to_gemini_schema(openapi_schema) @@ -88,18 +94,38 @@ class TestToGeminiSchema: assert gemini_schema.properties["nullable_object"].type == Type.OBJECT assert gemini_schema.properties["nullable_object"].nullable - assert gemini_schema.properties["multi_types_nullable"].any_of == [ - Schema(type=Type.STRING), - Schema(type=Type.INTEGER), - ] + assert gemini_schema.properties["object_nullable"].type == Type.OBJECT + assert gemini_schema.properties["object_nullable"].nullable + + assert gemini_schema.properties["multi_types_nullable"].type == Type.STRING assert gemini_schema.properties["multi_types_nullable"].nullable - assert gemini_schema.properties["only_null"].type is None + assert gemini_schema.properties["only_null"].type == Type.OBJECT assert gemini_schema.properties["only_null"].nullable + assert gemini_schema.properties["multi_types_nullable"].type == Type.STRING + assert gemini_schema.properties["multi_types_nullable"].nullable + assert gemini_schema.properties["empty_default_object"].type == Type.OBJECT assert gemini_schema.properties["empty_default_object"].nullable is None + assert gemini_schema.properties["empty_list_type"].type == Type.OBJECT + assert not gemini_schema.properties["empty_list_type"].nullable + + assert ( + gemini_schema.properties["multi_type_with_array_nullable"].type + == Type.ARRAY + ) + assert gemini_schema.properties["multi_type_with_array_nullable"].nullable + + assert ( + gemini_schema.properties["multi_type_with_array_nonnullable"].type + == Type.ARRAY + ) + assert not gemini_schema.properties[ + "multi_type_with_array_nonnullable" + ].nullable + def test_to_gemini_schema_nested_objects(self): openapi_schema = { "type": "object", @@ -144,6 +170,20 @@ class TestToGeminiSchema: gemini_schema = _to_gemini_schema(openapi_schema) assert gemini_schema.items.properties["name"].type == Type.STRING + def test_to_gemini_schema_array_without_items_gets_default(self): + openapi_schema = {"type": "array"} + gemini_schema = _to_gemini_schema(openapi_schema) + assert gemini_schema.type == Type.ARRAY + assert not gemini_schema.nullable + assert gemini_schema.items.type == Type.STRING + + def test_to_gemini_schema_nullable_array_without_items_gets_default(self): + openapi_schema = {"type": ["array", "null"]} + gemini_schema = _to_gemini_schema(openapi_schema) + assert gemini_schema.type == Type.ARRAY + assert gemini_schema.nullable + assert gemini_schema.items.type == Type.STRING + def test_to_gemini_schema_any_of(self): openapi_schema = { "anyOf": [{"type": "string"}, {"type": "integer"}], @@ -200,7 +240,7 @@ class TestToGeminiSchema: }, } gemini_schema = _to_gemini_schema(openapi_schema) - # Since metadata is neither properties nor item, it will call to_gemini_schema recursively. + # Since metadata is not properties nor item, it will call to_gemini_schema recursively. assert isinstance(gemini_schema.properties["metadata"], Schema) assert ( gemini_schema.properties["metadata"].type == Type.OBJECT @@ -544,7 +584,7 @@ class TestToGeminiSchema: "properties": { "case_id": { "description": "The ID of the case.", - "title": "Case ID", + "title": "Case Id", "type": "string", }, "next_page_token": { @@ -567,7 +607,7 @@ class TestToGeminiSchema: "properties": { "case_id": { "description": "The ID of the case.", - "title": "Case ID", + "title": "Case Id", "type": "string", }, "next_page_token": {