Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,56 @@ ARequest
ARun
AServer
AServers
ASGI
AStarlette
EUR
GBP
INR
JPY
JSONRPCt
Llm
Nominatim
WEA
aconnect
adk
agentic
ainvoke
airbnb
asgi
autouse
bnb
cla
cls
coc
codegen
codelabs
coro
datamodel
dunders
fastmcp
genai
geolocator
gle
gridpoint
htmlcov
inmemory
ipynb
kwarg
langgraph
lifecycles
linting
llm
mcp
nominatim
nosetests
npx
oauthoidc
openbnb
opensource
pyversions
socio
sse
tagwords
uvx
vulnz
webassets
1 change: 1 addition & 0 deletions examples/a2a-adk-app/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
91 changes: 91 additions & 0 deletions examples/a2a-adk-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Build Agents using A2A SDK
----
> *⚠️ DISCLAIMER: THIS DEMO IS INTENDED FOR DEMONSTRATION PURPOSES ONLY. IT IS NOT INTENDED FOR USE IN A PRODUCTION ENVIRONMENT.*

> *⚠️ Important: A2A is a work in progress (WIP) thus, in the near future there might be changes that are different from what demonstrated here.*
----

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.

### Architecture

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.

![architecture](assets/A2A_multi_agent.png)

### App UI
![screenshot](assets/screenshot.png)


## Setup and Deployment

### Prerequisites

Before running the application locally, ensure you have the following installed:

1. **Node.js:** Required to run the Airbnb MCP server (if testing its functionality locally).
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/)
3. **python 3.13** Python 3.13 is required to run a2a-sdk
4. **set up .env**


- create .env file in `airbnb_agent` and `weather_agent`folder with the following content
```bash
GOOGLE_API_KEY="your_api_key_here"
```

- create .env file in `host_agent/adk_agent`folder with the following content:

```bash
GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT="your project"
GOOGLE_CLOUD_LOCATION=us-central1
AIR_AGENT_URL=http://localhost:10002
WEA_AGENT_URL=http://localhost:10001
```

## Install SDK
Go to `a2a-adk-app` folder in terminal:
```bash
uv sync
```


## 1. Run Airbnb server

Run Remote server

```bash
cd airbnb_agent
uv run .
```

## 2. Run Weather server
Open a new terminal, go to `a2a-adk-app` folder run the server

```bash
cd weather_agent
uv run .
```

## 3. Run Host Agent
Open a new terminal, go to `a2a-adk-app` folder run the server

```bash
cd host_agent
uv run app.py
```


## 4. Test at the UI

Here are example questions:

- "Tell me about weather in LA, CA"

- "Please find a room in LA, CA, June 20-25, 2025, two adults"

## References
- https://github.com/google/a2a-python
- https://codelabs.developers.google.com/intro-a2a-purchasing-concierge#1
- https://google.github.io/adk-docs/
6 changes: 6 additions & 0 deletions examples/a2a-adk-app/airbnb_agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
GOOGLE_API_KEY="your key"
GOOGLE_GENAI_USE_VERTEXAI=True

# Vertex AI backend config
GOOGLE_CLOUD_PROJECT="your project id"
GOOGLE_CLOUD_LOCATION="us-central1"
13 changes: 13 additions & 0 deletions examples/a2a-adk-app/airbnb_agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Remote agent built by LangGraph

## Getting started

1. Create an environment file with your API key:
```bash
echo "GOOGLE_API_KEY=your_api_key_here" > .env
```

2. Start the server
```bash
uv run .
```
200 changes: 200 additions & 0 deletions examples/a2a-adk-app/airbnb_agent/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import os
import sys
from typing import Dict, Any, List
import asyncio
from contextlib import asynccontextmanager

import click
import uvicorn

from agent import AirbnbAgent
from agent_executor import AirbnbAgentExecutor
from dotenv import load_dotenv

from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentSkill,
)
from a2a.server.tasks import InMemoryTaskStore
from langchain_mcp_adapters.client import MultiServerMCPClient

load_dotenv(override=True)

SERVER_CONFIGS = {
"bnb": {
"command": "npx",
"args": ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"],
"transport": "stdio",
},
}

app_context: Dict[str, Any] = {}


@asynccontextmanager
async def app_lifespan(context: Dict[str, Any]):
"""Manages the lifecycle of shared resources like the MCP client and tools."""
print("Lifespan: Initializing MCP client and tools...")

# This variable will hold the MultiServerMCPClient instance
mcp_client_instance: MultiServerMCPClient | None = None

try:
# Following Option 1 from the error message for MultiServerMCPClient initialization:
# 1. client = MultiServerMCPClient(...)
mcp_client_instance = MultiServerMCPClient(SERVER_CONFIGS)
mcp_tools = await mcp_client_instance.get_tools()
context["mcp_tools"] = mcp_tools

tool_count = len(mcp_tools) if mcp_tools else 0
print(f"Lifespan: MCP Tools preloaded successfully ({tool_count} tools found).")
yield # Application runs here
except Exception as e:
print(f"Lifespan: Error during initialization: {e}", file=sys.stderr)
# If an exception occurs, mcp_client_instance might exist and need cleanup.
# The finally block below will handle this.
raise
finally:
print("Lifespan: Shutting down MCP client...")
if (
mcp_client_instance
): # Check if the MultiServerMCPClient instance was created
# The original code called __aexit__ on the MultiServerMCPClient instance
# (which was mcp_client_manager). We assume this is still the correct cleanup method.
if hasattr(mcp_client_instance, "__aexit__"):
try:
print(
f"Lifespan: Calling __aexit__ on {type(mcp_client_instance).__name__} instance..."
)
await mcp_client_instance.__aexit__(None, None, None)
print("Lifespan: MCP Client resources released via __aexit__.")
except Exception as e:
print(
f"Lifespan: Error during MCP client __aexit__: {e}",
file=sys.stderr,
)
else:
# This would be unexpected if only the context manager usage changed.
# Log an error as this could lead to resource leaks.
print(
f"Lifespan: CRITICAL - {type(mcp_client_instance).__name__} instance does not have __aexit__ method for cleanup. Resource leak possible.",
file=sys.stderr,
)
else:
# This case means MultiServerMCPClient() constructor likely failed or was not reached.
print(
"Lifespan: MCP Client instance was not created, no shutdown attempt via __aexit__."
)

# Clear the application context as in the original code.
print("Lifespan: Clearing application context.")
context.clear()


@click.command()
@click.option(
"--host", "host", default="localhost", help="Hostname to bind the server to."
)
@click.option(
"--port", "port", default=10002, type=int, help="Port to bind the server to."
)
@click.option("--log-level", "log_level", default="info", help="Uvicorn log level.")
def cli_main(host: str, port: int, log_level: str):
"""Command Line Interface to start the Airbnb Agent server."""
if not os.getenv("GOOGLE_API_KEY"):
print("GOOGLE_API_KEY environment variable not set.", file=sys.stderr)
sys.exit(1)

async def run_server_async():
async with app_lifespan(app_context):
if not app_context.get("mcp_tools"):
print(
"Warning: MCP tools were not loaded. Agent may not function correctly.",
file=sys.stderr,
)
# Depending on requirements, you could sys.exit(1) here

# Initialize AirbnbAgentExecutor with preloaded tools
airbnb_agent_executor = AirbnbAgentExecutor(
mcp_tools=app_context.get("mcp_tools", [])
)

request_handler = DefaultRequestHandler(
agent_executor=airbnb_agent_executor,
task_store=InMemoryTaskStore(),
)

# Create the A2AServer instance
a2a_server = A2AStarletteApplication(
agent_card=get_agent_card(host, port), http_handler=request_handler
)

# Get the ASGI app from the A2AServer instance
asgi_app = a2a_server.build()

config = uvicorn.Config(
app=asgi_app,
host=host,
port=port,
log_level=log_level.lower(),
lifespan="auto",
)

uvicorn_server = uvicorn.Server(config)

print(
f"Starting Uvicorn server at http://{host}:{port} with log-level {log_level}..."
)
try:
await uvicorn_server.serve()
except KeyboardInterrupt:
print("Server shutdown requested (KeyboardInterrupt).")
finally:
print("Uvicorn server has stopped.")
# The app_lifespan's finally block handles mcp_client shutdown

try:
asyncio.run(run_server_async())
except RuntimeError as e:
if "cannot be called from a running event loop" in str(e):
print(
"Critical Error: Attempted to nest asyncio.run(). This should have been prevented.",
file=sys.stderr,
)
else:
print(f"RuntimeError in cli_main: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred in cli_main: {e}", file=sys.stderr)
sys.exit(1)


def get_agent_card(host: str, port: int):
"""Returns the Agent Card for the Currency Agent."""
capabilities = AgentCapabilities(streaming=True, pushNotifications=True)
skill = AgentSkill(
id="airbnb_search",
name="Search airbnb accommodation",
description="Helps with accommodation search using airbnb",
tags=["airbnb accommodation"],
examples=[
"Please find a room in LA, CA, April 15, 2025, checkout date is april 18, 2 adults"
],
)
return AgentCard(
name="Airbnb Agent",
description="Helps with searching accommodation",
url=f"http://{host}:{port}/",
version="1.0.0",
defaultInputModes=AirbnbAgent.SUPPORTED_CONTENT_TYPES,
defaultOutputModes=AirbnbAgent.SUPPORTED_CONTENT_TYPES,
capabilities=capabilities,
skills=[skill],
)


if __name__ == "__main__":
cli_main()
Loading