Repository navigation
fix(mcp): bound the MCP output schema sent to Gemini - #7709
danieldutu wants to merge 1 commit into
Conversation
Somay-kousis
left a comment
There was a problem hiding this comment.
What I ran (head e6cd6a1, merge base f08f9d6):
pytest tests/unittests/tools/mcp_toolon head: 454 passed.- The 3 new tests with
src/reverted to the merge base: 1 failed (the oversized test), 2 passed. That matches the description. - A probe (probes/probe_7709.py) that builds
MCPTooldeclarations withJSON_SCHEMA_FOR_FUNC_DECLon and drivesprocess_llm_request, on head and on the base.
Findings
-
The bound is measured after fencing, so the model loses schemas far below 1024 characters (blocking). On the model path,
process_llm_requestswaps in_build_fenced_declaration(). In_build_declaration,fence_schema_descriptionsruns first (mcp_tool.py:411-415) and the length check comes after (:417-428). Eachdescriptiongains about 78 characters of fence markers. Probe: an output schema with 12 short property descriptions is 600 characters compact._get_declaration()keeps it, but the fenced declaration the model gets is 1536 characters and drops it. So the dev UI (get_tools_infouses_get_declaration) shows a response schema the model never receives, and the real limit shrinks with every description. It also means the description's claim thatlist_log_names(578) andquery_range(730) survive is probably wrong once fenced, if those schemas have any descriptions. I could not check those two servers. Suggest measuring the unfencedoutput_schemaonce, before fencing, and using that decision for both declarations. -
Non-ASCII schemas are measured as
\uXXXXescapes (worth considering).json.dumpsat :418 keeps the defaultensure_ascii=True, so each non-ASCII character counts as 6. Probe: a schema that is 256 characters with real characters is measured as 1156 and dropped on head. It is sent on the base. Any server with Japanese, Chinese or Korean descriptions hits the bound at roughly a sixth of the intended size.ensure_ascii=Falsefixes the measure. -
The warning fires twice per tool on every model call (worth considering).
BaseTool.process_llm_requestbuilds_get_declaration(), then MCPTool replaces it with_build_fenced_declaration(), and both log. Probe: one oversized tool over 3 model calls logs 6Dropping output_schemawarnings. With the 16 dropped Google Cloud tools from the description, that is 32 warnings per turn. The new test only checks a single_get_declaration()call. Warning once per tool instance (ordebug, like_append_response_schema_to_descriptionin lite_llm.py) would avoid the noise.
Not a finding, but relevant: #7708 targets the same Gemini oversized-schema problem with a different approach. Worth settling on one.
What I did not check: a live Gemini request, the real Google Cloud MCP schemas, and token counts.
Probe and raw output: probes/probe_7709.py, review_7709_evidence.txt.
McpTool._build_declaration set response_json_schema from the MCP tool's outputSchema with no size limit whenever JSON_SCHEMA_FOR_FUNC_DECL (the default) is on. Google Cloud's remote MCP servers serialize proto-derived output schemas to over 70,000 characters, and Gemini expands each $ref when tokenizing, so 18 declarations from those servers pushed the request past the 1,048,576-token input limit on every call (google#7690). Bound the serialized output schema at 1024 characters, the same ceiling _append_response_schema_to_description applies to the same schema on the OpenAI-compatible path (lite_llm.py). Larger schemas are dropped with one warning per tool instance naming the tool and the serialized length. The length is measured on the unfenced schema with ensure_ascii=False, so the plain and fenced declarations decide the same way and non-ASCII text counts one character each. Fixes google#7690
e6cd6a1 to
1edde71
Compare
|
Thanks for running the probes.
#7708 (sun04222, same issue) caps the same 1024 characters and logs a warning, with these differences in its diff: it measures after fencing ( Not covered: a live Gemini request, the real Google Cloud schemas after this change, token counts, a per-toolset opt-out (option i), and a configurable threshold. 25 failures in |
|
Reran on 1edde71: the 256-char non-ASCII schema measures by characters and is sent, and the fenced and unfenced paths now make the same call for a 600-char schema. Thanks. |
Link to Issue
Fixes: #7690
Problem
On the Gemini path,
McpTool._build_declarationsends the MCP tool'soutputSchemato the model asFunctionDeclaration.response_json_schemawith no size limit wheneverJSON_SCHEMA_FOR_FUNC_DECLis enabled, which is the default (src/google/adk/features/_feature_registry.py). Google Cloud's remote MCP servers (https://{logging,monitoring,cloudtrace,clouderrorreporting}.googleapis.com/mcp) return proto-derived output schemas that serialize to over 70,000 characters each, and Gemini counts every$refas if it were inlined when tokenizing. Sending all 18 of those declarations fails ongemini-3.5-flash,gemini-3.8-flashandgemini-4-argon:Without
response_json_schema, the same 18 declarations use 14,754 prompt tokens.get_dashboard's declaration alone costs 494,684 prompt tokens.Solution
Bound the serialized output schema at
_MAX_RESPONSE_JSON_SCHEMA_LENGTH = 1024characters in_build_declaration. The length is measured once on the server's unfenced schema withensure_ascii=False, and that one decision applies to both_get_declarationand_build_fenced_declaration. When a schema exceeds the bound,response_json_schemais dropped and one warning per tool instance is logged naming the tool and the serialized length. This mirrors the existing ceiling for the same schema on the OpenAI-compatible path:_append_response_schema_to_description(src/google/adk/models/lite_llm.py,_MAX_TOOL_DESCRIPTION_LENGTH = 1024, #6815) omits the rendered schema past 1024 characters. The Anthropic path never sends the schema.With this bound, from the Google Cloud servers only
list_log_names(578) andquery_range(730) survive when measured unfenced. The bound is applied before fencing, so the fenced length of those two schemas does not affect the decision. The 18 declarations drop to roughly the 14,754-token baseline. The$defslive inside the serialized document, so at 1024 characters the$ref-inlining expansion Gemini applies is bounded.Question for maintainers: is 1024 the threshold you want, or should the bound be a different value, and should it be configurable?
Testing Plan
uv run pytest tests/unittests/tools/mcp_tool→457 passed, 143 warnings in 5.03suv run pytest tests/unittests/tools→25 failed, 3283 passed; the 25 failures are intest_bash_tool.py(5) andtest_skill_toolset.py(20), outside the touched files. The same 25 fail on basef08f9d67(source restored to base, run with the new tests present:27 failed, 3281 passed; the 2 extra failures are new tests intest_mcp_tool.pythat need the fix).uv run pre-commit run --files src/google/adk/tools/mcp_tool/mcp_tool.py tests/unittests/tools/mcp_tool/test_mcp_tool.py→ all hooks Passedtests/unittests/tools/mcp_tool/test_mcp_tool.py::TestMCPToolWithJsonSchema:test_get_declaration_with_output_schema_under_bound_sent,test_get_declaration_with_oversized_output_schema_dropped_with_warning,test_get_declaration_without_output_schema_and_json_schema_for_func_decl_enabled,test_fenced_declaration_keeps_output_schema_under_bound_before_fencing(12 short descriptions, under 1024 compact, must survive in the fenced declaration),test_get_declaration_measures_non_ascii_output_schema_by_characters(256 non-ASCII characters, 1156 with escapes, 600 characters),test_oversized_output_schema_warns_once_per_tool(three builds across both declaration paths, one warning). The three review tests fail on the previous commit (3 failed, 118 deselected) and pass with the fix.uv run --with 'google-adk[mcp]' ... repro.py <vertex-project>.Checklist
Additional context
The tox multi-version run was left to CI. No documentation change is needed: the bound is client-side with no new public option.
Not covered
outputSchemaentirely (the issue's option i) is not in this change; the fixed bound handles the reported failure. If maintainers want an opt-out, it can be a follow-up.JSON_SCHEMA_FOR_FUNC_DECLpath; the legacy_to_gemini_schemapath never sentresponse_json_schema.