Multi-Agent Media Pipeline with ADK

Build a 3-agent RAG system with Google ADK, ChromaDB, and MCP.

Introduction

30 Second Summary

Every streaming platform needs a way to help content managers discover, tag, and recommend the right shows to the right audiences. Behind the scenes, this involves searching through catalogs, generating metadata, and coordinating multiple specialized systems.

In this project, you will build a multi-agent media content discovery pipeline using Google's Agent Development Kit (ADK). Three AI agents will collaborate to search a vector database, enrich content with marketing metadata, and expose catalog tools through the Model Context Protocol (MCP).

What You'll Build

A media content discovery system where a coordinator agent delegates tasks to specialized retrieval and enrichment agents, backed by a ChromaDB vector database and a custom FastMCP server.

By the end of this project, you'll have:

  • A ChromaDB vector database loaded with a media catalog that supports semantic search using RAG.
  • A 3-agent hierarchical system built with Google ADK that uses LLM-driven delegation to coordinate retrieval and enrichment tasks.
  • A custom MCP server exposing media catalog tools, connected to your agents via McpToolset.
  • Full observability into agent decisions, tool calls, and token usage through ADK's Dev UI.
  • A secret mission where you add API key authentication to your MCP server for secure agentic workflows.

Are there any prerequisites?

You need basic Python familiarity. No prior experience with ADK, MCP, or vector databases is required. The project uses a free Gemini API key from Google AI Studio, so no GCP billing is needed.

Set Up Your Development Environment

You are about to build a multi-agent system using Google ADK. Your agents will search a ChromaDB vector database for media content and expose tools through an MCP server.

Before writing any code, you need the right tools and libraries in place. This step installs everything your project depends on.

In this step, get ready to:
  • Install Node.js for MCP server support.
  • Create a Python virtual environment and install project dependencies.
  • Get a free Gemini API key from Google AI Studio.
Install Node.js

Some MCP servers run using npx, a tool that comes bundled with Node.js. Installing Node.js now prepares your environment for the MCP integration you will build later.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check if Node.js is already installed by running this command:
node -v

I see a version number

Node.js is ready. Skip ahead to the next substep.

Command not found

No problem. Install Node.js now using the instructions for your operating system.

macOS

  • Install Node.js using Homebrew by running this command:
brew install node
Seeing brew: command not found?

You need to install Homebrew first. Visit brew.sh and follow the installation instructions, then re-run the command above.

Windows

  • Open your browser and go to nodejs.org/en/download.
  • Download the Windows installer (.msi) for the LTS version.
  • Run the downloaded installer and accept the default settings at each prompt.
  • Close and reopen your PowerShell window after the install completes.
  • Verify the installation by running these commands:
node -v
npm -v

You should see version numbers for both Node.js and npm printed in the terminal.

Still not seeing a version number?

Make sure you closed and reopened your terminal after installing. The terminal needs to reload its PATH to find newly installed programs.

help me install Node.js on my machine

Create your project and install dependencies

Your project needs a dedicated folder and a Python virtual environment to keep its dependencies isolated. You will also install the two core packages: google-adk for the agent framework and chromadb for the vector database.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir media-content-pipeline
cd media-content-pipeline
  • Confirm you are inside the new folder by running:
pwd

You should see a path ending in media-content-pipeline.

  • Create a Python virtual environment by running this command:
python3 -m venv .venv
  • Activate the virtual environment by running the command for your operating system:

macOS

source .venv/bin/activate

This command also works on Linux.

Windows

.venv\Scripts\Activate.ps1

You should see (.venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Not seeing (.venv) in your prompt?

On Windows, PowerShell may block scripts by default. Run this command to allow scripts, then try the activation command again:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

On macOS, make sure you used python3 (not python) when creating the virtual environment. The .venv folder must exist before you can activate it.

help me activate my Python virtual environment

  • Install the project dependencies by running this command:
pip install google-adk==1.31.1 chromadb==1.5.8

What are these packages?

  • google-adk is Google's Agent Development Kit. It provides the framework for building, orchestrating, and running multi-agent AI systems.
  • chromadb is a vector database that stores data as embeddings and lets you search by meaning instead of exact keywords.
  • The mcp package (for building MCP servers) is included automatically as a dependency of google-adk.

You should see Successfully installed messages in the terminal output. The install may take a minute since it downloads several dependencies.

Install failing or timing out?

  • Make sure your virtual environment is active. You should see (.venv) in your prompt. If not, run the activation command from the previous step.
  • Check your internet connection. Both packages are downloaded from PyPI.
  • If you see a Python version error, confirm you are using Python 3.10 or later by running python3 --version.

help me fix my pip install error

Get your Gemini API key

Your agents need an API key to communicate with Google's Gemini language model. Google AI Studio provides free API keys with no billing or GCP project required.

  • Open your browser and go to aistudio.google.com.
  • Sign in with any Google account.
  • Click Get API key in the left sidebar.
  • Click Create API key.
  • If prompted to choose a Google Cloud project, select Create API key in new project.
  • Copy the generated API key and save it somewhere safe. Never share it publicly or commit it to a repository.

Why Google AI Studio instead of Vertex AI?

Google AI Studio gives you a free Gemini API key instantly. No billing account, no GCP project setup, and no credit card needed. This is how Google FDEs prototype locally before deploying to GCP.

Having trouble creating an API key?

Make sure you are signed in with a Google account. Any personal Google account works. If you do not see Get API key in the sidebar, try refreshing the page.

help me create a Gemini API key in Google AI Studio

Your environment is ready. Next, you will build the media content catalog and load it into a ChromaDB vector database so your agents have something to search.

Build the Media Content Vector Database

Your development environment is ready with Python, Node.js, and the packages you need. Now it's time to build the data layer that powers your entire agent pipeline.

In this step, you'll create a media catalog with 10 fictional titles and load it into a ChromaDB vector database. ChromaDB converts each title's text into a numerical representation called an embedding. These embeddings let you search by meaning instead of exact keywords.

In this step, get ready to:
  • Create a media catalog with 10 fictional titles and metadata.
  • Load the catalog into a ChromaDB vector database.
  • Test semantic search with a natural language query.
Create the media catalog data

Each media title needs structured metadata for the vector database to index. You'll define 10 fictional titles with fields like genre, description, cast, and rating.

  • In your code editor, create a new file called media_catalog.py in the project folder you set up in Step 1.
  • Paste the following catalog data into the file:
import chromadb

# Media catalog data with 10 fictional titles
MEDIA_CATALOG = [
    {
        "id": "media_001",
        "title": "Neon Horizon",
        "genre": "Sci-Fi",
        "description": "In a dystopian 2150, a rogue AI engineer discovers a hidden neural network that could either save humanity or enslave it. She must navigate corporate espionage and underground hacker collectives to make the right choice.",
        "cast": "Zara Chen, Marcus Webb, Priya Patel",
        "release_year": "2024",
        "rating": "8.5",
    },
    {
        "id": "media_002",
        "title": "The Last Lighthouse",
        "genre": "Drama",
        "description": "A retired sea captain returns to the remote lighthouse where she grew up, only to find that the new keeper is hiding a dangerous secret. A slow-burn character study about isolation, memory, and forgiveness.",
        "cast": "Helen Mirren, Tom Burke, Saoirse Ronan",
        "release_year": "2023",
        "rating": "9.1",
    },
    {
        "id": "media_003",
        "title": "Velocity",
        "genre": "Action",
        "description": "An elite fighter pilot is pulled from retirement to lead a covert mission against a rogue military faction armed with experimental drone technology. Features intense aerial combat sequences and a strong female protagonist.",
        "cast": "Lupita Nyong'o, Oscar Isaac, John Boyega",
        "release_year": "2024",
        "rating": "7.8",
    },
    {
        "id": "media_004",
        "title": "Whispers in the Garden",
        "genre": "Mystery",
        "description": "When a famous botanist is found dead in her award-winning garden, a detective with synesthesia must use her unique sensory abilities to unravel a web of jealousy and deception among the victim's colleagues.",
        "cast": "Cate Blanchett, Dev Patel, Florence Pugh",
        "release_year": "2024",
        "rating": "8.9",
    },
    {
        "id": "media_005",
        "title": "Cosmic Drift",
        "genre": "Sci-Fi",
        "description": "A family of space colonists is stranded on an uncharted planet after their ship malfunctions. They must survive hostile alien wildlife while uncovering ancient ruins that suggest they are not the first humans to visit.",
        "cast": "Pedro Pascal, Awkwafina, Timothee Chalamet",
        "release_year": "2025",
        "rating": "8.2",
    },
    {
        "id": "media_006",
        "title": "Rhythm & Blues",
        "genre": "Musical",
        "description": "A young musician from a small town moves to Chicago to pursue her dream of becoming a blues singer. Along the way she discovers the hidden history of her grandmother's legendary jazz career.",
        "cast": "Janelle Monae, Brian Tyree Henry, Viola Davis",
        "release_year": "2023",
        "rating": "8.7",
    },
    {
        "id": "media_007",
        "title": "Frostbite",
        "genre": "Thriller",
        "description": "A team of climate scientists at an Arctic research station discovers that the permafrost is releasing more than just methane. Something ancient and dangerous has thawed, and their communications are down.",
        "cast": "Mads Mikkelsen, Rebecca Ferguson, John David Washington",
        "release_year": "2024",
        "rating": "7.6",
    },
    {
        "id": "media_008",
        "title": "The Spice Route",
        "genre": "Documentary",
        "description": "A visually stunning journey along the ancient spice trade routes from Kerala to Constantinople, exploring how the quest for flavor shaped empires, cultures, and the modern global economy.",
        "cast": "Narrated by Riz Ahmed",
        "release_year": "2024",
        "rating": "9.0",
    },
    {
        "id": "media_009",
        "title": "Shadow Protocol",
        "genre": "Action",
        "description": "A disgraced intelligence analyst uncovers a conspiracy within her own agency. With no one to trust, she goes rogue across three continents to expose the truth before a catastrophic false flag operation is launched.",
        "cast": "Gal Gadot, Idris Elba, Sandra Oh",
        "release_year": "2025",
        "rating": "7.9",
    },
    {
        "id": "media_010",
        "title": "Little Wonders",
        "genre": "Animation",
        "description": "A microscopic civilization living inside a backyard garden must band together when a human family decides to pave over their world to build a swimming pool. A heartwarming tale about community and resilience.",
        "cast": "Voices of Ke Huy Quan, Zendaya, Jack Black",
        "release_year": "2025",
        "rating": "8.4",
    },
]

What does this code do?

  • MEDIA_CATALOG is a list of 10 dictionaries, each representing a fictional media title.
  • Each entry includes an id, title, genre, description, cast, release_year, and rating.
  • The description field is the most important for semantic search. ChromaDB uses it to understand what each title is about.
  • Save media_catalog.py.
  • Confirm the file appears in your code editor's file explorer sidebar.
Load the catalog into ChromaDB

Your catalog data needs to be converted into embeddings and stored in a searchable format. ChromaDB handles this automatically. You just need to format each entry as a document string and add it to a collection.

  • In media_catalog.py, add the following function below the MEDIA_CATALOG list:
def create_media_collection():
    """Create and populate a ChromaDB collection with media catalog data."""
    # Create an in-memory ChromaDB instance
    client = chromadb.Client()
    collection = client.get_or_create_collection(name="media_content")

    # Prepare lists for batch insertion
    documents = []
    metadatas = []
    ids = []

    for item in MEDIA_CATALOG:
        # Combine metadata fields into a single searchable text document
        doc_text = (
            f"Title: {item['title']}. Genre: {item['genre']}. "
            f"Description: {item['description']} "
            f"Cast: {item['cast']}. Year: {item['release_year']}. "
            f"Rating: {item['rating']}/10."
        )
        documents.append(doc_text)
        metadatas.append({
            "title": item["title"],
            "genre": item["genre"],
            "release_year": item["release_year"],
            "rating": item["rating"],
        })
        ids.append(item["id"])

    # Add all documents at once (ChromaDB generates embeddings automatically)
    collection.add(documents=documents, metadatas=metadatas, ids=ids)
    return collection

What does this code do?

  • chromadb.Client() creates an in-memory vector database. No files or external servers needed.
  • get_or_create_collection creates a collection called media_content, or reuses it if it already exists.
  • The loop builds a text document for each title by combining its metadata fields into one string.
  • collection.add() inserts all documents at once. ChromaDB automatically generates embeddings for each document behind the scenes.
  • Save media_catalog.py.
  • You won't see any output yet. The next substep adds a search function and runs the file to verify everything works.

Why does ChromaDB handle embeddings automatically?

ChromaDB ships with a default embedding model, so you don't need to configure one yourself. When you call collection.add(), it converts each document string into a numerical vector. When you query later, it converts your query the same way and finds the closest vectors. This is the core of RAG: retrieving relevant documents based on semantic similarity, then passing them to an LLM.

Test semantic search

Now you'll write a function that queries the vector database using natural language. ChromaDB compares your query's embedding against every document's embedding and returns the closest matches.

  • In media_catalog.py, add the following search_media function below create_media_collection:
def search_media(query: str, n_results: int = 3) -> list[dict]:
    """Search the media catalog using semantic similarity.

    Args:
        query: A natural language search query.
        n_results: Number of results to return.

    Returns:
        A list of matching media items with their metadata.
    """
    # Load the collection and run a semantic query
    collection = create_media_collection()
    results = collection.query(query_texts=[query], n_results=n_results)

    # Format each result into a clean dictionary
    matches = []
    for i in range(len(results["ids"][0])):
        matches.append({
            "id": results["ids"][0][i],
            "document": results["documents"][0][i],
            "metadata": results["metadatas"][0][i],
            "distance": results["distances"][0][i],
        })
    return matches

What does this code do?

  • collection.query() takes your natural language query, converts it to an embedding, and finds the closest matching documents.
  • n_results=3 limits the output to the top 3 matches.
  • The function returns a list of dictionaries. Each dictionary contains the matching document's text, metadata, and a distance score. Lower distance means a closer semantic match.
  • You won't see output yet. The next code block adds a test script that runs this function.
  • Add the following test block at the bottom of media_catalog.py:
if __name__ == "__main__":
    # Load the catalog and verify the collection
    print("Loading media catalog into ChromaDB...")
    collection = create_media_collection()
    print(f"Loaded {collection.count()} items into the media_content collection.\n")

    # Run a test query to verify semantic search works
    test_query = "action movies with strong female leads"
    print(f"Test query: '{test_query}'\n")
    results = search_media(test_query)
    for match in results:
        meta = match["metadata"]
        print(f"  - {meta['title']} ({meta['genre']}, {meta['release_year']}) "
              f"Rating: {meta['rating']} | Distance: {match['distance']:.4f}")

What does this code do?

  • The if __name__ == "__main__": guard ensures this code only runs when you execute media_catalog.py directly. It won't run when other files import from it.
  • It loads the catalog into ChromaDB, runs the test query "action movies with strong female leads", and prints the top 3 results with their titles, genres, ratings, and distance scores.
  • Save media_catalog.py.

Awesome, I've got everything!

Double check you've saved your file before running it.

I'd like to double check the full code

import chromadb


MEDIA_CATALOG = [
    {
        "id": "media_001",
        "title": "Neon Horizon",
        "genre": "Sci-Fi",
        "description": "In a dystopian 2150, a rogue AI engineer discovers a hidden neural network that could either save humanity or enslave it. She must navigate corporate espionage and underground hacker collectives to make the right choice.",
        "cast": "Zara Chen, Marcus Webb, Priya Patel",
        "release_year": "2024",
        "rating": "8.5",
    },
    {
        "id": "media_002",
        "title": "The Last Lighthouse",
        "genre": "Drama",
        "description": "A retired sea captain returns to the remote lighthouse where she grew up, only to find that the new keeper is hiding a dangerous secret. A slow-burn character study about isolation, memory, and forgiveness.",
        "cast": "Helen Mirren, Tom Burke, Saoirse Ronan",
        "release_year": "2023",
        "rating": "9.1",
    },
    {
        "id": "media_003",
        "title": "Velocity",
        "genre": "Action",
        "description": "An elite fighter pilot is pulled from retirement to lead a covert mission against a rogue military faction armed with experimental drone technology. Features intense aerial combat sequences and a strong female protagonist.",
        "cast": "Lupita Nyong'o, Oscar Isaac, John Boyega",
        "release_year": "2024",
        "rating": "7.8",
    },
    {
        "id": "media_004",
        "title": "Whispers in the Garden",
        "genre": "Mystery",
        "description": "When a famous botanist is found dead in her award-winning garden, a detective with synesthesia must use her unique sensory abilities to unravel a web of jealousy and deception among the victim's colleagues.",
        "cast": "Cate Blanchett, Dev Patel, Florence Pugh",
        "release_year": "2024",
        "rating": "8.9",
    },
    {
        "id": "media_005",
        "title": "Cosmic Drift",
        "genre": "Sci-Fi",
        "description": "A family of space colonists is stranded on an uncharted planet after their ship malfunctions. They must survive hostile alien wildlife while uncovering ancient ruins that suggest they are not the first humans to visit.",
        "cast": "Pedro Pascal, Awkwafina, Timothee Chalamet",
        "release_year": "2025",
        "rating": "8.2",
    },
    {
        "id": "media_006",
        "title": "Rhythm & Blues",
        "genre": "Musical",
        "description": "A young musician from a small town moves to Chicago to pursue her dream of becoming a blues singer. Along the way she discovers the hidden history of her grandmother's legendary jazz career.",
        "cast": "Janelle Monae, Brian Tyree Henry, Viola Davis",
        "release_year": "2023",
        "rating": "8.7",
    },
    {
        "id": "media_007",
        "title": "Frostbite",
        "genre": "Thriller",
        "description": "A team of climate scientists at an Arctic research station discovers that the permafrost is releasing more than just methane. Something ancient and dangerous has thawed, and their communications are down.",
        "cast": "Mads Mikkelsen, Rebecca Ferguson, John David Washington",
        "release_year": "2024",
        "rating": "7.6",
    },
    {
        "id": "media_008",
        "title": "The Spice Route",
        "genre": "Documentary",
        "description": "A visually stunning journey along the ancient spice trade routes from Kerala to Constantinople, exploring how the quest for flavor shaped empires, cultures, and the modern global economy.",
        "cast": "Narrated by Riz Ahmed",
        "release_year": "2024",
        "rating": "9.0",
    },
    {
        "id": "media_009",
        "title": "Shadow Protocol",
        "genre": "Action",
        "description": "A disgraced intelligence analyst uncovers a conspiracy within her own agency. With no one to trust, she goes rogue across three continents to expose the truth before a catastrophic false flag operation is launched.",
        "cast": "Gal Gadot, Idris Elba, Sandra Oh",
        "release_year": "2025",
        "rating": "7.9",
    },
    {
        "id": "media_010",
        "title": "Little Wonders",
        "genre": "Animation",
        "description": "A microscopic civilization living inside a backyard garden must band together when a human family decides to pave over their world to build a swimming pool. A heartwarming tale about community and resilience.",
        "cast": "Voices of Ke Huy Quan, Zendaya, Jack Black",
        "release_year": "2025",
        "rating": "8.4",
    },
]


def create_media_collection():
    """Create and populate a ChromaDB collection with media catalog data."""
    client = chromadb.Client()
    collection = client.get_or_create_collection(name="media_content")

    documents = []
    metadatas = []
    ids = []

    for item in MEDIA_CATALOG:
        doc_text = (
            f"Title: {item['title']}. Genre: {item['genre']}. "
            f"Description: {item['description']} "
            f"Cast: {item['cast']}. Year: {item['release_year']}. "
            f"Rating: {item['rating']}/10."
        )
        documents.append(doc_text)
        metadatas.append({
            "title": item["title"],
            "genre": item["genre"],
            "release_year": item["release_year"],
            "rating": item["rating"],
        })
        ids.append(item["id"])

    collection.add(documents=documents, metadatas=metadatas, ids=ids)
    return collection


def search_media(query: str, n_results: int = 3) -> list[dict]:
    """Search the media catalog using semantic similarity.

    Args:
        query: A natural language search query.
        n_results: Number of results to return.

    Returns:
        A list of matching media items with their metadata.
    """
    collection = create_media_collection()
    results = collection.query(query_texts=[query], n_results=n_results)

    matches = []
    for i in range(len(results["ids"][0])):
        matches.append({
            "id": results["ids"][0][i],
            "document": results["documents"][0][i],
            "metadata": results["metadatas"][0][i],
            "distance": results["distances"][0][i],
        })
    return matches


if __name__ == "__main__":
    print("Loading media catalog into ChromaDB...")
    collection = create_media_collection()
    print(f"Loaded {collection.count()} items into the media_content collection.\n")

    test_query = "action movies with strong female leads"
    print(f"Test query: '{test_query}'\n")
    results = search_media(test_query)
    for match in results:
        meta = match["metadata"]
        print(f"  - {meta['title']} ({meta['genre']}, {meta['release_year']}) "
              f"Rating: {meta['rating']} | Distance: {match['distance']:.4f}")
  • Run the file to test your vector database by running this command:
python3 media_catalog.py

You should see output showing that 10 items were loaded, followed by 3 search results. The top results should include titles like "Velocity" and "Shadow Protocol" since they feature action and strong female protagonists.

What should I see?

Your output should look something like this:

Loading media catalog into ChromaDB...
Loaded 10 items into the media_content collection.

Test query: 'action movies with strong female leads'

  - Velocity (Action, 2024) Rating: 7.8 | Distance: 0.XXXX
  - Shadow Protocol (Action, 2025) Rating: 7.9 | Distance: 0.XXXX
  - Neon Horizon (Sci-Fi, 2024) Rating: 8.5 | Distance: 0.XXXX

The exact distance values and the order of results may vary slightly. The important thing is that the top results are semantically relevant to the query.

Seeing an error when running the file?

  • If you see ModuleNotFoundError: No module named 'chromadb', make sure your virtual environment is activated. Run source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate (Windows), then try again.
  • If you see a SyntaxError, check that all opening brackets and parentheses in your MEDIA_CATALOG list have matching closing ones. Each dictionary entry should end with }, and the entire list should end with ].
  • On Windows, try python media_catalog.py instead of python3.

Help me debug my media_catalog.py file

Your vector database is loaded and searchable. Next up, you'll build the AI agents that use this database to find and enrich media content automatically.

Build the Multi-Agent System with ADK

Your media catalog is loaded into ChromaDB and returning semantic search results.

Now it is time to build the AI agents that query that data and generate enriched metadata. Google ADK uses a hierarchical agent pattern where a coordinator agent delegates tasks to specialized sub-agents. You will create three agents: one that searches your vector database, one that enriches content metadata, and one that routes requests between them.

In this step, get ready to:
  • Create a retrieval agent that searches your ChromaDB vector database.
  • Create an enrichment agent that generates enhanced metadata for media content.
  • Build a coordinator agent that delegates tasks between the retrieval and enrichment agents.
Create the retrieval agent

The retrieval agent is a specialist. Its only job is to take a natural language query, search your ChromaDB vector database, and return the most relevant results.

Before writing any agent code, you need to create the directory structure that ADK expects. Each agent lives in its own Python package with an __init__.py and an agent.py file.

  • Create the agent directories by running this command from your project folder:
mkdir -p agents/retrieval_agent agents/enrichment_agent agents/coordinator_agent
  • Confirm the directories exist by running:
ls agents/

You should see retrieval_agent, enrichment_agent, and coordinator_agent listed.

What is this directory structure?

ADK discovers agents by looking for Python packages that contain an agent.py file with a root_agent variable. Each agent directory needs an __init__.py to make Python treat it as an importable package.

  • Create three __init__.py files, one in each agent directory, by running these commands:
echo 'from . import agent' > agents/retrieval_agent/__init__.py
echo 'from . import agent' > agents/enrichment_agent/__init__.py
echo 'from . import agent' > agents/coordinator_agent/__init__.py

Getting a "No such file or directory" error?

Make sure you ran the mkdir -p command first. Double-check that you are in your project root directory (the folder containing media_catalog.py).

Help me troubleshoot this directory error.

Now it is time to write the retrieval agent itself. This agent needs a tool function that wraps your search_media function from media_catalog.py.

  • Create a new file called agent.py inside agents/retrieval_agent/ and add the imports and tool function:
from google.adk.agents import LlmAgent

from media_catalog import search_media


def search_media_tool(query: str) -> dict:
    """Search the media catalog for content matching a query.

    Uses semantic similarity search over a vector database
    to find the most relevant media content.

    Args:
        query: A natural language search query describing what
            kind of content the user is looking for.

    Returns:
        dict: A dictionary containing the search results with
            matching titles, metadata, and relevance scores.
    """
    # Call the ChromaDB search function from media_catalog
    results = search_media(query, n_results=3)
    formatted = []
    for match in results:
        # Reshape each result into a clean dictionary for the agent
        formatted.append({
            "title": match["metadata"]["title"],
            "genre": match["metadata"]["genre"],
            "year": match["metadata"]["release_year"],
            "rating": match["metadata"]["rating"],
            "description": match["document"],
            "relevance_score": round(1 - match["distance"], 4),
        })
    return {"query": query, "results": formatted, "total_found": len(formatted)}

What does this code do?

  • search_media_tool is a wrapper around the search_media function you built in Step 2. ADK registers it as a callable tool for the agent.
  • The detailed docstring matters. ADK passes it to the LLM so the model knows when and how to use the tool.
  • The relevance score is calculated as 1 - distance. ChromaDB returns a distance where lower is better, so flipping it gives a score where higher means more relevant.

You will not see a change yet. The next code block defines the agent that uses this tool.

  • Below the search_media_tool function in the same file, add the agent definition:
retrieval_agent = LlmAgent(
    name="retrieval_agent",
    model="gemini-2.0-flash",
    description="Searches the media content catalog using semantic similarity. Delegate to this agent when the user wants to find or discover content.",
    instruction=(
        "You are a media content retrieval specialist. "
        "Use the search_media_tool to find content matching user queries. "
        "Present results in a clear, organized format showing the title, "
        "genre, year, rating, and a brief description for each match. "
        "Highlight why each result is relevant to the query."
    ),
    tools=[search_media_tool],
)

What does this code do?

  • LlmAgent creates an agent powered by the gemini-2.0-flash model.
  • The description field is critical for multi-agent systems. The coordinator agent reads this to decide when to delegate tasks to the retrieval agent.
  • The instruction tells the agent how to behave when it receives a task. It guides response formatting and tool usage.
  • The tools list gives the agent access to search_media_tool. The agent decides when to call it based on the user's query.
  • Save agents/retrieval_agent/agent.py.
  • Verify the agent loads correctly by running:
python3 -c "from agents.retrieval_agent.agent import retrieval_agent; print(f'Agent loaded: {retrieval_agent.name}')"

You should see Agent loaded: retrieval_agent printed in your terminal.

Seeing a ModuleNotFoundError?

  • Make sure you are running the command from your project root directory (the folder containing media_catalog.py).
  • Check that agents/retrieval_agent/__init__.py exists and contains from . import agent.
  • Make sure your virtual environment is still activated. You should see (.venv) at the start of your terminal prompt.

Help me debug my ModuleNotFoundError.

Awesome, I've got everything!

Great. Double-check you have saved agents/retrieval_agent/agent.py before moving on.

I'd like to double check the full code

Here is the complete agents/retrieval_agent/agent.py file:

from google.adk.agents import LlmAgent

from media_catalog import search_media


def search_media_tool(query: str) -> dict:
    """Search the media catalog for content matching a query.

    Uses semantic similarity search over a vector database
    to find the most relevant media content.

    Args:
        query: A natural language search query describing what
            kind of content the user is looking for.

    Returns:
        dict: A dictionary containing the search results with
            matching titles, metadata, and relevance scores.
    """
    results = search_media(query, n_results=3)
    formatted = []
    for match in results:
        formatted.append({
            "title": match["metadata"]["title"],
            "genre": match["metadata"]["genre"],
            "year": match["metadata"]["release_year"],
            "rating": match["metadata"]["rating"],
            "description": match["document"],
            "relevance_score": round(1 - match["distance"], 4),
        })
    return {"query": query, "results": formatted, "total_found": len(formatted)}


retrieval_agent = LlmAgent(
    name="retrieval_agent",
    model="gemini-2.0-flash",
    description="Searches the media content catalog using semantic similarity. Delegate to this agent when the user wants to find or discover content.",
    instruction=(
        "You are a media content retrieval specialist. "
        "Use the search_media_tool to find content matching user queries. "
        "Present results in a clear, organized format showing the title, "
        "genre, year, rating, and a brief description for each match. "
        "Highlight why each result is relevant to the query."
    ),
    tools=[search_media_tool],
)
Create the enrichment agent

The enrichment agent takes a different approach from the retrieval agent. Instead of using external tools, it relies entirely on the LLM's generative capabilities to produce enhanced metadata like audience segments, content tags, and marketing summaries.

  • Create a new file called agent.py inside agents/enrichment_agent/ and add this code:
from google.adk.agents import LlmAgent


# This agent has no tools - it uses the LLM's built-in knowledge
enrichment_agent = LlmAgent(
    name="enrichment_agent",
    model="gemini-2.0-flash",
    description="Generates enhanced metadata for media content including audience segments, tags, and marketing summaries. Delegate to this agent when the user wants to enrich or enhance content metadata.",
    instruction=(
        "You are a media content enrichment specialist for a streaming platform. "
        "When given details about a media title, generate the following enhanced metadata:\n\n"
        "1. **Target Audience Segments**: Identify 2-3 specific audience demographics "
        "(e.g., 'Teens 13-17 interested in sci-fi', 'Adults 25-40 who enjoy slow-burn dramas')\n"
        "2. **Content Tags**: Generate 5-8 descriptive tags for content categorization "
        "(e.g., 'dystopian', 'female-protagonist', 'cerebral')\n"
        "3. **Marketing Summary**: Write a 2-sentence marketing blurb optimized for the platform's "
        "recommendation engine\n"
        "4. **Similar Titles**: Suggest 2-3 real-world titles that fans of this content would enjoy\n"
        "5. **Mood/Tone**: Classify the content mood (e.g., 'Dark and suspenseful', 'Uplifting and heartwarming')\n\n"
        "Format your response clearly with labeled sections."
    ),
)

What does this code do?

  • The enrichment agent has no tools list. It relies on the LLM's training data to generate creative metadata.
  • The detailed instruction acts as a structured prompt template. It tells the model exactly what five categories of metadata to produce.
  • The description again helps the coordinator decide when to send tasks to this agent.
  • Save agents/enrichment_agent/agent.py.
  • Verify the agent loads correctly by running:
python3 -c "from agents.enrichment_agent.agent import enrichment_agent; print(f'Agent loaded: {enrichment_agent.name}')"

You should see Agent loaded: enrichment_agent printed in your terminal.

Agent not loading?

  • Check that agents/enrichment_agent/__init__.py exists and contains from . import agent.
  • Make sure the filename is exactly agent.py (not agents.py or enrichment_agent.py).

Help me debug my enrichment agent import error.

Awesome, I've got everything!

Great. Double-check you have saved agents/enrichment_agent/agent.py before moving on.

I'd like to double check the full code

Here is the complete agents/enrichment_agent/agent.py file:

from google.adk.agents import LlmAgent


enrichment_agent = LlmAgent(
    name="enrichment_agent",
    model="gemini-2.0-flash",
    description="Generates enhanced metadata for media content including audience segments, tags, and marketing summaries. Delegate to this agent when the user wants to enrich or enhance content metadata.",
    instruction=(
        "You are a media content enrichment specialist for a streaming platform. "
        "When given details about a media title, generate the following enhanced metadata:\n\n"
        "1. **Target Audience Segments**: Identify 2-3 specific audience demographics "
        "(e.g., 'Teens 13-17 interested in sci-fi', 'Adults 25-40 who enjoy slow-burn dramas')\n"
        "2. **Content Tags**: Generate 5-8 descriptive tags for content categorization "
        "(e.g., 'dystopian', 'female-protagonist', 'cerebral')\n"
        "3. **Marketing Summary**: Write a 2-sentence marketing blurb optimized for the platform's "
        "recommendation engine\n"
        "4. **Similar Titles**: Suggest 2-3 real-world titles that fans of this content would enjoy\n"
        "5. **Mood/Tone**: Classify the content mood (e.g., 'Dark and suspenseful', 'Uplifting and heartwarming')\n\n"
        "Format your response clearly with labeled sections."
    ),
)
Create the coordinator agent with hierarchical delegation

The coordinator is the brain of your multi-agent system. It receives all user requests and decides which specialist agent should handle each one.

What is hierarchical delegation?

When you pass agents to the sub_agents parameter, ADK automatically enables LLM-driven delegation. The coordinator's LLM reads each sub-agent's description and dynamically decides which one to transfer a task to.

You do not write any routing logic yourself. The LLM generates a transfer_to_agent(agent_name='retrieval_agent') call, and ADK's AutoFlow intercepts it and routes execution to the correct agent.

  • Create a new file called agent.py inside agents/coordinator_agent/ and add this code:
from google.adk.agents import LlmAgent

from agents.retrieval_agent.agent import retrieval_agent
from agents.enrichment_agent.agent import enrichment_agent


# root_agent is the required entry point variable name for ADK
root_agent = LlmAgent(
    name="coordinator_agent",
    model="gemini-2.0-flash",
    description="Main coordinator that delegates content discovery and enrichment tasks.",
    instruction=(
        "You are the coordinator for a media content discovery pipeline. "
        "Your job is to help content managers find and enrich media content.\n\n"
        "When a user asks to find or search for content, delegate to the "
        "retrieval_agent by transferring to it.\n\n"
        "When a user asks to enrich, enhance, or generate metadata for content, "
        "delegate to the enrichment_agent by transferring to it.\n\n"
        "Always explain what you are doing and summarize results clearly."
    ),
    sub_agents=[retrieval_agent, enrichment_agent],
)

What does this code do?

  • The variable must be named root_agent. This is ADK's required entry point. When you run adk web later, it looks for this variable to discover your agent.
  • The sub_agents list registers both the retrieval and enrichment agents as children. ADK automatically sets up the transfer_to_agent mechanism for them.
  • The instruction tells the coordinator when to delegate to each sub-agent. The LLM reads this to make routing decisions dynamically.
  • Save agents/coordinator_agent/agent.py.
  • Verify all three agents load correctly by running:
python3 -c "from agents.coordinator_agent.agent import root_agent; print(f'Coordinator loaded: {root_agent.name}'); print(f'Sub-agents: {[a.name for a in root_agent.sub_agents]}')"

You should see the coordinator loaded with both sub-agents listed:

Coordinator loaded: coordinator_agent
Sub-agents: ['retrieval_agent', 'enrichment_agent']

Getting an ImportError for sub-agents?

  • Verify that all three __init__.py files exist by running ls agents/*/__init__.py.
  • Check that the import paths match exactly: from agents.retrieval_agent.agent import retrieval_agent.
  • Make sure you are running the command from your project root (the directory that contains the agents/ folder and media_catalog.py).

Help me debug my coordinator agent ImportError.

Awesome, I've got everything!

Great. Double-check you have saved agents/coordinator_agent/agent.py before moving on.

I'd like to double check the full code

Here is the complete agents/coordinator_agent/agent.py at this point:

from google.adk.agents import LlmAgent

from agents.retrieval_agent.agent import retrieval_agent
from agents.enrichment_agent.agent import enrichment_agent


# root_agent is the required entry point variable name for ADK
root_agent = LlmAgent(
    name="coordinator_agent",
    model="gemini-2.0-flash",
    description="Main coordinator that delegates content discovery and enrichment tasks.",
    instruction=(
        "You are the coordinator for a media content discovery pipeline. "
        "Your job is to help content managers find and enrich media content.\n\n"
        "When a user asks to find or search for content, delegate to the "
        "retrieval_agent by transferring to it.\n\n"
        "When a user asks to enrich, enhance, or generate metadata for content, "
        "delegate to the enrichment_agent by transferring to it.\n\n"
        "Always explain what you are doing and summarize results clearly."
    ),
    sub_agents=[retrieval_agent, enrichment_agent],
)

Why LLM-driven delegation instead of SequentialAgent?

ADK offers workflow agents like SequentialAgent that run sub-agents in a fixed order. The coordinator pattern used here is more flexible. The LLM reads the user's request and dynamically picks which agent to delegate to.

This means a single coordinator can handle "find me action movies" and "enrich this title" without any hardcoded routing logic. The model figures it out from the agent descriptions.

Your three-agent system is wired up and ready. Next up, you will build a custom MCP server and connect it to your coordinator for even more tool capabilities.

Build and Connect an MCP Server

Your multi-agent system can now search and enrich media content. But agents are only as useful as the tools they can access.

In this step, you will build a custom MCP server that exposes catalog-level tools, then wire it directly into your coordinator agent. By the end, your agents will be able to call MCP tools alongside their native function tools.

In this step, get ready to:
  • Create a FastMCP server with two media catalog tools.
  • Wire the MCP server into your coordinator agent using McpToolset.
  • Configure environment variables for your Gemini API key.
Create a FastMCP server

Your agents currently use function tools defined directly in Python. MCP (Model Context Protocol) is an open standard that lets agents discover and call tools hosted on separate servers.

What is MCP?

Think of MCP as a universal adapter between AI agents and external tools. Instead of hardcoding every tool into your agent, you spin up an MCP server that advertises its tools. Any MCP-compatible agent can then discover and call them.

FastMCP is a Python library (bundled with google-adk) that makes building MCP servers as simple as adding a decorator to a function.

Your MCP server will expose two tools: one that lists available genres and another that returns catalog statistics like total titles and average rating.

  • Create a new file called mcp_server.py in your project root (the same folder where media_catalog.py lives).
  • Add the imports, server initialization, and first tool by copying this code into mcp_server.py:
import os
import json
from mcp.server.fastmcp import FastMCP

from media_catalog import MEDIA_CATALOG

# Initialize the MCP server with a descriptive name
mcp = FastMCP("media_catalog_server")


@mcp.tool()
def list_genres() -> str:
    """List all unique genres available in the media catalog.

    Returns:
        A JSON string containing the list of available genres.
    """
    genres = sorted(set(item["genre"] for item in MEDIA_CATALOG))
    return json.dumps({"genres": genres, "count": len(genres)})

What does this code do?

  • FastMCP("media_catalog_server") creates a new MCP server instance. The name identifies your server when agents connect to it.
  • @mcp.tool() is a decorator that registers a function as an MCP tool. Any agent that connects to this server can discover and call it.
  • list_genres() extracts unique genres from your media catalog and returns them as a JSON string. MCP tools communicate with JSON.

You won't see a change yet. The next function completes the server.

  • Add the second tool and the server entry point below the list_genres function in mcp_server.py:

@mcp.tool()
def get_catalog_stats() -> str:
    """Get statistics about the media catalog.

    Returns:
        A JSON string containing catalog statistics including
        total titles, average rating, and genre distribution.
    """
    total = len(MEDIA_CATALOG)
    avg_rating = sum(float(item["rating"]) for item in MEDIA_CATALOG) / total

    # Count how many titles belong to each genre
    genre_counts = {}
    for item in MEDIA_CATALOG:
        genre = item["genre"]
        genre_counts[genre] = genre_counts.get(genre, 0) + 1

    return json.dumps({
        "total_titles": total,
        "average_rating": round(avg_rating, 2),
        "genre_distribution": genre_counts,
    })


if __name__ == "__main__":
    mcp.run(transport="stdio")

What does this code do?

  • get_catalog_stats() calculates total titles, average rating, and genre distribution across the entire catalog. This gives agents a high-level view of the data.
  • mcp.run(transport="stdio") starts the server using standard input/output as its transport layer. This lets other processes (like your ADK agent) communicate with it as a subprocess.
  • Save mcp_server.py.
  • Test the tool functions by running this command:
python3 -c "from mcp_server import list_genres, get_catalog_stats; print(list_genres()); print(get_catalog_stats())"

You should see two JSON strings printed. The first lists 8 genres. The second shows 10 total titles, an average rating, and genre distribution counts.

Seeing a ModuleNotFoundError?

  • If the error mentions mcp, make sure your virtual environment is activated. Run source .venv/bin/activate and try again. The mcp package is bundled with google-adk.
  • If the error mentions media_catalog, make sure mcp_server.py and media_catalog.py are in the same folder.
  • Why is my MCP server test failing?

Awesome, I've got everything!

Double check you have saved mcp_server.py.

I'd like to double check the full code

import os
import json
from mcp.server.fastmcp import FastMCP

from media_catalog import MEDIA_CATALOG

mcp = FastMCP("media_catalog_server")


@mcp.tool()
def list_genres() -> str:
    """List all unique genres available in the media catalog.

    Returns:
        A JSON string containing the list of available genres.
    """
    genres = sorted(set(item["genre"] for item in MEDIA_CATALOG))
    return json.dumps({"genres": genres, "count": len(genres)})


@mcp.tool()
def get_catalog_stats() -> str:
    """Get statistics about the media catalog.

    Returns:
        A JSON string containing catalog statistics including
        total titles, average rating, and genre distribution.
    """
    total = len(MEDIA_CATALOG)
    avg_rating = sum(float(item["rating"]) for item in MEDIA_CATALOG) / total

    genre_counts = {}
    for item in MEDIA_CATALOG:
        genre = item["genre"]
        genre_counts[genre] = genre_counts.get(genre, 0) + 1

    return json.dumps({
        "total_titles": total,
        "average_rating": round(avg_rating, 2),
        "genre_distribution": genre_counts,
    })


if __name__ == "__main__":
    mcp.run(transport="stdio")
Wire the MCP server to your coordinator agent

Your MCP server exists, but your agents do not know about it yet. ADK provides a class called McpToolset that connects an agent to an MCP server and automatically discovers its tools.

How does McpToolset work?

When you add an McpToolset to an agent's tools list, ADK launches the MCP server as a subprocess. It then queries the server for its available tools and makes them callable by the agent. The agent treats MCP tools the same as native Python function tools.

  • Open agents/coordinator_agent/agent.py in your code editor.
  • Replace the entire contents of the file with this updated version that adds MCP support:
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from mcp import StdioServerParameters

from agents.retrieval_agent.agent import retrieval_agent
from agents.enrichment_agent.agent import enrichment_agent


root_agent = LlmAgent(
    name="coordinator_agent",
    model="gemini-2.0-flash",
    description="Main coordinator that delegates content discovery and enrichment tasks.",
    instruction=(
        "You are the coordinator for a media content discovery pipeline. "
        "Your job is to help content managers find and enrich media content.\n\n"
        "When a user asks to find or search for content, delegate to the "
        "retrieval_agent by transferring to it.\n\n"
        "When a user asks to enrich, enhance, or generate metadata for content, "
        "delegate to the enrichment_agent by transferring to it.\n\n"
        "You also have access to MCP tools for catalog-level operations:\n"
        "- Use list_genres to show available genres\n"
        "- Use get_catalog_stats to show catalog statistics\n\n"
        "Always explain what you are doing and summarize results clearly."
    ),
    sub_agents=[retrieval_agent, enrichment_agent],
    # Connect to the MCP server as a subprocess
    tools=[
        McpToolset(
            connection_params=StdioServerParameters(
                command="python3",
                args=["mcp_server.py"],
            ),
        ),
    ],
)

What changed in this file?

Three things are new compared to the version you wrote in Step 3:

  • Two new imports: McpToolset from ADK's MCP module and StdioServerParameters from the mcp package.
  • Updated instruction: The coordinator now knows it has MCP tools for catalog-level operations. This helps the LLM decide when to use them.
  • New tools parameter: StdioServerParameters tells ADK to launch mcp_server.py as a subprocess using python3. ADK then auto-discovers all tools the server exposes.
  • Save agents/coordinator_agent/agent.py.

You won't see a visible change yet. The next substep configures the API key so your agents can call Gemini.

Awesome, I've got everything!

Double check you have saved agents/coordinator_agent/agent.py.

I'd like to double check the full code

from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from mcp import StdioServerParameters

from agents.retrieval_agent.agent import retrieval_agent
from agents.enrichment_agent.agent import enrichment_agent


root_agent = LlmAgent(
    name="coordinator_agent",
    model="gemini-2.0-flash",
    description="Main coordinator that delegates content discovery and enrichment tasks.",
    instruction=(
        "You are the coordinator for a media content discovery pipeline. "
        "Your job is to help content managers find and enrich media content.\n\n"
        "When a user asks to find or search for content, delegate to the "
        "retrieval_agent by transferring to it.\n\n"
        "When a user asks to enrich, enhance, or generate metadata for content, "
        "delegate to the enrichment_agent by transferring to it.\n\n"
        "You also have access to MCP tools for catalog-level operations:\n"
        "- Use list_genres to show available genres\n"
        "- Use get_catalog_stats to show catalog statistics\n\n"
        "Always explain what you are doing and summarize results clearly."
    ),
    sub_agents=[retrieval_agent, enrichment_agent],
    tools=[
        McpToolset(
            connection_params=StdioServerParameters(
                command="python3",
                args=["mcp_server.py"],
            ),
        ),
    ],
)
Configure environment variables

ADK needs two environment variables to authenticate with the Gemini API. ADK looks for a .env file inside the agent's own directory.

  • Create a new file called .env inside the agents/coordinator_agent/ folder.
  • Add the following two lines to the file:
GOOGLE_API_KEY=your-api-key-here
GOOGLE_GENAI_USE_VERTEXAI=FALSE
  • Replace your-api-key-here with the Gemini API key you saved from Google AI Studio in Step 1.
  • Save the .env file.

What do these variables do?

  • GOOGLE_API_KEY authenticates your agents with the Gemini model. This is the free API key from Google AI Studio.
  • GOOGLE_GENAI_USE_VERTEXAI=FALSE tells ADK to use the free Gemini API through Google AI Studio instead of Vertex AI. No billing or GCP project required.

Keep your API key private

Never commit your .env file to Git or share it publicly. If you plan to push this project to GitHub, add .env to your .gitignore file first.

Your MCP server is built, your coordinator agent is wired up, and your API key is configured. Next up, you will launch the ADK Dev UI to test the full pipeline and watch your agents collaborate in real time.

Run, Test, and Observe Your Agent System

Your MCP server is built, your agents are wired up, and your coordinator can delegate to both sub-agents and MCP tools. It is time to see everything in action.

ADK includes a browser-based Dev UI that lets you chat with your agents and watch delegation happen in real time. In this step, you will launch the Dev UI, run queries through the full pipeline, and explore the observability features.

In this step, get ready to:
  • Launch the ADK Dev UI and connect to your coordinator agent.
  • Test the full pipeline with search queries and MCP tool calls.
  • Inspect agent traces, tool calls, and token usage in the event timeline.
Launch the ADK Dev UI

The ADK Dev UI is a browser-based interface for testing and debugging your agents. You launch it from the terminal, and it auto-discovers all agent packages in the directory you point it to.

  • Make sure your virtual environment is still activated in your terminal.
  • Navigate to your project root directory (the folder that contains media_catalog.py and the agents/ folder).
  • Launch the Dev UI by running this command:
adk web agents

What does this command do?

  • adk web starts a local web server with the ADK Dev UI on port 8000.
  • agents tells ADK where to find your agent packages. It scans the agents/ directory for subdirectories that contain __init__.py and agent.py files.

You should see a startup message in your terminal confirming the server is running on http://localhost:8000.

  • Open http://localhost:8000 in your browser.
  • In the top-left corner of the UI, click the agent dropdown.
  • Select coordinator_agent from the list.

Don't see coordinator_agent in the dropdown?

  • Make sure you ran adk web agents from your project root directory, not from inside the agents/ folder.
  • Verify that agents/coordinator_agent/ contains all three files: __init__.py, agent.py, and .env.
  • Check that your .env file inside agents/coordinator_agent/ has your Gemini API key set correctly.
  • On Windows, if you see a NotImplementedError, try running adk web --no-reload agents instead.
  • Help me troubleshoot why coordinator_agent doesn't appear in the ADK Dev UI dropdown.
Test the full pipeline

Your coordinator agent can handle two types of requests. It delegates content searches to the retrieval agent, which queries ChromaDB. It delegates enrichment tasks to the enrichment agent.

It also has direct access to MCP tools for catalog-level operations like listing genres and viewing statistics.

  • In the chat input at the bottom of the Dev UI, type the following message and press Enter:
Find sci-fi content suitable for teenagers

What should happen?

  • The coordinator agent recognizes this as a search request and delegates to the retrieval agent using ADK's transfer_to_agent mechanism.
  • The retrieval agent calls search_media_tool, which queries your ChromaDB vector database for semantically relevant results.
  • The retrieval agent formats and presents the matching titles back to you.

You should see results like "Neon Horizon" and "Cosmic Drift" in the response, since these are the sci-fi titles in your catalog.

  • Now test the MCP tools by typing this message and pressing Enter:
What genres are available in the catalog?

What should happen?

  • The coordinator agent handles this directly using the list_genres MCP tool instead of delegating to a sub-agent.
  • The MCP server subprocess starts, processes the tool call, and returns the genre list.
  • You should see all genres from your catalog listed: Sci-Fi, Drama, Action, Mystery, Musical, Thriller, Documentary, and Animation.
  • Try one more query to test multi-agent delegation end-to-end. Type this message and press Enter:
Find action movies and enrich the top result with marketing metadata

This time, the coordinator should delegate first to the retrieval agent for the search, then to the enrichment agent to generate audience segments, content tags, and a marketing summary.

Getting unexpected responses or errors?

  • If the agent responds but does not delegate correctly, check the instruction text in your coordinator agent's agent.py. It needs clear routing rules for search vs. enrichment requests.
  • If you see an API error, verify your GOOGLE_API_KEY in agents/coordinator_agent/.env is valid.
  • If the MCP tool call fails, make sure mcp_server.py is in your project root directory. The StdioServerParameters in your coordinator agent runs it as a subprocess from the working directory.
  • Help me debug why my coordinator agent is not delegating to sub-agents correctly.
Inspect traces and observability

The Dev UI is more than a chat window. It records every event that happens during agent execution, giving you full visibility into the decision chain.

  • Look at the right-hand panel in the Dev UI. This is the Events panel, showing the timeline of events for your most recent interaction.
  • Click on any event in the timeline to expand its details.

Each event shows you what happened at that moment in the agent's execution. You can see which agent was active, what tool was called, and what the LLM generated.

  • Find a tool call event in the timeline and click it to see the tool name, input arguments, and output.
  • Look for transfer_to_agent events. These show exactly when the coordinator delegated to a sub-agent.

What can you observe in the event timeline?

  • Agent transitions: transfer_to_agent events show the delegation chain, letting you trace how the coordinator routes requests to the retrieval agent or enrichment agent.
  • Tool calls: each tool invocation (including MCP tools like list_genres and get_catalog_stats) appears with its full input and output.
  • LLM interactions: you can see the prompts sent to Gemini and the raw responses, including token counts for each agent.
  • Event sequencing: the timeline shows the exact order of operations, letting you trace the full lifecycle of a multi-agent request from start to finish.

Pro tip: use traces for debugging

When an agent misbehaves, the event timeline is your best debugging tool. Instead of guessing which agent went wrong, you can trace the exact decision point where the LLM chose the wrong tool or delegated to the wrong agent.

Secret mission

Add API Key Authentication to Your MCP Server

Your MCP server currently accepts requests from any process that can reach it. In this challenge, you'll add an authentication layer that validates an API key before executing any tool. This is a key pattern for building secure agentic workflows in production.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with a free Gemini API key, so there are no ongoing costs.

Resources you used:

  • Python virtual environment (.venv) with google-adk and chromadb installed.
  • Project files: media_catalog.py, mcp_server.py, and the agents/ directory.
  • Gemini API key from Google AI Studio.
  • Node.js (installed via Homebrew).
  • ADK Dev UI (local server on http://localhost:8000).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your multi-agent pipeline.

  • Your project files, virtual environment, and API key remain in place.
  • You can relaunch the ADK Dev UI anytime by running adk web from the parent directory.
  • ChromaDB runs in-memory, so no data persists between sessions. Your catalog is recreated each time you run the agents.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files stay on disk so you can pick up where you left off.

  • Stop the ADK Dev UI by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running deactivate in your terminal.
  • To resume later, navigate to your project directory, activate the virtual environment with source .venv/bin/activate, and run adk web again.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Stop the ADK Dev UI by pressing Ctrl+C in the terminal.
  • Deactivate your virtual environment by running deactivate.
  • Delete the entire project directory and its virtual environment.
  • If you want to revoke your Gemini API key, go to Google AI Studio, open your API keys page, and delete the key you created for this project.
  • Node.js can be uninstalled by running brew uninstall node if you no longer need it for other projects.

Mission accomplished

Nice Work!

You just built a production-patterned multi-agent RAG system using Google ADK, ChromaDB, and MCP. This is the exact stack that Google Cloud GenAI teams use to prototype intelligent content pipelines.

Here's what you accomplished:

  • Built a ChromaDB vector database loaded with a media catalog and used RAG-based semantic search to retrieve content from natural language queries.
  • Created a 3-agent hierarchical system using Google ADK with LLM-driven delegation to coordinate retrieval and enrichment tasks dynamically.
  • Built a custom FastMCP server, connected it to your agents via McpToolset, and traced every agent decision and tool call using ADK's Dev UI.
  • Added API key authentication to your MCP server for secure agentic workflows.

Ready to quiz yourself?