Deep diveSelf-paced

Build your own A2A integration

MCP connects an agent to tools. A2A (Agent2Agent) connects an agent to *another agent* - across a process, a team, or an organisational boundary, where you do not control the other side's internals and should not need to. This deep dive builds the smallest possible A2A exchange so the protocol stops being an acronym.

MCP vs A2A, the line that matters

Wrong mental model

"A2A is just MCP for agents" - treating a remote agent as a fancy tool call with one input and one output.

Right mental model

MCP exposes capabilities (tools, resources, prompts) to a model. A2A exposes an *agent* - something with its own reasoning, that can ask clarifying questions, stream progress, and take multiple turns to finish a task.

The three things an A2A agent publishes

Agent Card

Discovery

A JSON document (conventionally at /.well-known/agent-card.json) describing who this agent is, what skills it offers, and how to authenticate to it.

Skills

Capabilities

Named capabilities the agent card advertises, each with a description and example inputs, so a calling agent can decide whether this agent can help.

Tasks

Unit of work

A stateful unit of work with a lifecycle (submitted, working, input-required, completed, failed) - not a single request/response.

Build the smallest possible A2A server (Python)

requirements
pip install a2a-sdk uvicorn
agent_card.py + server sketch
from a2a.types import AgentCard, AgentSkill, AgentCapabilities
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue

SKILL = AgentSkill(
    id="triage-support-ticket",
    name="Triage a support ticket",
    description="Reads a support ticket and returns severity, owner, and next action.",
    tags=["support", "triage"],
    examples=["Triage this ticket: customer can't log in after password reset"],
)

CARD = AgentCard(
    name="Triage Agent",
    description="Specialist agent that triages inbound support tickets.",
    url="http://localhost:9000/",
    version="1.0.0",
    capabilities=AgentCapabilities(streaming=True),
    skills=[SKILL],
    default_input_modes=["text"],
    default_output_modes=["text"],
)

class TriageExecutor(AgentExecutor):
    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        ticket_text = context.get_user_input()
        # ... your real triage logic (an LLM call, a classifier, a rules engine) goes here
        result = f"Severity: high. Owner: on-call. Reason: login is blocked for a paying customer."
        await event_queue.enqueue_event(context.new_agent_text_message(result))

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
        raise NotImplementedError("Cancellation not supported")

handler = DefaultRequestHandler(agent_executor=TriageExecutor(), task_store=InMemoryTaskStore())
app = A2AStarletteApplication(agent_card=CARD, http_handler=handler)

# Run with: uvicorn agent_card:app.build() --port 9000

Call it from another agent (the client side)

client sketch
from a2a.client import A2AClient
import httpx

async def ask_triage_agent(ticket_text: str) -> str:
    async with httpx.AsyncClient() as http_client:
        client = await A2AClient.get_client_from_agent_card_url(
            http_client, "http://localhost:9000"
        )
        response = await client.send_message(ticket_text)
        return response.text

Where this fits an orchestrator

In an ADK or LangGraph orchestrator pattern (Week 3 multi-agent lesson), a coordinator node calling this triage agent looks identical to calling any other tool from the coordinator's point of view: send a message, get a result. What differs is what happens *inside* the triage agent - it can be a different codebase, a different team, a different vendor, running on its own infrastructure, and your orchestrator never needs to know.

Critical

Auth is not optional once you cross a real boundary

The moment an A2A server is reachable outside your laptop, add authentication (the Agent Card declares the scheme) and treat every incoming task the same way you would treat prompt-injected content - untrusted until validated.

Watch out

Common mistakes

  • Modelling a multi-turn agent-to-agent negotiation as a single stateless HTTP call.
  • Skipping the Agent Card and hard-coding the URL and skill name on the client, which breaks the moment the server changes.
  • Exposing an A2A server publicly with no auth because "it's just for the demo."
  • Reaching for A2A when a plain function call in the same process would do - only cross the boundary when the other side is genuinely a separate agent you do not own.