From bb6aa08be569b89297626dfd8bfa89225d5ac35a Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Thu, 6 Nov 2025 23:19:16 +0000 Subject: [PATCH 1/2] initial addition of Go snippets to mcp-tools docs --- docs/tools/mcp-tools.md | 1616 ++++++++++++++++++++++----------------- 1 file changed, 911 insertions(+), 705 deletions(-) diff --git a/docs/tools/mcp-tools.md b/docs/tools/mcp-tools.md index 8e3ca0cec5..f7e57b8619 100644 --- a/docs/tools/mcp-tools.md +++ b/docs/tools/mcp-tools.md @@ -22,7 +22,7 @@ This guide covers two primary integration patterns: Before you begin, ensure you have the following set up: * **Set up ADK:** Follow the standard ADK [setup instructions](../get-started/quickstart.md/#venv-install) in the quickstart. -* **Install/update Python/Java:** MCP requires Python version of 3.9 or higher for Python or Java 17 or higher. +* **Install/update Python/Java/Go:** MCP requires Python version of 3.9 or higher for Python, Java 17 or higher, or Go 1.22 or higher. * **Setup Node.js and npx:** **(Python only)** Many community MCP servers are distributed as Node.js packages and run using `npx`. Install Node.js (which includes npx) if you haven't already. For details, see [https://nodejs.org/en](https://nodejs.org/en). * **Verify Installations:** **(Python only)** Confirm `adk` and `npx` are in your PATH within the activated virtual environment: @@ -59,50 +59,51 @@ Create an `agent.py` file (e.g., in `./adk_agent_samples/mcp_agent/agent.py`). T * **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. -```python -# ./adk_agent_samples/mcp_agent/agent.py -import os # Required for path operations -from google.adk.agents import LlmAgent -from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset -from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams -from mcp import StdioServerParameters - -# 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, - -root_agent = LlmAgent( - model='gemini-2.0-flash', - name='filesystem_assistant_agent', - instruction='Help the user manage their files. You can list files, read files, etc.', - tools=[ - MCPToolset( - connection_params=StdioConnectionParams( - server_params = StdioServerParameters( - 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 - # 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), - ], +=== "Python" + ```python + # ./adk_agent_samples/mcp_agent/agent.py + import os # Required for path operations + from google.adk.agents import LlmAgent + from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset + from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams + from mcp import StdioServerParameters + + # 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, + + root_agent = LlmAgent( + model='gemini-2.0-flash', + name='filesystem_assistant_agent', + instruction='Help the user manage their files. You can list files, read files, etc.', + tools=[ + MCPToolset( + connection_params=StdioConnectionParams( + server_params = StdioServerParameters( + 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 + # 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), + ], + ), ), - ), - # Optional: Filter which tools from the MCP server are exposed - # tool_filter=['list_directory', 'read_file'] - ) - ], -) -``` + # Optional: Filter which tools from the MCP server are exposed + # tool_filter=['list_directory', 'read_file'] + ) + ], + ) + ``` #### Step 2: Create an `__init__.py` file @@ -141,99 +142,197 @@ You should see the agent interacting with the MCP file system server, and the se MCP with ADK Web - FileSystem Example - For Java, refer to the following sample to define an agent that initializes the `MCPToolset`: -```java -package agents; - -import com.google.adk.JsonBaseModel; -import com.google.adk.agents.LlmAgent; -import com.google.adk.agents.RunConfig; -import com.google.adk.runner.InMemoryRunner; -import com.google.adk.tools.mcp.McpTool; -import com.google.adk.tools.mcp.McpToolset; -import com.google.adk.tools.mcp.McpToolset.McpToolsAndToolsetResult; -import com.google.genai.types.Content; -import com.google.genai.types.Part; -import io.modelcontextprotocol.client.transport.ServerParameters; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public class McpAgentCreator { - - /** - * Initializes an McpToolset, retrieves tools from an MCP server using stdio, - * creates an LlmAgent with these tools, sends a prompt to the agent, - * and ensures the toolset is closed. - * @param args Command line arguments (not used). - */ - public static void main(String[] args) { - //Note: you may have permissions issues if the folder is outside home - String yourFolderPath = "~/path/to/folder"; - - ServerParameters connectionParams = ServerParameters.builder("npx") - .args(List.of( - "-y", - "@modelcontextprotocol/server-filesystem", - yourFolderPath - )) - .build(); - - try { - CompletableFuture futureResult = - McpToolset.fromServer(connectionParams, JsonBaseModel.getMapper()); - - McpToolsAndToolsetResult result = futureResult.join(); - - try (McpToolset toolset = result.getToolset()) { - List tools = result.getTools(); - - LlmAgent agent = LlmAgent.builder() - .model("gemini-2.0-flash") - .name("enterprise_assistant") - .description("An agent to help users access their file systems") - .instruction( - "Help user accessing their file systems. You can list files in a directory." - ) - .tools(tools) - .build(); - - System.out.println("Agent created: " + agent.name()); - - InMemoryRunner runner = new InMemoryRunner(agent); - String userId = "user123"; - String sessionId = "1234"; - String promptText = "Which files are in this directory - " + yourFolderPath + "?"; - - // Explicitly create the session first - try { - // appName for InMemoryRunner defaults to agent.name() if not specified in constructor - runner.sessionService().createSession(runner.appName(), userId, null, sessionId).blockingGet(); - System.out.println("Session created: " + sessionId + " for user: " + userId); - } catch (Exception sessionCreationException) { - System.err.println("Failed to create session: " + sessionCreationException.getMessage()); - sessionCreationException.printStackTrace(); - return; - } +=== "Java" + ```java + package agents; + + import com.google.adk.JsonBaseModel; + import com.google.adk.agents.LlmAgent; + import com.google.adk.agents.RunConfig; + import com.google.adk.runner.InMemoryRunner; + import com.google.adk.tools.mcp.McpTool; + import com.google.adk.tools.mcp.McpToolset; + import com.google.adk.tools.mcp.McpToolset.McpToolsAndToolsetResult; + import com.google.genai.types.Content; + import com.google.genai.types.Part; + import io.modelcontextprotocol.client.transport.ServerParameters; + + import java.util.List; + import java.util.concurrent.CompletableFuture; + + public class McpAgentCreator { + + /** + * Initializes an McpToolset, retrieves tools from an MCP server using stdio, + * creates an LlmAgent with these tools, sends a prompt to the agent, + * and ensures the toolset is closed. + * @param args Command line arguments (not used). + */ + public static void main(String[] args) { + //Note: you may have permissions issues if the folder is outside home + String yourFolderPath = "~/path/to/folder"; + + ServerParameters connectionParams = ServerParameters.builder("npx") + .args(List.of( + "-y", + "@modelcontextprotocol/server-filesystem", + yourFolderPath + )) + .build(); + + try { + CompletableFuture futureResult = + McpToolset.fromServer(connectionParams, JsonBaseModel.getMapper()); + + McpToolsAndToolsetResult result = futureResult.join(); + + try (McpToolset toolset = result.getToolset()) { + List tools = result.getTools(); + + LlmAgent agent = LlmAgent.builder() + .model("gemini-2.0-flash") + .name("enterprise_assistant") + .description("An agent to help users access their file systems") + .instruction( + "Help user accessing their file systems. You can list files in a directory." + ) + .tools(tools) + .build(); + + System.out.println("Agent created: " + agent.name()); + + InMemoryRunner runner = new InMemoryRunner(agent); + String userId = "user123"; + String sessionId = "1234"; + String promptText = "Which files are in this directory - " + yourFolderPath + "?"; + + // Explicitly create the session first + try { + // appName for InMemoryRunner defaults to agent.name() if not specified in constructor + runner.sessionService().createSession(runner.appName(), userId, null, sessionId).blockingGet(); + System.out.println("Session created: " + sessionId + " for user: " + userId); + } catch (Exception sessionCreationException) { + System.err.println("Failed to create session: " + sessionCreationException.getMessage()); + sessionCreationException.printStackTrace(); + return; + } - Content promptContent = Content.fromParts(Part.fromText(promptText)); + Content promptContent = Content.fromParts(Part.fromText(promptText)); - System.out.println("\nSending prompt: \"" + promptText + "\" to agent...\n"); + System.out.println("\nSending prompt: \"" + promptText + "\" to agent...\n"); - runner.runAsync(userId, sessionId, promptContent, RunConfig.builder().build()) - .blockingForEach(event -> { - System.out.println("Event received: " + event.toJson()); - }); + runner.runAsync(userId, sessionId, promptContent, RunConfig.builder().build()) + .blockingForEach(event -> { + System.out.println("Event received: " + event.toJson()); + }); + } + } catch (Exception e) { + System.err.println("An error occurred: " + e.getMessage()); + e.printStackTrace(); } - } catch (Exception e) { - System.err.println("An error occurred: " + e.getMessage()); - e.printStackTrace(); } } -} -``` + ``` + +For Go, you can create an `LlmAgent` with an `MCPToolset` to connect to the file system server. This example shows a self-contained application that initializes the agent and runs it with a sample prompt. + +=== "Go" + ```go + package main + + import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/mcptoolset" + "google.golang.org/genai" + ) + + func main() { + // 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 the current working directory. + // REPLACE THIS with an actual absolute path if needed for your setup. + targetFolderPath, err := filepath.Abs("./path/to/your/folder") + if err != nil { + log.Fatalf("Failed to get absolute path: %v", err) + } + // Ensure the directory exists + if err := os.MkdirAll(targetFolderPath, 0755); err != nil { + log.Fatalf("Failed to create target directory: %v", err) + } + fmt.Printf("Using target folder: %s\n", targetFolderPath) + + ctx := context.Background() + // Get the API Key from an environment variable + apiKey := os.Getenv("GEMINI_API_KEY") + if apiKey == "" { + log.Fatal("GEMINI_API_KEY environment variable not set") + } + + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{APIKey: apiKey}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + mcpToolset, err := mcptoolset.New(mcptoolset.Config{ + Transport: &mcp.CommandTransport{ + Command: exec.Command("npx", + "-y", + "@modelcontextprotocol/server-filesystem", + targetFolderPath, + ), + }, + // Optional: Filter which tools from the MCP server are exposed + // ToolFilter: tool.StringPredicate([]string{"list_directory", "read_file"}), + }) + if err != nil { + log.Fatalf("Failed to create MCP toolset: %v", err) + } + + agent, err := llmagent.New(llmagent.Config{ + Name: "filesystem_assistant_agent", + Model: model, + Description: "Help the user manage their files. You can list files, read files, etc.", + Instruction: "Help the user manage their files. You can list files, read files, etc.", + Toolsets: []tool.Toolset{mcpToolset}, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + r, err := runner.New(runner.Config{ + AppName: "mcp_filesystem_app", + Agent: agent, + SessionService: session.InMemoryService(), + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + prompt := "List files in the current directory." + fmt.Printf("\nSending prompt: %q to agent...\n\n", prompt) + + for event, err := range r.Run(ctx, "user123", "session123", genai.NewContentFromText(prompt, genai.RoleUser), agent.RunConfig{}) { + if err != nil { + log.Fatalf("Runner returned an error: %v", err) + } + fmt.Printf("Event received: %s\n", event.ToJSON()) + } + } + ``` Assuming a folder containing three files named `first`, `second` and `third`, successful response will look like this: @@ -263,53 +362,54 @@ This example demonstrates connecting to the Google Maps MCP server. 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. -```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 -from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams -from mcp import StdioServerParameters - -# 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" -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. - -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.', - tools=[ - MCPToolset( - connection_params=StdioConnectionParams( - server_params = StdioServerParameters( - 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. - env={ - "GOOGLE_MAPS_API_KEY": google_maps_api_key - } +=== "Python" + ```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 + from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams + from mcp import StdioServerParameters + + # 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" + 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. + + 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.', + tools=[ + MCPToolset( + connection_params=StdioConnectionParams( + server_params = StdioServerParameters( + 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. + 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'] - ) - ], -) -``` + # You can filter for specific Maps tools if needed: + # tool_filter=['get_directions', 'find_place_by_id'] + ) + ], + ) + ``` #### Step 3: Ensure `__init__.py` Exists @@ -349,103 +449,193 @@ You should see the agent use the Google Maps MCP tools to provide directions or For Java, refer to the following sample to define an agent that initializes the `MCPToolset`: +=== "Java" + + ```java + package agents; + + import com.google.adk.JsonBaseModel; + import com.google.adk.agents.LlmAgent; + import com.google.adk.agents.RunConfig; + import com.google.adk.runner.InMemoryRunner; + import com.google.adk.tools.mcp.McpTool; + import com.google.adk.tools.mcp.McpToolset; + import com.google.adk.tools.mcp.McpToolset.McpToolsAndToolsetResult; + + + import com.google.genai.types.Content; + import com.google.genai.types.Part; + + import io.modelcontextprotocol.client.transport.ServerParameters; + + import java.util.List; + import java.util.Map; + import java.util.Collections; + import java.util.HashMap; + import java.util.concurrent.CompletableFuture; + import java.util.Arrays; + + public class MapsAgentCreator { + + /** + * Initializes an McpToolset for Google Maps, retrieves tools, + * creates an LlmAgent, sends a map-related prompt, and closes the toolset. + * @param args Command line arguments (not used). + */ + public static void main(String[] args) { + // TODO: Replace with your actual Google Maps API key, on a project with the Places API enabled. + String googleMapsApiKey = "YOUR_GOOGLE_MAPS_API_KEY"; + + Map envVariables = new HashMap<>(); + envVariables.put("GOOGLE_MAPS_API_KEY", googleMapsApiKey); + + ServerParameters connectionParams = ServerParameters.builder("npx") + .args(List.of( + "-y", + "@modelcontextprotocol/server-google-maps" + )) + .env(Collections.unmodifiableMap(envVariables)) + .build(); + + try { + CompletableFuture futureResult = + McpToolset.fromServer(connectionParams, JsonBaseModel.getMapper()); + + McpToolsAndToolsetResult result = futureResult.join(); + + try (McpToolset toolset = result.getToolset()) { + List tools = result.getTools(); + + LlmAgent agent = LlmAgent.builder() + .model("gemini-2.0-flash") + .name("maps_assistant") + .description("Maps assistant") + .instruction("Help user with mapping and directions using available tools.") + .tools(tools) + .build(); + + System.out.println("Agent created: " + agent.name()); + + InMemoryRunner runner = new InMemoryRunner(agent); + String userId = "maps-user-" + System.currentTimeMillis(); + String sessionId = "maps-session-" + System.currentTimeMillis(); + + String promptText = "Please give me directions to the nearest pharmacy to Madison Square Garden."; + + try { + runner.sessionService().createSession(runner.appName(), userId, null, sessionId).blockingGet(); + System.out.println("Session created: " + sessionId + " for user: " + userId); + } catch (Exception sessionCreationException) { + System.err.println("Failed to create session: " + sessionCreationException.getMessage()); + sessionCreationException.printStackTrace(); + return; + } -```java -package agents; - -import com.google.adk.JsonBaseModel; -import com.google.adk.agents.LlmAgent; -import com.google.adk.agents.RunConfig; -import com.google.adk.runner.InMemoryRunner; -import com.google.adk.tools.mcp.McpTool; -import com.google.adk.tools.mcp.McpToolset; -import com.google.adk.tools.mcp.McpToolset.McpToolsAndToolsetResult; - - -import com.google.genai.types.Content; -import com.google.genai.types.Part; - -import io.modelcontextprotocol.client.transport.ServerParameters; - -import java.util.List; -import java.util.Map; -import java.util.Collections; -import java.util.HashMap; -import java.util.concurrent.CompletableFuture; -import java.util.Arrays; - -public class MapsAgentCreator { - - /** - * Initializes an McpToolset for Google Maps, retrieves tools, - * creates an LlmAgent, sends a map-related prompt, and closes the toolset. - * @param args Command line arguments (not used). - */ - public static void main(String[] args) { - // TODO: Replace with your actual Google Maps API key, on a project with the Places API enabled. - String googleMapsApiKey = "YOUR_GOOGLE_MAPS_API_KEY"; - - Map envVariables = new HashMap<>(); - envVariables.put("GOOGLE_MAPS_API_KEY", googleMapsApiKey); - - ServerParameters connectionParams = ServerParameters.builder("npx") - .args(List.of( - "-y", - "@modelcontextprotocol/server-google-maps" - )) - .env(Collections.unmodifiableMap(envVariables)) - .build(); - - try { - CompletableFuture futureResult = - McpToolset.fromServer(connectionParams, JsonBaseModel.getMapper()); - - McpToolsAndToolsetResult result = futureResult.join(); - - try (McpToolset toolset = result.getToolset()) { - List tools = result.getTools(); - - LlmAgent agent = LlmAgent.builder() - .model("gemini-2.0-flash") - .name("maps_assistant") - .description("Maps assistant") - .instruction("Help user with mapping and directions using available tools.") - .tools(tools) - .build(); - - System.out.println("Agent created: " + agent.name()); - - InMemoryRunner runner = new InMemoryRunner(agent); - String userId = "maps-user-" + System.currentTimeMillis(); - String sessionId = "maps-session-" + System.currentTimeMillis(); - - String promptText = "Please give me directions to the nearest pharmacy to Madison Square Garden."; - - try { - runner.sessionService().createSession(runner.appName(), userId, null, sessionId).blockingGet(); - System.out.println("Session created: " + sessionId + " for user: " + userId); - } catch (Exception sessionCreationException) { - System.err.println("Failed to create session: " + sessionCreationException.getMessage()); - sessionCreationException.printStackTrace(); - return; - } - - Content promptContent = Content.fromParts(Part.fromText(promptText)) + Content promptContent = Content.fromParts(Part.fromText(promptText)) - System.out.println("\nSending prompt: \"" + promptText + "\" to agent...\n"); + System.out.println("\nSending prompt: \"" + promptText + "\" to agent...\n"); - runner.runAsync(userId, sessionId, promptContent, RunConfig.builder().build()) - .blockingForEach(event -> { - System.out.println("Event received: " + event.toJson()); - }); + runner.runAsync(userId, sessionId, promptContent, RunConfig.builder().build()) + .blockingForEach(event -> { + System.out.println("Event received: " + event.toJson()); + }); + } + } catch (Exception e) { + System.err.println("An error occurred: " + e.getMessage()); + e.printStackTrace(); } - } catch (Exception e) { - System.err.println("An error occurred: " + e.getMessage()); - e.printStackTrace(); } } -} -``` + ``` + +For Go, the setup is similar. You will need to provide the `GOOGLE_MAPS_API_KEY` as an environment variable to the command that runs the MCP server. +=== "Go" + + ```go + package main + + import ( + "context" + "fmt" + "log" + "os" + "os/exec" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/mcptoolset" + "google.golang.org/genai" + ) + + func main() { + // Retrieve the API key from an environment variable. + // This is the recommended approach for security. + googleMapsAPIKey := os.Getenv("GOOGLE_MAPS_API_KEY") + if googleMapsAPIKey == "" { + log.Fatal("GOOGLE_MAPS_API_KEY environment variable not set") + } + + ctx := context.Background() + // Get the Gemini API Key from an environment variable + geminiAPIKey := os.Getenv("GEMINI_API_KEY") + if geminiAPIKey == "" { + log.Fatal("GEMINI_API_KEY environment variable not set") + } + + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{APIKey: geminiAPIKey}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + cmd := exec.Command("npx", "-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. + cmd.Env = append(os.Environ(), fmt.Sprintf("GOOGLE_MAPS_API_KEY=%s", googleMapsAPIKey)) + + mcpToolset, err := mcptoolset.New(mcptoolset.Config{ + Transport: &mcp.CommandTransport{Command: cmd}, + // You can filter for specific Maps tools if needed: + // ToolFilter: tool.StringPredicate([]string{"get_directions", "find_place_by_id"}), + }) + if err != nil { + log.Fatalf("Failed to create MCP toolset: %v", err) + } + + agent, err := llmagent.New(llmagent.Config{ + Name: "maps_assistant_agent", + Model: model, + Description: "Help the user with mapping, directions, and finding places using Google Maps tools.", + Instruction: "Help the user with mapping, directions, and finding places using Google Maps tools.", + Toolsets: []tool.Toolset{mcpToolset}, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + r, err := runner.New(runner.Config{ + AppName: "mcp_maps_app", + Agent: agent, + SessionService: session.InMemoryService(), + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + prompt := "Get directions from GooglePlex to SFO." + fmt.Printf("\nSending prompt: %q to agent...\n\n", prompt) + + for event, err := range r.Run(ctx, "user123", "session123", genai.NewContentFromText(prompt, genai.RoleUser), agent.RunConfig{}) { + if err != nil { + log.Fatalf("Runner returned an error: %v", err) + } + fmt.Printf("Event received: %s\n", event.ToJSON()) + } + } + ``` A successful response will look like this: ```shell @@ -483,125 +673,125 @@ Create a new Python file for your MCP server, for example, `my_adk_mcp_server.py ### Step 2: Implement the Server Logic Add the following code to `my_adk_mcp_server.py`. This script sets up an MCP server that exposes the ADK `load_web_page` tool. +=== "Python" + ```python + # my_adk_mcp_server.py + import asyncio + import json + import os + from dotenv import load_dotenv + + # MCP Server Imports + from mcp import types as mcp_types # Use alias to avoid conflict + from mcp.server.lowlevel import Server, NotificationOptions + from mcp.server.models import InitializationOptions + import mcp.server.stdio # For running as a stdio server + + # ADK Tool Imports + from google.adk.tools.function_tool import FunctionTool + from google.adk.tools.load_web_page import load_web_page # Example ADK tool + # ADK <-> MCP Conversion Utility + from google.adk.tools.mcp_tool.conversion_utils import adk_to_mcp_tool_type + + # --- Load Environment Variables (If ADK tools need them, e.g., API keys) --- + load_dotenv() # Create a .env file in the same directory if needed + + # --- Prepare the ADK Tool --- + # Instantiate the ADK tool you want to expose. + # This tool will be wrapped and called by the MCP server. + print("Initializing ADK load_web_page tool...") + adk_tool_to_expose = FunctionTool(load_web_page) + print(f"ADK tool '{adk_tool_to_expose.name}' initialized and ready to be exposed via MCP.") + # --- End ADK Tool Prep --- + + # --- MCP Server Setup --- + print("Creating MCP Server instance...") + # Create a named MCP Server instance using the mcp.server library + app = Server("adk-tool-exposing-mcp-server") + + # Implement the MCP server's handler to list available tools + @app.list_tools() + async def list_mcp_tools() -> list[mcp_types.Tool]: + """MCP handler to list tools this server exposes.""" + print("MCP Server: Received list_tools request.") + # Convert the ADK tool's definition to the MCP Tool schema format + mcp_tool_schema = adk_to_mcp_tool_type(adk_tool_to_expose) + print(f"MCP Server: Advertising tool: {mcp_tool_schema.name}") + return [mcp_tool_schema] + + # Implement the MCP server's handler to execute a tool call + @app.call_tool() + async def call_mcp_tool( + name: str, arguments: dict + ) -> list[mcp_types.Content]: # MCP uses mcp_types.Content + """MCP handler to execute a tool call requested by an MCP client.""" + print(f"MCP Server: Received call_tool request for '{name}' with args: {arguments}") + + # Check if the requested tool name matches our wrapped ADK tool + if name == adk_tool_to_expose.name: + try: + # Execute the ADK tool's run_async method. + # Note: tool_context is None here because this MCP server is + # running the ADK tool outside of a full ADK Runner invocation. + # If the ADK tool requires ToolContext features (like state or auth), + # this direct invocation might need more sophisticated handling. + adk_tool_response = await adk_tool_to_expose.run_async( + args=arguments, + tool_context=None, + ) + print(f"MCP Server: ADK tool '{name}' executed. Response: {adk_tool_response}") + + # Format the ADK tool's response (often a dict) into an MCP-compliant format. + # Here, we serialize the response dictionary as a JSON string within TextContent. + # Adjust formatting based on the ADK tool's output and client needs. + response_text = json.dumps(adk_tool_response, indent=2) + # MCP expects a list of mcp_types.Content parts + return [mcp_types.TextContent(type="text", text=response_text)] + + except Exception as e: + print(f"MCP Server: Error executing ADK tool '{name}': {e}") + # Return an error message in MCP format + error_text = json.dumps({"error": f"Failed to execute tool '{name}': {str(e)}"}) + return [mcp_types.TextContent(type="text", text=error_text)] + else: + # 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)] -```python -# my_adk_mcp_server.py -import asyncio -import json -import os -from dotenv import load_dotenv - -# MCP Server Imports -from mcp import types as mcp_types # Use alias to avoid conflict -from mcp.server.lowlevel import Server, NotificationOptions -from mcp.server.models import InitializationOptions -import mcp.server.stdio # For running as a stdio server - -# ADK Tool Imports -from google.adk.tools.function_tool import FunctionTool -from google.adk.tools.load_web_page import load_web_page # Example ADK tool -# ADK <-> MCP Conversion Utility -from google.adk.tools.mcp_tool.conversion_utils import adk_to_mcp_tool_type - -# --- Load Environment Variables (If ADK tools need them, e.g., API keys) --- -load_dotenv() # Create a .env file in the same directory if needed - -# --- Prepare the ADK Tool --- -# Instantiate the ADK tool you want to expose. -# This tool will be wrapped and called by the MCP server. -print("Initializing ADK load_web_page tool...") -adk_tool_to_expose = FunctionTool(load_web_page) -print(f"ADK tool '{adk_tool_to_expose.name}' initialized and ready to be exposed via MCP.") -# --- End ADK Tool Prep --- - -# --- MCP Server Setup --- -print("Creating MCP Server instance...") -# Create a named MCP Server instance using the mcp.server library -app = Server("adk-tool-exposing-mcp-server") - -# Implement the MCP server's handler to list available tools -@app.list_tools() -async def list_mcp_tools() -> list[mcp_types.Tool]: - """MCP handler to list tools this server exposes.""" - print("MCP Server: Received list_tools request.") - # Convert the ADK tool's definition to the MCP Tool schema format - mcp_tool_schema = adk_to_mcp_tool_type(adk_tool_to_expose) - print(f"MCP Server: Advertising tool: {mcp_tool_schema.name}") - return [mcp_tool_schema] - -# Implement the MCP server's handler to execute a tool call -@app.call_tool() -async def call_mcp_tool( - name: str, arguments: dict -) -> list[mcp_types.Content]: # MCP uses mcp_types.Content - """MCP handler to execute a tool call requested by an MCP client.""" - print(f"MCP Server: Received call_tool request for '{name}' with args: {arguments}") - - # Check if the requested tool name matches our wrapped ADK tool - if name == adk_tool_to_expose.name: - try: - # Execute the ADK tool's run_async method. - # Note: tool_context is None here because this MCP server is - # running the ADK tool outside of a full ADK Runner invocation. - # If the ADK tool requires ToolContext features (like state or auth), - # this direct invocation might need more sophisticated handling. - adk_tool_response = await adk_tool_to_expose.run_async( - args=arguments, - tool_context=None, + # --- MCP Server Runner --- + async def run_mcp_stdio_server(): + """Runs the MCP server, listening for connections over standard input/output.""" + # Use the stdio_server context manager from the mcp.server.stdio library + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + print("MCP Stdio Server: Starting handshake with client...") + await app.run( + read_stream, + write_stream, + InitializationOptions( + server_name=app.name, # Use the server name defined above + server_version="0.1.0", + capabilities=app.get_capabilities( + # Define server capabilities - consult MCP docs for options + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), ) - print(f"MCP Server: ADK tool '{name}' executed. Response: {adk_tool_response}") - - # Format the ADK tool's response (often a dict) into an MCP-compliant format. - # Here, we serialize the response dictionary as a JSON string within TextContent. - # Adjust formatting based on the ADK tool's output and client needs. - response_text = json.dumps(adk_tool_response, indent=2) - # MCP expects a list of mcp_types.Content parts - return [mcp_types.TextContent(type="text", text=response_text)] + print("MCP Stdio Server: Run loop finished or client disconnected.") + if __name__ == "__main__": + print("Launching MCP Server to expose ADK tools via stdio...") + try: + asyncio.run(run_mcp_stdio_server()) + except KeyboardInterrupt: + print("\nMCP Server (stdio) stopped by user.") except Exception as e: - print(f"MCP Server: Error executing ADK tool '{name}': {e}") - # Return an error message in MCP format - error_text = json.dumps({"error": f"Failed to execute tool '{name}': {str(e)}"}) - return [mcp_types.TextContent(type="text", text=error_text)] - else: - # 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 --- -async def run_mcp_stdio_server(): - """Runs the MCP server, listening for connections over standard input/output.""" - # Use the stdio_server context manager from the mcp.server.stdio library - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - print("MCP Stdio Server: Starting handshake with client...") - await app.run( - read_stream, - write_stream, - InitializationOptions( - server_name=app.name, # Use the server name defined above - server_version="0.1.0", - capabilities=app.get_capabilities( - # Define server capabilities - consult MCP docs for options - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - print("MCP Stdio Server: Run loop finished or client disconnected.") - -if __name__ == "__main__": - print("Launching MCP Server to expose ADK tools via stdio...") - try: - asyncio.run(run_mcp_stdio_server()) - except KeyboardInterrupt: - print("\nMCP Server (stdio) stopped by user.") - except Exception as e: - print(f"MCP Server (stdio) encountered an error: {e}") - finally: - print("MCP Server (stdio) process exiting.") -# --- End MCP Server --- -``` + print(f"MCP Server (stdio) encountered an error: {e}") + finally: + print("MCP Server (stdio) process exiting.") + # --- End MCP Server --- + ``` ### Step 3: Test your Custom MCP Server with an ADK Agent @@ -609,44 +799,45 @@ Now, create an ADK agent that will act as a client to the MCP server you just bu Create an `agent.py` (e.g., in `./adk_agent_samples/mcp_client_agent/agent.py`): -```python -# ./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 -from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams -from mcp import StdioServerParameters - -# 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 - -root_agent = LlmAgent( - model='gemini-2.0-flash', - name='web_reader_mcp_client_agent', - instruction="Use the 'load_web_page' tool to fetch content from a URL provided by the user.", - tools=[ - MCPToolset( - connection_params=StdioConnectionParams( - server_params = StdioServerParameters( - command='python3', # Command to run your MCP server script - args=[PATH_TO_YOUR_MCP_SERVER_SCRIPT], # Argument is the path to the script +=== "Python" + ```python + # ./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 + from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams + from mcp import StdioServerParameters + + # 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 + + root_agent = LlmAgent( + model='gemini-2.0-flash', + name='web_reader_mcp_client_agent', + instruction="Use the 'load_web_page' tool to fetch content from a URL provided by the user.", + tools=[ + MCPToolset( + connection_params=StdioConnectionParams( + server_params = StdioServerParameters( + 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 ) - # tool_filter=['load_web_page'] # Optional: ensure only specific tools are loaded - ) - ], -) -``` + ], + ) + ``` -And an `__init__.py` in the same directory: -```python -# ./adk_agent_samples/mcp_client_agent/__init__.py -from . import agent -``` + And an `__init__.py` in the same directory: + ```python + # ./adk_agent_samples/mcp_client_agent/__init__.py + from . import agent + ``` **To run the test:** @@ -691,101 +882,102 @@ The following example is modified from the "Example 1: File System MCP Server" e 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. - -```python -# agent.py (modify get_tools_async and other parts as needed) -# ./adk_agent_samples/mcp_agent/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 -from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams -from mcp import 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 StdioConnectionParams for local process communication - connection_params=StdioConnectionParams( - server_params = StdioServerParameters( - command='npx', # Command to run the server - args=["-y", # Arguments for the command - "@modelcontextprotocol/server-filesystem", - TARGET_FOLDER_PATH], +=== "Python" + + ```python + # agent.py (modify get_tools_async and other parts as needed) + # ./adk_agent_samples/mcp_agent/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 + from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams + from mcp import 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 StdioConnectionParams for local process communication + connection_params=StdioConnectionParams( + server_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 SseConnectionParams instead: - # connection_params=SseConnectionParams(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.") - -if __name__ == '__main__': - try: - asyncio.run(async_main()) - except Exception as e: - print(f"An error occurred: {e}") -``` + tool_filter=['read_file', 'list_directory'] # Optional: filter specific tools + # For remote servers, you would use SseConnectionParams instead: + # connection_params=SseConnectionParams(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.") + + if __name__ == '__main__': + try: + asyncio.run(async_main()) + except Exception as e: + print(f"An error occurred: {e}") + ``` ## Key considerations @@ -814,39 +1006,40 @@ When deploying ADK agents that use MCP tools to production environments like Clo **⚠️ Important:** When deploying agents with MCP tools, the agent and its MCPToolset must be defined **synchronously** in your `agent.py` file. While `adk web` allows for asynchronous agent creation, deployment environments require synchronous instantiation. -```python -# ✅ CORRECT: Synchronous agent definition for deployment -import os -from google.adk.agents.llm_agent import LlmAgent -from google.adk.tools.mcp_tool import StdioConnectionParams -from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset -from mcp import StdioServerParameters - -_allowed_path = os.path.dirname(os.path.abspath(__file__)) - -root_agent = LlmAgent( - model='gemini-2.0-flash', - name='enterprise_assistant', - instruction=f'Help user accessing their file systems. Allowed directory: {_allowed_path}', - tools=[ - MCPToolset( - connection_params=StdioConnectionParams( - server_params=StdioServerParameters( - command='npx', - args=['-y', '@modelcontextprotocol/server-filesystem', _allowed_path], +=== "Python" + ```python + # ✅ CORRECT: Synchronous agent definition for deployment + import os + from google.adk.agents.llm_agent import LlmAgent + from google.adk.tools.mcp_tool import StdioConnectionParams + from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset + from mcp import StdioServerParameters + + _allowed_path = os.path.dirname(os.path.abspath(__file__)) + + root_agent = LlmAgent( + model='gemini-2.0-flash', + name='enterprise_assistant', + instruction=f'Help user accessing their file systems. Allowed directory: {_allowed_path}', + tools=[ + MCPToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command='npx', + args=['-y', '@modelcontextprotocol/server-filesystem', _allowed_path], + ), + timeout=5, # Configure appropriate timeouts ), - timeout=5, # Configure appropriate timeouts - ), - # Filter tools for security in production - tool_filter=[ - 'read_file', 'read_multiple_files', 'list_directory', - 'directory_tree', 'search_files', 'get_file_info', - 'list_allowed_directories', - ], - ) - ], -) -``` + # Filter tools for security in production + tool_filter=[ + 'read_file', 'read_multiple_files', 'list_directory', + 'directory_tree', 'search_files', 'get_file_info', + 'list_allowed_directories', + ], + ) + ], + ) + ``` ```python # ❌ WRONG: Asynchronous patterns don't work in deployment @@ -919,118 +1112,120 @@ MCPToolset( For production deployments requiring scalability, deploy MCP servers as separate services and connect via Streamable HTTP: **MCP Server Deployment (Cloud Run):** -```python -# deploy_mcp_server.py - Separate Cloud Run service using Streamable HTTP -import contextlib -import logging -from collections.abc import AsyncIterator -from typing import Any - -import anyio -import click -import mcp.types as types -from mcp.server.lowlevel import Server -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from starlette.applications import Starlette -from starlette.routing import Mount -from starlette.types import Receive, Scope, Send - -logger = logging.getLogger(__name__) - -def create_mcp_server(): - """Create and configure the MCP server.""" - app = Server("adk-mcp-streamable-server") - - @app.call_tool() - async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: - """Handle tool calls from MCP clients.""" - # Example tool implementation - replace with your actual ADK tools - if name == "example_tool": - result = arguments.get("input", "No input provided") +=== "Python" + ```python + # deploy_mcp_server.py - Separate Cloud Run service using Streamable HTTP + import contextlib + import logging + from collections.abc import AsyncIterator + from typing import Any + + import anyio + import click + import mcp.types as types + from mcp.server.lowlevel import Server + from mcp.server.streamable_http_manager import StreamableHTTPSessionManager + from starlette.applications import Starlette + from starlette.routing import Mount + from starlette.types import Receive, Scope, Send + + logger = logging.getLogger(__name__) + + def create_mcp_server(): + """Create and configure the MCP server.""" + app = Server("adk-mcp-streamable-server") + + @app.call_tool() + async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: + """Handle tool calls from MCP clients.""" + # Example tool implementation - replace with your actual ADK tools + if name == "example_tool": + result = arguments.get("input", "No input provided") + return [ + types.TextContent( + type="text", + text=f"Processed: {result}" + ) + ] + else: + raise ValueError(f"Unknown tool: {name}") + + @app.list_tools() + async def list_tools() -> list[types.Tool]: + """List available tools.""" return [ - types.TextContent( - type="text", - text=f"Processed: {result}" + types.Tool( + name="example_tool", + description="Example tool for demonstration", + inputSchema={ + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "Input text to process" + } + }, + "required": ["input"] + } ) ] - else: - raise ValueError(f"Unknown tool: {name}") - - @app.list_tools() - async def list_tools() -> list[types.Tool]: - """List available tools.""" - return [ - types.Tool( - name="example_tool", - description="Example tool for demonstration", - inputSchema={ - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "Input text to process" - } - }, - "required": ["input"] - } - ) - ] - - return app -def main(port: int = 8080, json_response: bool = False): - """Main server function.""" - logging.basicConfig(level=logging.INFO) + return app - app = create_mcp_server() + def main(port: int = 8080, json_response: bool = False): + """Main server function.""" + logging.basicConfig(level=logging.INFO) - # Create session manager with stateless mode for scalability - session_manager = StreamableHTTPSessionManager( - app=app, - event_store=None, - json_response=json_response, - stateless=True, # Important for Cloud Run scalability - ) + app = create_mcp_server() - async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None: - await session_manager.handle_request(scope, receive, send) + # Create session manager with stateless mode for scalability + session_manager = StreamableHTTPSessionManager( + app=app, + event_store=None, + json_response=json_response, + stateless=True, # Important for Cloud Run scalability + ) - @contextlib.asynccontextmanager - async def lifespan(app: Starlette) -> AsyncIterator[None]: - """Manage session manager lifecycle.""" - async with session_manager.run(): - logger.info("MCP Streamable HTTP server started!") - try: - yield - finally: - logger.info("MCP server shutting down...") - - # Create ASGI application - starlette_app = Starlette( - debug=False, # Set to False for production - routes=[ - Mount("/mcp", app=handle_streamable_http), - ], - lifespan=lifespan, - ) + async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None: + await session_manager.handle_request(scope, receive, send) + + @contextlib.asynccontextmanager + async def lifespan(app: Starlette) -> AsyncIterator[None]: + """Manage session manager lifecycle.""" + async with session_manager.run(): + logger.info("MCP Streamable HTTP server started!") + try: + yield + finally: + logger.info("MCP server shutting down...") + + # Create ASGI application + starlette_app = Starlette( + debug=False, # Set to False for production + routes=[ + Mount("/mcp", app=handle_streamable_http), + ], + lifespan=lifespan, + ) - import uvicorn - uvicorn.run(starlette_app, host="0.0.0.0", port=port) + import uvicorn + uvicorn.run(starlette_app, host="0.0.0.0", port=port) -if __name__ == "__main__": - main() -``` + if __name__ == "__main__": + main() + ``` **Agent Configuration for Remote MCP:** -```python -# Your ADK agent connects to the remote MCP service via Streamable HTTP -MCPToolset( - connection_params=StreamableHTTPConnectionParams( - url="https://your-mcp-server-url.run.app/mcp", - headers={"Authorization": "Bearer your-auth-token"} - ), -) -``` +=== "Python" + ```python + # Your ADK agent connects to the remote MCP service via Streamable HTTP + MCPToolset( + connection_params=StreamableHTTPConnectionParams( + url="https://your-mcp-server-url.run.app/mcp", + headers={"Authorization": "Bearer your-auth-token"} + ), + ) + ``` #### Pattern 3: Sidecar MCP Servers (GKE) @@ -1109,86 +1304,97 @@ When deploying agents with MCP tools to production: ### Environment-Specific Configurations #### Cloud Run -```python -# Cloud Run environment variables for MCP configuration -import os - -# Detect Cloud Run environment -if os.getenv('K_SERVICE'): - # Use remote MCP servers in Cloud Run - mcp_connection = SseConnectionParams( - url=os.getenv('MCP_SERVER_URL'), - headers={'Authorization': f"Bearer {os.getenv('MCP_AUTH_TOKEN')}"} - ) -else: - # Use stdio for local development - mcp_connection = StdioConnectionParams( - server_params=StdioServerParameters( - command='npx', - args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] +=== "Python" + + ```python + # Cloud Run environment variables for MCP configuration + import os + + # Detect Cloud Run environment + if os.getenv('K_SERVICE'): + # Use remote MCP servers in Cloud Run + mcp_connection = SseConnectionParams( + url=os.getenv('MCP_SERVER_URL'), + headers={'Authorization': f"Bearer {os.getenv('MCP_AUTH_TOKEN')}"} + ) + else: + # Use stdio for local development + mcp_connection = StdioConnectionParams( + server_params=StdioServerParameters( + command='npx', + args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + ) ) - ) -MCPToolset(connection_params=mcp_connection) -``` + MCPToolset(connection_params=mcp_connection) + ``` #### GKE -```python -# GKE-specific MCP configuration -# Use service discovery for MCP servers within the cluster -MCPToolset( - connection_params=SseConnectionParams( - url="http://mcp-service.default.svc.cluster.local:8080/sse" - ), -) -``` + +=== "Python" + + ```python + # GKE-specific MCP configuration + # Use service discovery for MCP servers within the cluster + MCPToolset( + connection_params=SseConnectionParams( + url="http://mcp-service.default.svc.cluster.local:8080/sse" + ), + ) + ``` #### Vertex AI Agent Engine -```python -# Agent Engine managed deployment -# Prefer lightweight, self-contained MCP servers or external services -MCPToolset( - connection_params=SseConnectionParams( - url="https://your-managed-mcp-service.googleapis.com/sse", - headers={'Authorization': 'Bearer $(gcloud auth print-access-token)'} - ), -) -``` +=== "Python" + + ```python + # Agent Engine managed deployment + # Prefer lightweight, self-contained MCP servers or external services + MCPToolset( + connection_params=SseConnectionParams( + url="https://your-managed-mcp-service.googleapis.com/sse", + headers={'Authorization': 'Bearer $(gcloud auth print-access-token)'} + ), + ) + ``` ### Troubleshooting Deployment Issues **Common MCP Deployment Problems:** 1. **Stdio Process Startup Failures** - ```python - # Debug stdio connection issues - MCPToolset( - connection_params=StdioConnectionParams( - server_params=StdioServerParameters( - command='npx', - args=["-y", "@modelcontextprotocol/server-filesystem", "/app/data"], - # Add environment debugging - env={'DEBUG': '1'} - ), - ), - ) - ``` + + === "Python" + ```python + # Debug stdio connection issues + MCPToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command='npx', + args=["-y", "@modelcontextprotocol/server-filesystem", "/app/data"], + # Add environment debugging + env={'DEBUG': '1'} + ), + ), + ) + ``` 2. **Network Connectivity Issues** - ```python - # Test remote MCP connectivity - import aiohttp - async def test_mcp_connection(): - async with aiohttp.ClientSession() as session: - async with session.get('https://your-mcp-server.com/health') as resp: - print(f"MCP Server Health: {resp.status}") - ``` + === "Python" + ```python + # Test remote MCP connectivity + import aiohttp + + async def test_mcp_connection(): + async with aiohttp.ClientSession() as session: + async with session.get('https://your-mcp-server.com/health') as resp: + print(f"MCP Server Health: {resp.status}") + ``` 3. **Resource Exhaustion** - - Monitor container memory usage when using stdio MCP servers - - Set appropriate limits in Kubernetes deployments - - Use remote MCP servers for resource-intensive operations + - Monitor container memory usage when using stdio MCP servers + - Set appropriate limits in Kubernetes deployments + - Use remote MCP servers for resource-intensive operations ## Further Resources From e0b551888616dc6f09d03469c152b93076a740c6 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Fri, 7 Nov 2025 20:18:11 +0000 Subject: [PATCH 2/2] moving go mcp_tool snippets to separate examples --- docs/tools/mcp-tools.md | 179 +----------------- .../filesystem_tool/filesystem_tool.go | 97 ++++++++++ .../filesystem_tool/test_data/sample.txt | 8 + .../tools-custom/maps_tool/maps_tool.go | 87 +++++++++ 4 files changed, 195 insertions(+), 176 deletions(-) create mode 100644 examples/go/snippets/tools-custom/filesystem_tool/filesystem_tool.go create mode 100644 examples/go/snippets/tools-custom/filesystem_tool/test_data/sample.txt create mode 100644 examples/go/snippets/tools-custom/maps_tool/maps_tool.go diff --git a/docs/tools/mcp-tools.md b/docs/tools/mcp-tools.md index 39df66f2a4..5b10bea87f 100644 --- a/docs/tools/mcp-tools.md +++ b/docs/tools/mcp-tools.md @@ -22,7 +22,7 @@ This guide covers two primary integration patterns: Before you begin, ensure you have the following set up: * **Set up ADK:** Follow the standard ADK [setup instructions](../get-started/quickstart.md/#venv-install) in the quickstart. -* **Install/update Python/Java/Go:** MCP requires Python version of 3.9 or higher for Python, Java 17 or higher, or Go 1.22 or higher. +* **Install/update Python/Java/Go:** MCP requires Python version of 3.9 or higher for Python, Go 1.22 or higher, or Java 17 or higher. * **Setup Node.js and npx:** **(Python only)** Many community MCP servers are distributed as Node.js packages and run using `npx`. Install Node.js (which includes npx) if you haven't already. For details, see [https://nodejs.org/en](https://nodejs.org/en). * **Verify Installations:** **(Python only)** Confirm `adk` and `npx` are in your PATH within the activated virtual environment: @@ -240,98 +240,7 @@ For Go, you can create an `LlmAgent` with an `MCPToolset` to connect to the file === "Go" ```go - package main - - import ( - "context" - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - - "github.com/modelcontextprotocol/go-sdk/mcp" - "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/model/gemini" - "google.golang.org/adk/runner" - "google.golang.org/adk/session" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/mcptoolset" - "google.golang.org/genai" - ) - - func main() { - // 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 the current working directory. - // REPLACE THIS with an actual absolute path if needed for your setup. - targetFolderPath, err := filepath.Abs("./path/to/your/folder") - if err != nil { - log.Fatalf("Failed to get absolute path: %v", err) - } - // Ensure the directory exists - if err := os.MkdirAll(targetFolderPath, 0755); err != nil { - log.Fatalf("Failed to create target directory: %v", err) - } - fmt.Printf("Using target folder: %s\n", targetFolderPath) - - ctx := context.Background() - // Get the API Key from an environment variable - apiKey := os.Getenv("GEMINI_API_KEY") - if apiKey == "" { - log.Fatal("GEMINI_API_KEY environment variable not set") - } - - model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{APIKey: apiKey}) - if err != nil { - log.Fatalf("Failed to create model: %v", err) - } - - mcpToolset, err := mcptoolset.New(mcptoolset.Config{ - Transport: &mcp.CommandTransport{ - Command: exec.Command("npx", - "-y", - "@modelcontextprotocol/server-filesystem", - targetFolderPath, - ), - }, - // Optional: Filter which tools from the MCP server are exposed - // ToolFilter: tool.StringPredicate([]string{"list_directory", "read_file"}), - }) - if err != nil { - log.Fatalf("Failed to create MCP toolset: %v", err) - } - - agent, err := llmagent.New(llmagent.Config{ - Name: "filesystem_assistant_agent", - Model: model, - Description: "Help the user manage their files. You can list files, read files, etc.", - Instruction: "Help the user manage their files. You can list files, read files, etc.", - Toolsets: []tool.Toolset{mcpToolset}, - }) - if err != nil { - log.Fatalf("Failed to create agent: %v", err) - } - - r, err := runner.New(runner.Config{ - AppName: "mcp_filesystem_app", - Agent: agent, - SessionService: session.InMemoryService(), - }) - if err != nil { - log.Fatalf("Failed to create runner: %v", err) - } - - prompt := "List files in the current directory." - fmt.Printf("\nSending prompt: %q to agent...\n\n", prompt) - - for event, err := range r.Run(ctx, "user123", "session123", genai.NewContentFromText(prompt, genai.RoleUser), agent.RunConfig{}) { - if err != nil { - log.Fatalf("Runner returned an error: %v", err) - } - fmt.Printf("Event received: %s\n", event.ToJSON()) - } - } + --8<-- "examples/go/snippets/tools-custom/filesystem_tool/filesystem_tool.go" ``` Assuming a folder containing three files named `first`, `second` and `third`, successful response will look like this: @@ -552,89 +461,7 @@ For Go, the setup is similar. You will need to provide the `GOOGLE_MAPS_API_KEY` === "Go" ```go - package main - - import ( - "context" - "fmt" - "log" - "os" - "os/exec" - - "github.com/modelcontextprotocol/go-sdk/mcp" - "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/model/gemini" - "google.golang.org/adk/runner" - "google.golang.org/adk/session" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/mcptoolset" - "google.golang.org/genai" - ) - - func main() { - // Retrieve the API key from an environment variable. - // This is the recommended approach for security. - googleMapsAPIKey := os.Getenv("GOOGLE_MAPS_API_KEY") - if googleMapsAPIKey == "" { - log.Fatal("GOOGLE_MAPS_API_KEY environment variable not set") - } - - ctx := context.Background() - // Get the Gemini API Key from an environment variable - geminiAPIKey := os.Getenv("GEMINI_API_KEY") - if geminiAPIKey == "" { - log.Fatal("GEMINI_API_KEY environment variable not set") - } - - model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{APIKey: geminiAPIKey}) - if err != nil { - log.Fatalf("Failed to create model: %v", err) - } - - cmd := exec.Command("npx", "-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. - cmd.Env = append(os.Environ(), fmt.Sprintf("GOOGLE_MAPS_API_KEY=%s", googleMapsAPIKey)) - - mcpToolset, err := mcptoolset.New(mcptoolset.Config{ - Transport: &mcp.CommandTransport{Command: cmd}, - // You can filter for specific Maps tools if needed: - // ToolFilter: tool.StringPredicate([]string{"get_directions", "find_place_by_id"}), - }) - if err != nil { - log.Fatalf("Failed to create MCP toolset: %v", err) - } - - agent, err := llmagent.New(llmagent.Config{ - Name: "maps_assistant_agent", - Model: model, - Description: "Help the user with mapping, directions, and finding places using Google Maps tools.", - Instruction: "Help the user with mapping, directions, and finding places using Google Maps tools.", - Toolsets: []tool.Toolset{mcpToolset}, - }) - if err != nil { - log.Fatalf("Failed to create agent: %v", err) - } - - r, err := runner.New(runner.Config{ - AppName: "mcp_maps_app", - Agent: agent, - SessionService: session.InMemoryService(), - }) - if err != nil { - log.Fatalf("Failed to create runner: %v", err) - } - - prompt := "Get directions from GooglePlex to SFO." - fmt.Printf("\nSending prompt: %q to agent...\n\n", prompt) - - for event, err := range r.Run(ctx, "user123", "session123", genai.NewContentFromText(prompt, genai.RoleUser), agent.RunConfig{}) { - if err != nil { - log.Fatalf("Runner returned an error: %v", err) - } - fmt.Printf("Event received: %s\n", event.ToJSON()) - } - } + --8<-- "examples/go/snippets/tools-custom/maps_tool/maps_tool.go" ``` A successful response will look like this: diff --git a/examples/go/snippets/tools-custom/filesystem_tool/filesystem_tool.go b/examples/go/snippets/tools-custom/filesystem_tool/filesystem_tool.go new file mode 100644 index 0000000000..d77bc1c5ea --- /dev/null +++ b/examples/go/snippets/tools-custom/filesystem_tool/filesystem_tool.go @@ -0,0 +1,97 @@ +// 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. + +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/cmd/launcher/adk" + "google.golang.org/adk/cmd/launcher/full" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/server/restapi/services" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/mcptoolset" + "google.golang.org/genai" +) + +func main() { + // 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 the current working directory. + // REPLACE THIS with an actual absolute path if needed for your setup. + targetFolderPath, err := filepath.Abs("./test_data") + if err != nil { + log.Fatalf("Failed to get absolute path: %v", err) + } + // Ensure the directory exists + if err := os.MkdirAll(targetFolderPath, 0755); err != nil { + log.Fatalf("Failed to create target directory: %v", err) + } + fmt.Printf("Using target folder: %s\n", targetFolderPath) + + ctx := context.Background() + // Get the API Key from an environment variable + apiKey := os.Getenv("GOOGLE_API_KEY") + if apiKey == "" { + log.Fatal("GOOGLE_API_KEY environment variable not set") + } + + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{APIKey: apiKey}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + mcpToolset, err := mcptoolset.New(mcptoolset.Config{ + Transport: &mcp.CommandTransport{ + Command: exec.Command("npx", + "-y", + "@modelcontextprotocol/server-filesystem", + targetFolderPath, + ), + }, + // Optional: Filter which tools from the MCP server are exposed + // ToolFilter: tool.StringPredicate([]string{"list_directory", "read_file"}), + }) + if err != nil { + log.Fatalf("Failed to create MCP toolset: %v", err) + } + + agent, err := llmagent.New(llmagent.Config{ + Name: "filesystem_assistant_agent", + Model: model, + Description: "Help the user manage their files. You can list files, read files, etc.", + Instruction: "Help the user manage their files. You can list files, read files, etc.", + Toolsets: []tool.Toolset{mcpToolset}, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + config := &adk.Config{ + AgentLoader: services.NewSingleAgentLoader(agent), + } + l := full.NewLauncher() + err = l.Execute(ctx, config, os.Args[1:]) + if err != nil { + log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax()) + } +} diff --git a/examples/go/snippets/tools-custom/filesystem_tool/test_data/sample.txt b/examples/go/snippets/tools-custom/filesystem_tool/test_data/sample.txt new file mode 100644 index 0000000000..69953f1d6c --- /dev/null +++ b/examples/go/snippets/tools-custom/filesystem_tool/test_data/sample.txt @@ -0,0 +1,8 @@ +Bananas +Oranges +Cherries +Avocados +Paper towels +Dish soap +Flour +Baking Soda diff --git a/examples/go/snippets/tools-custom/maps_tool/maps_tool.go b/examples/go/snippets/tools-custom/maps_tool/maps_tool.go new file mode 100644 index 0000000000..a5cbcb7122 --- /dev/null +++ b/examples/go/snippets/tools-custom/maps_tool/maps_tool.go @@ -0,0 +1,87 @@ +// 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. + +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/cmd/launcher/adk" + "google.golang.org/adk/cmd/launcher/full" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/server/restapi/services" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/mcptoolset" + "google.golang.org/genai" +) + +func main() { + // Retrieve the API key from an environment variable. + // This is the recommended approach for security. + googleMapsAPIKey := os.Getenv("GOOGLE_MAPS_API_KEY") + if googleMapsAPIKey == "" { + log.Fatal("GOOGLE_MAPS_API_KEY environment variable not set") + } + + ctx := context.Background() + // Get the Gemini API Key from an environment variable + geminiAPIKey := os.Getenv("GOOGLE_API_KEY") + if geminiAPIKey == "" { + log.Fatal("GOOGLE_API_KEY environment variable not set") + } + + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{APIKey: geminiAPIKey}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + cmd := exec.Command("npx", "-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. + cmd.Env = append(os.Environ(), fmt.Sprintf("GOOGLE_MAPS_API_KEY=%s", googleMapsAPIKey)) + + mcpToolSet, err := mcptoolset.New(mcptoolset.Config{ + Transport: &mcp.CommandTransport{Command: cmd}, + }) + if err != nil { + log.Fatalf("Failed to create MCP tool set: %v", err) + } + + // Create LLMAgent with MCP tool set + agent, err := llmagent.New(llmagent.Config{ + Name: "maps_assistant_agent", + Model: model, + Description: "Help the user with mapping, directions, and finding places using Google Maps tools.", + Instruction: "Help the user with mapping, directions, and finding places using Google Maps tools.", + Toolsets: []tool.Toolset{mcpToolSet}, + }) + + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + config := &adk.Config{ + AgentLoader: services.NewSingleAgentLoader(agent), + } + l := full.NewLauncher() + err = l.Execute(ctx, config, os.Args[1:]) + if err != nil { + log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax()) + } +}