8000 tests: Add unit tests for mcp_tool by pkduongsu · Pull Request #663 · google/adk-python · GitHub
[go: up one dir, main page]

Skip to content

tests: Add unit tests for mcp_tool #663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 44 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
683e86d
add unit_test for mcptool and mcptoolset
pkduongsu May 10, 2025
8faa342
split mcp_tool and mcp_toolset tests
pkduongsu May 10, 2025
a6b0ad1
add conversion utils tests
pkduongsu May 10, 2025
88a8f51
Merge branch 'main' into mcp-unit-tests
pkduongsu May 11, 2025
c4a23d9
fix import
pkduongsu May 14, 2025
e035f22
Merge branch 'mcp-unit-tests' of https://github.com/pkduongsu/adk-pyt…
pkduongsu May 14, 2025
df1658b
merge branch 'mcp-unit-tests'
pkduongsu May 14, 2025
cec6595
Delete uv.lock
pkduongsu May 14, 2025
b5adaf8
Merge branch 'main' of https://github.com/pkduongsu/adk-python into m…
pkduongsu May 14, 2025
d144074
Merge branch 'main' into mcp-unit-tests
pkduongsu May 14, 2025
f2e0dd4
Merge branch 'main' into mcp-unit-tests
pkdu 8000 ongsu May 14, 2025
b60a979
Delete src/google/adk/models/gemini_llm_connection.py
pkduongsu May 15, 2025
a43efc8
Delete src/google/adk/sessions/in_memory_session_service.py
pkduongsu May 15, 2025
e0896c4
Delete src/google/adk/tools/mcp_tool/mcp_toolset.py
pkduongsu May 15, 2025
cb7ac8e
Delete tests/unittests/cli/utils/test_cli_tools_click.py
pkduongsu May 15, 2025
12c77c2
put files under mcp_tool folder
pkduongsu May 15, 2025
f9165b9
Merge branch 'mcp-unit-tests' of https://github.com/pkduongsu/adk-pyt…
pkduongsu May 15, 2025
752bbc1
add tests for mcp toolset and mcp tool
pkduongsu May 15, 2025
5504b79
Merge branch 'mcp-unit-tests' of https://github.com/pkduongsu/adk-pyt…
pkduongsu May 15, 2025
fdbf7fe
put files under mcp_tool folder
pkduongsu May 15, 2025
05cea2b
Merge branch 'mcp-unit-tests' of https://github.com/pkduongsu/adk-pyt…
pkduongsu May 15, 2025
42fc674
add tests for mcp_tool, mcp_toolset, conversion_utils
pkduongsu May 15, 2025
17874f9
Merge pull request #1 from pkduongsu/recovery-branch
pkduongsu May 15, 2025
8a673f9
update
pkduongsu May 15, 2025
8ca465a
update branch
pkduongsu May 15, 2025
60a976a
Merge branch 'mcp-unit-tests' of https://github.com/pkduongsu/adk-pyt…
pkduongsu May 15, 2025
a50d9e5
remove duplicate file
pkduongsu May 15, 2025
c86c0ad
Merge branch 'google:main' into mcp-unit-tests
pkduongsu May 15, 2025
5089008
Merge branch 'google:main' into mcp-unit-tests
pkduongsu May 16, 2025
19bfb19
improve test_coverage, fix test_mcp_toolset to match new refactored ver
pkduongsu May 16, 2025
8c76197
"revert changes for mcp_session_manager"
pkduongsu May 16, 2025
f80ccf7
Merge branch 'google:main' into mcp-unit-tests
pkduongsu May 19, 2025
9695052
Merge branch 'google:main' into mcp-unit-tests
pkduongsu May 21, 2025
e5d18b5
Merge branch 'main' into mcp-unit-tests
pkduongsu May 22, 2025
a49237a
Merge branch 'main' into mcp-unit-tests
pkduongsu May 27, 2025
d6a26bd
Merge branch 'main' into mcp-unit-tests
hangfei May 30, 2025
4f61621
Merge branch 'main' into mcp-unit-tests
hangfei Jun 12, 2025
c0e1368
Merge branch 'google:main' into mcp-unit-tests
pkduongsu Jun 16, 2025
71bf490
remove python 3.9 test
pkduongsu Jun 16, 2025
0a5321a
Merge branch 'main' into mcp-unit-tests
pkduongsu Jul 11, 2025
e5372c6
Remove old test_mcp_tool
pkduongsu Jul 11, 2025
68dd494
'remove old tests'
pkduongsu Jul 11, 2025
842c8a0
Merge branch 'mcp-unit-tests' of https://github.com/pkduongsu/adk-pyt…
pkduongsu Jul 11, 2025
891afb4
Merge branch 'main' into mcp-unit-tests
hangfei Jul 18, 2025
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
104 changes: 3 additions & 101 deletions src/google/adk/sessions/in_memory_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.

import copy
import logging
import time
from typing import Any
from typing import Optional
Expand All @@ -29,15 +28,12 @@
from .session import Session
from .state import State

logger = logging.getLogger(__name__)


class InMemorySessionService(BaseSessionService):
"""An in-memory implementation of the session service."""

def __init__(self):
# A map from app name to a map from user ID to a map from session ID to
# session.
# A map from app name to a map from user ID to a map from session ID to session.
self.sessions: dict[str, dict[str, dict[str, Session]]] = {}
# A map from app name to a map from user ID to a map from key to the value.
self.user_state: dict[str, dict[str, dict[str, Any]]] = {}
Expand All @@ -52,37 +48,6 @@ def create_session(
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
) -> Session:
return self._create_session_impl(
app_name=app_name,
user_id=user_id,
state=state,
session_id=session_id,
)

def create_session_sync(
self,
*,
app_name: str,
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
) -> Session:
logger.warning('Deprecated. Please migrate to the async method.')
return self._create_session_impl(
app_name=app_name,
user_id=user_id,
state=state,
session_id=session_id,
)

def _create_session_impl(
self,
*,
app_name: str,
user_id: str,
state: Optional[dict[str, Any]] = None,
session_id: Optional[str] = None,
) -> Session:
session_id = (
session_id.strip()
Expand Down Expand Up @@ -114,37 +79,6 @@ def get_session(
user_id: str,
session_id: str,
config: Optional[GetSessionConfig] = None,
) -> Session:
return self._get_session_impl(
app_name=app_name,
user_id=user_id,
session_id=session_id,
config=config,
)

def get_session_sync(
self,
*,
app_name: str,
user_id: str,
session_id: str,
config: Optional[GetSessionConfig] = None,
) -> Session:
logger.warning('Deprecated. Please migrate to the async method.')
return self._get_session_impl(
app_name=app_name,
user_id=user_id,
session_id=session_id,
config=config,
)

def _get_session_impl(
self,
*,
app_name: str,
user_id: str,
session_id: str,
config: Optional[GetSessionConfig] = None,
) -> Session:
if app_name not in self.sessions:
return None
Expand Down Expand Up @@ -196,17 +130,6 @@ def _merge_state(self, app_name: str, user_id: str, copied_session: Session):
@override
def list_sessions(
self, *, app_name: str, user_id: str
) -> ListSessionsResponse:
return self._list_sessions_impl(app_name=app_name, user_id=user_id)

def list_sessions_sync(
self, *, app_name: str, user_id: str
) -> ListSessionsResponse:
logger.warning('Deprecated. Please migrate to the async method.')
return self._list_sessions_impl(app_name=app_name, user_id=user_id)

def _list_sessions_impl(
self, *, app_name: str, user_id: str
) -> ListSessionsResponse:
empty_response = ListSessionsResponse()
if app_name not in self.sessions:
Expand All @@ -222,26 +145,12 @@ def _list_sessions_impl(
sessions_without_events.append(copied_session)
return ListSessionsResponse(sessions=sessions_without_events)

@override
def delete_session(
self, *, app_name: str, user_id: str, session_id: str
) -> None:
self._delete_session_impl(
app_name=app_name, user_id=user_id, session_id=session_id
)

def delete_session_sync(
self, *, app_name: str, user_id: str, session_id: str
) -> None:
8000 logger.warning('Deprecated. Please migrate to the async method.')
self._delete_session_impl(
app_name=app_name, user_id=user_id, session_id=session_id
)

def _delete_session_impl(
self, *, app_name: str, user_id: str, session_id: str
) -> None:
if (
self._get_session_impl(
self.get_session(
app_name=app_name, user_id=user_id, session_id=session_id
)
is None
Expand All @@ -252,13 +161,6 @@ def _delete_session_impl(

@override
def append_event(self, session: Session, event: Event) -> Event:
return self._append_event_impl(session=session, event=event)

def append_event_sync(self, session: Session, event: Event) -> Event:
logger.warning('Deprecated. Please migrate to the async method.')
return self._append_event_impl(session=session, event=event)

def _append_event_impl(self, session: Session, event: Event) -> Event:
# Update the in-memory session.
super().append_event(session=session, event=event)
session.last_update_time = event.timestamp
Expand Down
112 changes: 112 additions & 0 deletions src/google/adk/tools/google_api_tool/google_api_tool_sets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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.


import logging

from .google_api_tool_set import GoogleApiToolSet

logger = logging.getLogger(__name__)

_bigquery_tool_set = None
_calendar_tool_set = None
_gmail_tool_set = None
_youtube_tool_set = None
_slides_tool_set = None
_sheets_tool_set = None
_docs_tool_set = None


def __getattr__(name):
"""This method dynamically loads and returns GoogleApiToolSet instances for

various Google APIs. It uses a lazy loading approach, initializing each
tool set only when it is first requested. This avoids unnecessary loading
of tool sets that are not used in a given session.

Args:
name (str): The name of the tool set to retrieve (e.g.,
"bigquery_tool_set").

Returns:
GoogleApiToolSet: The requested tool set instance.

Raises:
AttributeError: If the requested tool set name is not recognized.
"""
global _bigquery_tool_set, _calendar_tool_set, _gmail_tool_set, _youtube_tool_set, _slides_tool_set, _sheets_tool_set, _docs_tool_set

match name:
case "bigquery_tool_set":
if _bigquery_tool_set is None:
_bigquery_tool_set = GoogleApiToolSet.load_tool_set(
api_name="bigquery",
api_version="v2",
)

return _bigquery_tool_set

case "calendar_tool_set":
if _calendar_tool_set is None:
_calendar_tool_set = GoogleApiToolSet.load_tool_set(
api_name="calendar",
api_version="v3",
)

return _calendar_tool_set

case "gmail_tool_set":
if _gmail_tool_set is None:
_gmail_tool_set = GoogleApiToolSet.load_tool_set(
api_name="gmail",
api_version="v1",
)

return _gmail_tool_set

case "youtube_tool_set":
if _youtube_tool_set is None:
_youtube_tool_set = GoogleApiToolSet.load_tool_set(
api_name="youtube",
api_version="v3",
)

return _youtube_tool_set

case "slides_tool_set":
if _slides_tool_set is None:
_slides_tool_set = GoogleApiToolSet.load_tool_set(
api_name="slides",
api_version="v1",
)

return _slides_tool_set

case "sheets_tool_set":
if _sheets_tool_set is None:
_sheets_tool_set = GoogleApiToolSet.load_tool_set(
api_name="sheets",
api_version="v4",
)

return _sheets_tool_set

case "docs_tool_set":
if _docs_tool_set is None:
_docs_tool_set = GoogleApiToolSet.load_tool_set(
api_name="docs",
api_version="v1",
)

return _docs_tool_set
60 changes: 23 additions & 37 deletions src/google/adk/tools/mcp_tool/mcp_toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@

from contextlib import AsyncExitStack
import sys
from typing import List, Union
from typing import List
from typing import Optional
from typing import override
from typing import TextIO

from typing_extensions import override

from ...agents.readonly_context import ReadonlyContext
from ..base_toolset import BaseToolset
from ..base_toolset import ToolPredicate
Expand Down Expand Up @@ -68,74 +67,61 @@ def __init__(
*,
connection_params: StdioServerParameters | SseServerParams,
errlog: TextIO = sys.stderr,
tool_filter: Optional[Union[ToolPredicate, List[str]]] = None,
tool_predicate: Optional[ToolPredicate] = None,
):
"""Initializes the MCPToolset.

Args:
connection_params: The connection parameters to the MCP server. Can be:
`StdioServerParameters` for using local mcp server (e.g. using `npx` or
`python3`); or `SseServerParams` for a local/remote SSE server.
errlog: (Optional) TextIO stream for error logging. Use only for
initializing a local stdio MCP session.
"""

if not connection_params:
raise ValueError('Missing connection params in MCPToolset.')
self._connection_params = connection_params
self._errlog = errlog
self._exit_stack = AsyncExitStack()

self._session_manager = MCPSessionManager(
connection_params=self._connection_params,
exit_stack=self._exit_stack,
errlogger=self._errlog,
self.connection_params = connection_params
self.errlog = errlog
self.exit_stack = AsyncExitStack()

self.session_manager = MCPSessionManager(
connection_params=self.connection_params,
exit_stack=self.exit_stack,
errlog=self.errlog,
)
self._session = None
self.tool_filter = tool_filter
self.session = None
self.tool_predicate = tool_predicate

async def _initialize(self) -> ClientSession:
"""Connects to the MCP Server and initializes the ClientSession."""
self._session = await self._session_manager.create_session()
return self._session

def _is_selected(
self, tool: ..., readonly_context: Optional[ReadonlyContext]
) -> bool:
"""Checks if a tool should be selected based on the tool filter."""
if self.tool_filter is None:
return True
if isinstance(self.tool_filter, ToolPredicate):
return self.tool_filter(tool, readonly_context)
if isinstance(self.tool_filter, list):
return tool.name in self.tool_filter
return False
self.session = await self.session_manager.create_session()
return self.session

@override
async def close(self):
"""Closes the connection to MCP Server."""
await self._exit_stack.aclose()
await self.exit_stack.aclose()

@retry_on_closed_resource('_initialize')
@override
async def get_tools(
self,
readonly_context: Optional[ReadonlyContext] = None,
readony_context: ReadonlyContext = None,
) -> List[MCPTool]:
"""Loads all tools from the MCP Server.

Returns:
A list of MCPTools imported from the MCP Server.
"""
if not self._session:
if not self.session:
await self._initialize()
tools_response: ListToolsResult = await self._session.list_tools()
tools_response: ListToolsResult = await self.session.list_tools()
return [
MCPTool(
mcp_tool=tool,
mcp_session=self._session,
mcp_session_manager=self._session_manager,
mcp_session=self.session,
mcp_session_manager=self.session_manager,
)
for tool in tools_response.tools
if self._is_selected(tool, readonly_context)
if self.tool_predicate is None
or self.tool_predicate(tool, readony_context)
]
Loading
0