Free lesson · GenAI Application Engineering

Build a Google ADK agent with MCP + multi-agent delegation

You will build an ADKAgent in agents/adk_agent.py using google.adk.agents.Agent with model='gemini-2.5-flash'. MCP tools integrate via google.adk.tools.mcp_tool.MCPToolset.from_server() pointing to your MCP servers. You implement a root agent delegating to sub-agents: DataAgent for database queries, WebAgent for web retrieval, and AnalysisAgent for data analysis. Each sub-agent is a google.adk.agents.Agent with focused tools and instructions. The root uses agent_transfer() to delegate based on intent classification. ADKConfig Pydantic model configures model selection, tool server URIs, and delegation rules. FastAPI endpoint POST /api/v1/adk/run initializes the runner and streams responses. A PostgreSQL-backed session service maintains conversation state across multi-turn interactions.

Course: Full-Stack GenAI Applications · Chapter 8 · MCP, Tool Execution & Agentic Backends

Free to read — no subscription required.

Introduction

When you outgrow a single-agent prototype and need to orchestrate several specialists, the loop bookkeeping, session plumbing, and routing logic you hand-rolled with Pydantic AI start to dwarf the business logic they wrap. Teams that ignore this signal end up rewriting both the agent layer and its tests once a second specialist lands — a refactor that blocks every dependent ship. By the end of this lesson you'll be able to wire an MCP-backed Google ADK agent into a delegating root/sub-agent hierarchy and stream its execution events from a Cloud Run HTTP endpoint.

Key Terminology

  • Agent: The core ADK class wrapping a model, instructions, tools, and optional sub-agents into a single executable unit.
  • MCPToolset: An adapter that converts an MCP server's discoverable tools into ADK-compatible tool objects at runtime.
  • Sub-agent delegation: A pattern where the root agent's instructions route specific task types to named child agents rather than handling them directly.
  • Runner: The ADK execution engine that manages agent invocation, session state, and event streaming.
  • InMemorySessionService: A lightweight session store suitable for development and stateless deployments where session persistence is not required.

Concepts

This section contrasts ADK's framework-owned agentic loop against the hand-rolled Pydantic AI loop from an earlier goal, then covers the Cloud Run operational settings — concurrency, minimum-instances, request timeout, and stdio-vs-SSE MCP transport — that determine whether a multi-agent ADK service stays healthy under production traffic.

How ADK agents differ from Pydantic AI agents

Before writing code, it is important to understand the architectural distinction. In the Pydantic AI approach from another goal, you instantiated a single Agent object, registered typed tool functions via decorators, and controlled the agentic loop yourself—tracking tool call history, enforcing iteration limits, and streaming results through SSE. ADK shifts these responsibilities into the framework itself. The google.adk.agents.Agent class manages its own observe-think-act cycle internally, supports automatic tool call retries, and provides built-in session and memory management. More importantly, ADK's Agent constructor accepts a sub_agents parameter, enabling you to build agent trees where the root agent delegates to specialized children based on the task context.

  • Agent: The core ADK class wrapping a model, instructions, tools, and optional sub-agents into a single executable unit.
  • MCPToolset: An adapter that converts an MCP server's discoverable tools into ADK-compatible tool objects at runtime.
  • Sub-agent delegation: A pattern where the root agent's instructions tell it to route specific task types to named child agents rather than handling them directly.
  • Runner: The ADK execution engine that manages agent invocation, session state, and event streaming.
  • InMemorySessionService: A lightweight session store suitable for development and stateless deployments where session persistence is not required.

Cloud Run deployment considerations

When deploying this system to Cloud Run, several operational details require attention. First, the MCP server runs as a subprocess within the same container, so your Dockerfile must include both the ADK application and the MCP server code. Set the Cloud Run concurrency to match the number of simultaneous MCP server connections your subprocess can handle—typically one per instance for stdio-based servers. Second, configure the minimum instances to at least one if you need to avoid cold starts, since initializing the MCP tool discovery adds 2-3 seconds to startup. Third, set the TIMEOUT environment variable to at least 300 seconds for complex multi-agent workflows where the orchestrator may make several sequential delegations.

For production deployments beyond a single container, replace the stdio-based MCP connection with SSE transport. This lets you deploy the MCP server as a separate Cloud Run service, enabling independent scaling of the tool server and the agent service. Update StdioServerParameters to SseServerParameters with the MCP server's Cloud Run URL, and ADK handles the rest transparently—the tool objects returned by MCPToolset.from_server() behave identically regardless of transport.

Code Walkthrough

Now that you have the framework distinction and Cloud Run operational settings from the Concepts section, the walkthrough proceeds in three steps: first the MCPToolset.from_server() call that turns your existing MCP server's tools into ADK tool objects, then the root/sub-agent hierarchy constructed via the Agent(sub_agents=[...]) parameter, and finally a FastAPI lifespan-managed Runner that exposes the multi-agent system as a streaming Cloud Run endpoint.

Wiring MCP tools into an ADK agent

The first step is connecting your existing MCP server—the one exposing @mcp.tool() decorated functions from another goal—to an ADK agent. ADK provides MCPToolset.from_server() as a factory method that takes an MCP server connection configuration and returns a list of tool objects the agent can invoke. This bridges the two protocols: your MCP server speaks JSON-RPC over stdio or SSE, while ADK expects tool objects conforming to its internal BaseTool interface. The StdioServerParameters class configures how ADK launches and communicates with the MCP server process, specifying the command, arguments, and optional environment variables needed to start the server.

The full wiring — including the exit_stack lifecycle that keeps the subprocess alive — appears inside the multi-agent example in the next subsection, where MCPToolset.from_server() is invoked once and its tool list is assigned to a single specialist child agent via the tools= parameter. The exit_stack returned alongside the agent must be closed when the Cloud Run instance shuts down, otherwise the MCP server subprocess leaks across scale-to-zero cycles; this teardown is wired into the FastAPI lifespan handler shown later in this section.

Multi-agent delegation architecture

With a single agent wired to MCP tools, you can already handle straightforward tool-calling tasks. But real production systems require task decomposition—a user request like "analyze last quarter's revenue and draft a summary email" spans two distinct capabilities: data analysis and content generation. ADK's delegation model lets you split these into separate agents with their own tools and instructions, orchestrated by a root agent that decides which child to invoke.

The following diagram illustrates the delegation flow where a root orchestrator agent routes incoming requests to specialized child agents based on task classification:

A Root Orchestrator Agent receives each user request and delegates to three specialized sub-agents—Data Analyst, Content Writer, and Ops—based on task type. The Data Analyst and Ops agents connect to dedicated MCP Servers for database queries, metrics, and cloud API monitoring, while the Content Writer uses native tools for template rendering. Each sub-agent returns results to the orchestrator, which merges them into a single aggregated response, enabling parallel, domain-specific tool execution across providers.

Code snippet mermaid
Loading diagram...
  • Line 1: Declares a Mermaid flowchart with top-down (TD) layout direction.
  • Line 2: Defines the entry node A labeled "User Request" and connects it via an edge to node B labeled "Root Orchestrator Agent", establishing the orchestrator as the central routing point.
  • Lines 3-5: Define three conditional edges from the Root Orchestrator Agent (B) to specialized sub-agents: C (Data Analyst Agent) for data tasks, D (Content Writer Agent) for content tasks, and E (Ops Agent) for infrastructure tasks — each edge labeled with its routing condition.
  • Lines 6-8: Connect each sub-agent to its respective tooling layer: C uses MCP Tools to reach an MCP Server handling DB queries and metrics, D uses Native Tools for template rendering and formatting, and E uses MCP Tools to reach an MCP Server for Cloud APIs and monitoring.
  • Lines 9-11: Define return edges from each sub-agent (C, D, E) back to the Root Orchestrator Agent (B), representing the results flowing back to the orchestrator after task completion.
  • Line 12: Connects the Root Orchestrator Agent (B) to the final node I labeled "Aggregated Response to User", representing the orchestrator combining all sub-agent results into a single unified response delivered to the user.

The root agent does not call tools directly. Instead, its instructions explicitly name the child agents and describe when to delegate to each one. ADK handles the mechanics of passing context between parent and child, collecting the child's response, and returning control to the parent for final synthesis.

The following code demonstrates building a multi-agent hierarchy using ADK's sub_agents parameter. The create_multi_agent_system function constructs three specialized agents—a data analyst with MCP tools for database access, a content writer with native ADK tools for text formatting, and a root orchestrator whose instructions reference both children by name. The format_report function is defined as a standard Python callable and wrapped into an ADK-compatible tool using the framework's function tool adapter.

Code snippet python
1# agents/multi_agent.py 2from google.adk.agents import Agent 3from google.adk.tools.mcp_tool import MCPToolset 4from google.adk.tools import FunctionTool 5from mcp import StdioServerParameters 6 7def format_report(title: str, sections: list[str]) -> str: 8 """Format sections into a markdown report with a title.""" 9 body = "\n\n".join(f"## {s}" for s in sections) 10 return f"# {title}\n\n{body}" 11 12async def create_multi_agent_system(): 13 mcp_tools, exit_stack = await MCPToolset.from_server( 14 connection_params=StdioServerParameters( 15 command="python", 16 args=["-m", "mcp_server.main"], 17 ) 18 ) 19 20 data_agent = Agent( 21 model="gemini-2.5-flash", 22 name="data_analyst", 23 instruction=( 24 "You analyze data using database tools. Return raw results " 25 "with column headers. Never fabricate data points." 26 ), 27 tools=mcp_tools, 28 ) 29 30 writer_agent = Agent( 31 model="gemini-2.5-flash", 32 name="content_writer", 33 instruction=( 34 "You write clear, structured reports from provided data. " 35 "Use the format_report tool to produce markdown output." 36 ), 37 tools=[FunctionTool(format_report)], 38 ) 39 40 root_agent = Agent( 41 model="gemini-2.5-flash", 42 name="orchestrator", 43 instruction=( 44 "You coordinate complex tasks by delegating to specialists.\n" 45 "- For data retrieval and analysis: delegate to 'data_analyst'\n" 46 "- For writing and formatting: delegate to 'content_writer'\n" 47 "Synthesize their outputs into a final response." 48 ), 49 sub_agents=[data_agent, writer_agent], 50 ) 51 return root_agent, exit_stack
  • Lines 1-5: Import Agent for agent construction, MCPToolset for MCP integration, FunctionTool for wrapping plain Python functions as ADK tools, and StdioServerParameters for MCP server connection configuration.
  • Lines 7-10: Define format_report as a standard Python function with type annotations. ADK uses these annotations to generate the tool's parameter schema automatically, similar to how @mcp.tool() extracts JSON schema from type hints on the server side.
  • Lines 12-18: Discover MCP tools exactly as in the single-agent example. The same tool set gets assigned exclusively to the data analyst agent, enforcing separation of concerns—the writer agent cannot accidentally query the database.
  • Lines 20-28: Create the data_analyst child agent with MCP tools. Its instruction constrains it to data operations only and prohibits fabrication, which is critical when the agent has direct database access.
  • Lines 30-38: Create the content_writer child agent with the format_report function wrapped in FunctionTool. This agent has no MCP tools—it only formats data it receives from the orchestrator's context.
  • Lines 40-51: The root orchestrator agent receives both children via sub_agents. Its instruction explicitly names each child and describes the delegation criteria. ADK uses these instructions to guide the model's routing decisions. The root agent has no tools of its own—it operates purely through delegation.

Deploying to Cloud Run with the ADK Runner

The final piece is packaging this multi-agent system as an HTTP service. ADK provides a Runner class that executes agents against sessions, and an InMemorySessionService for lightweight session management. In a Cloud Run deployment, each request creates or resumes a session, invokes the runner, and streams events back to the client. The following code sets up a FastAPI endpoint that initializes the multi-agent system on startup, creates a runner, and processes user messages through the orchestrator agent while streaming intermediate events—including sub-agent delegations and tool calls—via server-sent events.

Code snippet python
1# app/main.py 2from contextlib import asynccontextmanager 3from fastapi import FastAPI 4from fastapi.responses import StreamingResponse 5from google.adk.runners import Runner 6from google.adk.sessions import InMemorySessionService 7from pydantic import BaseModel 8from agents.multi_agent import create_multi_agent_system 9from google.genai import types 10 11agent_system = {} 12 13@asynccontextmanager 14async def lifespan(app: FastAPI): 15 root_agent, exit_stack = await create_multi_agent_system() 16 session_service = InMemorySessionService() 17 runner = Runner( 18 agent=root_agent, 19 app_name="genai_platform", 20 session_service=session_service, 21 ) 22 agent_system["runner"] = runner 23 agent_system["session_service"] = session_service 24 yield 25 await exit_stack.aclose() 26 27app = FastAPI(lifespan=lifespan) 28 29class ChatRequest(BaseModel): 30 session_id: str 31 message: str 32 33@app.post("/chat") 34async def chat(req: ChatRequest): 35 runner = agent_system["runner"] 36 session_svc = agent_system["session_service"] 37 38 session = await session_svc.get_session( 39 app_name="genai_platform", 40 user_id="default", 41 session_id=req.session_id, 42 ) 43 if session is None: 44 session = await session_svc.create_session( 45 app_name="genai_platform", 46 user_id="default", 47 session_id=req.session_id, 48 ) 49 50 user_content = types.Content( 51 role="user", 52 parts=[types.Part.from_text(req.message)], 53 ) 54 55 async def event_stream(): 56 async for event in runner.run_async( 57 user_id="default", 58 session_id=session.id, 59 new_message=user_content, 60 ): 61 if event.content and event.content.parts: 62 text = event.content.parts[0].text or "" 63 author = event.author or "system" 64 yield f"data: [{author}] {text}\n\n" 65 yield "data: [done]\n\n" 66 67 return StreamingResponse(event_stream(), media_type="text/event-stream")
  • Lines 1-9: Import FastAPI for the HTTP layer, Runner and InMemorySessionService from ADK for agent execution and session management, and types from Google's GenAI SDK for constructing message content objects.
  • Lines 11-25: The lifespan context manager initializes the multi-agent system once at startup. The Runner binds the root orchestrator agent to a named application and session service. The exit_stack cleanup in the yield teardown ensures the MCP server subprocess terminates when the Cloud Run instance shuts down.
  • Lines 29-31: The ChatRequest model validates incoming requests, requiring a session_id for conversation continuity and the user's message text.
  • Lines 33-48: The /chat endpoint retrieves or creates a session. The InMemorySessionService stores conversation history in memory, which is acceptable for Cloud Run because each container handles its own sessions. For production persistence, you would swap in ADK's DatabaseSessionService backed by Firestore or Cloud SQL.
  • Lines 50-53: Construct a Content object with the user's message. ADK requires messages in this structured format rather than plain strings, enabling multimodal inputs in future extensions.
  • Lines 55-65: The event_stream generator invokes runner.run_async, which drives the orchestrator's agentic loop. Each yielded event may represent a tool call, a sub-agent delegation, or a text response. The event.author field identifies which agent produced the output—this is how clients distinguish between the orchestrator's synthesis and a child agent's raw data response. The SSE format matches the streaming pattern from another goal, enabling a unified client-side event handler.

Do's and Don'ts

Do's

  1. Do close the exit_stack returned by MCPToolset.from_server() inside the FastAPI lifespan handler — an unclosed stack lets the MCP server subprocess outlive the Cloud Run instance's lifecycle, silently exhausting process slots and causing cold-start failures on scale-to-zero restarts.
  2. Do write the root orchestrator's instructions to explicitly name each sub-agent and the task type that triggers delegation to it — ADK routes requests based solely on the root agent's instructions, not on tool signatures; omitting per-child routing conditions gives the orchestrator no classification signal and it answers the request directly instead of delegating to the appropriate specialist.
  3. Do wrap native Python callables with FunctionTool before adding them to a child agent's tools= parameter — ADK's tool registry expects objects conforming to BaseTool; an unwrapped callable bypasses schema registration entirely, making the function silently invisible to the model regardless of what the agent's instructions say.

Don'ts

  1. Don't attach tools directly to the root orchestrator Agent alongside a sub_agents=[...] list — the root's sole function is routing; giving it direct tools causes ADK to attempt local execution before delegating, conflating orchestration with task execution and defeating the multi-agent hierarchy you built.
  2. Don't discard the exit_stack that MCPToolset.from_server() returns alongside the tool list — the stack is the handle that keeps the MCP subprocess alive for the session; letting it fall out of scope immediately terminates the subprocess, so every subsequent tool invocation the child agent attempts returns a broken-pipe error against an already-dead server.
  3. Don't omit required environment variables from StdioServerParameters when the MCP server subprocess needs them — ADK spawns the subprocess in the container's restricted runtime environment, not a developer shell; missing env vars cause the MCP server to start but silently fail tool registration, and MCPToolset.from_server() returns an empty tool list with no error surfaced to the caller.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Application Engineering subscription.

From · cancel anytime

More free lessons in Full-Stack GenAI Applications

All free lessons in GenAI Application Engineering