The evolution of AI agents has led to powerful, specialized models capable of complex tasks. The Google Agent Development Kit (ADK) – a toolkit designed to simplify the construction and management of language model-based applications – makes it easy for developers to build agents, usually equipped with tools via the Model Context Protocol (MCP) for tasks like web scraping. However, to unlock their full potential, these agents must be able to collaborate. The Agent-to-Agent (A2A) framework – a standardized communication protocol that allows disparate agents to discover each other, understand their capabilities, and interact securely – provides the standard for this interoperability.
This guide provides a step-by-step process for converting a standalone ADK agent that uses an MCP tool into a fully A2A-compatible component, ready to participate in a larger, multi-agent ecosystem. We will use a MultiURLBrowser agent, designed to scrape web content, as a practical example
Step 1: Define the core agent and its MCP tool (agent.py)
The foundation of your agent remains its core logic. The key is to properly initialize the ADK LlmAgent and configure its MCPToolset to connect with its external tool.
In agent.py, the _build_agent method is where you specify the LLM and its tools. The MCPToolset is configured to launch the firecrawl-mcp tool, passing the required API key through its environment variables
- code_block
- <ListValue: [StructValue([(‘code’, ‘# agents/search_agent/agent.pyrnimport osrnfrom adk.agent import LlmAgentrnfrom adk.mcp import MCPToolsetrnfrom adk.mcp.servers import StdioServerParametersrn# … other importsrnrnclass MultiURLBrowser:rn def _build_agent(self) -> LlmAgent:rn firecrawl_api_key = os.getenv(“FIRECRAWL_API_KEY”)rn if not firecrawl_api_key:rn raise ValueError(“FIRECRAWL_API_KEY environment variable not set.”)rnrn return LlmAgent(rn model=”gemini-1.5-pro-preview-0514″,rn name=”MultiURLBrowserAgent”,rn description=”Assists users by intelligently crawling and extracting information from multiple specified URLs.”,rn instruction=”You are an expert web crawler…”,rn tools=[rn MCPToolset(rn connection_params=StdioServerParameters(rn command=’npx’,rn args=[“-y”, “firecrawl-mcp”],rn env={“FIRECRAWL_API_KEY”: firecrawl_api_key}rn )rn )rn ]rn )rn # …’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d015b0>)])]>
Step 2: Establish a public identity (__main__.py)
For other agents to discover and understand your agent, it needs a public identity. This is achieved through the AgentSkill and AgentCard in the __main__.py file, which also serves as the entry point for the A2A server.
1. Define AgentSkill: This object acts as a declaration of the agent’s capabilities. It includes a unique ID, a human-readable name, a description, and examples
- code_block
- <ListValue: [StructValue([(‘code’, ‘# agents/search_agent/__main__.pyrnfrom a2a.skills.skill_declarations import AgentSkillrnrnskill = AgentSkill(rn id=”MultiURLBrowser”,rn name=”MultiURLBrowser_Agent”,rn description=”Agent to scrape content from the URLs specified by the user.”,rn tags=[“multi-url”, “browser”, “scraper”, “web”],rn examples=[rn “Scrape the URL: https://example.com/page1”,rn “Extract data from: https://example.com/page1 and https://example.com/page2″rn ]rn)’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d01610>)])]>
2. Define AgentCard: This is the agent’s primary metadata for discovery. It includes the agent’s name, URL, version, and, crucially, the list of skills it possesses.
- code_block
- <ListValue: [StructValue([(‘code’, ‘# agents/search_agent/__main__.pyrnfrom a2a.cards.agent_card import AgentCard, AgentCapabilitiesrnrnagent_card = AgentCard(rn name=”MultiURLBrowser”,rn description=”Agent designed to efficiently scrape content from URLs.”,rn url=f”http://{host}:{port}/”,rn version=”1.0.0″,rn defaultInputModes=[‘text’],rn defaultOutputModes=[‘text’],rn capabilities=AgentCapabilities(streaming=True),rn skills=[skill],rn supportsAuthenticatedExtendedCard=True,rn)’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d01670>)])]>
Step 3: Implement the A2A task manager (task_manager.py)
The AgentTaskManager is the bridge between the A2A framework and your agent’s logic. It implements the AgentExecutor interface, which requires execute and cancel methods.
The execute method is triggered by the A2A server upon receiving a request. It manages the task’s lifecycle, invokes the agent, and streams status updates and results back to the server via an EventQueue and TaskUpdater.
- code_block
- <ListValue: [StructValue([(‘code’, ‘# agents/search_agent/task_manager.pyrnfrom a2a.server.task_manager import AgentExecutor, RequestContext, EventQueue, TaskUpdaterrnfrom a2a.server.task_protocols import TaskState, new_task, new_agent_text_messagernfrom .agent import MultiURLBrowserrnrnclass AgentTaskManager(AgentExecutor):rn def __init__(self):rn self.agent = MultiURLBrowser()rnrn async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:rn query = context.get_user_input()rn task = context.current_task or new_task(context.message)rn await event_queue.enqueue_event(task)rn updater = TaskUpdater(event_queue, task.id, task.contextId)rnrn try:rn async for item in self.agent.invoke(query, task.contextId):rn if not item.get(‘is_task_complete’, False):rn await updater.update_status(rn TaskState.working,rn new_agent_text_message(item.get(‘updates’), task.contextId, task.id)rn )rn else:rn message = new_agent_text_message(item.get(‘content’), task.contextId, task.id)rn await updater.update_status(TaskState.completed, message)rn breakrn except Exception as e:rn error_message = f”An error occurred: {str(e)}”rn await updater.update_status(rn TaskState.failed,rn new_agent_text_message(error_message, task.contextId, task.id)rn )rn raise’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d016d0>)])]>
Step 4: Create the agent’s invoke method (agent.py)
The invoke method is the entry point into the agent’s core ADK logic. It is called by the AgentTaskManager and is responsible for running the ADK Runner. As the runner processes the query, this asynchronous generator yields events, allowing for streaming of progress updates and the final response.
- code_block
- <ListValue: [StructValue([(‘code’, ‘# agents/search_agent/agent.pyrn# …rnfrom adk.runner import Runnerrn# …rnrnclass MultiURLBrowser:rn # … __init__ and _build_agent …rnrn async def invoke(self, query: str, session_id: str) -> AsyncIterable[dict]:rn # … session management …rnrn user_content = types.Content(role=”user”, parts=[types.Part.from_text(text=query)])rnrn async for event in self._runner.run_async(rn user_id=self._user_id,rn session_id=session.id,rn new_message=user_contentrn ):rn if event.is_final_response():rn response_text = event.content.parts[-1].text if event.content and event.content.parts else “”rn yield {‘is_task_complete’: True, ‘content’: response_text}rn else:rn yield {‘is_task_complete’: False, ‘updates’: “Processing request…”}’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d01730>)])]>
With all components correctly configured, the MultiURLBrowser agent is now a fully operational A2A agent. When a client sends it a request to scrape content, it processes the task and returns the final result. The terminal output below shows a successful interaction, where the agent has received a mission and provided the extracted information as its final response.
Once you have A2A-compatible agents, you can create an “Orchestrator Agent” that delegates sub-tasks to them. This allows for the completion of complex, multi-step workflows.
Step 1: Discover available agents
An orchestrator must first know what other agents are available. This can be achieved by querying a known registry endpoint that lists the AgentCard for all registered agents.
- code_block
- <ListValue: [StructValue([(‘code’, ‘# Scrap_Translate/agent.pyrnimport httpxrnrnAGENT_REGISTRY_BASE_URL = “http://localhost:10000″rnrnasync with httpx.AsyncClient() as httpx_client:rn base_url = AGENT_REGISTRY_BASE_URL.rstrip(“/”)rn resolver = A2ACardResolver(rn httpx_client=httpx_client,rn base_url=base_url,rn # agent_card_path and extended_agent_card_path use defaults if not specifiedrn )rn final_agent_card_to_use: AgentCard | None = Nonernrn try:rn # Fetches the AgentCard from the standard public path.rn public_card = await resolver.get_agent_card()rn final_agent_card_to_use = public_cardrn except Exception as e:rn # Handle exceptions as needed for your specific use case.rn # For a blog post, you might simplify or omit detailed error handlingrn # if the focus is purely on the successful path.rn print(f”An error occurred: {e}”)’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d01790>)])]>
Step 2: Call other agents as tools
The orchestrator interacts with other agents using the a2a.client. The call_agent function demonstrates how to construct a SendMessageRequest and dispatch it to a target agent.
- code_block
- <ListValue: [StructValue([(‘code’, “# Scrap_Translate/agent.pyrnfrom a2a.client import A2AClientrnfrom a2a.client.protocols import SendMessageRequest, MessageSendParamsrnfrom uuid import uuid4rnrnasync def call_agent(agent_name: str, message: str) -> str:rn # In a real implementation, you would resolve the agent’s URL firstrn # using its card from list_agents().rn client = A2AClient(httpx_client=httpx.AsyncClient(timeout=300), agent_card=cards)rnrn payload = {rn ‘message’: {rn ‘role’: ‘user’,rn ‘parts’: [{‘kind’: ‘text’, ‘text’: message}],rn ‘messageId’: uuid4().hex,rn },rn }rn request = SendMessageRequest(id=str(uuid4()), params=MessageSendParams(**payload))rnrn response_record = await client.send_message(request)rn # Extract the text content from the response recordrn response_model = response_record.model_dump(mode=’json’, exclude_none=True)rn return response_model[‘result’][‘status’][‘message’][‘parts’][0][‘text’]”), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d017f0>)])]>
Step 3: Configure the orchestrator’s LLM
Finally, configure the orchestrator’s LlmAgent to use the discovery and delegation functions as tools. Provide a system instruction that guides the LLM on how to use these tools to break down user requests and coordinate with other agents
- code_block
- <ListValue: [StructValue([(‘code’, ‘# Scrap_Translate/agent.pyrnfrom adk.agent import LlmAgentrnfrom adk.tools import FunctionToolrnrnsystem_instr = (rn “You are a root orchestrator agent. You have two tools:\n”rn “1) list_agents() → Use this tool to see available agents.\n”rn “2) call_agent(agent_name: str, message: str) → Use this tool to send a task to another agent.\n”rn “Fulfill user requests by discovering and interacting with other agents.”rn)rnrnroot_agent = LlmAgent(rn model=”gemini-1.5-pro-preview-0514″,rn name=”root_orchestrator”,rn instruction=system_instr,rn tools=[rn FunctionTool(list_agents),rn FunctionTool(call_agent),rn ],rn)’), (‘language’, ‘lang-py’), (‘caption’, <wagtail.rich_text.RichText object at 0x3e1909d01850>)])]>
By following these steps, you can create both specialized, A2A-compatible agents and powerful orchestrators that leverage them, forming a robust and collaborative multi-agent system.
The true power of this architecture becomes visible when the orchestrator agent is run. Guided by its instructions, the LLM correctly interprets a user’s complex request and uses its specialized tools to coordinate with other agents. The screenshot below from a debugging UI shows the orchestrator in action: it first calls list_agents to discover available capabilities and then proceeds to call_agent to delegate the web-scraping task, perfectly illustrating the multi-agent workflow we set out to build.
Get started
This guide details the conversion of a standalone ADK/MCP agent into an A2A-compatible component and demonstrates how to build an orchestrator to manage such agents. The complete source code for all examples, along with official documentation, is available at the links below.