Skip to content

Commit c11ac7d

Browse files
gaoflowxuanyang15
authored andcommitted
fix(tools): dereference draft-07 definitions in MCP tool schemas
Merge google#5941 Co-authored-by: Xuan Yang <xygoogle@google.com> COPYBARA_INTEGRATE_REVIEW=google#5941 from gaoflow:fix-5940-draft07-definitions 4e56be5 PiperOrigin-RevId: 929433469
1 parent 654145a commit c11ac7d

2 files changed

Lines changed: 71 additions & 4 deletions

File tree

src/google/adk/tools/_gemini_schema_util.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ def _sanitize_schema_type(
106106
def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]:
107107
"""Resolves $ref pointers in a JSON schema."""
108108

109-
defs = schema.get("$defs", {})
109+
# Support both the draft 2019-09+/2020-12 keyword (`$defs`) and the
110+
# draft-07 keyword (`definitions`). The MCP specification allows tool
111+
# `inputSchema`s to use either, so a server sending draft-07 schemas with
112+
# `definitions` + `$ref: "#/definitions/..."` must dereference correctly.
113+
# `$defs` takes precedence on the (pathological) key collision.
114+
defs = {**schema.get("definitions", {}), **schema.get("$defs", {})}
110115

111116
def _resolve_refs(sub_schema: Any, path_refs: frozenset[str]) -> Any:
112117
if isinstance(sub_schema, dict):
@@ -148,9 +153,11 @@ def _resolve_refs(sub_schema: Any, path_refs: frozenset[str]) -> Any:
148153
return sub_schema
149154

150155
dereferenced_schema = _resolve_refs(schema, frozenset())
151-
# Remove the definitions block after resolving.
152-
if "$defs" in dereferenced_schema:
153-
del dereferenced_schema["$defs"]
156+
# Remove the definition blocks after resolving so the leftover keywords do
157+
# not leak into the Gemini schema (which would otherwise raise a KeyError).
158+
for defs_keyword in ("$defs", "definitions"):
159+
if defs_keyword in dereferenced_schema:
160+
del dereferenced_schema[defs_keyword]
154161
return dereferenced_schema
155162

156163

tests/unittests/tools/test_gemini_schema_util.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,66 @@ def test_to_gemini_schema_nested_dict_with_defs_and_ref(self):
337337
]
338338
assert gemini_schema.properties["payload"].required == ["adDomain"]
339339

340+
def test_to_gemini_schema_draft_07_definitions_and_ref(self):
341+
"""Draft-07 schemas use `definitions`/`#/definitions/...` instead of `$defs`.
342+
343+
The MCP spec allows tool `inputSchema`s to use JSON Schema draft-07, so a
344+
server sending `definitions` + `$ref: "#/definitions/..."` must dereference
345+
correctly instead of raising `KeyError: 'definitions'`.
346+
"""
347+
openapi_schema = {
348+
"$schema": "http://json-schema.org/draft-07/schema#",
349+
"definitions": {
350+
"DeviceEnum": {
351+
"enum": ["GLOBAL", "desktop", "mobile"],
352+
"title": "DeviceEnum",
353+
"type": "string",
354+
},
355+
"DomainPayload": {
356+
"properties": {
357+
"adDomain": {
358+
"description": "List of one or many domains.",
359+
"items": {"type": "string"},
360+
"title": "Addomain",
361+
"type": "array",
362+
},
363+
"device": {
364+
"$ref": "#/definitions/DeviceEnum",
365+
"default": "GLOBAL",
366+
},
367+
},
368+
"required": ["adDomain"],
369+
"title": "DomainPayload",
370+
"type": "object",
371+
},
372+
},
373+
"properties": {"payload": {"$ref": "#/definitions/DomainPayload"}},
374+
"required": ["payload"],
375+
"title": "query_domainsArguments",
376+
"type": "object",
377+
}
378+
gemini_schema = _to_gemini_schema(openapi_schema)
379+
assert gemini_schema.type == Type.OBJECT
380+
assert gemini_schema.properties["payload"].type == Type.OBJECT
381+
assert (
382+
gemini_schema.properties["payload"].properties["adDomain"].type
383+
== Type.ARRAY
384+
)
385+
assert (
386+
gemini_schema.properties["payload"].properties["adDomain"].items.type
387+
== Type.STRING
388+
)
389+
assert (
390+
gemini_schema.properties["payload"].properties["device"].type
391+
== Type.STRING
392+
)
393+
assert gemini_schema.properties["payload"].properties["device"].enum == [
394+
"GLOBAL",
395+
"desktop",
396+
"mobile",
397+
]
398+
assert gemini_schema.properties["payload"].required == ["adDomain"]
399+
340400
def test_sanitize_integer_formats(self):
341401
"""Test that int32 and int64 formats are preserved for integer types"""
342402
openapi_schema = {

0 commit comments

Comments
 (0)