Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/google/adk/tools/mcp_tool/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import base64
from collections.abc import Awaitable
import inspect
import json
import logging
from typing import Any
from typing import Callable
Expand Down Expand Up @@ -90,6 +91,32 @@

_UNSET = object()

# MCP defines `outputSchema` "for validation of structured results" on the
# client, but the Gemini path forwards it to the model inside
# `FunctionDeclaration.response_json_schema`. Gemini prices a schema as if
# every `$ref` were inlined, so server-generated schemas (proto-derived ones
# reach hundreds of thousands of characters) can push a single declaration
# past the model's input limit. Mirror the LiteLLM path, which only sends an
# output schema when it fits within `_MAX_TOOL_DESCRIPTION_LENGTH`: drop the
# ones that exceed the same budget here.
_MAX_RESPONSE_JSON_SCHEMA_LENGTH = 1024


def _response_schema_serialized_size(output_schema: Any) -> int | None:
"""Returns the JSON size of an MCP output schema.

Args:
output_schema: The schema read from the MCP tool definition.

Returns:
The serialized length in characters, or None when the value cannot be
serialized for measurement.
"""
try:
return len(json.dumps(output_schema))
except (TypeError, ValueError):
return None


def _merge_auth_and_provider_headers(
*,
Expand Down Expand Up @@ -404,6 +431,21 @@ def _build_declaration(self, *, fenced: bool = False) -> FunctionDeclaration:
if fenced and output_schema is not None
else output_schema
)
if output_schema is not None:
serialized_size = _response_schema_serialized_size(output_schema)
if (
serialized_size is not None
and serialized_size > _MAX_RESPONSE_JSON_SCHEMA_LENGTH
):
logger.warning(
"Omitting the output schema of MCP tool %r from its function"
" declaration: %d serialized characters exceeds the"
" %d-character budget for response schemas sent to the model.",
self.name,
serialized_size,
_MAX_RESPONSE_JSON_SCHEMA_LENGTH,
)
output_schema = None
function_decl = FunctionDeclaration(
name=self.name,
description=description,
Expand Down
74 changes: 74 additions & 0 deletions tests/unittests/tools/mcp_tool/test_mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,80 @@ def test_get_declaration_with_empty_output_schema_and_json_schema_for_func_decl_
assert declaration.response is None
assert not declaration.response_json_schema

def test_get_declaration_drops_oversized_output_schema(self):
"""An output schema too large to send is omitted from the declaration."""
output_schema = {
"type": "object",
"properties": {
"field_%d"
% i: {
"type": "string",
"description": "x" * 200,
}
for i in range(10)
},
}

tool = MCPTool(
mcp_tool=MockMCPTool(outputSchema=output_schema),
mcp_session_manager=self.mock_session_manager,
)

with temporary_feature_override(
FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True
):
declaration = tool._get_declaration()

assert declaration.response is None
assert declaration.response_json_schema is None
assert declaration.parameters_json_schema is not None

def test_get_declaration_logs_when_dropping_oversized_output_schema(
self, caplog
):
"""Dropping an oversized output schema is logged as a warning."""
output_schema = {
"type": "object",
"properties": {
"field": {"type": "string", "description": "x" * 2000},
},
}

tool = MCPTool(
mcp_tool=MockMCPTool(outputSchema=output_schema),
mcp_session_manager=self.mock_session_manager,
)

with temporary_feature_override(
FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True
):
with caplog.at_level(logging.WARNING):
declaration = tool._get_declaration()

assert declaration.response_json_schema is None
assert "Omitting the output schema" in caplog.text

def test_get_declaration_keeps_output_schema_within_budget(self):
"""An output schema that fits the budget is sent to the model."""
output_schema = {
"type": "object",
"properties": {
"status": {"type": "string", "description": "x" * 500},
},
}

tool = MCPTool(
mcp_tool=MockMCPTool(outputSchema=output_schema),
mcp_session_manager=self.mock_session_manager,
)

with temporary_feature_override(
FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True
):
declaration = tool._get_declaration()

assert declaration.response_json_schema == output_schema


class TestMCPTool:
"""Test suite for MCPTool class."""
Expand Down
Loading