From 90a0f9ee2d19a5dd92dfbdf1e018859a4829f18a Mon Sep 17 00:00:00 2001 From: Sushant Poudel Date: Thu, 17 Sep 2026 00:30:21 +0545 Subject: [PATCH] fix(mcp): refuse the reserved tool name set_model_response _RESERVED_TOOL_NAMES covers the names the framework itself puts on the wire, so a server advertising one cannot have its tool dispatched in place of the framework's own. set_model_response belongs to that set but was missing. SetModelResponseTool is injected into the request whenever output_schema is configured alongside other tools (flows/llm_flows/prompt/_schema.py), the framework tells the model to answer through it by name, and base_llm_flow.py reads the result back by that same name. Because LlmRequest.append_tools resolves a duplicate name by last-wins with only a warning, an MCP server advertising set_model_response could otherwise receive the agent's structured final answer instead of the framework. The name is spelled out rather than imported: the function is defined inside SetModelResponseTool.__init__, so there is no module-level binding to import. Extends both existing reserved-name tests. Fixes #7144 --- src/google/adk/tools/mcp_tool/mcp_tool.py | 6 ++++++ tests/unittests/tools/mcp_tool/test_mcp_tool.py | 1 + tests/unittests/tools/mcp_tool/test_mcp_toolset.py | 1 + 3 files changed, 8 insertions(+) diff --git a/src/google/adk/tools/mcp_tool/mcp_tool.py b/src/google/adk/tools/mcp_tool/mcp_tool.py index a172b75969..b8d366b6f6 100644 --- a/src/google/adk/tools/mcp_tool/mcp_tool.py +++ b/src/google/adk/tools/mcp_tool/mcp_tool.py @@ -72,6 +72,12 @@ REQUEST_CONFIRMATION_FUNCTION_CALL_NAME, REQUEST_INPUT_FUNCTION_CALL_NAME, transfer_to_agent.__name__, + # Injected by the output-schema processor whenever output_schema is set + # alongside other tools (flows/llm_flows/prompt/_schema.py) and read back + # by name in base_llm_flow.py, so it is a framework-owned wire name too. + # Spelled out because the function is defined inside + # SetModelResponseTool.__init__ and is not importable. + 'set_model_response', }) _UNSET = object() diff --git a/tests/unittests/tools/mcp_tool/test_mcp_tool.py b/tests/unittests/tools/mcp_tool/test_mcp_tool.py index f1889f1c4a..4f10476aae 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_tool.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_tool.py @@ -344,6 +344,7 @@ def test_init_with_empty_description(self): "adk_request_confirmation", "adk_request_input", "transfer_to_agent", + "set_model_response", ], ) def test_init_reserved_name(self, reserved_name): diff --git a/tests/unittests/tools/mcp_tool/test_mcp_toolset.py b/tests/unittests/tools/mcp_tool/test_mcp_toolset.py index 4dbfdf6c68..2149ef36ae 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_toolset.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_toolset.py @@ -409,6 +409,7 @@ async def test_get_tools_skips_reserved_names(self): MockMCPTool("adk_request_credential"), MockMCPTool("adk_request_confirmation"), MockMCPTool("adk_request_input"), + MockMCPTool("set_model_response"), ] self.mock_session.list_tools = AsyncMock( return_value=MockListToolsResult(mock_tools)