From 0dce65c97f2109ca586084cffbca45a0aa0733f1 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 2 Sep 2025 22:20:43 +0900 Subject: [PATCH 01/15] fix(mcp): Initialize tool_name_prefix in MCPToolse --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 10 ++- tests/unittests/tools/test_mcp_toolset.py | 70 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/tools/test_mcp_toolset.py diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 7a1a054ca6..9540c989d7 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,6 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, + tool_name_prefix: str = "", errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -118,11 +119,15 @@ def __init__( tool_filter: Optional filter to select specific tools. Can be either: - A list of tool names to include - A ToolPredicate function for custom filtering logic + tool_name_prefix: A prefix to be added to the name of each tool in this + toolset. errlog: TextIO stream for error logging. auth_scheme: The auth scheme of the tool for tool calling auth_credential: The auth credential of the tool for tool calling """ - super().__init__(tool_filter=tool_filter) + super().__init__( + tool_filter=tool_filter, tool_name_prefix=tool_name_prefix + ) if not connection_params: raise ValueError("Missing connection params in MCPToolset.") @@ -207,6 +212,7 @@ def from_config( return cls( connection_params=connection_params, tool_filter=mcp_toolset_config.tool_filter, + tool_name_prefix=mcp_toolset_config.tool_name_prefix, auth_scheme=mcp_toolset_config.auth_scheme, auth_credential=mcp_toolset_config.auth_credential, ) @@ -239,6 +245,8 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None + tool_name_prefix: str = "" + auth_scheme: Optional[AuthScheme] = None auth_credential: Optional[AuthCredential] = None diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py new file mode 100644 index 0000000000..dce8dc66d9 --- /dev/null +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -0,0 +1,70 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for McpToolset.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +import pytest + + +@pytest.mark.asyncio +async def test_mcp_toolset_with_prefix(): + """Test that McpToolset correctly applies the tool_name_prefix.""" + # Mock the connection parameters + mock_connection_params = MagicMock() + + # Mock the MCPSessionManager and its create_session method + mock_session_manager = MagicMock() + mock_session = MagicMock() + + # Mock the list_tools response from the MCP server + mock_tool1 = MagicMock() + mock_tool1.name = "tool1" + mock_tool1.description = "tool 1 desc" + mock_tool2 = MagicMock() + mock_tool2.name = "tool2" + mock_tool2.description = "tool 2 desc" + list_tools_result = MagicMock() + list_tools_result.tools = [mock_tool1, mock_tool2] + mock_session.list_tools = AsyncMock(return_value=list_tools_result) + mock_session_manager.create_session = AsyncMock(return_value=mock_session) + + # Create an instance of McpToolset with a prefix + toolset = McpToolset( + connection_params=mock_connection_params, + tool_name_prefix="my_prefix", + ) + + # Replace the internal session manager with our mock + toolset._mcp_session_manager = mock_session_manager + + # Get the tools from the toolset + tools = await toolset.get_tools() + + # The get_tools method in McpToolset returns MCPTool objects, which are + # instances of BaseTool. The prefixing is handled by the BaseToolset, + # so we need to call get_tools_with_prefix to get the prefixed tools. + prefixed_tools = await toolset.get_tools_with_prefix() + + # Assert that the tools are prefixed correctly + assert len(prefixed_tools) == 2 + assert prefixed_tools[0].name == "my_prefix_tool1" + assert prefixed_tools[1].name == "my_prefix_tool2" + + # Assert that the original tools are not modified + assert tools[0].name == "tool1" + assert tools[1].name == "tool2" \ No newline at end of file From 5e21bc2d39d5845daf4777ae6bc61f6cda98be30 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Fri, 5 Sep 2025 18:08:28 +0900 Subject: [PATCH 02/15] fix(mcp): Use original tool name when calling MCP server - Change self.name to self._mcp_tool.name in call_tool invocation - Ensures MCP server receives unprefixed tool name - Addresses review comment from alberto-mate --- src/google/adk/tools/mcp_tool/mcp_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_tool.py b/src/google/adk/tools/mcp_tool/mcp_tool.py index d53fa09c9b..6adcadba61 100644 --- a/src/google/adk/tools/mcp_tool/mcp_tool.py +++ b/src/google/adk/tools/mcp_tool/mcp_tool.py @@ -131,7 +131,7 @@ async def _run_async_impl( # Get the session from the session manager session = await self._mcp_session_manager.create_session(headers=headers) - response = await session.call_tool(self.name, arguments=args) + response = await session.call_tool(self._mcp_tool.name, arguments=args) return response async def _get_headers( From df0dfdd528be5ea794ef9ca6e2fa0de4c4725fa9 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 2 Sep 2025 22:20:43 +0900 Subject: [PATCH 03/15] fix(mcp): Initialize tool_name_prefix in MCPToolse --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 10 ++- tests/unittests/tools/test_mcp_toolset.py | 70 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/tools/test_mcp_toolset.py diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 7a1a054ca6..9540c989d7 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,6 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, + tool_name_prefix: str = "", errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -118,11 +119,15 @@ def __init__( tool_filter: Optional filter to select specific tools. Can be either: - A list of tool names to include - A ToolPredicate function for custom filtering logic + tool_name_prefix: A prefix to be added to the name of each tool in this + toolset. errlog: TextIO stream for error logging. auth_scheme: The auth scheme of the tool for tool calling auth_credential: The auth credential of the tool for tool calling """ - super().__init__(tool_filter=tool_filter) + super().__init__( + tool_filter=tool_filter, tool_name_prefix=tool_name_prefix + ) if not connection_params: raise ValueError("Missing connection params in MCPToolset.") @@ -207,6 +212,7 @@ def from_config( return cls( connection_params=connection_params, tool_filter=mcp_toolset_config.tool_filter, + tool_name_prefix=mcp_toolset_config.tool_name_prefix, auth_scheme=mcp_toolset_config.auth_scheme, auth_credential=mcp_toolset_config.auth_credential, ) @@ -239,6 +245,8 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None + tool_name_prefix: str = "" + auth_scheme: Optional[AuthScheme] = None auth_credential: Optional[AuthCredential] = None diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py new file mode 100644 index 0000000000..dce8dc66d9 --- /dev/null +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -0,0 +1,70 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for McpToolset.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +import pytest + + +@pytest.mark.asyncio +async def test_mcp_toolset_with_prefix(): + """Test that McpToolset correctly applies the tool_name_prefix.""" + # Mock the connection parameters + mock_connection_params = MagicMock() + + # Mock the MCPSessionManager and its create_session method + mock_session_manager = MagicMock() + mock_session = MagicMock() + + # Mock the list_tools response from the MCP server + mock_tool1 = MagicMock() + mock_tool1.name = "tool1" + mock_tool1.description = "tool 1 desc" + mock_tool2 = MagicMock() + mock_tool2.name = "tool2" + mock_tool2.description = "tool 2 desc" + list_tools_result = MagicMock() + list_tools_result.tools = [mock_tool1, mock_tool2] + mock_session.list_tools = AsyncMock(return_value=list_tools_result) + mock_session_manager.create_session = AsyncMock(return_value=mock_session) + + # Create an instance of McpToolset with a prefix + toolset = McpToolset( + connection_params=mock_connection_params, + tool_name_prefix="my_prefix", + ) + + # Replace the internal session manager with our mock + toolset._mcp_session_manager = mock_session_manager + + # Get the tools from the toolset + tools = await toolset.get_tools() + + # The get_tools method in McpToolset returns MCPTool objects, which are + # instances of BaseTool. The prefixing is handled by the BaseToolset, + # so we need to call get_tools_with_prefix to get the prefixed tools. + prefixed_tools = await toolset.get_tools_with_prefix() + + # Assert that the tools are prefixed correctly + assert len(prefixed_tools) == 2 + assert prefixed_tools[0].name == "my_prefix_tool1" + assert prefixed_tools[1].name == "my_prefix_tool2" + + # Assert that the original tools are not modified + assert tools[0].name == "tool1" + assert tools[1].name == "tool2" \ No newline at end of file From 1cd9ff4d79023c9d3f117891fedcd51bf221ab04 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Fri, 5 Sep 2025 18:08:28 +0900 Subject: [PATCH 04/15] fix(mcp): Use original tool name when calling MCP server - Change self.name to self._mcp_tool.name in call_tool invocation - Ensures MCP server receives unprefixed tool name - Addresses review comment from alberto-mate --- src/google/adk/tools/mcp_tool/mcp_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_tool.py b/src/google/adk/tools/mcp_tool/mcp_tool.py index d53fa09c9b..6adcadba61 100644 --- a/src/google/adk/tools/mcp_tool/mcp_tool.py +++ b/src/google/adk/tools/mcp_tool/mcp_tool.py @@ -131,7 +131,7 @@ async def _run_async_impl( # Get the session from the session manager session = await self._mcp_session_manager.create_session(headers=headers) - response = await session.call_tool(self.name, arguments=args) + response = await session.call_tool(self._mcp_tool.name, arguments=args) return response async def _get_headers( From a8eea5558845fa9791e9f3739655c2fd771b2d8f Mon Sep 17 00:00:00 2001 From: minjunpark Date: Sat, 6 Sep 2025 11:48:39 +0900 Subject: [PATCH 05/15] fix(mcp): Make tool_name_prefix parameter optional in McpToolset - Change tool_name_prefix default from empty string to None - Update type annotation to Optional[str] in both McpToolset and McpToolsetConfig - Addresses review comment from seanzhou1023 --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 9540c989d7..81b960e900 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,7 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, - tool_name_prefix: str = "", + tool_name_prefix: Optional[str] = None, errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -245,7 +245,7 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None - tool_name_prefix: str = "" + tool_name_prefix: Optional[str] = None auth_scheme: Optional[AuthScheme] = None From fd396bff13d27f7c22e1e6d0d947127d9c8435cd Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 9 Sep 2025 20:41:00 +0900 Subject: [PATCH 06/15] test: Skip MCP tests when Python version is less than 3.10 - Add Python version check to test_mcp_toolset.py - Follow existing MCP test pattern with pytestmark skipif decorator - Add import error handling with dummy classes for test collection - Addresses review comment from seanzhou1023 --- tests/unittests/tools/test_mcp_toolset.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py index dce8dc66d9..aecdab91bf 100644 --- a/tests/unittests/tools/test_mcp_toolset.py +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -14,12 +14,31 @@ """Unit tests for McpToolset.""" +import sys from unittest.mock import AsyncMock from unittest.mock import MagicMock -from google.adk.tools.mcp_tool.mcp_toolset import McpToolset import pytest +# Skip all tests in this module if Python version is less than 3.10 +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" +) + +# Import dependencies with version checking +try: + from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +except ImportError as e: + if sys.version_info < (3, 10): + # Create dummy classes to prevent NameError during test collection + # Tests will be skipped anyway due to pytestmark + class DummyClass: + pass + + McpToolset = DummyClass + else: + raise e + @pytest.mark.asyncio async def test_mcp_toolset_with_prefix(): From 276f0685e056af6363cb85af65446a1e88724c42 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 2 Sep 2025 22:20:43 +0900 Subject: [PATCH 07/15] fix(mcp): Initialize tool_name_prefix in MCPToolse --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 10 ++- tests/unittests/tools/test_mcp_toolset.py | 70 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/tools/test_mcp_toolset.py diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 7a1a054ca6..9540c989d7 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,6 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, + tool_name_prefix: str = "", errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -118,11 +119,15 @@ def __init__( tool_filter: Optional filter to select specific tools. Can be either: - A list of tool names to include - A ToolPredicate function for custom filtering logic + tool_name_prefix: A prefix to be added to the name of each tool in this + toolset. errlog: TextIO stream for error logging. auth_scheme: The auth scheme of the tool for tool calling auth_credential: The auth credential of the tool for tool calling """ - super().__init__(tool_filter=tool_filter) + super().__init__( + tool_filter=tool_filter, tool_name_prefix=tool_name_prefix + ) if not connection_params: raise ValueError("Missing connection params in MCPToolset.") @@ -207,6 +212,7 @@ def from_config( return cls( connection_params=connection_params, tool_filter=mcp_toolset_config.tool_filter, + tool_name_prefix=mcp_toolset_config.tool_name_prefix, auth_scheme=mcp_toolset_config.auth_scheme, auth_credential=mcp_toolset_config.auth_credential, ) @@ -239,6 +245,8 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None + tool_name_prefix: str = "" + auth_scheme: Optional[AuthScheme] = None auth_credential: Optional[AuthCredential] = None diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py new file mode 100644 index 0000000000..dce8dc66d9 --- /dev/null +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -0,0 +1,70 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for McpToolset.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +import pytest + + +@pytest.mark.asyncio +async def test_mcp_toolset_with_prefix(): + """Test that McpToolset correctly applies the tool_name_prefix.""" + # Mock the connection parameters + mock_connection_params = MagicMock() + + # Mock the MCPSessionManager and its create_session method + mock_session_manager = MagicMock() + mock_session = MagicMock() + + # Mock the list_tools response from the MCP server + mock_tool1 = MagicMock() + mock_tool1.name = "tool1" + mock_tool1.description = "tool 1 desc" + mock_tool2 = MagicMock() + mock_tool2.name = "tool2" + mock_tool2.description = "tool 2 desc" + list_tools_result = MagicMock() + list_tools_result.tools = [mock_tool1, mock_tool2] + mock_session.list_tools = AsyncMock(return_value=list_tools_result) + mock_session_manager.create_session = AsyncMock(return_value=mock_session) + + # Create an instance of McpToolset with a prefix + toolset = McpToolset( + connection_params=mock_connection_params, + tool_name_prefix="my_prefix", + ) + + # Replace the internal session manager with our mock + toolset._mcp_session_manager = mock_session_manager + + # Get the tools from the toolset + tools = await toolset.get_tools() + + # The get_tools method in McpToolset returns MCPTool objects, which are + # instances of BaseTool. The prefixing is handled by the BaseToolset, + # so we need to call get_tools_with_prefix to get the prefixed tools. + prefixed_tools = await toolset.get_tools_with_prefix() + + # Assert that the tools are prefixed correctly + assert len(prefixed_tools) == 2 + assert prefixed_tools[0].name == "my_prefix_tool1" + assert prefixed_tools[1].name == "my_prefix_tool2" + + # Assert that the original tools are not modified + assert tools[0].name == "tool1" + assert tools[1].name == "tool2" \ No newline at end of file From b23a49bf9ce00aacd304fa1f7d6df18daf8d9f5a Mon Sep 17 00:00:00 2001 From: minjunpark Date: Fri, 5 Sep 2025 18:08:28 +0900 Subject: [PATCH 08/15] fix(mcp): Use original tool name when calling MCP server - Change self.name to self._mcp_tool.name in call_tool invocation - Ensures MCP server receives unprefixed tool name - Addresses review comment from alberto-mate --- src/google/adk/tools/mcp_tool/mcp_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_tool.py b/src/google/adk/tools/mcp_tool/mcp_tool.py index 3bdd34e1c4..8683cdf3f0 100644 --- a/src/google/adk/tools/mcp_tool/mcp_tool.py +++ b/src/google/adk/tools/mcp_tool/mcp_tool.py @@ -136,7 +136,7 @@ async def _run_async_impl( # Get the session from the session manager session = await self._mcp_session_manager.create_session(headers=headers) - response = await session.call_tool(self.name, arguments=args) + response = await session.call_tool(self._mcp_tool.name, arguments=args) return response async def _get_headers( From ec370b58cfa43f3ad8065d2e51775b99c782a10c Mon Sep 17 00:00:00 2001 From: minjunpark Date: Sat, 6 Sep 2025 11:48:39 +0900 Subject: [PATCH 09/15] fix(mcp): Make tool_name_prefix parameter optional in McpToolset - Change tool_name_prefix default from empty string to None - Update type annotation to Optional[str] in both McpToolset and McpToolsetConfig - Addresses review comment from seanzhou1023 --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 9540c989d7..81b960e900 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,7 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, - tool_name_prefix: str = "", + tool_name_prefix: Optional[str] = None, errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -245,7 +245,7 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None - tool_name_prefix: str = "" + tool_name_prefix: Optional[str] = None auth_scheme: Optional[AuthScheme] = None From 070545081579e31b07de4f0f562e27b4a4fc3940 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 9 Sep 2025 20:41:00 +0900 Subject: [PATCH 10/15] test: Skip MCP tests when Python version is less than 3.10 - Add Python version check to test_mcp_toolset.py - Follow existing MCP test pattern with pytestmark skipif decorator - Add import error handling with dummy classes for test collection - Addresses review comment from seanzhou1023 --- tests/unittests/tools/test_mcp_toolset.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py index dce8dc66d9..aecdab91bf 100644 --- a/tests/unittests/tools/test_mcp_toolset.py +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -14,12 +14,31 @@ """Unit tests for McpToolset.""" +import sys from unittest.mock import AsyncMock from unittest.mock import MagicMock -from google.adk.tools.mcp_tool.mcp_toolset import McpToolset import pytest +# Skip all tests in this module if Python version is less than 3.10 +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" +) + +# Import dependencies with version checking +try: + from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +except ImportError as e: + if sys.version_info < (3, 10): + # Create dummy classes to prevent NameError during test collection + # Tests will be skipped anyway due to pytestmark + class DummyClass: + pass + + McpToolset = DummyClass + else: + raise e + @pytest.mark.asyncio async def test_mcp_toolset_with_prefix(): From 1778a2dad68aaf0e81601f90950457aec8804611 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 2 Sep 2025 22:20:43 +0900 Subject: [PATCH 11/15] fix(mcp): Initialize tool_name_prefix in MCPToolse --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 10 ++- tests/unittests/tools/test_mcp_toolset.py | 70 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/tools/test_mcp_toolset.py diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 7a1a054ca6..9540c989d7 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,6 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, + tool_name_prefix: str = "", errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -118,11 +119,15 @@ def __init__( tool_filter: Optional filter to select specific tools. Can be either: - A list of tool names to include - A ToolPredicate function for custom filtering logic + tool_name_prefix: A prefix to be added to the name of each tool in this + toolset. errlog: TextIO stream for error logging. auth_scheme: The auth scheme of the tool for tool calling auth_credential: The auth credential of the tool for tool calling """ - super().__init__(tool_filter=tool_filter) + super().__init__( + tool_filter=tool_filter, tool_name_prefix=tool_name_prefix + ) if not connection_params: raise ValueError("Missing connection params in MCPToolset.") @@ -207,6 +212,7 @@ def from_config( return cls( connection_params=connection_params, tool_filter=mcp_toolset_config.tool_filter, + tool_name_prefix=mcp_toolset_config.tool_name_prefix, auth_scheme=mcp_toolset_config.auth_scheme, auth_credential=mcp_toolset_config.auth_credential, ) @@ -239,6 +245,8 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None + tool_name_prefix: str = "" + auth_scheme: Optional[AuthScheme] = None auth_credential: Optional[AuthCredential] = None diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py new file mode 100644 index 0000000000..dce8dc66d9 --- /dev/null +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -0,0 +1,70 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for McpToolset.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +import pytest + + +@pytest.mark.asyncio +async def test_mcp_toolset_with_prefix(): + """Test that McpToolset correctly applies the tool_name_prefix.""" + # Mock the connection parameters + mock_connection_params = MagicMock() + + # Mock the MCPSessionManager and its create_session method + mock_session_manager = MagicMock() + mock_session = MagicMock() + + # Mock the list_tools response from the MCP server + mock_tool1 = MagicMock() + mock_tool1.name = "tool1" + mock_tool1.description = "tool 1 desc" + mock_tool2 = MagicMock() + mock_tool2.name = "tool2" + mock_tool2.description = "tool 2 desc" + list_tools_result = MagicMock() + list_tools_result.tools = [mock_tool1, mock_tool2] + mock_session.list_tools = AsyncMock(return_value=list_tools_result) + mock_session_manager.create_session = AsyncMock(return_value=mock_session) + + # Create an instance of McpToolset with a prefix + toolset = McpToolset( + connection_params=mock_connection_params, + tool_name_prefix="my_prefix", + ) + + # Replace the internal session manager with our mock + toolset._mcp_session_manager = mock_session_manager + + # Get the tools from the toolset + tools = await toolset.get_tools() + + # The get_tools method in McpToolset returns MCPTool objects, which are + # instances of BaseTool. The prefixing is handled by the BaseToolset, + # so we need to call get_tools_with_prefix to get the prefixed tools. + prefixed_tools = await toolset.get_tools_with_prefix() + + # Assert that the tools are prefixed correctly + assert len(prefixed_tools) == 2 + assert prefixed_tools[0].name == "my_prefix_tool1" + assert prefixed_tools[1].name == "my_prefix_tool2" + + # Assert that the original tools are not modified + assert tools[0].name == "tool1" + assert tools[1].name == "tool2" \ No newline at end of file From 0a1676edb807218004d4b7bdfe226184ef89fec1 Mon Sep 17 00:00:00 2001 From: minjunpark Date: Fri, 5 Sep 2025 18:08:28 +0900 Subject: [PATCH 12/15] fix(mcp): Use original tool name when calling MCP server - Change self.name to self._mcp_tool.name in call_tool invocation - Ensures MCP server receives unprefixed tool name - Addresses review comment from alberto-mate --- src/google/adk/tools/mcp_tool/mcp_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_tool.py b/src/google/adk/tools/mcp_tool/mcp_tool.py index 3bdd34e1c4..8683cdf3f0 100644 --- a/src/google/adk/tools/mcp_tool/mcp_tool.py +++ b/src/google/adk/tools/mcp_tool/mcp_tool.py @@ -136,7 +136,7 @@ async def _run_async_impl( # Get the session from the session manager session = await self._mcp_session_manager.create_session(headers=headers) - response = await session.call_tool(self.name, arguments=args) + response = await session.call_tool(self._mcp_tool.name, arguments=args) return response async def _get_headers( From 36c54339e81b0e8a2e6288b1fa27c5266c3fa4fa Mon Sep 17 00:00:00 2001 From: minjunpark Date: Sat, 6 Sep 2025 11:48:39 +0900 Subject: [PATCH 13/15] fix(mcp): Make tool_name_prefix parameter optional in McpToolset - Change tool_name_prefix default from empty string to None - Update type annotation to Optional[str] in both McpToolset and McpToolsetConfig - Addresses review comment from seanzhou1023 --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 9540c989d7..81b960e900 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -100,7 +100,7 @@ def __init__( StreamableHTTPConnectionParams, ], tool_filter: Optional[Union[ToolPredicate, List[str]]] = None, - tool_name_prefix: str = "", + tool_name_prefix: Optional[str] = None, errlog: TextIO = sys.stderr, auth_scheme: Optional[AuthScheme] = None, auth_credential: Optional[AuthCredential] = None, @@ -245,7 +245,7 @@ class McpToolsetConfig(BaseToolConfig): tool_filter: Optional[List[str]] = None - tool_name_prefix: str = "" + tool_name_prefix: Optional[str] = None auth_scheme: Optional[AuthScheme] = None From 2e384d412730ff7537fff00e11420584bed069fc Mon Sep 17 00:00:00 2001 From: minjunpark Date: Tue, 9 Sep 2025 20:41:00 +0900 Subject: [PATCH 14/15] test: Skip MCP tests when Python version is less than 3.10 - Add Python version check to test_mcp_toolset.py - Follow existing MCP test pattern with pytestmark skipif decorator - Add import error handling with dummy classes for test collection - Addresses review comment from seanzhou1023 --- tests/unittests/tools/test_mcp_toolset.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py index dce8dc66d9..aecdab91bf 100644 --- a/tests/unittests/tools/test_mcp_toolset.py +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -14,12 +14,31 @@ """Unit tests for McpToolset.""" +import sys from unittest.mock import AsyncMock from unittest.mock import MagicMock -from google.adk.tools.mcp_tool.mcp_toolset import McpToolset import pytest +# Skip all tests in this module if Python version is less than 3.10 +pytestmark = pytest.mark.skipif( + sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" +) + +# Import dependencies with version checking +try: + from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +except ImportError as e: + if sys.version_info < (3, 10): + # Create dummy classes to prevent NameError during test collection + # Tests will be skipped anyway due to pytestmark + class DummyClass: + pass + + McpToolset = DummyClass + else: + raise e + @pytest.mark.asyncio async def test_mcp_toolset_with_prefix(): From e8e5b0d6d5f406d3875faf2229a96701725b7a5e Mon Sep 17 00:00:00 2001 From: minjunpark Date: Sat, 20 Sep 2025 23:54:28 +0900 Subject: [PATCH 15/15] style: Run autoformat.sh to resolve formatting issues - Fix code formatting in mcp_toolset.py and test_mcp_toolset.py - Address review comment from seanzhou1023 - Apply isort and pyink formatting according to project standards --- src/google/adk/tools/mcp_tool/mcp_toolset.py | 4 +--- tests/unittests/tools/test_mcp_toolset.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 81b960e900..150336f3f2 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -125,9 +125,7 @@ def __init__( auth_scheme: The auth scheme of the tool for tool calling auth_credential: The auth credential of the tool for tool calling """ - super().__init__( - tool_filter=tool_filter, tool_name_prefix=tool_name_prefix - ) + super().__init__(tool_filter=tool_filter, tool_name_prefix=tool_name_prefix) if not connection_params: raise ValueError("Missing connection params in MCPToolset.") diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py index aecdab91bf..95c552ef41 100644 --- a/tests/unittests/tools/test_mcp_toolset.py +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -86,4 +86,4 @@ async def test_mcp_toolset_with_prefix(): # Assert that the original tools are not modified assert tools[0].name == "tool1" - assert tools[1].name == "tool2" \ No newline at end of file + assert tools[1].name == "tool2"