A2A vs MCP: When Your Agents Need to Talk to Other Agents (With a Working Example)
MCP connects an agent to tools. A2A connects agents to each other. Here is how Agent Cards, tasks, and streaming work in A2A, when you actually need it, and a minimal Python client and server you can run.
You built an agent that books travel. Another team built one that handles expense policy. A vendor ships a third that does invoice reconciliation. Now someone asks: "Can the travel agent check policy before booking?"
The naive answers are bad. Wrap the other agent as an MCP tool and you lose long-running task state, mid-task clarification questions, and streaming progress. Import their code and you couple deploy cycles, frameworks, and secrets. Copy their prompt and it drifts the next week.
A2A (Agent2Agent) exists for exactly this gap. It was originally released by Google and is now a Linux Foundation project, alongside MCP under the Agentic AI Foundation. This post explains what A2A actually standardises, how it differs from MCP, and gives you a minimal implementation to read and run.
Protocol details evolve. Method names and well-known paths below match the A2A spec at the time of writing; check the official spec at a2a-protocol.org before you build against it.
MCP and A2A solve different problems
| MCP | A2A | |
|---|---|---|
| Direction | Vertical: agent โ tools, data, APIs | Horizontal: agent โ agent |
| The other side is | A deterministic function or resource | Another autonomous agent with its own LLM, memory, and policy |
| Interaction shape | Request โ response | Long-running task with a lifecycle |
| Opaque internals | No, you see the tool schema | Yes, you see only the skills the agent advertises |
| Typical use | "Query this database" | "Reconcile these invoices and tell me what looks wrong" |
The quick test: if the callee needs to reason, ask you questions, or run for minutes, it is an agent, not a tool. Use A2A. If it is a function with a typed input and output, use MCP (or a plain API).
In production you usually combine them: A2A routes the task to a specialist agent, and that agent uses MCP to reach its own tools. Our MCP server guide covers the tool side.
The four concepts you need
1. Agent Card. A JSON document, served at a well-known URL (/.well-known/agent-card.json in recent spec versions; older drafts used agent.json), that describes the agent: name, endpoint URL, supported capabilities (streaming, push notifications), auth schemes, and a list of skills. This is discovery. A client reads the card, then decides whether and how to call.
2. Task. The unit of work. The client sends a message; the server creates a task with an ID and moves it through states. The common ones are submitted, working, input-required, completed, failed, and canceled (newer versions add states like rejected and auth-required). input-required is the key difference from a tool call: the remote agent can pause and ask your agent a question.
3. Message and Parts. Messages carry a role (user or agent) and a list of parts: text, files, or structured data. Results come back as artifacts, also made of parts.
4. Transport. JSON-RPC 2.0 over HTTPS, with Server-Sent Events for streaming. Newer spec versions also describe gRPC and REST bindings. Core JSON-RPC methods include message/send, message/stream, tasks/get, and tasks/cancel.
A minimal server
This sketch uses FastAPI to expose an "expense policy" agent. It implements the Agent Card and a simplified message/send. For real work, use the official SDK (a2a-sdk for Python) which handles task stores, streaming, and spec conformance. The point here is to show the wire shape.
# policy_agent.py (pip install fastapi uvicorn)
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
BASE = "http://localhost:9001"
AGENT_CARD = {
"name": "Expense Policy Agent",
"description": "Checks proposed spend against company policy.",
"url": f"{BASE}/rpc",
"version": "1.0.0",
"capabilities": {"streaming": False},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [{
"id": "check_policy",
"name": "Check expense policy",
"description": "Given a trip or purchase, say if it is within policy.",
"tags": ["finance", "policy"],
"examples": ["Is a $900 flight to Berlin within policy?"],
}],
}
@app.get("/.well-known/agent-card.json")
def card():
return AGENT_CARD
def evaluate(text: str) -> str:
# Replace with an LLM call + MCP tools to your policy store.
return "Within policy." if "$900" in text else "Needs manager approval."
@app.post("/rpc")
async def rpc(req: Request):
body = await req.json()
if body.get("method") != "message/send":
return {"jsonrpc": "2.0", "id": body.get("id"),
"error": {"code": -32601, "message": "Method not found"}}
parts = body["params"]["message"]["parts"]
text = " ".join(p.get("text", "") for p in parts)
task_id = str(uuid.uuid4())
return {
"jsonrpc": "2.0",
"id": body["id"],
"result": {
"kind": "task",
"id": task_id,
"status": {"state": "completed"},
"artifacts": [{
"artifactId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": evaluate(text)}],
}],
},
}
Run it with uvicorn policy_agent:app --port 9001.
A minimal client: discover, then delegate
# travel_agent_client.py (pip install httpx)
import uuid, httpx
REMOTE = "http://localhost:9001"
def discover(base: str) -> dict:
return httpx.get(f"{base}/.well-known/agent-card.json").raise_for_status().json()
def delegate(card: dict, text: str) -> str:
payload = {
"jsonrpc": "2.0",
"id": str(uuid.uuid4()),
"method": "message/send",
"params": {"message": {
"role": "user",
"messageId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": text}],
}},
}
r = httpx.post(card["url"], json=payload, timeout=60).raise_for_status().json()
if "error" in r:
raise RuntimeError(r["error"])
task = r["result"]
if task["status"]["state"] == "input-required":
# Your agent must answer the question, then continue the same task
# by sending another message with the task's id.
raise NotImplementedError("handle clarification turn")
return task["artifacts"][0]["parts"][0]["text"]
card = discover(REMOTE)
skills = {s["id"] for s in card["skills"]}
assert "check_policy" in skills, "remote agent does not offer the skill we need"
print(delegate(card, "Is a $900 flight to Berlin within policy?"))
Notice what the client does not know: which model the policy agent uses, what its prompt says, or what tools it holds. That opacity is the feature. Teams can change internals without breaking consumers, as long as the card's skills stay stable.
Handling long-running work
For anything beyond a few seconds, do not hold an HTTP request open and hope.
- Streaming (
message/stream): the server pushes status and artifact updates over SSE. Good when a human is watching progress. - Polling (
tasks/get): the client re-checks by task ID. Simple and robust. Use exponential backoff. - Push notifications: the client registers a webhook; the server calls it when state changes. Best for jobs that run minutes to hours. Verify webhook authenticity.
Treat the task ID as your idempotency handle. If your client crashes mid-call, it should be able to resume with tasks/get rather than resubmit and double-execute. This is the same discipline described in our post on durable execution for agents.
Do you actually need A2A? A decision checklist
Use A2A when at least two of these are true:
- The remote capability is owned by a different team, vendor, or org.
- It runs on a different framework or language than yours.
- It needs multi-turn clarification (
input-required). - Tasks run longer than one request timeout.
- You need to swap or add remote agents without redeploying the caller.
Stay with in-process multi-agent orchestration (LangGraph, your own router) when all agents live in one codebase and one deploy. A network protocol there is pure overhead. See our LangGraph production guide for that pattern.
Common pitfalls
- Treating a remote agent like a trusted function. Its output is untrusted input to your LLM. Remote agents can be compromised or return injected instructions. Apply the same defences as for tool output; see our prompt injection guide.
- Trusting the Agent Card blindly. A card is self-declared. Pin allowed agent URLs, verify TLS, and prefer signed cards where your spec version supports them. Never let an LLM pick an arbitrary URL to delegate to.
- No auth. The card declares required schemes (OAuth 2.0, API key, mTLS, OpenID Connect). Implement them. On-behalf-of user identity needs explicit token exchange, covered in our agent identity and OAuth post.
- Cost and latency blind spots. One user request can fan out into several remote agents, each calling an LLM. Propagate a trace ID and a budget. Our cost attribution and tracing posts apply directly.
- Ignoring
failedandinput-required. Happy-path-only clients hang forever. Set timeouts, cap clarification rounds, and define what your agent does when the remote cannot answer. - Over-granular skills. If your card lists 40 skills, callers' LLMs will pick wrong ones. Advertise a few well-described capabilities with examples.
- Building A2A where an API would do. If the callee is deterministic, a typed API or MCP tool is cheaper, faster, and easier to test.
For product managers
A2A changes the build-vs-buy conversation. Instead of "integrate the vendor's SDK", the question becomes "does the vendor expose a conformant agent, and what is its SLA, auth model, and evaluation story?" Ask for the Agent Card, the list of skills, expected latency per task, and how failures surface. Then add the remote agent to your eval suite as a black box, since you cannot inspect its internals. Our trajectory evaluation guide shows how to test multi-step behaviour end to end.
Next steps
- Install the official A2A Python SDK and rebuild the example above with its server classes, streaming, and a real task store.
- Wrap one existing internal agent with an Agent Card and call it from a second one.
- Add auth, a delegation allowlist, and trace propagation before showing it to anyone.
- Write three eval cases for the remote call: success,
input-required, andfailed.
Want to build multi-agent systems like this with feedback from practitioners? Take a look at the Joinloop AI engineering cohort.
Want to build this yourself?
Join a live cohort and go from reading about AI to shipping it, with practitioners who do it daily.