Skip to content

Commit 243668c

Browse files
committed
init
1 parent 3b811ca commit 243668c

30 files changed

+4187
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

examples/a2a-mcp-app/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Build Agents using A2A SDK
2+
----
3+
> *⚠️ DISCLAIMER: THIS DEMO IS INTENDED FOR DEMONSTRATION PURPOSES ONLY. IT IS NOT INTENDED FOR USE IN A PRODUCTION ENVIRONMENT.*
4+
5+
> *⚠️ Important: A2A is a work in progress (WIP) thus, in the near future there might be changes that are different from what demonstrated here.*
6+
----
7+
8+
This document describes a web application demonstrating the integration of Google's Agent to Agent (A2A), Agent Development Kit (ADK) for multi-agent orchestration with Model Context Protocol (MCP) clients. The application features a host agent coordinating tasks between remote agents that interact with various MCP servers to fulfill user requests.
9+
10+
### Architecture
11+
12+
The application utilizes a multi-agent architecture where a host agent delegates tasks to remote agents (Airbnb and Weather) based on the user's query. These agents then interact with corresponding MCP servers.
13+
14+
![architecture](assets/A2A_multi_agent.png)
15+
16+
### App UI
17+
![screenshot](assets/screenshot.png)
18+
19+
20+
## Setup and Deployment
21+
22+
### Prerequisites
23+
24+
Before running the application locally, ensure you have the following installed:
25+
26+
1. **Node.js:** Required to run the Airbnb MCP server (if testing its functionality locally).
27+
2. **uv:** The Python package management tool used in this project. Follow the installation guide: [https://docs.astral.sh/uv/getting-started/installation/](https://docs.astral.sh/uv/getting-started/installation/)
28+
3. **python 3.13** Python 3.13 is required to run a2a-sdk
29+
4. **set up .env**
30+
31+
32+
- create .env file in `airbnb_agent` and `weater_agent`folder with the following content
33+
```bash
34+
GOOGLE_API_KEY="your_api_key_here"
35+
```
36+
37+
- create .env file in `host_agent/adk_agent`folder with the following content:
38+
39+
```bash
40+
GOOGLE_GENAI_USE_VERTEXAI=TRUE
41+
GOOGLE_CLOUD_PROJECT="your project"
42+
GOOGLE_CLOUD_LOCATION=us-central1
43+
AIR_AGENT_URL=http://localhost:10002
44+
WEA_AGENT_URL=http://localhost:10001
45+
```
46+
47+
## Install SDK
48+
Go to `a2a-mcp-app` folder in terminal:
49+
```bash
50+
uv sync
51+
```
52+
53+
54+
## 1. Run Airbnb server
55+
56+
Run Remote server
57+
58+
```bash
59+
cd airbnb_agent
60+
uv run .
61+
```
62+
63+
## 2. Run Weather server
64+
Open a new terminal, go to `a2a-mcp-app` folder run the server
65+
66+
```bash
67+
cd weather_agent
68+
uv run .
69+
```
70+
71+
## 3. Run Host Agent
72+
Open a new terminal, go to `a2a-mcp-app` folder run the server
73+
74+
```bash
75+
cd host_agent
76+
uv run app.py
77+
```
78+
79+
80+
## 4. Test at the UI
81+
82+
Here're example questions:
83+
84+
- "Tell me about weather in LA, CA"
85+
86+
- "Please find a room in LA, CA, June 20-25, 2025, two adults"
87+
88+
## References
89+
- https://github.com/google/a2a-python
90+
- https://codelabs.developers.google.com/intro-a2a-purchasing-concierge#1
91+
- https://github.com/alphinside/purchasing-concierge-intro-a2a-codelab-starter
92+
- https://google.github.io/adk-docs/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
GOOGLE_API_KEY="your key"
2+
# Choose Model Backend: 0 -> ML Dev, 1 -> Vertex
3+
GOOGLE_GENAI_USE_VERTEXAI=True
4+
5+
# ML Dev backend config
6+
7+
# Vertex backend config
8+
GOOGLE_CLOUD_PROJECT="your project id"
9+
GOOGLE_CLOUD_LOCATION="us-central1"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
An example langgraph agent .
2+
3+
## Getting started
4+
5+
1. Create an environment file with your API key:
6+
```bash
7+
echo "GOOGLE_API_KEY=your_api_key_here" > .env
8+
```
9+
10+
2. Start the server
11+
```bash
12+
uv run .
13+
```
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import os
2+
import sys
3+
from typing import Dict, Any, List
4+
import asyncio
5+
from contextlib import asynccontextmanager
6+
7+
import click
8+
import uvicorn
9+
10+
from agent import AirbnbAgent
11+
from agent_executor import AirbnbAgentExecutor
12+
from dotenv import load_dotenv
13+
14+
from a2a.server.apps import A2AStarletteApplication
15+
from a2a.server.request_handlers import DefaultRequestHandler
16+
from a2a.types import (
17+
AgentCapabilities,
18+
AgentCard,
19+
AgentSkill,
20+
)
21+
from a2a.server.tasks import InMemoryTaskStore
22+
from langchain_mcp_adapters.client import MultiServerMCPClient
23+
24+
load_dotenv(override=True)
25+
26+
SERVER_CONFIGS = {
27+
"bnb": {
28+
"command": "npx",
29+
"args": ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"],
30+
"transport": "stdio",
31+
},
32+
}
33+
34+
app_context: Dict[str, Any] = {}
35+
36+
37+
@asynccontextmanager
38+
async def app_lifespan(context: Dict[str, Any]):
39+
"""Manages the lifecycle of shared resources like the MCP client and tools."""
40+
print("Lifespan: Initializing MCP client and tools...")
41+
42+
# This variable will hold the MultiServerMCPClient instance
43+
mcp_client_instance: MultiServerMCPClient | None = None
44+
45+
try:
46+
# Following Option 1 from the error message for MultiServerMCPClient initialization:
47+
# 1. client = MultiServerMCPClient(...)
48+
mcp_client_instance = MultiServerMCPClient(SERVER_CONFIGS)
49+
mcp_tools = await mcp_client_instance.get_tools()
50+
context["mcp_tools"] = mcp_tools
51+
52+
tool_count = len(mcp_tools) if mcp_tools else 0
53+
print(f"Lifespan: MCP Tools preloaded successfully ({tool_count} tools found).")
54+
yield # Application runs here
55+
except Exception as e:
56+
print(f"Lifespan: Error during initialization: {e}", file=sys.stderr)
57+
# If an exception occurs, mcp_client_instance might exist and need cleanup.
58+
# The finally block below will handle this.
59+
raise
60+
finally:
61+
print("Lifespan: Shutting down MCP client...")
62+
if (
63+
mcp_client_instance
64+
): # Check if the MultiServerMCPClient instance was created
65+
# The original code called __aexit__ on the MultiServerMCPClient instance
66+
# (which was mcp_client_manager). We assume this is still the correct cleanup method.
67+
if hasattr(mcp_client_instance, "__aexit__"):
68+
try:
69+
print(
70+
f"Lifespan: Calling __aexit__ on {type(mcp_client_instance).__name__} instance..."
71+
)
72+
await mcp_client_instance.__aexit__(None, None, None)
73+
print("Lifespan: MCP Client resources released via __aexit__.")
74+
except Exception as e:
75+
print(
76+
f"Lifespan: Error during MCP client __aexit__: {e}",
77+
file=sys.stderr,
78+
)
79+
else:
80+
# This would be unexpected if only the context manager usage changed.
81+
# Log an error as this could lead to resource leaks.
82+
print(
83+
f"Lifespan: CRITICAL - {type(mcp_client_instance).__name__} instance does not have __aexit__ method for cleanup. Resource leak possible.",
84+
file=sys.stderr,
85+
)
86+
else:
87+
# This case means MultiServerMCPClient() constructor likely failed or was not reached.
88+
print(
89+
"Lifespan: MCP Client instance was not created, no shutdown attempt via __aexit__."
90+
)
91+
92+
# Clear the application context as in the original code.
93+
print("Lifespan: Clearing application context.")
94+
context.clear()
95+
96+
97+
@click.command()
98+
@click.option(
99+
"--host", "host", default="localhost", help="Hostname to bind the server to."
100+
)
101+
@click.option(
102+
"--port", "port", default=10002, type=int, help="Port to bind the server to."
103+
)
104+
@click.option("--log-level", "log_level", default="info", help="Uvicorn log level.")
105+
def cli_main(host: str, port: int, log_level: str):
106+
"""Command Line Interface to start the Airbnb Agent server."""
107+
if not os.getenv("GOOGLE_API_KEY"):
108+
print("GOOGLE_API_KEY environment variable not set.", file=sys.stderr)
109+
sys.exit(1)
110+
111+
async def run_server_async():
112+
async with app_lifespan(app_context):
113+
if not app_context.get("mcp_tools"):
114+
print(
115+
"Warning: MCP tools were not loaded. Agent may not function correctly.",
116+
file=sys.stderr,
117+
)
118+
# Depending on requirements, you could sys.exit(1) here
119+
120+
# Initialize AirbnbAgentExecutor with preloaded tools
121+
airbnb_agent_executor = AirbnbAgentExecutor(
122+
mcp_tools=app_context.get("mcp_tools", [])
123+
)
124+
125+
request_handler = DefaultRequestHandler(
126+
agent_executor=airbnb_agent_executor,
127+
task_store=InMemoryTaskStore(),
128+
)
129+
130+
# Create the A2AServer instance
131+
a2a_server = A2AStarletteApplication(
132+
agent_card=get_agent_card(host, port), http_handler=request_handler
133+
)
134+
135+
# Get the ASGI app from the A2AServer instance
136+
asgi_app = a2a_server.build()
137+
138+
config = uvicorn.Config(
139+
app=asgi_app,
140+
host=host,
141+
port=port,
142+
log_level=log_level.lower(),
143+
lifespan="auto",
144+
)
145+
146+
uvicorn_server = uvicorn.Server(config)
147+
148+
print(
149+
f"Starting Uvicorn server at http://{host}:{port} with log-level {log_level}..."
150+
)
151+
try:
152+
await uvicorn_server.serve()
153+
except KeyboardInterrupt:
154+
print("Server shutdown requested (KeyboardInterrupt).")
155+
finally:
156+
print("Uvicorn server has stopped.")
157+
# The app_lifespan's finally block handles mcp_client shutdown
158+
159+
try:
160+
asyncio.run(run_server_async())
161+
except RuntimeError as e:
162+
if "cannot be called from a running event loop" in str(e):
163+
print(
164+
"Critical Error: Attempted to nest asyncio.run(). This should have been prevented.",
165+
file=sys.stderr,
166+
)
167+
else:
168+
print(f"RuntimeError in cli_main: {e}", file=sys.stderr)
169+
sys.exit(1)
170+
except Exception as e:
171+
print(f"An unexpected error occurred in cli_main: {e}", file=sys.stderr)
172+
sys.exit(1)
173+
174+
175+
def get_agent_card(host: str, port: int):
176+
"""Returns the Agent Card for the Currency Agent."""
177+
capabilities = AgentCapabilities(streaming=True, pushNotifications=True)
178+
skill = AgentSkill(
179+
id="airbnb_search",
180+
name="Search airbnb accommodation",
181+
description="Helps with accommodation search using airbnb",
182+
tags=["airbnb accommodation"],
183+
examples=[
184+
"Please find a room in LA, CA, April 15, 2025, checkout date is april 18, 2 adults"
185+
],
186+
)
187+
return AgentCard(
188+
name="Airbnb Agent",
189+
description="Helps with searching accommodation",
190+
url=f"http://{host}:{port}/",
191+
version="1.0.0",
192+
defaultInputModes=AirbnbAgent.SUPPORTED_CONTENT_TYPES,
193+
defaultOutputModes=AirbnbAgent.SUPPORTED_CONTENT_TYPES,
194+
capabilities=capabilities,
195+
skills=[skill],
196+
)
197+
198+
199+
if __name__ == "__main__":
200+
cli_main()

0 commit comments

Comments
 (0)