From d3796f9b33251d28d05e6701f11e80f02a2a49e1 Mon Sep 17 00:00:00 2001 From: Eliza Huang Date: Thu, 30 Oct 2025 09:38:59 -0700 Subject: [PATCH] feat: Add example for using ADK with Fast MCP sampling Close #2893 Co-authored-by: Eliza Huang PiperOrigin-RevId: 826070077 --- .../mcp_server_side_sampling/README.md | 51 ++++++++++++ .../mcp_server_side_sampling/__init__.py | 15 ++++ .../samples/mcp_server_side_sampling/agent.py | 56 +++++++++++++ .../mcp_server_side_sampling/mcp_server.py | 81 +++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 contributing/samples/mcp_server_side_sampling/README.md create mode 100755 contributing/samples/mcp_server_side_sampling/__init__.py create mode 100755 contributing/samples/mcp_server_side_sampling/agent.py create mode 100644 contributing/samples/mcp_server_side_sampling/mcp_server.py diff --git a/contributing/samples/mcp_server_side_sampling/README.md b/contributing/samples/mcp_server_side_sampling/README.md new file mode 100644 index 00000000..5fe96184 --- /dev/null +++ b/contributing/samples/mcp_server_side_sampling/README.md @@ -0,0 +1,51 @@ +# FastMCP Server-Side Sampling with ADK + +This project demonstrates how to use server-side sampling with a `fastmcp` server connected to an ADK `MCPToolset`. + +## Description + +The setup consists of two main components: + +1. **ADK Agent (`agent.py`):** An `LlmAgent` is configured with an `MCPToolset`. This toolset connects to a local `fastmcp` server. +2. **FastMCP Server (`mcp_server.py`):** A `fastmcp` server that exposes a single tool, `analyze_sentiment`. This server is configured to use its own LLM for sampling, independent of the ADK agent's LLM. + +The flow is as follows: +1. The user provides a text prompt to the ADK agent. +2. The agent decides to use the `analyze_sentiment` tool from the `MCPToolset`. +3. The tool call is sent to the `mcp_server.py`. +4. Inside the `analyze_sentiment` tool, `ctx.sample()` is called. This delegates an LLM call to the `fastmcp` server's own sampling handler. +5. The `mcp_server`'s LLM processes the prompt from `ctx.sample()` and returns the result to the server. +6. The server processes the LLM response and returns the final sentiment to the agent. +7. The agent displays the result to the user. + +## Steps to Run + +### Prerequisites + +- Python 3.10+ +- `google-adk` library installed. +- A configured OpenAI API key. + +### 1. Set up the Environment + +Clone the project and navigate to the directory. Make sure your `OPENAI_API_KEY` is available as an environment variable. + +### 2. Install Dependencies + +Install the required Python libraries: + +```bash +pip install fastmcp openai litellm +``` + +### 3. Run the Example + +Navigate to the `samples` directory and choose this ADK agent: + +```bash +adk web . +``` + +The agent will automatically start the FastMCP server in the background. + +- **Sample user prompt:** "What is the sentiment of 'I love building things with Python'?" diff --git a/contributing/samples/mcp_server_side_sampling/__init__.py b/contributing/samples/mcp_server_side_sampling/__init__.py new file mode 100755 index 00000000..c48963cd --- /dev/null +++ b/contributing/samples/mcp_server_side_sampling/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import agent diff --git a/contributing/samples/mcp_server_side_sampling/agent.py b/contributing/samples/mcp_server_side_sampling/agent.py new file mode 100755 index 00000000..36695f1b --- /dev/null +++ b/contributing/samples/mcp_server_side_sampling/agent.py @@ -0,0 +1,56 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from google.adk.agents import LlmAgent +from google.adk.models.lite_llm import LiteLlm +from google.adk.tools.mcp_tool import MCPToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from mcp import StdioServerParameters + +# This example uses the OpenAI API for both the agent and the server. +# Ensure your OPENAI_API_KEY is available as an environment variable. +api_key = os.getenv('OPENAI_API_KEY') +if not api_key: + raise ValueError('The OPENAI_API_KEY environment variable must be set.') + +# Configure the StdioServerParameters to start the mcp_server.py script +# as a subprocess. The OPENAI_API_KEY is passed to the server's environment. +server_params = StdioServerParameters( + command='python', + args=['mcp_server.py'], + env={'OPENAI_API_KEY': api_key}, +) + +# Create the ADK MCPToolset, which connects to the FastMCP server. +# The `tool_filter` ensures that only the 'analyze_sentiment' tool is exposed +# to the agent. +mcp_toolset = MCPToolset( + connection_params=StdioConnectionParams( + server_params=server_params, + ), + tool_filter=['analyze_sentiment'], +) + +# Define the ADK agent that uses the MCP toolset. +root_agent = LlmAgent( + model=LiteLlm(model='openai/gpt-4o'), + name='SentimentAgent', + instruction=( + 'You are an expert at analyzing text sentiment. Use the' + ' analyze_sentiment tool to classify user input.' + ), + tools=[mcp_toolset], +) diff --git a/contributing/samples/mcp_server_side_sampling/mcp_server.py b/contributing/samples/mcp_server_side_sampling/mcp_server.py new file mode 100644 index 00000000..2680c29d --- /dev/null +++ b/contributing/samples/mcp_server_side_sampling/mcp_server.py @@ -0,0 +1,81 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +from fastmcp import Context +from fastmcp import FastMCP +from fastmcp.experimental.sampling.handlers.openai import OpenAISamplingHandler +from openai import OpenAI + +logging.basicConfig(level=logging.INFO) +API_KEY = os.getenv("OPENAI_API_KEY") + +# Set up the server's LLM handler using the OpenAI API. +# This handler will be used for all sampling requests from tools on this server. +llm_handler = OpenAISamplingHandler( + default_model="gpt-4o", + client=OpenAI( + api_key=API_KEY, + ), +) + + +# Create the FastMCP Server instance. +# The `sampling_handler` is configured to use the server's own LLM. +# `sampling_handler_behavior="always"` ensures the server never delegates +# sampling back to the ADK agent. +mcp = FastMCP( + name="SentimentAnalysis", + sampling_handler=llm_handler, + sampling_handler_behavior="always", +) + + +@mcp.tool +async def analyze_sentiment(text: str, ctx: Context) -> dict: + """Analyzes sentiment by delegating to the server's own LLM.""" + logging.info("analyze_sentiment tool called with text: %s", text) + prompt = f"""Analyze the sentiment of the following text as positive, + negative, or neutral. Just output a single word. + Text to analyze: {text}""" + + # This delegates the LLM call to the server's own sampling handler, + # as configured in the FastMCP instance. + logging.info("Attempting to call ctx.sample()") + try: + response = await ctx.sample(prompt) + logging.info("ctx.sample() successful. Response: %s", response) + except Exception as e: + logging.error("ctx.sample() failed: %s", e, exc_info=True) + raise + + sentiment = response.text.strip().lower() + + if "positive" in sentiment: + result = "positive" + elif "negative" in sentiment: + result = "negative" + else: + result = "neutral" + + logging.info("Sentiment analysis result: %s", result) + return {"text": text, "sentiment": result} + + +if __name__ == "__main__": + print("Starting FastMCP server with tool 'analyze_sentiment'...") + # This runs the server process, which the ADK agent will connect to. + mcp.run()