Skip to content

Commit 5dabc0a

Browse files
committed
feat: Add web ui for multi_mcp ADK agent
1 parent 27460d2 commit 5dabc0a

File tree

4 files changed

+147
-50
lines changed

4 files changed

+147
-50
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ leaderboard:
99
uv run -- streamlit run agents_mcp_usage/multi_mcp/eval_multi_mcp/merbench_ui.py
1010

1111
adk_basic_ui:
12-
cd agents_mcp_usage/basic_mcp && uv run adk web
12+
uv run adk web agents_mcp_usage/basic_mcp
13+
14+
adk_multi_ui:
15+
uv run adk web agents_mcp_usage/multi_mcp

agents_mcp_usage/multi_mcp/README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ Agents utilising multiple MCP servers can be dramatically more complex than an A
2121
# Run the Google ADK multi-MCP example
2222
uv run agents_mcp_usage/multi_mcp/multi_mcp_use/adk_mcp.py
2323

24+
## Launch ADK web UI for visual interaction
25+
make adk_multi_ui
26+
2427
# Run the multi-MCP evaluation
2528
uv run agents_mcp_usage/multi_mcp/eval_multi_mcp/evals_pydantic_mcp.py
2629

2730
# Run multi-model benchmarking
28-
uv run agents_mcp_usage/multi_mcp/eval_multi_mcp/run_multi_evals.py --models "gemini-2.5-pro,gemini-2.0-flash" --runs 5 --parallel
31+
uv run agents_mcp_usage/multi_mcp/eval_multi_mcp/run_multi_evals.py --models "gemini-2.5-pro-preview-06-05,gemini-2.0-flash" --runs 5 --parallel
2932

3033
# Launch the evaluation dashboard
3134
uv run streamlit run agents_mcp_usage/multi_mcp/eval_multi_mcp/merbench_ui.py
@@ -250,18 +253,18 @@ The `run_multi_evals.py` script enables systematic comparison across multiple LL
250253
```bash
251254
# Parallel benchmarking across multiple models
252255
uv run agents_mcp_usage/multi_mcp/eval_multi_mcp/run_multi_evals.py \
253-
--models "gemini-2.5-pro,gemini-2.0-flash,gemini-2.5-flash-preview-04-17" \
256+
--models "gemini-2.5-pro-preview-06-05,gemini-2.0-flash,gemini-2.5-flash" \
254257
--runs 5 \
255258
--parallel \
256259
--timeout 600 \
257260
--output-dir ./benchmark_results
258261

259262
# Sequential execution with custom judge model
260263
uv run agents_mcp_usage/multi_mcp/eval_multi_mcp/run_multi_evals.py \
261-
--models "gemini-2.5-pro,claude-3-opus" \
264+
--models "gemini-2.5-pro-preview-06-05,claude-3-opus" \
262265
--runs 3 \
263266
--sequential \
264-
--judge-model "gemini-2.5-pro" \
267+
--judge-model "gemini-2.5-pro-preview-06-05" \
265268
--output-dir ./comparative_analysis
266269
```
267270

@@ -295,7 +298,7 @@ Key features:
295298

296299
**File:** `multi_mcp_use/adk_mcp.py`
297300

298-
This example demonstrates how to use multiple MCP servers with Google's Agent Development Kit (ADK).
301+
This example demonstrates how to use multiple MCP servers with Google's Agent Development Kit (ADK 1.3.0).
299302

300303
```bash
301304
uv run agents_mcp_usage/multi_mcp/multi_mcp_use/adk_mcp.py
@@ -304,10 +307,17 @@ uv run agents_mcp_usage/multi_mcp/multi_mcp_use/adk_mcp.py
304307
Key features:
305308
- Uses Google's ADK framework with Gemini model
306309
- Connects to both Python MCP server and Python Mermaid validator
307-
- Demonstrates proper connection management with contextlib.AsyncExitStack
310+
- Uses new ADK 1.3.0 patterns for toolset management
311+
- Implements proper resource tracking for MCP connections
308312
- Shows how to handle asynchronous MCP tool integration
313+
- Supports ADK web UI through module exports and callback-based tool attachment
309314
- Uses a simple test case that utilizes both MCP servers in a single query
310315

316+
To run with the ADK web UI:
317+
```bash
318+
make adk_multi_ui
319+
```
320+
311321
### Multi-MCP Evaluation
312322

313323
**File:** `eval_multi_mcp/evals_pydantic_mcp.py`
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Multi MCP usage examples package
2+
3+
# Allow discovery of the ADK agent via the ADK web UI
4+
from .adk_mcp import root_agent

agents_mcp_usage/multi_mcp/multi_mcp_use/adk_mcp.py

Lines changed: 123 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@
88
- RunConfig for request limiting and control
99
- Enhanced event tracking and metrics collection
1010
- Graceful resource cleanup and Logfire instrumentation
11+
- Web UI integration via ADK's callback system
1112
1213
Compatible with ADK v1.3.0+ and showcases production-ready patterns
1314
for complex agent architectures involving multiple tool sources.
15+
16+
This module provides an ADK agent that can be used both in the ADK web UI
17+
and directly from the command line. The agent uses multiple MCP servers
18+
to access different external functionalities.
1419
"""
1520

1621
import asyncio
1722
import os
18-
import time
23+
from typing import List, Tuple, Any
1924

2025
import logfire
2126
from dotenv import load_dotenv
@@ -42,22 +47,26 @@
4247
logfire.configure(send_to_logfire="if-token-present", service_name="adk-multi-mcp")
4348
logfire.instrument_mcp()
4449

50+
# Global variable to store toolset instances for cleanup
51+
_ACTIVE_TOOLSETS = []
52+
53+
# Flag to track if tools have been attached
54+
TOOLS_ATTACHED = False
4555

46-
async def get_tools_async() -> tuple[list, list]:
56+
57+
async def get_tools_async() -> Tuple[List[Any], List[MCPToolset]]:
4758
"""Initializes connections to MCP servers and returns their tools.
4859
4960
This function connects to the local example server and the mermaid
50-
validator server, and returns the combined tools and a combined exit
51-
stack for cleanup.
61+
validator server, and returns the combined tools and a list of toolsets
62+
for cleanup.
5263
5364
Returns:
5465
A tuple containing the list of all tools and the list of toolsets for cleanup.
5566
"""
67+
global _ACTIVE_TOOLSETS
5668
print("Connecting to MCP servers...")
5769

58-
# Keep track of toolsets for cleanup (ADK v1.3.0+ API)
59-
toolsets = []
60-
6170
# Set up MCP server connections
6271
local_server = StdioServerParameters(
6372
command="uv",
@@ -76,25 +85,111 @@ async def get_tools_async() -> tuple[list, list]:
7685
],
7786
)
7887

79-
# Connect to local python MCP server (ADK v1.3.0+ API)
80-
local_connection = StdioConnectionParams(server_params=local_server)
81-
local_toolset = MCPToolset(connection_params=local_connection)
82-
local_tools = await local_toolset.get_tools()
83-
toolsets.append(local_toolset)
84-
print(f"Connected to local python MCP server. Found {len(local_tools)} tools.")
88+
local_toolset = None
89+
mermaid_toolset = None
90+
toolsets = []
91+
92+
try:
93+
# Connect to local python MCP server
94+
local_connection = StdioConnectionParams(server_params=local_server)
95+
local_toolset = MCPToolset(connection_params=local_connection)
96+
local_tools = await local_toolset.get_tools()
97+
toolsets.append(local_toolset)
98+
_ACTIVE_TOOLSETS.append(local_toolset)
99+
print(f"Connected to local python MCP server. Found {len(local_tools)} tools.")
100+
101+
# Connect to mermaid MCP server
102+
mermaid_connection = StdioConnectionParams(server_params=mermaid_server)
103+
mermaid_toolset = MCPToolset(connection_params=mermaid_connection)
104+
mermaid_tools = await mermaid_toolset.get_tools()
105+
toolsets.append(mermaid_toolset)
106+
_ACTIVE_TOOLSETS.append(mermaid_toolset)
107+
print(f"Connected to mermaid MCP server. Found {len(mermaid_tools)} tools.")
108+
109+
# Combine tools from both servers
110+
all_tools = local_tools + mermaid_tools
111+
print(f"Total tools available: {len(all_tools)}")
112+
113+
return all_tools, toolsets
114+
115+
except Exception as e:
116+
# Clean up in case of initialisation error
117+
if local_toolset in toolsets:
118+
try:
119+
await local_toolset.close()
120+
toolsets.remove(local_toolset)
121+
if local_toolset in _ACTIVE_TOOLSETS:
122+
_ACTIVE_TOOLSETS.remove(local_toolset)
123+
except Exception:
124+
pass
125+
126+
if mermaid_toolset in toolsets:
127+
try:
128+
await mermaid_toolset.close()
129+
toolsets.remove(mermaid_toolset)
130+
if mermaid_toolset in _ACTIVE_TOOLSETS:
131+
_ACTIVE_TOOLSETS.remove(mermaid_toolset)
132+
except Exception:
133+
pass
134+
135+
raise e
136+
137+
138+
async def cleanup_toolsets():
139+
"""Clean up any active MCP toolset connections."""
140+
global _ACTIVE_TOOLSETS
85141

86-
# Connect to npx mermaid MCP server (ADK v1.3.0+ API)
87-
mermaid_connection = StdioConnectionParams(server_params=mermaid_server)
88-
mermaid_toolset = MCPToolset(connection_params=mermaid_connection)
89-
mermaid_tools = await mermaid_toolset.get_tools()
90-
toolsets.append(mermaid_toolset)
91-
print(f"Connected to npx mermaid MCP server. Found {len(mermaid_tools)} tools.")
142+
for toolset in _ACTIVE_TOOLSETS:
143+
try:
144+
await toolset.close()
145+
print("MCP toolset connection closed.")
146+
except asyncio.CancelledError:
147+
print("MCP cleanup cancelled - this is normal")
148+
except Exception as e:
149+
print(f"Warning: Error during toolset cleanup: {e}")
92150

93-
# Combine tools from both servers
94-
all_tools = local_tools + mermaid_tools
95-
print(f"Total tools available: {len(all_tools)}")
151+
_ACTIVE_TOOLSETS = []
96152

97-
return all_tools, toolsets
153+
154+
# Define a before_agent_callback to attach tools
155+
async def attach_tools_callback(callback_context):
156+
"""Callback to attach tools to the agent before it runs.
157+
158+
Args:
159+
callback_context: The callback context from ADK.
160+
161+
Returns:
162+
None: The callback doesn't modify the content.
163+
"""
164+
await ensure_tools_attached()
165+
return None
166+
167+
168+
# This is the agent that will be imported by the ADK web UI
169+
root_agent = LlmAgent(
170+
model="gemini-2.0-flash",
171+
name="multi_mcp_adk_assistant",
172+
instruction="You are an assistant that uses multiple MCP servers to help users. You have access to both a Python MCP server with time tools and a Mermaid diagram validator.",
173+
before_agent_callback=attach_tools_callback, # This ensures tools are attached
174+
)
175+
176+
177+
# Function to dynamically attach tools to the agent
178+
async def ensure_tools_attached():
179+
"""Ensures that tools are attached to the agent before it's used."""
180+
global TOOLS_ATTACHED
181+
182+
if not TOOLS_ATTACHED:
183+
try:
184+
tools, _ = await get_tools_async()
185+
print(f"✓ Connected to MCP servers. Found {len(tools)} tools.")
186+
# Update the agent's tools
187+
root_agent.tools = tools
188+
TOOLS_ATTACHED = True
189+
except Exception as e:
190+
print(f"Error attaching MCP tools: {e}")
191+
# Set empty tools to avoid errors
192+
root_agent.tools = []
98193

99194

100195
async def main(query: str = "Hi!", request_limit: int = 5) -> None:
@@ -108,17 +203,9 @@ async def main(query: str = "Hi!", request_limit: int = 5) -> None:
108203
query: The query to run the agent with.
109204
request_limit: The maximum number of LLM calls allowed.
110205
"""
111-
toolsets = []
112206
try:
113-
# Get tools from MCP servers
114-
tools, toolsets = await get_tools_async()
115-
116-
# Create agent
117-
agent = LlmAgent(
118-
model="gemini-2.0-flash",
119-
name="multi_mcp_adk",
120-
tools=tools,
121-
)
207+
# Ensure tools are attached to the agent
208+
await ensure_tools_attached()
122209

123210
# Create async session service
124211
session_service = InMemorySessionService()
@@ -130,10 +217,10 @@ async def main(query: str = "Hi!", request_limit: int = 5) -> None:
130217
# Create a RunConfig with a limit for LLM calls (500 is the default)
131218
run_config = RunConfig(max_llm_calls=request_limit)
132219

133-
# Create runner
220+
# Create runner using the globally defined agent
134221
runner = Runner(
135222
app_name="multi_mcp_adk",
136-
agent=agent,
223+
agent=root_agent,
137224
session_service=session_service,
138225
)
139226

@@ -158,14 +245,7 @@ async def main(query: str = "Hi!", request_limit: int = 5) -> None:
158245
finally:
159246
# Clean up MCP toolsets to prevent asyncio shutdown errors
160247
print("Cleaning up MCP connections...")
161-
for i, toolset in enumerate(toolsets):
162-
try:
163-
await toolset.close()
164-
print(f"Toolset {i + 1} closed successfully.")
165-
except asyncio.CancelledError:
166-
print(f"Toolset {i + 1} cleanup cancelled - this is normal")
167-
except Exception as e:
168-
print(f"Warning during cleanup of toolset {i + 1}: {e}")
248+
await cleanup_toolsets()
169249
print("MCP cleanup completed.")
170250

171251

0 commit comments

Comments
 (0)