Agent Interoperability — the A2A Protocol
MCP connects an agent to tools. A2A (Agent-to-Agent) connects an agent to other agents — built by different teams, on different frameworks, running behind different companies' walls. This chapter is the protocol that lets agents discover and delegate to each other as peers, and why that's a different problem from tool use.
Learning objectives
- Explain the problem A2A solves and how it differs from MCP.
- Describe the core concepts: Agent Card, tasks, messages, artifacts.
- Walk a discovery → delegate → stream-updates → result flow.
- Contrast A2A with in-process multi-agent frameworks (CrewAI/AutoGen).
- Name the trust, auth, and reliability concerns of cross-org agent calls.
The problem: agents behind walls essential
Your agent is great at its job. But the task needs a capability owned by another agent — a different team's logistics agent, a vendor's specialist agent, a partner company's service. You can't import their code (it's not yours), and they're not a "tool" you call — they're an autonomous agent with their own reasoning, state, and long-running work. You need a standard way for agents to talk to agents across organizational and technical boundaries. That's A2A (introduced by Google in 2025, now under open governance).
This picture contrasts two protocols side by side by the direction their arrows point. The whole lesson hangs on that one difference, so read it as "down vs sideways".
- On the left (MCP): the top box is your agent, and the arrow points down to a box labelled
tools / data. Downward means "reaches into things it uses" — a tool is a function your agent calls and owns. - On the right (A2A): your agent sits next to an other agent, and there are two sideways arrows between them — one going each way. Sideways means "talks to a peer", and the two arrows show it's a back-and-forth conversation, not a one-shot call.
- The solid arrow is a request your agent sends; the faint/dashed arrow is the reply coming back. Same convention is reused in the second diagram, so it's worth noticing here.
- The caption line "peer collaboration across a boundary" is the takeaway: the other agent is autonomous — it has its own reasoning and state — so you delegate to it rather than call it.
In short: MCP = arrow pointing down to a tool you own; A2A = arrows pointing sideways to a peer agent you don't control. If you only remember the arrow directions, you've got the lesson.
A2A vs MCP — complementary, not competing essential
| MCP | A2A | |
|---|---|---|
| Connects | Agent → tools / data / resources | Agent → other agents (peers) |
| Other side is… | A capability you invoke | An autonomous agent with its own reasoning |
| Interaction | Call a tool, get a result | Delegate a task; it may run long, stream updates, ask back |
| Boundary | Often within your control | Often across teams / orgs / vendors |
| Analogy | An API your program calls | A colleague you hand a project to |
Core concepts essential
A2A gives agents a shared vocabulary for discovery and delegation. Four pieces carry most of it.
| Concept | What it is |
|---|---|
| Agent Card | A published descriptor (JSON, at a well-known URL) advertising an agent's identity, skills, endpoint, and auth requirements — how others discover what it can do |
| Task | A unit of work delegated to a remote agent; has a lifecycle (submitted → working → input-required → completed/failed) — it can run long |
| Message | A turn of communication between client-agent and remote-agent within a task (text and structured parts) |
| Artifact | An output the remote agent produces for the task — the deliverable(s) handed back |
Lab I2.1 · A delegation flow intermediate
Walk the lifecycle: a client agent discovers a remote agent, delegates a task, receives streamed progress, and gets the result. This is the supervisor topology (L1/M1) stretched across a network boundary.
input-required state and asks back.
This is a time sequence, not a static map. Two agents face each other — client agent on the left, remote agent on the right — and the numbered arrows between them happen top to bottom, in order. Read it like a chat transcript flowing downward.
- Arrow 1 (fetch Agent Card) goes left→right: the client asks the remote "what can you do and how do I reach you?". The Agent Card is the published menu of the remote's skills — this is the discover step.
- Arrow 2 (send task) also goes left→right: the client hands over a unit of work — this is the delegate step, like a supervisor handing a job to a worker.
- Arrows 3 (stream status + messages) point back right→left and are dashed/faint: the remote keeps sending progress updates while it works. Multiple arrows = many updates over time, because a task can run for minutes, not milliseconds.
- Arrow 4 (return artifact), shown in green, is the final delivery: the finished output(s) come back and the task is done. Green marks the successful end state.
- Not drawn but noted in the caption: if the remote needs more info mid-task it enters an
input-requiredstate and asks back — the flow can pause and resume, which is why the updates go both ways.
In short: The four steps are discover → delegate → stream → deliver. Down-the-page = later in time; solid arrows = the client driving, faint/green arrows = the remote responding.
| Step | What happens | Familiar from… |
|---|---|---|
| Discover | Client reads the remote's Agent Card (skills, endpoint, auth) | Tool/server discovery (I1, C2) |
| Delegate | Client submits a task with an initial message | Supervisor → worker handoff (L1, M1) |
| Collaborate | Task streams status; may hit input-required and ask back | Streaming (C2); human-in-the-loop shape (L5) |
| Deliver | Remote returns artifacts; task reaches a terminal state | Artifacts / tool results (V3, C2) |
A2A vs in-process multi-agent (M1/M2) intermediate
You already coordinate multiple agents with CrewAI and AutoGen. When do you need A2A instead?
| Use in-process (CrewAI/AutoGen) when… | Use A2A when… |
|---|---|
| All agents are yours, in one codebase | Agents are built by different teams/companies |
| They share a framework & runtime | They run on different frameworks/stacks |
| You control deployment of all of them | You call an agent you don't operate |
| Tight coupling is fine | You need a stable contract across a boundary |
Trust, auth & reliability across the boundary intermediate
Calling an agent you don't control raises every concern that calling your own tools does — amplified, because the other side is autonomous and outside your walls.
| Concern | Why it's sharper across A2A |
|---|---|
| Authentication | The Agent Card declares auth requirements; you present credentials to a service you don't run — scope them tightly (I1, T1) |
| Trust of output | A remote agent's artifacts are untrusted input — they can carry prompt injection just like tool results (T1). Validate before acting |
| Reliability | The peer can be slow, down, or fail mid-task — treat it like any remote dependency: timeouts, retries, circuit breaker (A6, O3) |
| Data exposure | Delegating a task sends data across a boundary — decide what's safe to share, mind residency & privacy (O4) |
| Cost & loops | Agents calling agents can fan out expensively or loop — bound it, as with any multi-agent system (M2) |
Common pitfalls advanced
| Pitfall | Fix |
|---|---|
| Reaching for A2A when in-process would do | Only cross-boundary needs A2A; keep your own agents in-process |
| Modeling a peer agent as a sync function | Treat tasks as long-running, streamed, stateful |
| Trusting a remote agent's artifacts | Untrusted input — validate; guard against injection (T1) |
| No timeout/retry on remote calls | It's a remote dependency — apply A6/O3 resiliency |
| Over-broad credentials to a peer | Least privilege; scope to the delegated task |
| Unbounded agent-to-agent fan-out | Cap delegation depth & cost (M2) |
Exercises advanced
Exercise I2.1 — MCP or A2A?
Context: The core interop instinct is telling a capability you invoke (MCP) from an autonomous peer you delegate to (A2A), and four concrete cases sharpen it.
Your task: For each of four cases, decide MCP or A2A and justify: (a) query your Postgres, (b) a partner's fraud-scoring agent assesses a transaction, (c) read files, (d) hand a research sub-task to a vendor's autonomous research agent.
Requirements:
- Classify each of the four cases MCP or A2A
- Owned tools/data (Postgres, files) → MCP
- Autonomous peers across an org boundary (fraud-scoring, vendor research) → A2A
- Justify each with the invoke-vs-delegate rule
- State the rule: capability you invoke → MCP; autonomous peer you delegate to → A2A
💡 Hint: Whether you own and invoke the target or delegate to an autonomous peer decides it every time.
Show answers
(a) MCP — a tool/data source you own. (b) A2A — a peer agent across an org boundary. (c) MCP — a capability. (d) A2A — delegating a task to an autonomous external agent. Rule: capability you invoke → MCP; autonomous peer you delegate to → A2A.
Exercise I2.2 — Design an Agent Card
Context: To be delegated to safely, an agent has to publish its own Agent Card — and designing one for something you built surfaces exactly what a peer needs to know.
Your task: For an agent you've built in this course, sketch its Agent Card: identity, the skills it advertises, its endpoint, and what auth it would require.
Requirements:
- Give the agent an identity
- List the skills it advertises
- Specify its endpoint
- State the auth it would require
- Note what another team needs to know to safely delegate to it
💡 Hint: Write it from the caller's point of view — a peer should be able to discover, authenticate, and delegate from the card alone.
Exercise I2.3 — Threat-model a delegation
Context: Acting on an artifact returned by an external A2A agent opens real attack surface, so you threat-model it the way you would any untrusted input.
Your task: You delegate a task to an external A2A agent and act on its returned artifact: list three ways this could go wrong (injection, unreliability, data leak) and the specific mitigation for each.
Requirements:
- Name three failure modes: injection, unreliability, data leak
- Give a specific mitigation for each
- Draw on the injection (T1), guarded-delegation (A6), and vetting (L5) material
- Treat the returned artifact as untrusted input
💡 Hint: Each failure maps to a mitigation you've already seen — validate the artifact against injection, bound the call for reliability, and scope data to prevent leaks.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: An Agent Card is a JSON descriptor advertising an agent's skills, endpoint, and auth — A2A's discovery key that lets one agent learn a peer's capabilities without human wiring.
Your task: Given a card and a needed skill, write a function that says whether the agent can do it and where to reach it.
Requirements:
- Represent the card with name, endpoint, a skills list, and auth
- Check whether the needed skill is advertised
- Return the endpoint + auth scheme on a hit
- Return a clear “not advertised” miss otherwise
- Frame the card as the discovery key for delegation
💡 Hint: Membership-test the skill against the card's skills list, then hand back the endpoint and auth scheme to reach it.
Show solution
An Agent Card is just data you inspect before delegating. Runnable stdlib:
card = {
"name": "logistics-agent",
"endpoint": "https://vendor.example/a2a",
"skills": ["route-planning", "eta-estimate"],
"auth": "bearer",
}
def can_do(card, skill):
if skill in card["skills"]:
return f"yes -> POST {card['endpoint']} (auth: {card['auth']})"
return f"no -- {card['name']} does not advertise '{skill}'"
print(can_do(card, "eta-estimate")) # yes -> POST ...
print(can_do(card, "invoice-audit")) # no -- not advertised
The card is A2A's discovery key: you learn what a peer can do and how to reach it without a human wiring you together first.
Context: An A2A Task has a lifecycle — submitted → working → (input-required) → completed/failed — so a client must track state, not assume a one-shot reply.
Your task: Model the task lifecycle as a finite-state machine that rejects illegal transitions.
Requirements:
- Encode the allowed transitions (submitted, working, input-required, terminal completed/failed)
- Raise on an illegal transition
- Walk a valid path through the states
- Show a terminal-state transition being blocked
- Reinforce that a client tracks lifecycle, not a single reply
💡 Hint: A map from state to its allowed next states is the whole machine — anything not in the set raises, and completed/failed lead nowhere.
Show solution
Encode the allowed transitions and reject the rest. Runnable:
ALLOWED = {
"submitted": {"working", "failed"},
"working": {"input-required", "completed", "failed"},
"input-required": {"working", "failed"},
"completed": set(), # terminal
"failed": set(), # terminal
}
def transition(state, nxt):
if nxt in ALLOWED[state]:
return nxt
raise ValueError(f"illegal {state} -> {nxt}")
s = "submitted"
for nxt in ["working", "input-required", "working", "completed"]:
s = transition(s, nxt)
print("->", s)
try:
transition("completed", "working") # terminal, must raise
except ValueError as e:
print("blocked:", e)
Because a task can run long and pause for input, a client must track the lifecycle rather than assume a one-shot reply.
Context: Discovery in A2A means matching a peer that advertises the required skill AND an auth scheme the client can actually satisfy — skill-then-authenticable.
Your task: Given a registry of Agent Cards, write a matcher that finds a peer advertising a required skill and a supported auth scheme, returning the best endpoint or a clear miss.
Requirements:
- Filter the registry by the required skill
- Also require an auth scheme the client supports
- Return the first match's endpoint (or None)
- Note real discovery adds ranking (cost/latency/trust)
- The shape is match-skill-then-match-authenticable
💡 Hint: A card is only usable if you can both do the skill and authenticate to it — filter on both before returning.
Show solution
Discovery is a filter over cards, not a network call. Runnable:
REGISTRY = [
{"name": "a", "skills": ["translate"], "auth": "none"},
{"name": "b", "skills": ["route-planning"], "auth": "oauth2"},
{"name": "c", "skills": ["route-planning"], "auth": "bearer"},
]
def discover(registry, skill, supported_auth):
hits = [c for c in registry
if skill in c["skills"] and c["auth"] in supported_auth]
if not hits:
return None
return hits[0]["name"] # first match; could rank on latency/cost
print(discover(REGISTRY, "route-planning", {"bearer", "none"})) # c
print(discover(REGISTRY, "route-planning", {"none"})) # None
print(discover(REGISTRY, "translate", {"none"})) # a
Real discovery adds ranking (cost, latency, trust). The shape stays: match skill, then match what you can actually authenticate against.
Context: A2A isn't “call a tool, get a value” — a delegation streams status updates and ends with an Artifact, which you can model offline as a generator of events.
Your task: Simulate one A2A delegation offline: submit a task, receive streamed status updates, then collect the final Artifact, modelling the remote agent as an event generator.
Requirements:
- Model the remote agent as a generator yielding status events then an artifact
- Yield working-status updates (with progress) before completion
- Emit an artifact event, then a completed status
- Consumer folds the stream, printing progress and collecting artifacts
- Return the final state + artifacts
💡 Hint: A Python generator yielding status events and then an artifact captures the stream; the consumer just folds those events into progress + a final result.
Show solution
Model the remote as an event stream; the client folds it into a result. Runnable:
def remote_agent(task):
# a long-running peer that streams progress, then an artifact
yield {"type": "status", "state": "working", "pct": 30}
yield {"type": "status", "state": "working", "pct": 80}
yield {"type": "artifact", "name": "route.json",
"data": {"stops": task["stops"], "eta_min": 42}}
yield {"type": "status", "state": "completed"}
def delegate(task):
artifacts, final = [], None
for ev in remote_agent(task):
if ev["type"] == "status":
print("update:", ev.get("state"), ev.get("pct", ""))
final = ev["state"]
elif ev["type"] == "artifact":
artifacts.append(ev)
return final, artifacts
state, arts = delegate({"stops": ["A", "B", "C"]})
print("final:", state, "artifacts:", [a["name"] for a in arts])
Streaming updates + a final artifact are exactly why A2A is not "call a tool, get a value": the peer is autonomous and reports progress.
Context: In a mixed design, some connections go down to a tool you own (MCP) and some go sideways to an autonomous peer you delegate to (A2A) — they're layers, not rivals.
Your task: Write a classifier that, given a target's nature, picks the right protocol (MCP or A2A) and explains why.
Requirements:
- Return MCP for a function/resource (a capability you invoke and own)
- Return A2A for an autonomous agent (a peer you delegate a task to)
- Default to MCP
- Iterate a few sample targets to show the split
- Reinforce MCP=down/owned, A2A=sideways/autonomous
💡 Hint: Ask whether the target is a capability you own or an autonomous peer — that single distinction picks the protocol.
Show solution
The rule: own it and call it = MCP (down); autonomous peer you delegate to = A2A (sideways). Runnable:
def route(target):
# target: dict describing what we're connecting to
if target["kind"] == "function" or target["kind"] == "resource":
return "MCP", "a capability you invoke and own"
if target["kind"] == "agent" and target["autonomous"]:
return "A2A", "an autonomous peer you delegate a task to"
return "MCP", "default: treat as an owned capability"
for t in [
{"kind": "function", "name": "get_weather"},
{"kind": "agent", "autonomous": True, "name": "vendor-logistics"},
{"kind": "resource", "name": "s3-doc"},
]:
proto, why = route(t)
print(f"{t['name']:20} -> {proto:4} ({why})")
A mature agent uses MCP for its own tools and speaks A2A to hand whole sub-tasks to specialists it cannot absorb. They are layers, not rivals.
Context: Delegating to a partner-company agent adds trust and reliability concerns a tool call never has: you must check auth, confirm the skill was advertised, and bound the call with a timeout/retry policy before acting on its artifact.
Your task: Model a guarded cross-org delegation offline: enforce a valid auth token, an advertised skill, and a timeout/retry policy before trusting the returned artifact.
Requirements:
- Enforce trust first: skill must be advertised; bearer auth requires a token
- Then enforce reliability: a bounded retry loop over a flaky peer call
- Return ok/artifact after retry, or block on missing token / unadvertised skill
- Simulate transient failures in the peer call
- Note the real A2A SDK wires this over HTTP + an identity provider
💡 Hint: Check trust preconditions before you ever call, then wrap the call in a bounded retry — trust gates entry, retries handle transient failures.
Show solution
Crossing an org boundary means you cannot assume trust or liveness. Guard the call. Runnable:
import time
def guarded_delegate(card, skill, token, deadline_s=2, max_retries=2):
# 1) trust: skill must be advertised and auth must match
if skill not in card["skills"]:
return {"ok": False, "reason": "skill not advertised"}
if card["auth"] == "bearer" and not token:
return {"ok": False, "reason": "missing bearer token"}
# 2) reliability: bounded retries against a flaky peer
attempt = 0
while attempt <= max_retries:
attempt += 1
ok, artifact = _call_peer(card, skill, fail_first=1, attempt=attempt)
if ok:
return {"ok": True, "attempt": attempt, "artifact": artifact}
return {"ok": False, "reason": "peer unavailable after retries"}
def _call_peer(card, skill, fail_first, attempt):
time.sleep(0) # stand-in for network I/O
if attempt <= fail_first:
return False, None # simulate a transient failure
return True, {"skill": skill, "value": "OK"}
card = {"name": "partner", "skills": ["audit"], "auth": "bearer"}
print(guarded_delegate(card, "audit", token="t0k")) # ok after retry
print(guarded_delegate(card, "audit", token="")) # blocked: no token
print(guarded_delegate(card, "ship", token="t0k")) # blocked: not advertised
Across teams/vendors you own neither the code nor the uptime: verify the advertised skill, authenticate every call, and bound long-running/flaky work with timeouts and retries. The real A2A SDK wires this over HTTP + your identity provider (needs the SDK).
✓ Checkpoint — you can move on when you can…
- Explain why A2A exists and how it differs from MCP.
- Define Agent Card, task, message, and artifact.
- Walk the discover → delegate → stream → deliver flow.
- Decide A2A vs in-process multi-agent for a scenario.
- Name the trust/auth/reliability risks of cross-org agent calls and their mitigations.
Knowledge check check yourself
A2A and MCP are described as complementary, not competing. What does each connect, and why can't you model an A2A peer as a tool call?
Show answer
What is an Agent Card and why is it central to A2A's discover→delegate→stream→deliver flow?