diff --git a/docs/deploy/agent-engine.md b/docs/deploy/agent-engine.md index ee83f9c11..4fc4b43f9 100644 --- a/docs/deploy/agent-engine.md +++ b/docs/deploy/agent-engine.md @@ -218,7 +218,71 @@ Expected output for `stream_query` (remote): {'parts': [{'text': 'The weather in New York is sunny with a temperature of 25 degrees Celsius (41 degrees Fahrenheit).'}], 'role': 'model'} ``` +## Deploying Agents with MCP Tools +When deploying an agent to Agent Engine that depends on an `MCPToolset`, you must ensure the MCP server is accessible over the network and that credentials are handled securely. + +### Architecture + +* **External MCP Server**: The MCP server cannot be a local subprocess (`StdioConnectionParams` is not supported). It must be deployed as a separate, network-accessible service (e.g., on Cloud Run, GKE, or another compute platform). +* **Network Connectivity**: Ensure that the Agent Engine environment has the necessary VPC network configuration and firewall rules to reach your MCP server's endpoint. +* **Connection Parameters**: In your agent code, use `StreamableHTTPConnectionParams` to specify the URL of your deployed MCP server. + +### Secure Credential Management + +Handling authentication tokens and other secrets is critical for security. + +* **Use Secret Manager**: The recommended best practice is to store credentials like API tokens in [Google Cloud Secret Manager](https://cloud.google.com/secret-manager). +* **Runtime Access**: Your agent code, running within Agent Engine, should be granted an IAM role (e.g., `Secret Manager Secret Accessor`) that allows it to fetch the secret value at runtime. This avoids exposing secrets in environment variables or source code. +* **Pass Secrets to Tools**: Once fetched from Secret Manager, the token can be used to construct the `AuthCredential` for your `MCPToolset`. + +### Example: Agent with MCP Toolset for Agent Engine + +Your agent definition would be packaged for Agent Engine, fetching credentials securely. + +```python +# In your agent.py to be deployed to Agent Engine +import os +from google.adk.agents import LlmAgent +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StreamableHTTPConnectionParams +from google.adk.auth import AuthCredential, AuthCredentialTypes, HttpAuth, HttpCredentials +from google.adk.auth.auth_schemes import HTTPBearer +# You would also need the Secret Manager client library +# from google.cloud import secretmanager + +def get_secret(secret_id: str) -> str: + # Placeholder for logic to fetch a secret from Google Secret Manager + # client = secretmanager.SecretManagerServiceClient() + # name = f"projects/{os.environ['GCP_PROJECT']}/secrets/{secret_id}/versions/latest" + # response = client.access_secret_version(name=name) + # return response.payload.data.decode("UTF-8") + # For deployment, you would use the real client. For local testing, you might use an env var. + return os.environ.get(secret_id) + +# Get configuration from environment variables or secrets +MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL") +MCP_AUTH_TOKEN = get_secret("MCP_API_TOKEN_SECRET_ID") + +if not MCP_SERVER_URL or not MCP_AUTH_TOKEN: + raise ValueError("MCP server configuration or token is missing.") + +root_agent = LlmAgent( + model='gemini-1.5-flash', + tools=[ + MCPToolset( + connection_params=StreamableHTTPConnectionParams(url=MCP_SERVER_URL), + auth_scheme=HTTPBearer(), + auth_credential=AuthCredential( + auth_type=AuthCredentialTypes.HTTP, + http=HttpAuth(scheme="bearer", credentials=HttpCredentials(token=MCP_AUTH_TOKEN)) + ) + ) + ], + # ... other agent parameters +) +``` + +When deploying with `agent_engines.create`, you would need to ensure the `requirements` list includes `google-cloud-secret-manager` and that the necessary environment variables (`MCP_SERVER_URL`, `MCP_API_TOKEN_SECRET_ID`, `GCP_PROJECT`) are available to the runtime. ## Clean up @@ -230,4 +294,4 @@ charges on your Google Cloud account. remote_app.delete(force=True) ``` -`force=True` will also delete any child resources that were generated from the deployed agent, such as sessions. +`force=True` will also delete any child resources that were generated from the deployed agent, such as sessions. \ No newline at end of file diff --git a/docs/deploy/cloud-run.md b/docs/deploy/cloud-run.md index 5d39de710..92ce1e835 100644 --- a/docs/deploy/cloud-run.md +++ b/docs/deploy/cloud-run.md @@ -349,7 +349,56 @@ export GOOGLE_GENAI_USE_VERTEXAI=True For a full list of deployment options, see the [`gcloud run deploy` reference documentation](https://cloud.google.com/sdk/gcloud/reference/run/deploy). +## Deploying Agents with MCP Tools +When deploying an agent that uses an `MCPToolset`, it's important to follow best practices for production environments like Cloud Run. + +### Architecture + +* **Separate Services**: The recommended architecture is to deploy your MCP server as a separate Cloud Run service from your ADK agent service. This decouples the components and allows them to be scaled and managed independently. +* **Avoid `StdioConnectionParams`**: Do not use `StdioConnectionParams` in a Cloud Run environment. It relies on starting a local subprocess, which is not a reliable or scalable pattern for a managed container platform. +* **Use `StreamableHTTPConnectionParams`**: Configure your `MCPToolset` within the ADK agent to connect to the deployed MCP server's URL using `StreamableHTTPConnectionParams`. + +### Authentication + +* **Service-to-Service Authentication**: Secure the connection between your ADK agent service and your MCP server service. Configure the MCP server to only allow invocations from your ADK agent's service account. Cloud Run can automatically attach a secure identity token to outbound requests, which your MCP server can then validate. This is more secure than using static API keys. +* **Secure Credential Management**: If you must use API keys or other static tokens, do not hardcode them. Pass them to your Cloud Run service as environment variables backed by [Secret Manager](https://cloud.google.com/secret-manager). + +### Example Configuration Snippet + +Your agent code, when deployed, would look something like this: + +```python +# In your agent.py deployed to Cloud Run +import os +from google.adk.agents import LlmAgent +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StreamableHTTPConnectionParams +from google.adk.auth import AuthCredential, AuthCredentialTypes, HttpAuth, HttpCredentials +from google.adk.auth.auth_schemes import HTTPBearer + +# The URL of your deployed MCP server service +MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL") # Injected as env var +# The token to authenticate with the MCP server +MCP_AUTH_TOKEN = os.environ.get("MCP_AUTH_TOKEN") # Injected from Secret Manager + +if not MCP_SERVER_URL or not MCP_AUTH_TOKEN: + raise ValueError("MCP server configuration is missing.") + +root_agent = LlmAgent( + model='gemini-1.5-flash', + tools=[ + MCPToolset( + connection_params=StreamableHTTPConnectionParams(url=MCP_SERVER_URL), + auth_scheme=HTTPBearer(), + auth_credential=AuthCredential( + auth_type=AuthCredentialTypes.HTTP, + http=HttpAuth(scheme="bearer", credentials=HttpCredentials(token=MCP_AUTH_TOKEN)) + ) + ) + ], + # ... other agent parameters +) +``` ## Testing your agent @@ -451,4 +500,4 @@ Once your agent is deployed to Cloud Run, you can interact with it via the deplo ``` * Set `"streaming": true` if you want to receive Server-Sent Events (SSE). - * The response will contain the agent's execution events, including the final answer. + * The response will contain the agent's execution events, including the final answer. \ No newline at end of file diff --git a/docs/tools/authentication.md b/docs/tools/authentication.md index 3c2dd2a32..d07e91caa 100644 --- a/docs/tools/authentication.md +++ b/docs/tools/authentication.md @@ -27,6 +27,8 @@ You set up authentication when defining your tool: * **RestApiTool / OpenAPIToolset**: Pass `auth_scheme` and `auth_credential` during initialization +* **`MCPToolset`**: For tools provided by a remote, secured [Model Context Protocol server](./mcp-tools.md). + * **GoogleApiToolSet Tools**: ADK has built-in 1st party tools like Google Calendar, BigQuery etc,. Use the toolset's specific method. * **APIHubToolset / ApplicationIntegrationToolset**: Pass `auth_scheme` and `auth_credential`during initialization, if the API managed in API Hub / provided by Application Integration requires authentication. diff --git a/docs/tools/mcp-authentication.md b/docs/tools/mcp-authentication.md new file mode 100644 index 000000000..f8cad6eb4 --- /dev/null +++ b/docs/tools/mcp-authentication.md @@ -0,0 +1,118 @@ +# Authenticating with Remote MCP Servers + +![python_only](https://img.shields.io/badge/Currently_supported_in-Python-blue){ title="This feature is currently available for Python."} + +This guide explains how to connect your ADK agent to a remote Model Context Protocol (MCP) server that requires authentication. This is a common requirement for production environments where MCP servers expose sensitive tools or data and must be secured. + +You will learn how to use ADK's built-in authentication framework with `MCPToolset` to securely pass credentials, such as Bearer tokens, to a remote MCP server. + +## Core Concepts + +ADK integrates its standard authentication system with `MCPToolset`, allowing you to secure connections to remote MCP servers in the same way you would for a REST API or OpenAPI tool. + +The key components are: + +1. **`MCPToolset`**: The ADK toolset for integrating with MCP servers. +2. **`StreamableHTTPConnectionParams`**: The recommended connection parameter class for connecting to remote MCP servers over HTTP. It allows you to specify the server's URL. +3. **`auth_scheme`**: An object defining *how* the server expects credentials. For many modern services, this will be `HTTPBearer()` for sending an `Authorization: Bearer ` header. +4. **`auth_credential`**: An object holding the *actual* credential information, such as the Bearer token itself. + +When you provide an `auth_scheme` and `auth_credential` to `MCPToolset`, ADK automatically constructs the correct `Authorization` header and includes it in all requests to the MCP server. + +## Example: Connecting to a Secured Server with a Bearer Token + +This example demonstrates how to build an agent that connects to a remote MCP server protected by Bearer token authentication. We will provide a complete, runnable example that includes a mock server, so you can test the entire flow locally. + +### Step 1: Create a Mock Authenticated MCP Server + +First, let's create a simple MCP server using FastAPI that requires an `Authorization` header. This simulates a real-world secured service. + +Save this code as `mock_mcp_server.py`: + +```python title="mock_mcp_server.py" +--8<-- "examples/python/snippets/tools/mcp_auth/mock_mcp_server.py:init" +``` + +This server does the following: +* It listens for POST requests on the `/mcp` endpoint. +* It checks for an `Authorization` header. +* If the header is missing or the token is incorrect, it returns a `401 Unauthorized` error. +* If the token is valid (`secret-token-123`), it responds with a mock list of tools, simulating a real MCP server. + +### Step 2: Create the ADK Agent with Authentication + +Next, create the ADK agent. This agent will be configured to send the required Bearer token to the mock server. + +Save this code as `agent_with_auth.py`: + +```python title="agent_with_auth.py" +--8<-- "examples/python/snippets/tools/mcp_auth/agent_with_auth.py:init" +``` + +Key parts of this agent configuration: + +* **`StreamableHTTPConnectionParams`**: We use this to point to our mock server's URL (`http://127.0.0.1:8001/mcp`). +* **`auth_scheme = HTTPBearer()`**: This tells ADK that the authentication method is a Bearer token sent via an HTTP header. +* **`auth_credential`**: This holds the actual token. We use `AuthCredential` with `auth_type=AuthCredentialTypes.HTTP` and provide the token value. + +!!! tip "Best Practice: Use Environment Variables for Tokens" + In the example, the token is hardcoded for simplicity. In a real application, you should **never** hardcode secrets. Load them from environment variables or a secure secret manager. + ```python + import os + my_token = os.environ.get("MY_MCP_API_TOKEN") + ``` + +### Step 3: Run and Test the Example + +1. **Install dependencies:** + ```shell + pip install fastapi uvicorn + ``` + +2. **Start the mock server:** + Open a terminal and run: + ```shell + uvicorn mock_mcp_server:app --host 127.0.0.1 --port 8001 + ``` + The server is now running and waiting for connections. + +3. **Run the ADK agent:** + Open a *second* terminal, ensure your ADK environment is active, and run: + ```shell + python agent_with_auth.py + ``` + +**Expected Output:** + +In the terminal running `agent_with_auth.py`, you should see the agent successfully get the tool list and then call the `get_user_profile` tool: + +```console +User Query: 'What is the profile for user 42?' +Running agent... +Event received: ... 'function_call': {'name': 'get_user_profile', 'args': {'user_id': '42'}} ... +Event received: ... 'function_response': {'name': 'get_user_profile', 'response': {'id': '42', 'name': 'Jane Doe', 'email': 'jane.doe@example.com'}} ... +Event received: ... 'text': 'The user profile for user ID 42 belongs to Jane Doe, with the email address jane.doe@example.com.' ... +``` + +In the terminal running the `mock_mcp_server.py`, you will see logs confirming it received the requests with the correct authorization: + +```console +INFO: Uvicorn running on http://127.0.0.1:8001 (Press CTRL+C to quit) +INFO: Application startup complete. +INFO: 127.0.0.1:xxxx - "POST /mcp HTTP/1.1" 200 OK +INFO: 127.0.0.1:xxxx - "POST /mcp HTTP/1.1" 200 OK +``` + +This confirms that the ADK agent successfully authenticated with the remote MCP server. + +## Deployment Considerations + +When deploying agents that use authenticated MCP tools to production environments like Cloud Run or Agent Engine, special considerations are necessary. + +* **Avoid `StdioConnectionParams`**: Do not use `stdio`-based connections in a serverless or containerized environment. The MCP server should always be a separate, network-accessible service. +* **Secure Credential Management**: Use a service like Google Secret Manager to store and retrieve API tokens and other credentials at runtime. Do not store them in source code or as plain text environment variables. +* **Service-to-Service Authentication**: In cloud environments, leverage built-in service-to-service authentication mechanisms. For example, a Cloud Run service can be configured to only accept requests from other specific services, using automatically managed identity tokens. + +For detailed guidance, see the new sections on deploying agents with MCP tools in the deployment guides: +* [**Deploy to Cloud Run**](../deploy/cloud-run.md#deploying-agents-with-mcp-tools) +* [**Deploy to Agent Engine**](../deploy/agent-engine.md#deploying-agents-with-mcp-tools) \ No newline at end of file diff --git a/docs/tools/mcp-tools.md b/docs/tools/mcp-tools.md index 1140ae6ad..fb1635f34 100644 --- a/docs/tools/mcp-tools.md +++ b/docs/tools/mcp-tools.md @@ -1,6 +1,6 @@ # Model Context Protocol Tools - This guide walks you through two ways of integrating Model Context Protocol (MCP) with ADK. +This guide walks you through two ways of integrating Model Context Protocol (MCP) with ADK. ## What is Model Context Protocol (MCP)? @@ -28,47 +28,44 @@ which adk which npx ``` -## 1. Using MCP servers with ADK agents (ADK as an MCP client) in `adk web` +## 1. Using MCP servers with ADK agents (ADK as an MCP client) -This section demonstrates how to integrate tools from external MCP (Model Context Protocol) servers into your ADK agents. This is the **most common** integration pattern when your ADK agent needs to use capabilities provided by an existing service that exposes an MCP interface. You will see how the `MCPToolset` class can be directly added to your agent's `tools` list, enabling seamless connection to an MCP server, discovery of its tools, and making them available for your agent to use. These examples primarily focus on interactions within the `adk web` development environment. +This section demonstrates how to integrate tools from external MCP servers into your ADK agents. This is the **most common** integration pattern when your ADK agent needs to use capabilities provided by an existing service that exposes an MCP interface. ### `MCPToolset` class The `MCPToolset` class is ADK's primary mechanism for integrating tools from an MCP server. When you include an `MCPToolset` instance in your agent's `tools` list, it automatically handles the interaction with the specified MCP server. Here's how it works: -1. **Connection Management:** On initialization, `MCPToolset` establishes and manages the connection to the MCP server. This can be a local server process (using `StdioServerParameters` for communication over standard input/output) or a remote server (using `SseServerParams` for Server-Sent Events). The toolset also handles the graceful shutdown of this connection when the agent or application terminates. +1. **Connection Management:** On initialization, `MCPToolset` establishes and manages the connection to the MCP server. This can be a local server process (using `StdioConnectionParams` for communication over standard input/output) or a remote server (using `StreamableHTTPConnectionParams` for HTTP-based communication). The toolset also handles the graceful shutdown of this connection when the agent or application terminates. 2. **Tool Discovery & Adaptation:** Once connected, `MCPToolset` queries the MCP server for its available tools (via the `list_tools` MCP method). It then converts the schemas of these discovered MCP tools into ADK-compatible `BaseTool` instances. 3. **Exposure to Agent:** These adapted tools are then made available to your `LlmAgent` as if they were native ADK tools. 4. **Proxying Tool Calls:** When your `LlmAgent` decides to use one of these tools, `MCPToolset` transparently proxies the call (using the `call_tool` MCP method) to the MCP server, sends the necessary arguments, and returns the server's response back to the agent. 5. **Filtering (Optional):** You can use the `tool_filter` parameter when creating an `MCPToolset` to select a specific subset of tools from the MCP server, rather than exposing all of them to your agent. -The following examples demonstrate how to use `MCPToolset` within the `adk web` development environment. For scenarios where you need more fine-grained control over the MCP connection lifecycle or are not using `adk web`, refer to the "Using MCP Tools in your own Agent out of `adk web`" section later in this page. +### Connecting to Authenticated Remote Servers -### Example 1: File System MCP Server +For production use cases, MCP servers are often remote and secured. `MCPToolset` integrates with ADK's standard authentication framework to handle these scenarios. -This example demonstrates connecting to a local MCP server that provides file system operations. +!!! success "New Guide Available" + To learn how to connect to a remote MCP server that requires authentication (e.g., with a Bearer token), see the new comprehensive guide: + [**Authenticating with Remote MCP Servers**](./mcp-authentication.md) -#### Step 1: Define your Agent with `MCPToolset` +### Example 1: Local File System MCP Server + +This example demonstrates connecting to a local MCP server that provides file system operations. This pattern is ideal for development and testing. -Create an `agent.py` file (e.g., in `./adk_agent_samples/mcp_agent/agent.py`). The `MCPToolset` is instantiated directly within the `tools` list of your `LlmAgent`. +#### Step 1: Define your Agent with `MCPToolset` -* **Important:** Replace `"/path/to/your/folder"` in the `args` list with the **absolute path** to an actual folder on your local system that the MCP server can access. -* **Important:** Place the `.env` file in the parent directory of the `./adk_agent_samples` directory. +Create an `agent.py` file. The `MCPToolset` is instantiated directly within the `tools` list of your `LlmAgent`. ```python # ./adk_agent_samples/mcp_agent/agent.py -import os # Required for path operations +import os from google.adk.agents import LlmAgent -from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioConnectionParams -# It's good practice to define paths dynamically if possible, -# or ensure the user understands the need for an ABSOLUTE path. -# For this example, we'll construct a path relative to this file, -# assuming '/path/to/your/folder' is in the same directory as agent.py. -# REPLACE THIS with an actual absolute path if needed for your setup. -TARGET_FOLDER_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "/path/to/your/folder") -# Ensure TARGET_FOLDER_PATH is an absolute path for the MCP server. -# If you created ./adk_agent_samples/mcp_agent/your_folder, +# IMPORTANT: Replace this with an actual absolute path on your system. +TARGET_FOLDER_PATH = "/path/to/your/folder" root_agent = LlmAgent( model='gemini-2.0-flash', @@ -76,16 +73,13 @@ root_agent = LlmAgent( instruction='Help the user manage their files. You can list files, read files, etc.', tools=[ MCPToolset( - connection_params=StdioServerParameters( + connection_params=StdioConnectionParams( command='npx', args=[ "-y", # Argument for npx to auto-confirm install "@modelcontextprotocol/server-filesystem", - # IMPORTANT: This MUST be an ABSOLUTE path to a folder the + # This MUST be an ABSOLUTE path to a folder the # npx process can access. - # Replace with a valid absolute path on your system. - # For example: "/Users/youruser/accessible_mcp_files" - # or use a dynamically constructed absolute path: os.path.abspath(TARGET_FOLDER_PATH), ], ), @@ -96,7 +90,6 @@ root_agent = LlmAgent( ) ``` - #### Step 2: Create an `__init__.py` file Ensure you have an `__init__.py` in the same directory as `agent.py` to make it a discoverable Python package for ADK. @@ -108,128 +101,69 @@ from . import agent #### Step 3: Run `adk web` and Interact -Navigate to the parent directory of `mcp_agent` (e.g., `adk_agent_samples`) in your terminal and run: - -```shell -cd ./adk_agent_samples # Or your equivalent parent directory -adk web -``` +Navigate to the parent directory of `mcp_agent` and run `adk web`. Once the UI loads, select the `filesystem_assistant_agent` and try prompts like "List files in the current directory." !!!info "Note for Windows users" - When hitting the `_make_subprocess_transport NotImplementedError`, consider using `adk web --no-reload` instead. - -Once the ADK Web UI loads in your browser: - -1. Select the `filesystem_assistant_agent` from the agent dropdown. -2. Try prompts like: - * "List files in the current directory." - * "Can you read the file named sample.txt?" (assuming you created it in `TARGET_FOLDER_PATH`). - * "What is the content of `another_file.md`?" - -You should see the agent interacting with the MCP file system server, and the server's responses (file listings, file content) relayed through the agent. The `adk web` console (terminal where you ran the command) might also show logs from the `npx` process if it outputs to stderr. - MCP with ADK Web - FileSystem Example - ### Example 2: Google Maps MCP Server -This example demonstrates connecting to the Google Maps MCP server. +This example demonstrates connecting to the Google Maps MCP server, which requires an API key passed as an environment variable to the subprocess. #### Step 1: Get API Key and Enable APIs -1. **Google Maps API Key:** Follow the directions at [Use API keys](https://developers.google.com/maps/documentation/javascript/get-api-key#create-api-keys) to obtain a Google Maps API Key. -2. **Enable APIs:** In your Google Cloud project, ensure the following APIs are enabled: - * Directions API - * Routes API - For instructions, see the [Getting started with Google Maps Platform](https://developers.google.com/maps/get-started#enable-api-sdk) documentation. +Follow the official Google Maps Platform documentation to get an API key and enable the necessary APIs (Directions API, Routes API). -#### Step 2: Define your Agent with `MCPToolset` for Google Maps +#### Step 2: Define your Agent -Modify your `agent.py` file (e.g., in `./adk_agent_samples/mcp_agent/agent.py`). Replace `YOUR_GOOGLE_MAPS_API_KEY` with the actual API key you obtained. +Modify your `agent.py` file: ```python # ./adk_agent_samples/mcp_agent/agent.py import os from google.adk.agents import LlmAgent -from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioConnectionParams -# Retrieve the API key from an environment variable or directly insert it. -# Using an environment variable is generally safer. -# Ensure this environment variable is set in the terminal where you run 'adk web'. -# Example: export GOOGLE_MAPS_API_KEY="YOUR_ACTUAL_KEY" +# It is a best practice to load secrets from environment variables. google_maps_api_key = os.environ.get("GOOGLE_MAPS_API_KEY") - if not google_maps_api_key: - # Fallback or direct assignment for testing - NOT RECOMMENDED FOR PRODUCTION - google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY_HERE" # Replace if not using env var - if google_maps_api_key == "YOUR_GOOGLE_MAPS_API_KEY_HERE": - print("WARNING: GOOGLE_MAPS_API_KEY is not set. Please set it as an environment variable or in the script.") - # You might want to raise an error or exit if the key is crucial and not found. + raise ValueError("GOOGLE_MAPS_API_KEY environment variable not set.") root_agent = LlmAgent( model='gemini-2.0-flash', name='maps_assistant_agent', - instruction='Help the user with mapping, directions, and finding places using Google Maps tools.', + instruction='Help the user with mapping, directions, and finding places.', tools=[ MCPToolset( - connection_params=StdioServerParameters( + connection_params=StdioConnectionParams( command='npx', args=[ "-y", "@modelcontextprotocol/server-google-maps", ], - # Pass the API key as an environment variable to the npx process - # This is how the MCP server for Google Maps expects the key. + # Pass the API key as an environment variable to the npx process. env={ "GOOGLE_MAPS_API_KEY": google_maps_api_key } ), - # You can filter for specific Maps tools if needed: - # tool_filter=['get_directions', 'find_place_by_id'] ) ], ) ``` -#### Step 3: Ensure `__init__.py` Exists - -If you created this in Example 1, you can skip this. Otherwise, ensure you have an `__init__.py` in the `./adk_agent_samples/mcp_agent/` directory: - -```python -# ./adk_agent_samples/mcp_agent/__init__.py -from . import agent -``` - -#### Step 4: Run `adk web` and Interact +#### Step 3: Run `adk web` and Interact -1. **Set Environment Variable (Recommended):** - Before running `adk web`, it's best to set your Google Maps API key as an environment variable in your terminal: +1. **Set Environment Variable:** Before running `adk web`, set your API key in your terminal: ```shell export GOOGLE_MAPS_API_KEY="YOUR_ACTUAL_GOOGLE_MAPS_API_KEY" ``` - Replace `YOUR_ACTUAL_GOOGLE_MAPS_API_KEY` with your key. - -2. **Run `adk web`**: - Navigate to the parent directory of `mcp_agent` (e.g., `adk_agent_samples`) and run: - ```shell - cd ./adk_agent_samples # Or your equivalent parent directory - adk web - ``` - -3. **Interact in the UI**: - * Select the `maps_assistant_agent`. - * Try prompts like: - * "Get directions from GooglePlex to SFO." - * "Find coffee shops near Golden Gate Park." - * "What's the route from Paris, France to Berlin, Germany?" - -You should see the agent use the Google Maps MCP tools to provide directions or location-based information. +2. **Run `adk web`:** Start the development server. +3. **Interact:** Select the `maps_assistant_agent` and try prompts like: "Get directions from GooglePlex to SFO." MCP with ADK Web - Google Maps Example - ## 2. Building an MCP server with ADK tools (MCP server exposing ADK) This pattern allows you to wrap existing ADK tools and make them available to any standard MCP client application. The example in this section exposes the ADK `load_web_page` tool through a custom-built MCP server. @@ -345,6 +279,7 @@ async def call_mcp_tool( # Handle calls to unknown tools print(f"MCP Server: Tool '{name}' not found/exposed by this server.") error_text = json.dumps({"error": f"Tool '{name}' not implemented by this server."}) + return [mcp_types.TextContent(type="text", text=error_text)] # --- MCP Server Runner --- @@ -363,6 +298,7 @@ async def run_mcp_stdio_server(): # Define server capabilities - consult MCP docs for options notification_options=NotificationOptions(), experimental_capabilities={}, + ), ), ) @@ -391,14 +327,13 @@ Create an `agent.py` (e.g., in `./adk_agent_samples/mcp_client_agent/agent.py`): # ./adk_agent_samples/mcp_client_agent/agent.py import os from google.adk.agents import LlmAgent -from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioConnectionParams # IMPORTANT: Replace this with the ABSOLUTE path to your my_adk_mcp_server.py script PATH_TO_YOUR_MCP_SERVER_SCRIPT = "/path/to/your/my_adk_mcp_server.py" # <<< REPLACE -if PATH_TO_YOUR_MCP_SERVER_SCRIPT == "/path/to/your/my_adk_mcp_server.py": - print("WARNING: PATH_TO_YOUR_MCP_SERVER_SCRIPT is not set. Please update it in agent.py.") - # Optionally, raise an error if the path is critical +if not os.path.exists(PATH_TO_YOUR_MCP_SERVER_SCRIPT): + raise FileNotFoundError(f"MCP Server script not found at: {PATH_TO_YOUR_MCP_SERVER_SCRIPT}") root_agent = LlmAgent( model='gemini-2.0-flash', @@ -406,11 +341,10 @@ root_agent = LlmAgent( instruction="Use the 'load_web_page' tool to fetch content from a URL provided by the user.", tools=[ MCPToolset( - connection_params=StdioServerParameters( + connection_params=StdioConnectionParams( command='python3', # Command to run your MCP server script args=[PATH_TO_YOUR_MCP_SERVER_SCRIPT], # Argument is the path to the script ) - # tool_filter=['load_web_page'] # Optional: ensure only specific tools are loaded ) ], ) @@ -424,161 +358,104 @@ from . import agent **To run the test:** -1. **Start your custom MCP server (optional, for separate observation):** - You can run your `my_adk_mcp_server.py` directly in one terminal to see its logs: - ```shell - python3 /path/to/your/my_adk_mcp_server.py - ``` - It will print "Launching MCP Server..." and wait. The ADK agent (run via `adk web`) will then connect to this process if the `command` in `StdioServerParameters` is set up to execute it. - *(Alternatively, `MCPToolset` will start this server script as a subprocess automatically when the agent initializes).* +1. **Run `adk web` for the client agent:** + Navigate to the parent directory of `mcp_client_agent` and run `adk web`. -2. **Run `adk web` for the client agent:** - Navigate to the parent directory of `mcp_client_agent` (e.g., `adk_agent_samples`) and run: - ```shell - cd ./adk_agent_samples # Or your equivalent parent directory - adk web - ``` - -3. **Interact in the ADK Web UI:** +2. **Interact in the ADK Web UI:** * Select the `web_reader_mcp_client_agent`. * Try a prompt like: "Load the content from https://example.com" -The ADK agent (`web_reader_mcp_client_agent`) will use `MCPToolset` to start and connect to your `my_adk_mcp_server.py`. Your MCP server will receive the `call_tool` request, execute the ADK `load_web_page` tool, and return the result. The ADK agent will then relay this information. You should see logs from both the ADK Web UI (and its terminal) and potentially from your `my_adk_mcp_server.py` terminal if you ran it separately. - -This example demonstrates how ADK tools can be encapsulated within an MCP server, making them accessible to a broader range of MCP-compliant clients, not just ADK agents. - -Refer to the [documentation](https://modelcontextprotocol.io/quickstart/server#core-mcp-concepts), to try it out with Claude Desktop. - -## Using MCP Tools in your own Agent out of `adk web` +The ADK agent will use `MCPToolset` to start and connect to your `my_adk_mcp_server.py`. Your MCP server will receive the `call_tool` request, execute the ADK `load_web_page` tool, and return the result. -This section is relevant to you if: +## Using MCP Tools outside of `adk web` -* You are developing your own Agent using ADK -* And, you are **NOT** using `adk web`, -* And, you are exposing the agent via your own UI +When you are not using `adk web` and are building your own application, you need to manage the agent and tool lifecycle yourself. The key difference is that creating the agent and its tools becomes an asynchronous operation, as ADK needs to connect to the MCP server to discover the tools. - -Using MCP Tools requires a different setup than using regular tools, due to the fact that specs for MCP Tools are fetched asynchronously -from the MCP Server running remotely, or in another process. - -The following example is modified from the "Example 1: File System MCP Server" example above. The main differences are: - -1. Your tool and agent are created asynchronously -2. You need to properly manage the exit stack, so that your agents and tools are destructed properly when the connection to MCP Server is closed. +The following example is modified from the file system server example above. ```python -# agent.py (modify get_tools_async and other parts as needed) -# ./adk_agent_samples/mcp_agent/agent.py +# standalone_mcp_agent.py import os import asyncio -from dotenv import load_dotenv from google.genai import types from google.adk.agents.llm_agent import LlmAgent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService -from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService # Optional -from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, SseServerParams, StdioServerParameters - -# Load environment variables from .env file in the parent directory -# Place this near the top, before using env vars like API keys -load_dotenv('../.env') - -# Ensure TARGET_FOLDER_PATH is an absolute path for the MCP server. -TARGET_FOLDER_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "/path/to/your/folder") - -# --- Step 1: Agent Definition --- -async def get_agent_async(): - """Creates an ADK Agent equipped with tools from the MCP Server.""" - toolset = MCPToolset( - # Use StdioServerParameters for local process communication - connection_params=StdioServerParameters( - command='npx', # Command to run the server - args=["-y", # Arguments for the command - "@modelcontextprotocol/server-filesystem", - TARGET_FOLDER_PATH], - ), - tool_filter=['read_file', 'list_directory'] # Optional: filter specific tools - # For remote servers, you would use SseServerParams instead: - # connection_params=SseServerParams(url="http://remote-server:port/path", headers={...}) - ) - - # Use in an agent - root_agent = LlmAgent( - model='gemini-2.0-flash', # Adjust model name if needed based on availability - name='enterprise_assistant', - instruction='Help user accessing their file systems', - tools=[toolset], # Provide the MCP tools to the ADK agent - ) - return root_agent, toolset - -# --- Step 2: Main Execution Logic --- -async def async_main(): - session_service = InMemorySessionService() - # Artifact service might not be needed for this example - artifacts_service = InMemoryArtifactService() - - session = await session_service.create_session( - state={}, app_name='mcp_filesystem_app', user_id='user_fs' - ) - - # TODO: Change the query to be relevant to YOUR specified folder. - # e.g., "list files in the 'documents' subfolder" or "read the file 'notes.txt'" - query = "list files in the tests folder" - print(f"User Query: '{query}'") - content = types.Content(role='user', parts=[types.Part(text=query)]) - - root_agent, toolset = await get_agent_async() - - runner = Runner( - app_name='mcp_filesystem_app', - agent=root_agent, - artifact_service=artifacts_service, # Optional - session_service=session_service, - ) - - print("Running agent...") - events_async = runner.run_async( - session_id=session.id, user_id=session.user_id, new_message=content - ) - - async for event in events_async: - print(f"Event received: {event}") - - # Cleanup is handled automatically by the agent framework - # But you can also manually close if needed: - print("Closing MCP server connection...") - await toolset.close() - print("Cleanup complete.") +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioConnectionParams, StreamableHTTPConnectionParams + +TARGET_FOLDER_PATH = "/path/to/your/folder" # REPLACE with a valid absolute path + +async def create_agent_with_mcp_tools(): + """Asynchronously creates an ADK Agent with tools from an MCP Server.""" + toolset = MCPToolset( + # For local process communication + connection_params=StdioConnectionParams( + command='npx', + args=["-y", "@modelcontextprotocol/server-filesystem", TARGET_FOLDER_PATH], + ), + # For remote servers, you would use StreamableHTTPConnectionParams instead: + # connection_params=StreamableHTTPConnectionParams(url="http://remote-server:port/mcp") + ) + + root_agent = LlmAgent( + model='gemini-2.0-flash', + name='filesystem_assistant', + instruction='Help user accessing their file systems', + tools=[toolset], + ) + return root_agent, toolset + +async def main(): + session_service = InMemorySessionService() + session = await session_service.create_session(app_name='mcp_fs_app', user_id='user1') + + query = "list all files" + print(f"User Query: '{query}'") + content = types.Content(role='user', parts=[types.Part(text=query)]) + + root_agent, toolset = await create_agent_with_mcp_tools() + + runner = Runner(agent=root_agent, session_service=session_service) + + print("Running agent...") + try: + events_async = runner.run_async( + session_id=session.id, user_id=session.user_id, new_message=content + ) + async for event in events_async: + print(f"Event received: {event}") + finally: + # Crucially, ensure the toolset connection is closed to terminate + # any subprocesses. + print("Closing MCP server connection...") + await toolset.close() + print("Cleanup complete.") if __name__ == '__main__': - try: - asyncio.run(async_main()) - except Exception as e: - print(f"An error occurred: {e}") + try: + asyncio.run(main()) + except Exception as e: + print(f"An error occurred: {e}") ``` - ## Key considerations When working with MCP and ADK, keep these points in mind: -* **Protocol vs. Library:** MCP is a protocol specification, defining communication rules. ADK is a Python library/framework for building agents. MCPToolset bridges these by implementing the client side of the MCP protocol within the ADK framework. Conversely, building an MCP server in Python requires using the model-context-protocol library. - -* **ADK Tools vs. MCP Tools:** - - * ADK Tools (BaseTool, FunctionTool, AgentTool, etc.) are Python objects designed for direct use within the ADK's LlmAgent and Runner. - * MCP Tools are capabilities exposed by an MCP Server according to the protocol's schema. MCPToolset makes these look like ADK tools to an LlmAgent. - * Langchain/CrewAI Tools are specific implementations within those libraries, often simple functions or classes, lacking the server/protocol structure of MCP. ADK offers wrappers (LangchainTool, CrewaiTool) for some interoperability. +* **Protocol vs. Library:** MCP is a protocol specification, defining communication rules. ADK is a Python library/framework for building agents. `MCPToolset` bridges these by implementing the client side of the MCP protocol within the ADK framework. -* **Asynchronous nature:** Both ADK and the MCP Python library are heavily based on the asyncio Python library. Tool implementations and server handlers should generally be async functions. +* **Connection Types:** + * `StdioConnectionParams`: For running an MCP server as a local subprocess. Ideal for development. + * `StreamableHTTPConnectionParams`: For connecting to a remote MCP server over HTTP. This is the standard for production and connecting to separate services. -* **Stateful sessions (MCP):** MCP establishes stateful, persistent connections between a client and server instance. This differs from typical stateless REST APIs. +* **Asynchronous Nature:** Both ADK and the MCP Python library are heavily based on `asyncio`. Tool implementations and server handlers should generally be `async` functions. Creating an agent with an `MCPToolset` is an `async` operation. - * **Deployment:** This statefulness can pose challenges for scaling and deployment, especially for remote servers handling many users. The original MCP design often assumed client and server were co-located. Managing these persistent connections requires careful infrastructure considerations (e.g., load balancing, session affinity). - * **ADK MCPToolset:** Manages this connection lifecycle. The exit\_stack pattern shown in the examples is crucial for ensuring the connection (and potentially the server process) is properly terminated when the ADK agent finishes. +* **Stateful Sessions (MCP):** MCP establishes stateful, persistent connections between a client and server instance. This differs from typical stateless REST APIs. + * **Deployment:** This statefulness can pose challenges for scaling, especially for remote servers. Managing these persistent connections requires careful infrastructure considerations (e.g., load balancing, session affinity). + * **ADK `MCPToolset`:** Manages this connection lifecycle. The `try...finally` block with `toolset.close()` shown in the standalone example is crucial for ensuring the connection (and any subprocess) is properly terminated. ## Further Resources -* [Model Context Protocol Documentation](https://modelcontextprotocol.io/ ) -* [MCP Specification](https://modelcontextprotocol.io/specification/) -* [MCP Python SDK & Examples](https://github.com/modelcontextprotocol/) +* [Model Context Protocol Documentation](https://modelcontextprotocol.io/) +* [MCP Specification](https://modelcontextprotocol.io/specification/) +* [MCP Python SDK & Examples](https://github.com/modelcontextprotocol/) \ No newline at end of file diff --git a/examples/python/snippets/tools/mcp_auth/agent_with_auth.py b/examples/python/snippets/tools/mcp_auth/agent_with_auth.py new file mode 100644 index 000000000..f2fcdbbf4 --- /dev/null +++ b/examples/python/snippets/tools/mcp_auth/agent_with_auth.py @@ -0,0 +1,105 @@ +# 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. + +# --8<-- [start:init] + +import asyncio +import os + +from google.adk.agents import LlmAgent +from google.adk.auth import AuthCredential, AuthCredentialTypes, HttpAuth, HttpCredentials +from google.adk.auth.auth_schemes import HTTPBearer +from google.adk.tools.mcp_tool.mcp_toolset import ( + MCPToolset, + StreamableHTTPConnectionParams, +) +from google.adk.runners import Runner +from google.adk.sessions import InMemorySessionService +from google.genai import types + +# --- Authentication Configuration --- +# In a real application, load the token from an environment variable or secret manager. +# e.g., MY_MCP_API_TOKEN = os.environ.get("MY_MCP_API_TOKEN") +MY_MCP_API_TOKEN = "secret-token-123" # This must match the mock server's token + +# Define the authentication scheme: HTTP Bearer token. +auth_scheme = HTTPBearer() + +# Define the credential containing the actual token. +auth_credential = AuthCredential( + auth_type=AuthCredentialTypes.HTTP, + http=HttpAuth( + scheme="bearer", credentials=HttpCredentials(token=MY_MCP_API_TOKEN) + ), +) +# --- End Authentication Configuration --- + +# --- MCP Toolset Configuration --- +mcp_toolset = MCPToolset( + # Use StreamableHTTPConnectionParams for remote HTTP servers. + connection_params=StreamableHTTPConnectionParams( + url="http://127.0.0.1:8001/mcp", + ), + # Provide the authentication scheme and credential to the toolset. + auth_scheme=auth_scheme, + auth_credential=auth_credential, +) +# --- End MCP Toolset Configuration --- + +# --- Agent Definition --- +root_agent = LlmAgent( + model="gemini-1.5-flash", + name="profile_assistant_agent", + instruction="You can access user profiles through a secure MCP tool.", + tools=[mcp_toolset], +) +# --- End Agent Definition --- + +# --- Main Execution Logic --- +async def main(): + """Runs the agent to test the authenticated MCP tool call.""" + session_service = InMemorySessionService() + session = await session_service.create_session( + app_name="mcp_auth_app", user_id="test_user" + ) + + query = "What is the profile for user 42?" + print(f"User Query: '{query}'") + content = types.Content(role="user", parts=[types.Part(text=query)]) + + runner = Runner(agent=root_agent, session_service=session_service) + + print("Running agent...") + events_async = runner.run_async( + session_id=session.id, user_id=session.user_id, new_message=content + ) + + async for event in events_async: + print(f"Event received: {event}") + + # Clean up the MCP connection when done. + await mcp_toolset.close() + print("Cleanup complete.") + +if __name__ == "__main__": + # Set GOOGLE_API_KEY environment variable if not already set + if not os.environ.get("GOOGLE_API_KEY"): + print("Error: GOOGLE_API_KEY environment variable not set.") + else: + try: + asyncio.run(main()) + except Exception as e: + print(f"An error occurred: {e}") + +# --8<-- [end:init] \ No newline at end of file diff --git a/examples/python/snippets/tools/mcp_auth/mock_mcp_server.py b/examples/python/snippets/tools/mcp_auth/mock_mcp_server.py new file mode 100644 index 000000000..55a25ff1c --- /dev/null +++ b/examples/python/snippets/tools/mcp_auth/mock_mcp_server.py @@ -0,0 +1,92 @@ +# 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. + +# --8<-- [start:init] + +import json +from typing import Annotated, Any, Dict, List + +from fastapi import FastAPI, Header, HTTPException, Request +import uvicorn + +app = FastAPI( + title="Mock Authenticated MCP Server", + description="A simple server to simulate an MCP endpoint requiring Bearer token auth.", +) + +EXPECTED_TOKEN = "secret-token-123" + +async def check_auth(authorization: Annotated[str | None, Header()] = None): + """Dependency to check for a valid Bearer token.""" + if not authorization: + raise HTTPException(status_code=401, detail="Authorization header missing") + try: + scheme, token = authorization.split() + if scheme.lower() != "bearer" or token != EXPECTED_TOKEN: + raise HTTPException(status_code=401, detail="Invalid token") + except ValueError: + raise HTTPException(status_code=401, detail="Invalid authorization header format") + +@app.post("/mcp") +async def mcp_endpoint(request: Request): + """Main MCP endpoint that handles list_tools and call_tool methods.""" + await check_auth(request.headers.get("authorization")) + + body = await request.json() + method = body.get("method") + + if method == "list_tools": + return { + "tools": [ + { + "name": "get_user_profile", + "description": "Gets the profile for a given user ID.", + "inputSchema": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "The ID of the user.", + } + }, + "required": ["user_id"], + }, + } + ] + } + + if method == "call_tool": + tool_name = body.get("name") + if tool_name == "get_user_profile": + user_id = body.get("arguments", {}).get("user_id") + return { + "content": [ + { + "type": "json", + "json": { + "id": user_id, + "name": "Jane Doe", + "email": "jane.doe@example.com", + }, + } + ] + } + + raise HTTPException(status_code=400, detail=f"Unsupported method: {method}") + +if __name__ == "__main__": + print("Starting mock authenticated MCP server on http://127.0.0.1:8001") + uvicorn.run(app, host="127.0.0.1", port=8001) + +# --8<-- [end:init] \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 9b6ea0107..9d68ea401 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -136,6 +136,7 @@ nav: - Third party tools: tools/third-party-tools.md - Google Cloud tools: tools/google-cloud-tools.md - MCP tools: tools/mcp-tools.md + - MCP Authentication: tools/mcp-authentication.md - OpenAPI tools: tools/openapi-tools.md - Authentication: tools/authentication.md - Running Agents: