mirror of
https://github.com/EvolutionAPI/adk-python.git
synced 2025-12-22 05:12:18 -06:00
Agent Development Kit(ADK)
An easy-to-use and powerful framework to build AI agents.
This commit is contained in:
42
src/google/adk/tools/mcp_tool/__init__.py
Normal file
42
src/google/adk/tools/mcp_tool/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# 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.
|
||||
|
||||
__all__ = []
|
||||
|
||||
try:
|
||||
from .conversion_utils import adk_to_mcp_tool_type, gemini_to_json_schema
|
||||
from .mcp_tool import MCPTool
|
||||
from .mcp_toolset import MCPToolset
|
||||
|
||||
__all__.extend([
|
||||
'adk_to_mcp_tool_type',
|
||||
'gemini_to_json_schema',
|
||||
'MCPTool',
|
||||
'MCPToolset',
|
||||
])
|
||||
|
||||
except ImportError as e:
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
logger.warning(
|
||||
'MCP Tool requires Python 3.10 or above. Please upgrade your Python'
|
||||
' version.'
|
||||
)
|
||||
else:
|
||||
logger.debug('MCP Tool is not installed')
|
||||
logger.debug(e)
|
||||
161
src/google/adk/tools/mcp_tool/conversion_utils.py
Normal file
161
src/google/adk/tools/mcp_tool/conversion_utils.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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.
|
||||
|
||||
from typing import Any, Dict
|
||||
from google.genai.types import Schema, Type
|
||||
import mcp.types as mcp_types
|
||||
from ..base_tool import BaseTool
|
||||
|
||||
|
||||
def adk_to_mcp_tool_type(tool: BaseTool) -> mcp_types.Tool:
|
||||
"""Convert a Tool in ADK into MCP tool type.
|
||||
|
||||
This function transforms an ADK tool definition into its equivalent
|
||||
representation in the MCP (Model Control Plane) system.
|
||||
|
||||
Args:
|
||||
tool: The ADK tool to convert. It should be an instance of a class derived
|
||||
from `BaseTool`.
|
||||
|
||||
Returns:
|
||||
An object of MCP Tool type, representing the converted tool.
|
||||
|
||||
Examples:
|
||||
# Assuming 'my_tool' is an instance of a BaseTool derived class
|
||||
mcp_tool = adk_to_mcp_tool_type(my_tool)
|
||||
print(mcp_tool)
|
||||
"""
|
||||
tool_declaration = tool._get_declaration()
|
||||
if not tool_declaration:
|
||||
input_schema = {}
|
||||
else:
|
||||
input_schema = gemini_to_json_schema(tool._get_declaration().parameters)
|
||||
return mcp_types.Tool(
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
inputSchema=input_schema,
|
||||
)
|
||||
|
||||
|
||||
def gemini_to_json_schema(gemini_schema: Schema) -> Dict[str, Any]:
|
||||
"""Converts a Gemini Schema object into a JSON Schema dictionary.
|
||||
|
||||
Args:
|
||||
gemini_schema: An instance of the Gemini Schema class.
|
||||
|
||||
Returns:
|
||||
A dictionary representing the equivalent JSON Schema.
|
||||
|
||||
Raises:
|
||||
TypeError: If the input is not an instance of the expected Schema class.
|
||||
ValueError: If an invalid Gemini Type enum value is encountered.
|
||||
"""
|
||||
if not isinstance(gemini_schema, Schema):
|
||||
raise TypeError(
|
||||
f"Input must be an instance of Schema, got {type(gemini_schema)}"
|
||||
)
|
||||
|
||||
json_schema_dict: Dict[str, Any] = {}
|
||||
|
||||
# Map Type
|
||||
gemini_type = getattr(gemini_schema, "type", None)
|
||||
if gemini_type and gemini_type != Type.TYPE_UNSPECIFIED:
|
||||
json_schema_dict["type"] = gemini_type.lower()
|
||||
else:
|
||||
json_schema_dict["type"] = "null"
|
||||
|
||||
# Map Nullable
|
||||
if getattr(gemini_schema, "nullable", None) == True:
|
||||
json_schema_dict["nullable"] = True
|
||||
|
||||
# --- Map direct fields ---
|
||||
direct_mappings = {
|
||||
"title": "title",
|
||||
"description": "description",
|
||||
"default": "default",
|
||||
"enum": "enum",
|
||||
"format": "format",
|
||||
"example": "example",
|
||||
}
|
||||
for gemini_key, json_key in direct_mappings.items():
|
||||
value = getattr(gemini_schema, gemini_key, None)
|
||||
if value is not None:
|
||||
json_schema_dict[json_key] = value
|
||||
|
||||
# String validation
|
||||
if gemini_type == Type.STRING:
|
||||
str_mappings = {
|
||||
"pattern": "pattern",
|
||||
"min_length": "minLength",
|
||||
"max_length": "maxLength",
|
||||
}
|
||||
for gemini_key, json_key in str_mappings.items():
|
||||
value = getattr(gemini_schema, gemini_key, None)
|
||||
if value is not None:
|
||||
json_schema_dict[json_key] = value
|
||||
|
||||
# Number/Integer validation
|
||||
if gemini_type in (Type.NUMBER, Type.INTEGER):
|
||||
num_mappings = {
|
||||
"minimum": "minimum",
|
||||
"maximum": "maximum",
|
||||
}
|
||||
for gemini_key, json_key in num_mappings.items():
|
||||
value = getattr(gemini_schema, gemini_key, None)
|
||||
if value is not None:
|
||||
json_schema_dict[json_key] = value
|
||||
|
||||
# Array validation (Recursive call for items)
|
||||
if gemini_type == Type.ARRAY:
|
||||
items_schema = getattr(gemini_schema, "items", None)
|
||||
if items_schema is not None:
|
||||
json_schema_dict["items"] = gemini_to_json_schema(items_schema)
|
||||
|
||||
arr_mappings = {
|
||||
"min_items": "minItems",
|
||||
"max_items": "maxItems",
|
||||
}
|
||||
for gemini_key, json_key in arr_mappings.items():
|
||||
value = getattr(gemini_schema, gemini_key, None)
|
||||
if value is not None:
|
||||
json_schema_dict[json_key] = value
|
||||
|
||||
# Object validation (Recursive call for properties)
|
||||
if gemini_type == Type.OBJECT:
|
||||
properties_dict = getattr(gemini_schema, "properties", None)
|
||||
if properties_dict is not None:
|
||||
json_schema_dict["properties"] = {
|
||||
prop_name: gemini_to_json_schema(prop_schema)
|
||||
for prop_name, prop_schema in properties_dict.items()
|
||||
}
|
||||
|
||||
obj_mappings = {
|
||||
"required": "required",
|
||||
"min_properties": "minProperties",
|
||||
"max_properties": "maxProperties",
|
||||
# Note: Ignoring 'property_ordering' as it's not standard JSON Schema
|
||||
}
|
||||
for gemini_key, json_key in obj_mappings.items():
|
||||
value = getattr(gemini_schema, gemini_key, None)
|
||||
if value is not None:
|
||||
json_schema_dict[json_key] = value
|
||||
|
||||
# Map anyOf (Recursive call for subschemas)
|
||||
any_of_list = getattr(gemini_schema, "any_of", None)
|
||||
if any_of_list is not None:
|
||||
json_schema_dict["anyOf"] = [
|
||||
gemini_to_json_schema(sub_schema) for sub_schema in any_of_list
|
||||
]
|
||||
|
||||
return json_schema_dict
|
||||
113
src/google/adk/tools/mcp_tool/mcp_tool.py
Normal file
113
src/google/adk/tools/mcp_tool/mcp_tool.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# 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.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from google.genai.types import FunctionDeclaration
|
||||
from typing_extensions import override
|
||||
|
||||
# Attempt to import MCP Tool from the MCP library, and hints user to upgrade
|
||||
# their Python version to 3.10 if it fails.
|
||||
try:
|
||||
from mcp import ClientSession
|
||||
from mcp.types import Tool as McpBaseTool
|
||||
except ImportError as e:
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
raise ImportError(
|
||||
"MCP Tool requires Python 3.10 or above. Please upgrade your Python"
|
||||
" version."
|
||||
) from e
|
||||
else:
|
||||
raise e
|
||||
|
||||
from ..base_tool import BaseTool
|
||||
from ...auth.auth_credential import AuthCredential
|
||||
from ...auth.auth_schemes import AuthScheme
|
||||
from ..openapi_tool.openapi_spec_parser.rest_api_tool import to_gemini_schema
|
||||
from ..tool_context import ToolContext
|
||||
|
||||
|
||||
class MCPTool(BaseTool):
|
||||
"""Turns a MCP Tool into a Vertex Agent Framework Tool.
|
||||
|
||||
Internally, the tool initializes from a MCP Tool, and uses the MCP Session to
|
||||
call the tool.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mcp_tool: McpBaseTool,
|
||||
mcp_session: ClientSession,
|
||||
auth_scheme: Optional[AuthScheme] = None,
|
||||
auth_credential: Optional[AuthCredential] | None = None,
|
||||
):
|
||||
"""Initializes a MCPTool.
|
||||
|
||||
This tool wraps a MCP Tool interface and an active MCP Session. It invokes
|
||||
the MCP Tool through executing the tool from remote MCP Session.
|
||||
|
||||
Example:
|
||||
tool = MCPTool(mcp_tool=mcp_tool, mcp_session=mcp_session)
|
||||
|
||||
Args:
|
||||
mcp_tool: The MCP tool to wrap.
|
||||
mcp_session: The MCP session to use to call the tool.
|
||||
auth_scheme: The authentication scheme to use.
|
||||
auth_credential: The authentication credential to use.
|
||||
|
||||
Raises:
|
||||
ValueError: If mcp_tool or mcp_session is None.
|
||||
"""
|
||||
if mcp_tool is None:
|
||||
raise ValueError("mcp_tool cannot be None")
|
||||
if mcp_session is None:
|
||||
raise ValueError("mcp_session cannot be None")
|
||||
self.name = mcp_tool.name
|
||||
self.description = mcp_tool.description if mcp_tool.description else ""
|
||||
self.mcp_tool = mcp_tool
|
||||
self.mcp_session = mcp_session
|
||||
# TODO(cheliu): Support passing auth to MCP Server.
|
||||
self.auth_scheme = auth_scheme
|
||||
self.auth_credential = auth_credential
|
||||
|
||||
@override
|
||||
def _get_declaration(self) -> FunctionDeclaration:
|
||||
"""Gets the function declaration for the tool.
|
||||
|
||||
Returns:
|
||||
FunctionDeclaration: The Gemini function declaration for the tool.
|
||||
"""
|
||||
schema_dict = self.mcp_tool.inputSchema
|
||||
parameters = to_gemini_schema(schema_dict)
|
||||
function_decl = FunctionDeclaration(
|
||||
name=self.name, description=self.description, parameters=parameters
|
||||
)
|
||||
return function_decl
|
||||
|
||||
@override
|
||||
async def run_async(self, *, args, tool_context: ToolContext):
|
||||
"""Runs the tool asynchronously.
|
||||
|
||||
Args:
|
||||
args: The arguments as a dict to pass to the tool.
|
||||
tool_context: The tool context from upper level ADK agent.
|
||||
|
||||
Returns:
|
||||
Any: The response from the tool.
|
||||
"""
|
||||
# TODO(cheliu): Support passing tool context to MCP Server.
|
||||
response = await self.mcp_session.call_tool(self.name, arguments=args)
|
||||
return response
|
||||
272
src/google/adk/tools/mcp_tool/mcp_toolset.py
Normal file
272
src/google/adk/tools/mcp_tool/mcp_toolset.py
Normal file
@@ -0,0 +1,272 @@
|
||||
# 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.
|
||||
|
||||
from contextlib import AsyncExitStack
|
||||
from types import TracebackType
|
||||
from typing import Any, List, Optional, Tuple, Type
|
||||
|
||||
# Attempt to import MCP Tool from the MCP library, and hints user to upgrade
|
||||
# their Python version to 3.10 if it fails.
|
||||
try:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.types import ListToolsResult
|
||||
except ImportError as e:
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
raise ImportError(
|
||||
'MCP Tool requires Python 3.10 or above. Please upgrade your Python'
|
||||
' version.'
|
||||
) from e
|
||||
else:
|
||||
raise e
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .mcp_tool import MCPTool
|
||||
|
||||
|
||||
class SseServerParams(BaseModel):
|
||||
url: str
|
||||
headers: dict[str, Any] | None = None
|
||||
timeout: float = 5
|
||||
sse_read_timeout: float = 60 * 5
|
||||
|
||||
|
||||
class MCPToolset:
|
||||
"""Connects to a MCP Server, and retrieves MCP Tools into ADK Tools.
|
||||
|
||||
Usage:
|
||||
Example 1: (using from_server helper):
|
||||
```
|
||||
async def load_tools():
|
||||
return await MCPToolset.from_server(
|
||||
connection_params=StdioServerParameters(
|
||||
command='npx',
|
||||
args=["-y", "@modelcontextprotocol/server-filesystem"],
|
||||
)
|
||||
)
|
||||
|
||||
# Use the tools in an LLM agent
|
||||
tools, exit_stack = await load_tools()
|
||||
agent = LlmAgent(
|
||||
tools=tools
|
||||
)
|
||||
...
|
||||
await exit_stack.aclose()
|
||||
```
|
||||
|
||||
Example 2: (using `async with`):
|
||||
|
||||
```
|
||||
async def load_tools():
|
||||
async with MCPToolset(
|
||||
connection_params=SseServerParams(url="http://0.0.0.0:8090/sse")
|
||||
) as toolset:
|
||||
tools = await toolset.load_tools()
|
||||
|
||||
agent = LlmAgent(
|
||||
...
|
||||
tools=tools
|
||||
)
|
||||
```
|
||||
|
||||
Example 3: (provide AsyncExitStack):
|
||||
```
|
||||
async def load_tools():
|
||||
async_exit_stack = AsyncExitStack()
|
||||
toolset = MCPToolset(
|
||||
connection_params=StdioServerParameters(...),
|
||||
)
|
||||
async_exit_stack.enter_async_context(toolset)
|
||||
tools = await toolset.load_tools()
|
||||
agent = LlmAgent(
|
||||
...
|
||||
tools=tools
|
||||
)
|
||||
...
|
||||
await async_exit_stack.aclose()
|
||||
|
||||
```
|
||||
|
||||
Attributes:
|
||||
connection_params: The connection parameters to the MCP server. Can be
|
||||
either `StdioServerParameters` or `SseServerParams`.
|
||||
exit_stack: The async exit stack to manage the connection to the MCP server.
|
||||
session: The MCP session being initialized with the connection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *, connection_params: StdioServerParameters | SseServerParams
|
||||
):
|
||||
"""Initializes the MCPToolset.
|
||||
|
||||
Usage:
|
||||
Example 1: (using from_server helper):
|
||||
```
|
||||
async def load_tools():
|
||||
return await MCPToolset.from_server(
|
||||
connection_params=StdioServerParameters(
|
||||
command='npx',
|
||||
args=["-y", "@modelcontextprotocol/server-filesystem"],
|
||||
)
|
||||
)
|
||||
|
||||
# Use the tools in an LLM agent
|
||||
tools, exit_stack = await load_tools()
|
||||
agent = LlmAgent(
|
||||
tools=tools
|
||||
)
|
||||
...
|
||||
await exit_stack.aclose()
|
||||
```
|
||||
|
||||
Example 2: (using `async with`):
|
||||
|
||||
```
|
||||
async def load_tools():
|
||||
async with MCPToolset(
|
||||
connection_params=SseServerParams(url="http://0.0.0.0:8090/sse")
|
||||
) as toolset:
|
||||
tools = await toolset.load_tools()
|
||||
|
||||
agent = LlmAgent(
|
||||
...
|
||||
tools=tools
|
||||
)
|
||||
```
|
||||
|
||||
Example 3: (provide AsyncExitStack):
|
||||
```
|
||||
async def load_tools():
|
||||
async_exit_stack = AsyncExitStack()
|
||||
toolset = MCPToolset(
|
||||
connection_params=StdioServerParameters(...),
|
||||
)
|
||||
async_exit_stack.enter_async_context(toolset)
|
||||
tools = await toolset.load_tools()
|
||||
agent = LlmAgent(
|
||||
...
|
||||
tools=tools
|
||||
)
|
||||
...
|
||||
await async_exit_stack.aclose()
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
"""
|
||||
if not connection_params:
|
||||
raise ValueError('Missing connection params in MCPToolset.')
|
||||
self.connection_params = connection_params
|
||||
self.exit_stack = AsyncExitStack()
|
||||
|
||||
@classmethod
|
||||
async def from_server(
|
||||
cls,
|
||||
*,
|
||||
connection_params: StdioServerParameters | SseServerParams,
|
||||
async_exit_stack: Optional[AsyncExitStack] = None,
|
||||
) -> Tuple[List[MCPTool], AsyncExitStack]:
|
||||
"""Retrieve all tools from the MCP connection.
|
||||
|
||||
Usage:
|
||||
```
|
||||
async def load_tools():
|
||||
tools, exit_stack = await MCPToolset.from_server(
|
||||
connection_params=StdioServerParameters(
|
||||
command='npx',
|
||||
args=["-y", "@modelcontextprotocol/server-filesystem"],
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Args:
|
||||
connection_params: The connection parameters to the MCP server.
|
||||
async_exit_stack: The async exit stack to use. If not provided, a new
|
||||
AsyncExitStack will be created.
|
||||
|
||||
Returns:
|
||||
A tuple of the list of MCPTools and the AsyncExitStack.
|
||||
- tools: The list of MCPTools.
|
||||
- async_exit_stack: The AsyncExitStack used to manage the connection to
|
||||
the MCP server. Use `await async_exit_stack.aclose()` to close the
|
||||
connection when server shuts down.
|
||||
"""
|
||||
toolset = cls(connection_params=connection_params)
|
||||
async_exit_stack = async_exit_stack or AsyncExitStack()
|
||||
await async_exit_stack.enter_async_context(toolset)
|
||||
tools = await toolset.load_tools()
|
||||
return (tools, async_exit_stack)
|
||||
|
||||
async def _initialize(self) -> ClientSession:
|
||||
"""Connects to the MCP Server and initializes the ClientSession."""
|
||||
if isinstance(self.connection_params, StdioServerParameters):
|
||||
client = stdio_client(self.connection_params)
|
||||
elif isinstance(self.connection_params, SseServerParams):
|
||||
client = sse_client(
|
||||
url=self.connection_params.url,
|
||||
headers=self.connection_params.headers,
|
||||
timeout=self.connection_params.timeout,
|
||||
sse_read_timeout=self.connection_params.sse_read_timeout,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
'Unable to initialize connection. Connection should be'
|
||||
' StdioServerParameters or SseServerParams, but got'
|
||||
f' {self.connection_params}'
|
||||
)
|
||||
|
||||
transports = await self.exit_stack.enter_async_context(client)
|
||||
self.session = await self.exit_stack.enter_async_context(
|
||||
ClientSession(*transports)
|
||||
)
|
||||
await self.session.initialize()
|
||||
return self.session
|
||||
|
||||
async def _exit(self):
|
||||
"""Closes the connection to MCP Server."""
|
||||
await self.exit_stack.aclose()
|
||||
|
||||
async def load_tools(self) -> List[MCPTool]:
|
||||
"""Loads all tools from the MCP Server.
|
||||
|
||||
Returns:
|
||||
A list of MCPTools imported from the MCP Server.
|
||||
"""
|
||||
tools_response: ListToolsResult = await self.session.list_tools()
|
||||
return [
|
||||
MCPTool(mcp_tool=tool, mcp_session=self.session)
|
||||
for tool in tools_response.tools
|
||||
]
|
||||
|
||||
async def __aenter__(self):
|
||||
try:
|
||||
await self._initialize()
|
||||
return self
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc: Optional[BaseException],
|
||||
tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
await self._exit()
|
||||
Reference in New Issue
Block a user