Skip to content

MCP outputSchema is sent to Gemini unbounded, exceeding the token limit #7690

Description

@risu729

🔴 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.

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:

Other MCP clients don't send outputSchema to the model by default either (details below).

ADK code: LiteLLM and Anthropic paths

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

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.ts ignores the server's outputSchema and validates only against user-supplied schemas
  • OpenAI Agents SDK: mcp/util.py sets only params_json_schema
  • langchain-mcp-adapters: tools.py sets only args_schema=tool.inputSchema
  • Anthropic Python SDK: lib/tools/mcp.py sets only input_schema
  • Pydantic AI: opt-in; include_return_schema defaults to None, which resolves to False
  • MCP Python SDK client: uses it only in validate_tool_result

Steps to Reproduce:

  1. Run the code below with uv run --with 'google-adk[mcp]==2.12.0' --with 'mcp<2' python repro.py <vertex-project>. tools/list on these endpoints needs no credentials; only the generate_content calls need a Vertex AI project.
  2. 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 / McpTool option to skip outputSchema (similar to tool_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:

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:
    • $defs keys are proto message names, and descriptions are proto comments.
    • They contain *_UNSPECIFIED enum values and x-google-enum-descriptions.
    • They are returned even when the client negotiates 2025-03-26, a protocol version without outputSchema.
    • 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:

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

  1. sun04222 commented on Oct 24, 2026

    @sun04222

    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 omits response_json_schema when 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.

  2. danieldutu commented on Oct 24, 2026

    @danieldutu

    Posting on #7690 before the PR:

    MCP tool outputSchema reaches Gemini unbounded: McpTool._build_declaration (src/google/adk/tools/mcp_tool/mcp_tool.py) sets FunctionDeclaration.response_json_schema verbatim whenever JSON_SCHEMA_FOR_FUNC_DECL is 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 $ref while 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 in lite_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions