Agent Development Kit(ADK)

An easy-to-use and powerful framework to build AI agents.
This commit is contained in:
hangfei
2025-04-08 17:22:09 +00:00
parent f92478bd5c
commit 9827820143
299 changed files with 44398 additions and 2 deletions

View 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)

View 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

View 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

View 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()