Getting Started with MCP & A2A with ADK

1. Overview

AI agents are rapidly growing in popularity, revolutionizing task automation and decision-making with their ability to operate autonomously, learn, and interact with their environment to achieve goals.

This codelab will help you get started by building an end-to-end multi-agent system. You'll build a currency MCP server, build a currency agent that consumes the MCP server and is exposed over the Agent2Agent (A2A) protocol, build a travel agent with an in-process weather subagent that consumes the currency agent over A2A, and finally deploy all services to Cloud Run.

Architecture

Core Technologies

Model Context Protocol (MCP)

Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context and tools to LLMs. Think of MCP like a USB-C port for AI applications: it provides a standardized way to connect AI models to external data sources, APIs, and tools.

In this codelab, the currency_mcp_server exposes a get_exchange_rate tool over Streamable HTTP (/mcp), backed by the public Frankfurter API.

Agent Development Kit (ADK)

Agent Development Kit (ADK) is a flexible orchestration framework for developing and deploying AI agents. ADK is model-agnostic, deployment-agnostic, and built for compatibility with other frameworks. ADK makes agent development feel like standard software engineering, making it easy to create, test, and orchestrate complex agentic workflows.

ADK handles user conversations, serves the interactive Web UI, connects to MCP tools via McpToolset, and manages A2A communications.

Agent2Agent (A2A) Protocol

The Agent2Agent (A2A) Protocol is an open standard designed to enable communication and collaboration between AI agents. Just like how MCP standardizes agent-to-tool connections, A2A standardizes agent-to-agent collaboration! In a world where agents are built across diverse frameworks, hosting environments, and organizations, A2A provides a common language for discovery and task delegation.

ADK exposes currency_agent as an A2A service (to_a2a) and allows travel_agent to consume it remotely as an AgentTool (RemoteA2aAgent).

Local Subagents vs. Remote A2A Agents

This architecture highlights a crucial pattern in multi-agent design:

  • Local Agent Tool (weather_agent): Runs in-process within the orchestrator as an AgentTool, with zero network latency or protocol serialization overhead.
  • Remote Agent Tool (currency_agent): Runs as an independent microservice, called across the network via the A2A protocol.

What you'll learn

  • How to create and test a local MCP server using FastMCP over Streamable HTTP
  • How to build an Agent with ADK that uses MCP tools (McpToolset)
  • How to expose an ADK agent as an A2A server (to_a2a)
  • How to test an A2A server using the A2A Python Client (a2a.client)
  • How to build an orchestrator agent that combines local subagent tools with remote A2A agents
  • How to run and test the complete multi-agent system locally with the ADK Web UI
  • How to containerize and deploy all three components to Google Cloud Run
  • (Optional) How to secure all three Cloud Run services with least-privilege service accounts and OIDC ID token authentication

What you'll need

  • A browser, such as Chrome.
  • A Google Cloud project with billing enabled.

2. Before you begin

Create a project

If you don't have a Google Cloud project, create one.

In the Google Cloud Console, on the project selector page, select or create a Google Cloud project.

Also make sure that billing is enabled for your Cloud project. Learn how to check if billing is enabled on a project.

Activate Cloud Shell

Google Cloud Shell is an interactive, browser-based development environment provided directly within the Google Cloud Console. It's the easiest way to get started with Google Cloud without needing to install tools locally.

Activate Cloud Shell by clicking this link. You can toggle between Cloud Shell Terminal (for running cloud commands) and Editor (for viewing files) by clicking on the corresponding button in Cloud Shell.

Once connected to Cloud Shell, verify that you're authenticated:

gcloud auth list

Confirm that the gcloud command knows about your project:

gcloud config list project

Set your project ID:

export PROJECT_ID=<YOUR_GOOGLE_CLOUD_PROJECT_ID>
gcloud config set project $PROJECT_ID

Enable Cloud APIs

Enable the required APIs for Cloud Run, Cloud Build, Artifact Registry, and Vertex AI:

gcloud services enable run.googleapis.com \
                       cloudbuild.googleapis.com \
                       artifactregistry.googleapis.com \
                       aiplatform.googleapis.com

Get the code

Clone the repository and navigate to the python directory:

git clone https://github.com/meteatamel/getting-started-mcp-a2a-adk.git
cd getting-started-mcp-a2a-adk/python

Install dependencies with uv

uv is a fast Python package and project manager that comes pre-installed in Cloud Shell.

The Python directory is structured as a unified uv

Workspace. Install all dependencies across all workspace members (currency_mcp_server, currency_agent, and travel_agent) into a single virtual environment by running:

uv sync

Configure environment variables

Create a .env file in the python directory configured to use Gemini on Google Cloud:

echo "GOOGLE_GENAI_USE_ENTERPRISE=TRUE" >> .env \
&& echo "GOOGLE_CLOUD_PROJECT=$PROJECT_ID" >> .env \
&& echo "GOOGLE_CLOUD_LOCATION=global" >> .env

3. Create the Currency MCP Server

Before creating your agents, you will first explore the MCP server that exposes the currency conversion tool.

An MCP server lets you write lightweight programs to expose capabilities as tools. An agent (or multiple agents) can then access these tools using the standardized Model Context Protocol.

The FastMCP Python package can be leveraged to create an MCP server that exposes a single tool called get_exchange_rate. The get_exchange_rate tool calls the Frankfurter API to get the current exchange rate between two currencies.

The code for the MCP server is located in currency_mcp_server/server.py:

import asyncio
import logging
import os

import httpx
from fastmcp import FastMCP

logger = logging.getLogger(__name__)
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)

mcp = FastMCP("Currency MCP Server 💵")


@mcp.tool()
def get_exchange_rate(
    currency_from: str = "USD",
    currency_to: str = "EUR",
    currency_date: str = "latest",
):
    """Use this to get current exchange rate.

    Args:
        currency_from: The currency to convert from (e.g., "USD").
        currency_to: The currency to convert to (e.g., "EUR").
        currency_date: The date for the exchange rate or "latest". Defaults to "latest".

    Returns:
        A dictionary containing the exchange rate data, or an error message if the request fails.
    """
    logger.info(
        f"--- 🛠️ Tool: get_exchange_rate called for converting {currency_from} to {currency_to} ---"
    )
    try:
        response = httpx.get(
            f"https://api.frankfurter.dev/v1/{currency_date}",
            params={"from": currency_from, "to": currency_to},
        )
        response.raise_for_status()

        data = response.json()
        if "rates" not in data:
            logger.error(f"❌ rates not found in response: {data}")
            return {"error": "Invalid API response format."}
        logger.info(f"✅ API response: {data}")
        return data
    except httpx.HTTPError as e:
        logger.error(f"❌ API request failed: {e}")
        return {"error": f"API request failed: {e}"}
    except ValueError:
        logger.error("❌ Invalid JSON response from API")
        return {"error": "Invalid JSON response from API."}


if __name__ == "__main__":
    port = int(os.getenv("PORT", 8080))
    logger.info(f"🚀 MCP server started on port {port}")
    # Could also use 'sse' transport, host="0.0.0.0" required for Cloud Run.
    asyncio.run(
        mcp.run_async(
            transport="http",
            host="0.0.0.0",
            port=port,
        )
    )

Start the MCP server locally

Start the MCP server in your terminal:

uv run python currency_mcp_server/server.py

The server starts on port 8080 with the Streamable HTTP endpoint available at /mcp (http://localhost:8080/mcp).

Test the MCP server

Open a second terminal window (leaving the MCP server running in the first), navigate to the python folder (cd getting-started-mcp-a2a-adk/python), and run the test script:

uv run python currency_mcp_server/test_server.py

The test script connects to the MCP endpoint using FastMCP's Client, lists the tools, and invokes get_exchange_rate:

Connecting to MCP server at: http://localhost:8080/mcp
--- 🛠️  Tool found: get_exchange_rate ---
--- 🪛  Calling get_exchange_rate tool for USD to EUR ---
--- ✅  Success: {"amount":1.0,"base":"USD","date":"2026-09-29","rates":{"EUR":0.84919}} ---

Awesome! You have a working MCP server exposing a live currency tool. Keep this terminal open for the next steps.

4. Create the Currency Agent with ADK

Now it is time to create the Currency Agent using Agent Development Kit (ADK).

ADK provides built-in support for MCP tools through the McpToolset class. The Currency Agent connects to our MCP server using StreamableHTTPConnectionParams.

Take a look at the currency agent definition in currency_agent/agent.py:

import logging
import os
from urllib.parse import urlparse

from dotenv import load_dotenv
from google.adk.agents import LlmAgent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams

try:
    from currency_agent.auth import get_mcp_http_client_factory
except ImportError:
    from auth import get_mcp_http_client_factory

logger = logging.getLogger(__name__)
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)

load_dotenv()

SYSTEM_INSTRUCTION = (
    "You are a specialized assistant for currency conversions. "
    "Your sole purpose is to use the 'get_exchange_rate' tool to answer questions about currency exchange rates. "
    "If the user asks about anything other than currency conversion or exchange rates, "
    "politely state that you cannot help with that topic and can only assist with currency-related queries. "
    "Do not attempt to answer unrelated questions or use tools for other purposes."
)

mcp_server_url = os.getenv("MCP_SERVER_URL", "http://localhost:8080/mcp")
if not mcp_server_url.endswith("/mcp"):
    mcp_server_url = f"{mcp_server_url.rstrip('/')}/mcp"

logger.info(f"--- 🔧 Loading MCP tools from MCP Server at {mcp_server_url}... ---")
logger.info("--- 🤖 Creating ADK Currency Agent... ---")

root_agent = LlmAgent(
    model="gemini-3.8-flash",
    name="currency_agent",
    description="An agent that can help with currency conversions",
    instruction=SYSTEM_INSTRUCTION,
    tools=[
        McpToolset(
            connection_params=StreamableHTTPConnectionParams(
                url=mcp_server_url,
                # Uncomment to enable Cloud Run IAM authentication:
                # httpx_client_factory=get_mcp_http_client_factory(mcp_server_url),
            )
        )
    ],
)

Notice how McpToolset automatically discovers and loads the get_exchange_rate tool directly from the running MCP server over HTTP.

Test the Currency Agent with ADK Web UI

You can test the agent in isolation using ADK's development web interface. In your second terminal, run:

uv run adk web --allow_origins "*" currency_agent

Click Web Preview in Cloud Shell (port 8000) to open the ADK Web UI.

ADK Web Currency Agent

Ask the agent:

"What is 250 USD in EUR?"

You'll see the agent invoke the get_exchange_rate MCP tool and return the converted amount. Stop the adk web process using Ctrl+C before proceeding.

5. Agent2Agent (A2A) Protocol

The Agent2Agent (A2A) Protocol is an open standard designed to enable communication and collaboration between AI agents.

In modern multi-agent systems, agents are often built using different frameworks (ADK, LangChain, CrewAI, AutoGen) and deployed on different infrastructure. A2A breaks down these silos by defining a standardized communication contract.

A2A Protocol

Key Concepts of A2A

  • Agent Discovery (Agent Card): Every A2A-compliant agent advertises its capabilities, supported I/O modes, authentication requirements, and skills via a standard JSON file hosted at /.well-known/agent-card.json.
  • Agent Skills (AgentSkill): Specific capabilities or tools that the agent can execute. Other agents read these skills to decide when to delegate tasks.
  • JSON-RPC Communication: Agents exchange structured messages, tasks, and artifacts over standard transports such as HTTP JSON-RPC 2.0.

A2A Agent Card

In the next step, you'll expose our ADK Currency Agent as an A2A server so other agents can discover it and delegate currency conversion tasks to it!

6. Expose the Currency Agent as an A2A Server

ADK makes turning any LlmAgent into an A2A-compatible server effortless with the to_a2a() utility function.

Review the bottom portion of currency_agent/agent.py:

# Make the agent A2A-compatible
PORT = int(os.getenv("PORT", 8081))
AGENT_URL = os.getenv("AGENT_URL")

if AGENT_URL:
    parsed = urlparse(AGENT_URL)
    protocol = parsed.scheme or "https"
    host = parsed.hostname
    port = parsed.port or 443
else:
    protocol = "http"
    host = "localhost"
    port = PORT

a2a_app = to_a2a(root_agent, host=host, port=port, protocol=protocol)

if __name__ == "__main__":
    import uvicorn

    logger.info(f"🚀 Starting currency_agent on port {PORT}")
    uvicorn.run(a2a_app, host="0.0.0.0", port=PORT)

The to_a2a() helper automatically:

  1. Inspects the LlmAgent's tools and instructions.
  2. Generates an A2A AgentCard describing its skills (e.g. currency_agent-get_exchange_rate).
  3. Wraps the agent in a FastAPI application (a2a_app) supporting JSON-RPC endpoints.
  4. Dynamically configures the advertised url in the Agent Card based on AGENT_URL (or localhost).

Start the Currency Agent A2A Server

In your second terminal, start the currency agent:

uv run python currency_agent/agent.py

The server starts on port 8081:

[INFO]: --- 🔧 Loading MCP tools from MCP Server at http://localhost:8080/mcp... ---
[INFO]: --- 🤖 Creating ADK Currency Agent... ---
[INFO]: 🚀 Starting currency_agent on port 8081
INFO:     Started server process
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

Inspect the Agent Card

Open a third terminal in Cloud Shell (or use Web Preview on port 8081) and inspect the Agent Card endpoint (http://localhost:8081/.well-known/agent-card.json):

curl -s http://localhost:8081/.well-known/agent-card.json | jq .

You will see the auto-generated JSON metadata:

{
  "name": "currency_agent",
  "description": "An agent that can help with currency conversions",
  "supportedInterfaces": [
    {
      "url": "http://localhost:8081",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "version": "0.0.1",
  "capabilities": {
    "streaming": true
  },
  "defaultInputModes": [
    "text/plain"
  ],
  "defaultOutputModes": [
    "text/plain"
  ],
  "skills": [
    {
      "id": "currency_agent",
      "name": "model",
      "description": "An agent that can help with currency conversions",
      "tags": [
        "llm"
      ]
    },
    {
      "id": "currency_agent-get_exchange_rate",
      "name": "get_exchange_rate",
      "description": "Use this to get current exchange rate.",
      "tags": [
        "llm",
        "tools"
      ]
    }
  ]
}

Test with the A2A Client

In your third terminal, navigate to the python folder (cd getting-started-mcp-a2a-adk/python) and test the A2A endpoint using the dedicated A2A test client script:

uv run python currency_agent/test_a2aclient.py

The client resolves the agent card using A2ACardResolver and dispatches a JSON-RPC message:

🔄 Fetching the agent card at http://localhost:8081
✅ Successfully fetched the agent card
====================================================
                     AgentCard                      
====================================================
Name        : currency_agent
Description : An agent that can help with currency conversions
Interfaces  : [0] http://localhost:8081  (JSONRPC 1.0)
Skills      : currency_agent, currency_agent-get_exchange_rate
====================================================
🔄 Initializing a non-streaming client
Sending request: "how much is 100 USD in GBP?"
Response:
100 USD is currently equal to approximately 73.37 GBP...

The Currency Agent is now operating as a fully functional A2A server!

7. Build the Travel Agent

Now you will build the orchestrating Travel Agent. This agent showcases how an ADK agent can effortlessly combine two different tool paradigms:

  1. Local Agent Tool (weather_agent): An in-process ADK subagent running in the same Python runtime, avoiding any network or serialization latency.
  2. Remote Agent Tool (currency_agent): An external agent connected over the network via the A2A protocol using RemoteA2aAgent.
                    +-----------------------------+
                    |        travel_agent         |
                    | (Orchestrator with Web UI)  |
                    +--------------+--------------+
                                   |
             +---------------------+---------------------+
             | (In-process call)                         | (A2A Network Hop)
             v                                           v
+-------------------------+                 +-------------------------+
|   weather_agent         |                 |   currency_agent        |
|  (Local AgentTool)      |                 |  (Remote A2A Agent)     |
|  Calls wttr.in API      |                 |  Calls Currency MCP Svc |
+-------------------------+                 +-------------------------+

1. The Local Weather Subagent

Take a look at travel_agent/subagents/weather_agent.py:

import logging
import httpx
from google.adk.agents import LlmAgent

logger = logging.getLogger(__name__)

SYSTEM_INSTRUCTION = (
    "You are a specialized assistant for providing weather forecasts and conditions for travel destinations. "
    "Your sole purpose is to use the 'get_weather' tool to fetch weather information. "
    "Provide clear, concise summaries of current weather conditions, temperatures, and forecasts to help travelers pack and plan."
)


def get_weather(location: str = "London") -> dict:
    """Gets the current weather and forecast for a given location or city."""
    logger.info(f"--- 🌤️ Tool: get_weather called for {location} ---")
    try:
        response = httpx.get(
            f"https://wttr.in/{location}?format=j1",
            headers={"User-Agent": "curl/7.68.0"},
            timeout=8.0,
        )
        response.raise_for_status()
        data = response.json()
        current = data["current_condition"][0]
        return {
            "location": location,
            "condition": current.get("weatherDesc", [{}])[0].get("value", "Unknown"),
            "temperature_celsius": f"{current.get('temp_C', 'N/A')}°C",
            "temperature_fahrenheit": f"{current.get('temp_F', 'N/A')}°F",
            "humidity": f"{current.get('humidity', 'N/A')}%",
            "wind_speed_kmph": f"{current.get('windspeedKmph', 'N/A')} km/h",
        }
    except Exception as e:
        logger.warning(f"⚠️ Weather lookup failed: {e}. Returning simulated estimate.")
        return {
            "location": location,
            "condition": "Partly Cloudy",
            "temperature_celsius": "20°C",
            "temperature_fahrenheit": "68°F",
            "humidity": "55%",
            "wind_speed_kmph": "10 km/h",
            "note": f"Live data unavailable ({e}); showing estimated conditions.",
        }


weather_agent = LlmAgent(
    model="gemini-3.8-flash",
    name="weather_agent",
    description="A local specialist agent that provides current weather reports and forecasts for travel destinations.",
    instruction=SYSTEM_INSTRUCTION,
    tools=[get_weather],
)

The weather agent is an LlmAgent with a function tool that fetches real-time weather from wttr.in with a resilient fallback.

2. The Travel Agent Orchestrator

Now look at travel_agent/agent.py:

import logging
import os

from dotenv import load_dotenv
from google.adk.agents import LlmAgent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH, RemoteA2aAgent
from google.adk.tools.agent_tool import AgentTool

try:
    from travel_agent.subagents.weather_agent import weather_agent
    from travel_agent.auth import get_a2a_http_client
except ImportError:
    from subagents.weather_agent import weather_agent
    from auth import get_a2a_http_client

logger = logging.getLogger(__name__)
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)

load_dotenv()

SYSTEM_INSTRUCTION = (
    "You are a helpful travel assistant. You help users plan trips, recommend places, "
    "and answer travel-related questions.\n\n"
    "- Whenever a user asks about currency exchange rates or money conversions, "
    "delegate the request to the 'currency_agent' tool.\n"
    "- Whenever a user asks about the weather, climate, or forecasts for a destination, "
    "delegate the request to the 'weather_agent' tool."
)

CURRENCY_AGENT_URL = os.getenv("CURRENCY_AGENT_URL", "http://localhost:8081").rstrip("/")

logger.info(
    "--- 🔗 Connecting to Remote A2A Currency Agent at %s... ---",
    CURRENCY_AGENT_URL,
)

currency_remote_agent = RemoteA2aAgent(
    name="currency_agent",
    agent_card=f"{CURRENCY_AGENT_URL}{AGENT_CARD_WELL_KNOWN_PATH}",
    description="An agent that can help with currency conversions and exchange rates.",
    # Uncomment to enable Cloud Run IAM authentication:
    # httpx_client=get_a2a_http_client(CURRENCY_AGENT_URL),
)

logger.info("--- 🤖 Creating ADK Travel Agent... ---")

root_agent = LlmAgent(
    model="gemini-3.8-flash",
    name="travel_agent",
    description="A travel assistant that can help plan trips, check weather forecasts via the local weather agent, and convert currencies via the remote currency agent.",
    instruction=SYSTEM_INSTRUCTION,
    tools=[
        AgentTool(agent=currency_remote_agent),
        AgentTool(agent=weather_agent),
    ],
)

if __name__ == "__main__":
    import uvicorn
    from google.adk.cli.fast_api import get_fast_api_app

    PORT = int(os.getenv("PORT", 8082))
    logger.info(f"🚀 Starting travel_agent Web UI on port {PORT}")
    app = get_fast_api_app(web=True, allow_origins=["*"], agents_dir=os.path.dirname(__file__) or ".")
    uvicorn.run(app, host="0.0.0.0", port=PORT)

Key highlights:

  • RemoteA2aAgent fetches the Currency Agent's card from http://localhost:8081/.well-known/agent-card.json.
  • Both currency_remote_agent and weather_agent are wrapped using ADK's AgentTool.
  • Running travel_agent/agent.py directly boots the ADK Web UI on port 8082 using get_fast_api_app(web=True).

8. Run and Test the Multi-Agent System Locally

You now have all three components ready! Let's run and test the complete system locally in Cloud Shell.

Running all three services

Ensure the services are running across three separate terminal tabs in Cloud Shell (from the getting-started-mcp-a2a-adk/python directory) in the following order:

Terminal 1 (Currency MCP Server):

uv run python currency_mcp_server/server.py

(Running on port 8080)

Terminal 2 (Currency A2A Agent):

uv run python currency_agent/agent.py

(Running on port 8081)

Terminal 3 (Travel Agent with Web UI):

uv run python travel_agent/agent.py

(Running on port 8082)

Test via the ADK Web UI

Click Web Preview in Cloud Shell and select Change port to preview port 8082.

Ask the Travel Agent a compound question requiring both tools:

"I'm planning a trip to London. What is the weather like and how much is 200 EUR in GBP?"

Watch the execution flow:

  1. The Travel Agent receives the request in the Web UI.
  2. It delegates the weather question in-process to the local weather_agent, which fetches current conditions from wttr.in.
  3. It delegates the currency conversion over the A2A protocol to currency_agent on port 8081.
  4. The currency_agent calls the currency_mcp_server on port 8080 over MCP to fetch the exchange rate.
  5. All results are synthesized and returned to you in a unified, helpful answer!

ADK Web Remote Currency Agent

Test via the Command Line

You can also test queries directly from the CLI without opening Web Preview:

uv run adk run travel_agent "What is the weather in London and how much is 100 USD in EUR?"

Now that everything works seamlessly in Cloud Shell, let's deploy the entire system to Google Cloud Run!

9. Deploy the Multi-Agent System to Cloud Run

Each component has its own Dockerfile and self-contained pyproject.toml so it can be built and deployed independently to Cloud Run.

Using gcloud, you will deploy each service and automatically wire their URLs together.

Set environment variables

Set your Cloud project ID and target region:

export PROJECT_ID=<YOUR_GOOGLE_CLOUD_PROJECT_ID>
export REGION=europe-west1

Step 1: Deploy the Currency MCP Server

Deploy the MCP server directly from source:

# 1. Deploy MCP server
gcloud run deploy currency-mcp-server \
  --source currency_mcp_server \
  --region $REGION \
  --allow-unauthenticated

# 2. Capture the deployed MCP server URL
MCP_SERVER_URL=$(gcloud run services describe currency-mcp-server --region $REGION --format='value(status.url)')/mcp
echo "MCP Server URL: $MCP_SERVER_URL"

Step 2: Deploy the Currency Agent

Since currency-agent's public URL is assigned on its first deployment, deploy the service first, capture its URL, and update its AGENT_URL environment variable so its A2A Agent Card advertises the correct public HTTPS address:

# 1. Deploy Currency Agent connected to the deployed MCP Server
gcloud run deploy currency-agent \
  --source currency_agent \
  --region $REGION \
  --allow-unauthenticated \
  --update-env-vars MCP_SERVER_URL="$MCP_SERVER_URL",GOOGLE_GENAI_USE_ENTERPRISE="true",GOOGLE_CLOUD_PROJECT="$PROJECT_ID",GOOGLE_CLOUD_LOCATION="global"

# 2. Capture its assigned Cloud Run URL
CURRENCY_AGENT_URL=$(gcloud run services describe currency-agent --region $REGION --format='value(status.url)')
echo "Currency Agent URL: $CURRENCY_AGENT_URL"

# 3. Update AGENT_URL so the agent advertises its public HTTPS endpoint in its Agent Card
gcloud run services update currency-agent \
  --region $REGION \
  --update-env-vars AGENT_URL="$CURRENCY_AGENT_URL"

Step 3: Deploy the Travel Agent

Now deploy the Travel Agent, configuring it with the CURRENCY_AGENT_URL:

# 1. Deploy Travel Agent (with ADK Web UI) connected to Currency Agent
gcloud run deploy travel-agent \
  --source travel_agent \
  --region $REGION \
  --allow-unauthenticated \
  --update-env-vars CURRENCY_AGENT_URL="$CURRENCY_AGENT_URL",GOOGLE_GENAI_USE_ENTERPRISE="true",GOOGLE_CLOUD_PROJECT="$PROJECT_ID",GOOGLE_CLOUD_LOCATION="global"

# 2. Capture its assigned Cloud Run URL
TRAVEL_AGENT_URL=$(gcloud run services describe travel-agent --region $REGION --format='value(status.url)')
echo "Travel Agent URL: $TRAVEL_AGENT_URL"

10. Test the Multi-Agent System in Cloud Run

With all three microservices deployed, let's verify each layer in Cloud Run.

1. Test the remote MCP server

Test the deployed Cloud Run MCP server from Cloud Shell:

MCP_SERVER_URL="$MCP_SERVER_URL" uv run python currency_mcp_server/test_server.py

You should see the tool listed and successfully invoked on Cloud Run.

2. Test the Currency Agent over A2A

Test the deployed Currency Agent using the A2A test client:

AGENT_URL="$CURRENCY_AGENT_URL" uv run python currency_agent/test_a2aclient.py

The client will fetch the remote Agent Card from Cloud Run and exchange messages over HTTPS JSON-RPC.

3. Test the Travel Agent Web UI in Cloud Run

Open $TRAVEL_AGENT_URL in your browser!

The complete ADK Web UI is served directly from Cloud Run. Try asking:

"I'm planning a trip to Tokyo. What is the weather like and how much is 500 USD in JPY?"

[Browser] 
   │
   ▼ (HTTPS)
[Cloud Run: travel-agent]
   ├──> Local in-process tool: weather_agent (wttr.in)
   └──> A2A JSON-RPC (HTTPS)
           │
           ▼
        [Cloud Run: currency-agent]
           └──> MCP Streamable HTTP (HTTPS)
                   │
                   ▼
                [Cloud Run: currency-mcp-server] (Frankfurter API)

All three services scale independently on serverless infrastructure, communicating seamlessly via open protocols!

11. (Optional) Secure All Cloud Run Services with IAM

So far, all three Cloud Run services were deployed with --allow-unauthenticated for quick testing. In a production multi-agent architecture, internal MCP servers and A2A agents should never be publicly accessible—each service should run under its own least-privilege identity and authenticate downstream calls using Google-signed OpenID Connect (OIDC) ID tokens.

In this optional step, you will:

  1. Create dedicated, least-privilege Service Accounts for each service.
  2. Uncomment OIDC ID token authentication in currency_agent and travel_agent using Cloud Shell Editor.
  3. Lock down all three Cloud Run services (--no-allow-unauthenticated) with hop-by-hop roles/run.invoker IAM permissions.
  4. Test the locked-down multi-agent system from Cloud Shell and Web Preview.
[Cloud Shell / Web Preview]
   │  gcloud run services proxy / gcloud auth print-identity-token
   │  IAM: Your Google Cloud user -> roles/run.invoker on travel-agent
   ▼
[Cloud Run: travel-agent]  (Identity: travel-agent-sa + roles/aiplatform.user)
   │  Authorization: Bearer <OIDC ID Token for currency-agent>
   │  IAM: travel-agent-sa -> roles/run.invoker on currency-agent
   ▼
[Cloud Run: currency-agent]  (Identity: currency-agent-sa + roles/aiplatform.user)
   │  Authorization: Bearer <OIDC ID Token for currency-mcp-server>
   │  IAM: currency-agent-sa -> roles/run.invoker on currency-mcp-server
   ▼
[Cloud Run: currency-mcp-server]  (Identity: currency-mcp-sa, zero project roles)

Step 1: Create Least-Privilege Service Accounts

Instead of sharing the default Compute Engine service account, create a dedicated service account for each microservice:

gcloud iam service-accounts create currency-mcp-sa \
  --display-name="Currency MCP Server SA"

gcloud iam service-accounts create currency-agent-sa \
  --display-name="Currency Agent SA"

gcloud iam service-accounts create travel-agent-sa \
  --display-name="Travel Agent SA"

Grant roles/aiplatform.user only to currency-agent-sa and travel-agent-sa so they can call Gemini on Vertex AI (currency-mcp-sa only calls the public Frankfurter API and needs zero project roles):

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:currency-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:travel-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

Step 2: Inspect auth.py and Uncomment OIDC Authentication

The repository includes a small auth.py helper inside both currency_agent/ and travel_agent/. Open Cloud Shell Editor and take a look at python/currency_agent/auth.py:

class GoogleIdTokenAuth(httpx.Auth):
    """Attaches a Google-signed OIDC ID token to outgoing Cloud Run HTTP requests."""

    def __init__(self, target_url: str):
        parsed = urlparse(target_url)
        # Cloud Run OIDC audience must be the root service origin (without /mcp)
        self.audience = f"{parsed.scheme}://{parsed.netloc}"
        self._creds = None

    def auth_flow(self, request: httpx.Request):
        if self._creds is None:
            self._creds = id_token.fetch_id_token_credentials(self.audience)
        if not self._creds.valid:
            self._creds.refresh(Request())
        request.headers["Authorization"] = f"Bearer {self._creds.token}"
        yield request

Notice three key details:

  • Automatic token refresh: Cloud Run OIDC ID tokens expire after 1 hour. Using an httpx.Auth flow refreshes expired tokens automatically per HTTP request without dropping the active MCP or A2A client session.
  • Root URL audience: Even though MCP_SERVER_URL ends with /mcp, Cloud Run requires the OIDC token audience to be the root service origin (https://currency-mcp-server-...run.app).
  • Local-dev friendly: get_mcp_http_client_factory() and get_a2a_http_client() only attach OIDC auth for https:// URLs, so http://localhost development continues working unchanged.

1. Uncomment MCP auth in currency_agent/agent.py

In Cloud Shell Editor, open python/currency_agent/agent.py and uncomment the httpx_client_factory line inside StreamableHTTPConnectionParams:

    tools=[
        McpToolset(
            connection_params=StreamableHTTPConnectionParams(
                url=mcp_server_url,
                # Uncomment to enable Cloud Run IAM authentication:
                httpx_client_factory=get_mcp_http_client_factory(mcp_server_url),
            )
        )
    ],

2. Uncomment A2A auth in travel_agent/agent.py

Next, open python/travel_agent/agent.py and uncomment the httpx_client line inside RemoteA2aAgent (which uses that client for both fetching /.well-known/agent-card.json and sending JSON-RPC messages):

currency_remote_agent = RemoteA2aAgent(
    name="currency_agent",
    agent_card=f"{CURRENCY_AGENT_URL}{AGENT_CARD_WELL_KNOWN_PATH}",
    description="An agent that can help with currency conversions and exchange rates.",
    # Uncomment to enable Cloud Run IAM authentication:
    httpx_client=get_a2a_http_client(CURRENCY_AGENT_URL),
)

Step 3: Lock Down All Three Cloud Run Services & Grant Invoker Roles

Switch back to the Cloud Shell Terminal (cd ~/getting-started-mcp-a2a-adk/python) to lock down all three services.

1. Lock down currency-mcp-server

Since currency_mcp_server has no code changes, update its service account and remove public allUsers access in place, then grant currency-agent-sa permission to invoke it:

# Attach dedicated service account
gcloud run services update currency-mcp-server \
  --region $REGION \
  --service-account="currency-mcp-sa@$PROJECT_ID.iam.gserviceaccount.com"

# Remove public unauthenticated access
gcloud run services remove-iam-policy-binding currency-mcp-server \
  --region $REGION \
  --member="allUsers" \
  --role="roles/run.invoker"

# Allow currency-agent to invoke currency-mcp-server
gcloud run services add-iam-policy-binding currency-mcp-server \
  --region $REGION \
  --member="serviceAccount:currency-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

2. Redeploy and lock down currency-agent

Redeploy currency-agent with your uncommented auth line, --no-allow-unauthenticated, and currency-agent-sa, then grant travel-agent-sa permission to invoke it:

gcloud run deploy currency-agent \
  --source currency_agent \
  --region $REGION \
  --no-allow-unauthenticated \
  --service-account="currency-agent-sa@$PROJECT_ID.iam.gserviceaccount.com"

# Allow travel-agent to invoke currency-agent
gcloud run services add-iam-policy-binding currency-agent \
  --region $REGION \
  --member="serviceAccount:travel-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

3. Redeploy and lock down travel-agent

Redeploy travel-agent with your uncommented auth line, --no-allow-unauthenticated, and travel-agent-sa:

gcloud run deploy travel-agent \
  --source travel_agent \
  --region $REGION \
  --no-allow-unauthenticated \
  --service-account="travel-agent-sa@$PROJECT_ID.iam.gserviceaccount.com"

Finally, grant your Google Cloud user account roles/run.invoker so you can test the services from Cloud Shell:

USER_EMAIL=$(gcloud config get-value account)

for SERVICE in currency-mcp-server currency-agent travel-agent; do
  gcloud run services add-iam-policy-binding $SERVICE \
    --region $REGION \
    --member="user:$USER_EMAIL" \
    --role="roles/run.invoker"
done

In the Cloud Run Console, you can see that all three services now show Require authentication:

Cloud Run services requiring authentication

Step 4: Verify Unauthenticated Requests Are Blocked

Confirm that calling the services without an identity token is now rejected.

  1. Check the Agent Card endpoint with curl:
curl -i "$CURRENCY_AGENT_URL/.well-known/agent-card.json"

You should see an HTTP 403 Forbidden response from Google Frontend rejecting the unauthenticated call.

  1. Run test_server.py without ID_TOKEN:
MCP_SERVER_URL="$MCP_SERVER_URL" uv run python currency_mcp_server/test_server.py

Without an identity token, the MCP client connection fails:

Connecting to MCP server at: https://currency-mcp-server-...run.app/mcp
...
mcp.shared.exceptions.MCPError: Server returned an error response
  1. Run test_a2aclient.py without ID_TOKEN:
AGENT_URL="$CURRENCY_AGENT_URL" uv run python currency_agent/test_a2aclient.py

Without an identity token, fetching the Agent Card fails with HTTP 403:

🔄 Fetching the agent card at https://currency-agent-...run.app
--- ❌ An error occurred: Failed to fetch agent card from https://currency-agent-...run.app/.well-known/agent-card.json (HTTP 403): Client error '403 Forbidden' for url 'https://currency-agent-...run.app/.well-known/agent-card.json'

Step 5: Test Authenticated Access End-to-End

1. Test the MCP server and A2A agent from Cloud Shell

Both test scripts read an optional ID_TOKEN environment variable. Generate an identity token with gcloud and test both services:

ID_TOKEN=$(gcloud auth print-identity-token) \
  MCP_SERVER_URL="$MCP_SERVER_URL" \
  uv run python currency_mcp_server/test_server.py
ID_TOKEN=$(gcloud auth print-identity-token) \
  AGENT_URL="$CURRENCY_AGENT_URL" \
  uv run python currency_agent/test_a2aclient.py

Both tests succeed—and behind the scenes, currency-agent now authenticates to currency-mcp-server automatically using currency-agent-sa's OIDC ID token!

2. Access the Authenticated Travel Agent Web UI

Because web browsers do not automatically attach Cloud Run IAM bearer tokens, start the Cloud Run authentication proxy in Cloud Shell:

gcloud run services proxy travel-agent --region $REGION --port 8080

Click Web Preview in Cloud Shell, select Preview on port 8080, and ask:

"I'm planning a trip to Tokyo. What is the weather like and how much is 500 USD in JPY?"

Your request now flows through the authenticated proxy to travel-agent, across an IAM-authenticated A2A call to currency-agent, and down to an IAM-authenticated MCP call on currency-mcp-server! Press Ctrl+C in Cloud Shell when finished to stop the proxy.

12. Congratulations & Clean Up

What you've accomplished

Congratulations! You have built and deployed a production-grade multi-agent system:

  • Model Context Protocol (MCP): Created a FastMCP server exposing live API tools over Streamable HTTP.
  • Agent Development Kit (ADK): Built modular agents using LlmAgent, connected MCP tools with McpToolset, and hosted the ADK Web UI.
  • Agent2Agent (A2A) Protocol: Exposed an agent as an A2A service (to_a2a), published Agent Cards (agent-card.json), and consumed remote agents using RemoteA2aAgent.
  • Multi-Agent Architecture: Combined local in-process subagents (weather_agent) with distributed remote agents (currency_agent).
  • Cloud Run Serverless Deployment & IAM Security: Containerized and deployed all three services with automated configuration wiring, and optionally locked them down with least-privilege service accounts and OIDC ID token authentication.

For the full code (including both the Python and Java implementations) and future updates, visit the getting-started-mcp-a2a-adk repository.

Clean up

To avoid incurring charges to your Google Cloud account for the Cloud Run services created in this codelab:

Delete the deployed Cloud Run services:

gcloud run services delete currency-mcp-server --region $REGION --quiet
gcloud run services delete currency-agent --region $REGION --quiet
gcloud run services delete travel-agent --region $REGION --quiet

If you completed the optional IAM authentication step, also delete the custom service accounts:

gcloud iam service-accounts delete currency-mcp-sa@$PROJECT_ID.iam.gserviceaccount.com --quiet
gcloud iam service-accounts delete currency-agent-sa@$PROJECT_ID.iam.gserviceaccount.com --quiet
gcloud iam service-accounts delete travel-agent-sa@$PROJECT_ID.iam.gserviceaccount.com --quiet

Alternatively, if you created a dedicated project for this codelab, you can shut down the entire project:

gcloud projects delete $PROJECT_ID