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
classwrapping 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
classwrapping 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, andE(Ops Agent) for infrastructure tasks — each edge labeled with its routing condition. - Lines 6-8: Connect each sub-agent to its respective tooling layer:
Cuses MCP Tools to reach an MCP Server handling DB queries and metrics,Duses Native Tools for template rendering and formatting, andEuses MCP Tools to reach an MCP Server for Cloud APIs and monitoring. - Lines 9-11: Define
returnedges 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
Agentfor agent construction,MCPToolsetfor MCP integration,FunctionToolfor wrapping plain Python functions as ADK tools, andStdioServerParametersfor MCP server connection configuration. - Lines 7-10: Define
format_reportas 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_analystchild 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_writerchild agent with theformat_reportfunction wrapped inFunctionTool. This agent has no MCP tools—it only formats data it receives from the orchestrator's context. - Lines 40-51: The root
orchestratoragent receives both children viasub_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,
RunnerandInMemorySessionServicefrom ADK for agent execution and session management, andtypesfrom Google's GenAI SDK for constructing message content objects. - Lines 11-25: The
lifespancontext manager initializes the multi-agent system once at startup. TheRunnerbinds the root orchestrator agent to a named application and session service. Theexit_stackcleanup in theyieldteardown ensures the MCP server subprocess terminates when the Cloud Run instance shuts down. - Lines 29-31: The
ChatRequestmodel validates incoming requests, requiring asession_idfor conversation continuity and the user'smessagetext. - Lines 33-48: The
/chatendpoint retrieves or creates a session. TheInMemorySessionServicestores 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'sDatabaseSessionServicebacked by Firestore or Cloud SQL. - Lines 50-53: Construct a
Contentobject 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_streamgenerator invokesrunner.run_async, which drives the orchestrator's agentic loop. Each yieldedeventmay represent a tool call, a sub-agent delegation, or a text response. Theevent.authorfield 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
- ✓Do close the
exit_stackreturned byMCPToolset.from_server()inside the FastAPIlifespanhandler — 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. - ✓Do write the root orchestrator's
instructionsto 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. - ✓Do wrap native Python callables with
FunctionToolbefore adding them to a child agent'stools=parameter — ADK's tool registry expects objects conforming toBaseTool; 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
- ✗Don't attach tools directly to the root orchestrator
Agentalongside asub_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. - ✗Don't discard the
exit_stackthatMCPToolset.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. - ✗Don't omit required environment variables from
StdioServerParameterswhen 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, andMCPToolset.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
- Ch 3Implement Anthropic prompt caching with cache_control markers
- Ch 8Build an MCP server exposing business logic as tools
- Ch 8Build a Pydantic AI agent with typed tools and DI
- Ch 8Build a Google ADK agent with MCP + multi-agent delegationYou are here
- Ch 9Build an event broadcast system with Redis pub/sub
- Ch 10Build Llama Guard 4 content classifier
- Ch 14Build a semantic cache with Redis + embedding similarity