Repository navigation
MCP outputSchema is sent to Gemini unbounded, exceeding the token limit #7690
Description
🔴 Required Information
Describe the Bug:
On the Gemini path, McpTool._build_declaration() sends the MCP tool's outputSchema to the model as FunctionDeclaration.response_json_schema, verbatim and with no size limit. This happens whenever JSON_SCHEMA_FOR_FUNC_DECL is enabled, which is the default. McpToolset has no option to opt out per tool or per toolset.
adk-python/src/google/adk/tools/mcp_tool/mcp_tool.py
Lines 387 to 401 in 38381f6
| if is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL): | |
| output_schema = _read_field( | |
| self._mcp_tool, "outputSchema", "output_schema" | |
| ) | |
| output_schema = ( | |
| fence_schema_descriptions(output_schema) | |
| if fenced and output_schema is not None | |
| else output_schema | |
| ) | |
| function_decl = FunctionDeclaration( | |
| name=self.name, | |
| description=description, | |
| parameters_json_schema=input_schema, | |
| response_json_schema=output_schema, | |
| ) |
As a result, Google Cloud's own remote MCP servers (https://{logging,monitoring,cloudtrace,clouderrorreporting}.googleapis.com/mcp) cannot be used with ADK as-is. Their proto-derived output schemas push every request over Gemini's 1,048,576-token input limit.
ADK already handles this elsewhere:
- The LiteLLM path (fca7b88, fix(models): forward tool output schema on the LiteLLM path #6815) drops the output schema if it would push the description past 1024 characters.
- The Anthropic path does not send it at all.
Other MCP clients don't send outputSchema to the model by default either (details below).
ADK code: LiteLLM and Anthropic paths
adk-python/src/google/adk/models/lite_llm.py
Lines 2685 to 2693 in 38381f6
| if len(candidate) > _MAX_TOOL_DESCRIPTION_LENGTH: | |
| logger.debug( | |
| "Omitting output schema for tool %s: rendered description length %d" | |
| " exceeds limit %d", | |
| function_declaration.name, | |
| len(candidate), | |
| _MAX_TOOL_DESCRIPTION_LENGTH, | |
| ) | |
| return description |
adk-python/src/google/adk/models/anthropic_llm.py
Lines 869 to 873 in 38381f6
| return anthropic_types.ToolParam( | |
| name=function_declaration.name, | |
| description=function_declaration.description or "", | |
| input_schema=input_schema, |
Other MCP clients
These clients build the model-facing tool from inputSchema. At most, they use outputSchema to validate structuredContent.
- Vercel AI SDK:
mcp-client.tsignores the server'soutputSchemaand validates only against user-supplied schemas - OpenAI Agents SDK:
mcp/util.pysets onlyparams_json_schema - langchain-mcp-adapters:
tools.pysets onlyargs_schema=tool.inputSchema - Anthropic Python SDK:
lib/tools/mcp.pysets onlyinput_schema - Pydantic AI: opt-in;
include_return_schemadefaults toNone, which resolves toFalse - MCP Python SDK client: uses it only in
validate_tool_result
Steps to Reproduce:
- Run the code below with
uv run --with 'google-adk[mcp]==2.12.0' --with 'mcp<2' python repro.py <vertex-project>.tools/liston these endpoints needs no credentials; only thegenerate_contentcalls need a Vertex AI project. - Observe the 400 error when all tools are sent, and the per-tool prompt tokens.
Expected Behavior:
Tool declarations should fit within the model's input limit, and there should be a way to control whether output schemas are sent to the model. For example:
- a
McpToolset/McpTooloption to skipoutputSchema(similar totool_filter), and/or - a size limit on the Gemini path, like the one the LiteLLM path has.
Observed Behavior:
Sending all 18 declarations (prompt "hello", max_output_tokens=16) fails on gemini-3.5-flash, gemini-3.8-flash and gemini-4-argon alike:
400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'The input token count exceeds the maximum number of tokens allowed 1048576.', 'status': 'INVALID_ARGUMENT'}}
Without response_json_schema, the same 18 declarations use only 14,754 prompt tokens. Prompt tokens per tool on gemini-3.8-flash (largest first):
tool ADK default without response_json_schema
get_dashboard 494,684 151
list_dashboards 494,800 192
list_alert_policies 18,764 375
get_alert_policy 18,380 111
list_timeseries 12,410 8,525
...
get_alert 1,176 112
The cost is much larger than the JSON size suggests because Gemini counts a schema as if every $ref were inlined. get_dashboard's output schema is 79,181 characters as JSON (61 $defs, 90 $refs), but about 2.06M characters once inlined.
Full measurements
Declaration sizes in characters with ADK 2.12.0 and default flags. With ADK_DISABLE_JSON_SCHEMA_FOR_FUNC_DECL=1, the totals are params=33475 response=0.
logging get_bucket params= 571 response= 7488
logging get_view params= 366 response= 1282
logging list_buckets params= 1175 response= 7953
logging list_log_entries params= 3734 response= 22385
logging list_log_names params= 1648 response= 578
logging list_views params= 863 response= 1748
monitoring get_alert params= 313 response= 6285
monitoring get_alert_policy params= 289 response= 53749
monitoring get_dashboard params= 357 response= 79181
monitoring list_alert_policies params= 1608 response= 54449
monitoring list_alerts params= 1350 response= 6989
monitoring list_dashboards params= 702 response= 79649
monitoring list_metric_descriptors params= 2041 response= 13805
monitoring list_timeseries params= 20411 response= 21798
monitoring query_range params= 1909 response= 730
cloudtrace get_trace params= 340 response= 3165
cloudtrace list_traces params= 2421 response= 3645
clouderrorreporting list_group_stats params= 6435 response= 12488
TOTAL params=46533 response=377367
Prompt tokens per tool on gemini-3.8-flash:
tool ADK default without response_json_schema
get_bucket 1,669 168
get_view 369 119
list_buckets 1,855 280
list_log_entries 5,457 885
list_log_names 533 414
list_views 530 207
get_alert 1,176 112
get_alert_policy 18,380 111
get_dashboard 494,684 151
list_alert_policies 18,764 375
list_alerts 1,513 332
list_dashboards 494,800 192
list_metric_descriptors 3,696 459
list_timeseries 12,410 8,525
query_range 470 358
get_trace 804 137
list_traces 1,300 565
list_group_stats 3,595 1,381
$ref expansion check: a synthetic response schema whose properties reference the same ~4k-character $defs entry N times. It costs exactly as many tokens as the fully inlined schema.
N= 1 $ref json_chars=4206 prompt_tokens=768 | inline json_chars=4161 prompt_tokens=768
N= 5 $ref json_chars=4334 prompt_tokens=3760 | inline json_chars=20669 prompt_tokens=3760
N=20 $ref json_chars=4824 prompt_tokens=14990 | inline json_chars=82584 prompt_tokens=14990
count_tokens with the same tools returns 940 tokens whether or not response_json_schema is included, so it does not reveal the problem.
ADK_DISABLE_JSON_SCHEMA_FOR_FUNC_DECL=1 is not a practical workaround:
- It is global. Every
FunctionTooland MCP tool switches to the legacy_to_gemini_schemapath for parameters (see [legacy path] Nested pydantic model tool parameter fails under VERTEX_AI when the function also has a fallback-parsed parameter — only with JSON_SCHEMA_FOR_FUNC_DECL disabled #6984). - The flag was promoted to stable on
mainin ea298d4 (not yet released), and graduated flags may be removed.
Environment Details:
- ADK Library Version (pip show google-adk): 2.12.0 (same code path in 2.11.0)
- Desktop OS: macOS
- Python Version (python -V): 3.12
Model Information:
- Are you using LiteLLM: No
- Which model is being used: gemini-3.8-flash (also gemini-3.5-flash, gemini-4-argon)
🟡 Optional Information
Regression:
This started when MCP output schemas began to be propagated to FunctionDeclaration. It was added in c8e5340 for #2828, reverted in 92a7d19, and reintroduced behind JSON_SCHEMA_FOR_FUNC_DECL in 1dd97f5.
Additional Context:
#2828 asked for the output schema so the model could describe a small tool's result. That is reasonable for small schemas, but I think the decision of whether, and how much, of outputSchema to send to the model belongs to ADK as the client:
- The MCP spec defines
outputSchema"for validation of structured results". - It is unlikely to be fixed on the server side or in the ecosystem (details below).
Current workaround: subclass McpTool, or strip response_json_schema in a before_model_callback.
Spec and ecosystem details
- The spec also says the schema helps clients and LLMs, but it does not require sending it to the model. An MCP maintainer noted that, unlike input schemas, output schemas are not necessarily copied into the context window.
- The Google Cloud servers' output schemas are generated mechanically from protos:
$defskeys are proto message names, and descriptions are proto comments.- They contain
*_UNSPECIFIEDenum values andx-google-enum-descriptions. - They are returned even when the client negotiates
2025-03-26, a protocol version withoutoutputSchema. - I found no documented option or plan to omit or shrink them in the MCP docs, the MCP release notes, or the Monitoring MCP reference.
- The ecosystem is moving toward more output schemas, not smaller ones:
- The MCP Python SDK generates
outputSchemafrom return annotations by default, and so does FastMCP. - A spec proposal to address tool-definition token bloat (SEP-1576) was closed as dormant.
- The MCP Python SDK generates
Minimal Reproduction Code:
repro.py
import asyncio
import sys
from google import genai
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from google.genai import types
# tools/list on these endpoints does not require credentials
SERVICES = ["logging", "monitoring", "cloudtrace", "clouderrorreporting"]
MODEL = "gemini-3.8-flash"
async def declarations():
decls = []
for service in SERVICES:
toolset = McpToolset(
connection_params=StreamableHTTPConnectionParams(url=f"https://{service}.googleapis.com/mcp")
)
try:
decls += [tool._get_declaration() for tool in await toolset.get_tools()]
finally:
await toolset.close()
return decls
def prompt_tokens(client, decls):
try:
response = client.models.generate_content(
model=MODEL,
contents="hello",
config=types.GenerateContentConfig(
tools=[types.Tool(function_declarations=decls)], max_output_tokens=16
),
)
return response.usage_metadata.prompt_token_count
except Exception as e:
return f"error: {e}"
async def main():
client = genai.Client(vertexai=True, project=sys.argv[1], location="global")
decls = await declarations()
stripped = [d.model_copy(update={"response_json_schema": None}) for d in decls]
print("all tools:", prompt_tokens(client, decls))
print("all tools without response_json_schema:", prompt_tokens(client, stripped))
for d, s in zip(decls, stripped):
print(f"{d.name:26} {prompt_tokens(client, [d])!s:>10} {prompt_tokens(client, [s])!s:>8}")
asyncio.run(main())How often has this issue occurred?:
- Always (100%)
Activity
sun04222 commentedon Oct 24, 2026
Happy to take this one — a fix is prepared and tested locally.
Approach: mirror the LiteLLM path cited in the report.
McpTool._build_declaration()now omitsresponse_json_schemawhen the serialized output schema exceeds the same 1024-character budget the LiteLLM path already uses (_MAX_TOOL_DESCRIPTION_LENGTH), and logs a warning when it drops one. Schemas within the budget are still sent, so the #2828 use case (the model describing a small tool's result) keeps working, while every Google Cloud proto-derived schema in the issue's size table exceeds the budget and is omitted — bringing the 18-tool logging/monitoring/trace request back under the input limit.I kept it to the size cap (the smallest change that unblocks the reported servers); a per-toolset opt-out can follow as a separate change if maintainers would prefer one. PR incoming in a few minutes with three regression tests (oversized dropped + warning logged, within-budget preserved) and the full
tests/unittests/tools/mcp_tool/suite passing locally.danieldutu commentedon Oct 24, 2026
Posting on #7690 before the PR:
MCP tool
outputSchemareaches Gemini unbounded:McpTool._build_declaration(src/google/adk/tools/mcp_tool/mcp_tool.py) setsFunctionDeclaration.response_json_schemaverbatim wheneverJSON_SCHEMA_FOR_FUNC_DECLis on, which is the default. I reproduced the shape offline: a schema of 78,076 serialized characters is sent as-is, and Gemini expands each$refwhile tokenizing, which is how 18 Google Cloud MCP tools reach the 1,048,576-token input limit. I am preparing a PR that bounds the serialized output schema at 1024 characters on the Gemini path, the same ceiling the OpenAI-compatible path already applies inlite_llm.py; larger schemas are dropped with one warning naming the tool and the size. Keeping a per-toolset opt-out (the other option in this issue) out for now — it can follow if the bound alone proves too coarse.fix(tools): bound the MCP output schema sent to Gemini