MCP lets AI talk to tools. Function calling lets AI take actions. But what happens when you need AI talking to AI?
Your travel agent needs to coordinate with a booking agent, a weather agent, and a budgeting agent. Each is built by a different team, runs on a different stack, and knows nothing about the others.
How do they discover each other? How do they negotiate capabilities? How do they hand off tasks and stream results?
Google's A2A (Agent-to-Agent) protocol answers all of this. It's HTTP-based, JSON-first, and designed for the world where autonomous agents need to collaborate across organizational boundaries.
Why Not Just Use MCP?
MCP connects a model to tools. The model is the brain; tools are the hands.
A2A connects an agent to another agent. Both are brains. Both can make decisions. The interaction is a conversation between peers, not a command-and-control hierarchy.
MCP A2A
| Relationship | Model β Tool (master/servant) | Agent β Agent (peer-to-peer) |
|---|---|---|
| Intelligence | Only the model is smart | Both sides are smart |
| Communication | JSON-RPC, command/response | Task-based, conversational |
| Discovery | Server declares tools | Agent Card with capabilities |
| Use case | AI reads files, queries DB | AI delegates to another AI |
They're complementary, not competing. An agent uses MCP for its tools and A2A to collaborate with other agents.
A2A Architecture: Four Building Blocks
- Agent Card β The Business Card
Every A2A agent publishes an Agent Card β a JSON document describing who it is and what it can do:
json
{
"name": "Weather Agent",
"description": "Provides real-time weather data and forecasts for any location worldwide",
"url": "https://weather-agent.example.com",
"version": "1.0.0",
"capabilities": {
"streaming": true,
"pushNotifications": true},
"skills": [
{
"id": "current-weather",
"name": "Current Weather",
"description": "Get current weather conditions for a city",
"inputModes": ["text"],
"outputModes": ["text", "json"]
},
{
"id": "forecast",
"name": "Weather Forecast",
"description": "Get 7-day weather forecast",
"inputModes": ["text"],
"outputModes": ["text", "json"]
}],
"authentication": {
"schemes": ["bearer"]}
}
Agent Cards are hosted at a well-known URL: https://agent.example.com/.well-known/agent.json
Any client can discover an agent by fetching its card. No registration, no marketplace, no middleman.
- Tasks β Units of Work
Everything in A2A revolves around tasks. A client creates a task, the agent processes it, and the task goes through states:
Task Lifecycle:
submitted β working β completed
β failed
β canceled
β input-required (agent needs more info)json
{
"id": "task-abc123",
"status": {
"state": "working",
"message": "Fetching weather data for Mumbai..."},
"artifacts": [],
"history": [
{
"role": "user",
"parts": [
{"type": "text", "text": "What's the weather in Mumbai today?"}
]
}]
}
- Messages β The Conversation
Tasks contain messages (called "history"). Each message has parts β text, files, or structured data:
json
{
"role": "agent",
"parts": [
{
"type": "text",
"text": "Here's the current weather in Mumbai:"
},
{
"type": "data",
"data": {
"temperature": 32,
"unit": "celsius",
"conditions": "partly cloudy",
"humidity": 75
}
}]
}
Parts can carry multiple content types β text for humans, structured data for machines, files for media. This multi-modal approach means agents can communicate in whatever format makes sense.
- Artifacts β The Outputs
When an agent produces a result, it creates an artifact:
json
{
"name": "weather-report",
"parts": [
{
"type": "text",
"text": "Mumbai Weather Report - January 15, 2025\n\nTemperature: 32Β°C\nConditions: Partly Cloudy\nHumidity: 75%\nWind: 12 km/h NW"
}]
}
Artifacts are the final deliverables β they persist after the task completes and can be referenced by other agents.
π§
The A2A Protocol: HTTP Endpoints
A2A is a REST-ish protocol over HTTP. Here are the core endpoints:
Endpoint Method Purpose
/.well-known/agent.json GET Agent Card (discovery)
| /tasks/send | POST | Create or update a task |
|---|---|---|
| /tasks/sendSubscribe | POST | Create task + subscribe to SSE stream |
| /tasks/get | POST | Get task status and history |
| /tasks/cancel | POST | Cancel a running task |
| /tasks/pushNotification/set | POST | Register webhook for updates |
| /tasks/pushNotification/get | POST | Get notification config |
Creating a Task
python
import httpx
Discover the agent
card = httpx.get("https://weather-agent.example.com/.well-known/agent.json").json()
print(f"Found agent: {card['name']} with skills: {[s['name'] for s in card['skills']]}")Create a task
task = httpx.post(f"{card['url']}/tasks/send", json={
"jsonrpc": "2.0",
"id": "req-1",
"method": "tasks/send",
"params": {
"id": "task-001",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "7-day forecast for Mumbai"}]
}
}}).json()
print(f"Task status: {task['result']['status']['state']}")"completed" or "working"
Streaming Results
For long-running tasks, use Server-Sent Events:
python
import httpx
Subscribe to task updates via SSE
with httpx.stream("POST", f"{card['url']}/tasks/sendSubscribe", json={
"jsonrpc": "2.0",
"id": "req-2",
"method": "tasks/sendSubscribe",
"params": {
"id": "task-002",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "Detailed analysis of Mumbai weather trends"}]
}
}}) as response:
for line in response.iter_lines():
if line.startswith("data: "):
event = json.loads(line[6:])
status = event.get("result", {}).get("status", {})
print(f"Status: {status.get('state')} - {status.get('message', '')}")# Check for artifacts
artifact = event.get("result", {}).get("artifact")
if artifact:
print(f"Got artifact: {artifact['name']}")π
Multi-Agent Orchestration
Here's where A2A shines β a coordinator agent delegating to specialized agents:
python
class TravelPlannerAgent:
"""Coordinator that delegates to specialized agents."""def __init__(self):
# Discover agents
self.weather = discover_agent("https://weather.example.com")
self.flights = discover_agent("https://flights.example.com")
self.hotels = discover_agent("https://hotels.example.com")
self.budget = discover_agent("https://budget.example.com")async def plan_trip(self, destination, dates, budget):
# Launch parallel tasks to different agents
weather_task = self.weather.send_task(
f"Weather in {destination} during {dates}"
)
flights_task = self.flights.send_task(
f"Cheapest flights to {destination} for {dates}"
)
hotels_task = self.hotels.send_task(
f"Hotels in {destination} under ${budget/2} per night"
)# Wait for all agents to complete
weather, flights, hotels = await asyncio.gather(
weather_task, flights_task, hotels_task
)# Send combined results to budget agent
budget_task = await self.budget.send_task(
f"Analyze this trip plan:\n"
f"Weather: {weather.artifacts}\n"
f"Flights: {flights.artifacts}\n"
f"Hotels: {hotels.artifacts}\n"
f"Total budget: ${budget}"
)return budget_task.artifactsββββββββββββββββββββ
β Travel Planner β (Coordinator)
β Agent β
ββββββββ¬ββββββββββββ
β
ββββ Weather Agent βββ "32Β°C, partly cloudy, pack light"
β
ββββ Flights Agent βββ "Air India βΉ8,500, IndiGo βΉ7,200"
β
ββββ Hotels Agent βββ "Taj: βΉ12K/night, OYO: βΉ2K/night"
β
ββββ Budget Agent βββ "Total: βΉ45,000. Under budget by βΉ5K"A2A vs Other Agent Protocols
Feature A2A (Google) MCP (Anthropic) AutoGen CrewAI
| Scope | Agent β Agent | Model β Tools | Framework | Framework |
|---|---|---|---|---|
| Transport | HTTP/SSE | stdio/HTTP | In-process | In-process |
| Discovery | Agent Cards | Server lists tools | Code-defined | Code-defined |
| Streaming | β SSE | β Notifications | β | β |
| Cross-org | β Designed for it | β | β | β |
| Authentication | Built-in schemes | Not specified | N/A | N/A |
| Language-agnostic | β HTTP/JSON | β JSON-RPC | β Python | β Python |
The key distinction: AutoGen and CrewAI are frameworks (code libraries). A2A and MCP are protocols (standards). You can build with any framework and communicate via A2A.
Input-Required: Multi-Turn Agent Conversations
Agents can request more information mid-task, creating multi-turn conversations:
json
// Agent responds with input-required state
{
"id": "task-003",
"status": {
"state": "input-required",
"message": "I found 15 hotels. What's your preference?"},
"history": [
{
"role": "user",
"parts": [{"type": "text", "text": "Find hotels in Mumbai"}]
},
{
"role": "agent",
"parts": [{"type": "text", "text": "I found 15 hotels. Do you prefer: (1) Budget (under βΉ3K/night), (2) Mid-range (βΉ3K-8K), or (3) Luxury (βΉ8K+)?"}]
}]
}
python
Client sends follow-up
response = httpx.post(f"{url}/tasks/send", json={
"jsonrpc": "2.0",
"id": "req-3",
"method": "tasks/send",
"params": {
"id": "task-003", # Same task ID β continues the conversation
"message": {
"role": "user",
"parts": [{"type": "text", "text": "Budget please, under 3K"}]
}
}}).json()
π¬
Building an A2A Agent
Here's a minimal A2A agent implementation:
python
from flask import Flask, request, jsonify, Response
import json
import uuid
app = Flask(__name__)
tasks = {}Agent Card
AGENT_CARD = {
"name": "Sentiment Analyzer",
"description": "Analyzes text sentiment using AI",
"url": "http://localhost:5000",
"version": "1.0.0",
"capabilities": {"streaming": False, "pushNotifications": False},
"skills": [{
"id": "analyze",
"name": "Sentiment Analysis",
"description": "Determine if text is positive, negative, or neutral",
"inputModes": ["text"],
"outputModes": ["text", "json"]
}]}
@app.route("/.well-known/agent.json")
def agent_card():
return jsonify(AGENT_CARD)@app.route("/tasks/send", methods=["POST"])
def send_task():
req = request.json
params = req["params"]
task_id = params.get("id", str(uuid.uuid4()))
message = params["message"]
text = message["parts"][0]["text"]
# Simple sentiment analysis (replace with actual model)
sentiment = analyze_sentiment(text)
task = {
"id": task_id,
"status": {"state": "completed"},
"artifacts": [{
"name": "sentiment-result",
"parts": [
{"type": "text", "text": f"Sentiment: {sentiment['label']}"},
{"type": "data", "data": sentiment}
]
}],
"history": [message, {
"role": "agent",
"parts": [{"type": "text", "text": f"Analysis complete: {sentiment['label']}"}]
}]
}
tasks[task_id] = task
return jsonify({"jsonrpc": "2.0", "id": req["id"], "result": task})def analyze_sentiment(text):
# Your ML model here
return {"label": "positive", "score": 0.92, "text": text}if __name__ == "__main__":
app.run(port=5000)π‘οΈ
Practical Takeaways
A2A is for agent-to-agent communication β MCP for tools, A2A for peer agents
Agent Cards enable discovery β any agent can find and understand another via .well-known/agent.json
Tasks are the unit of work β submitted β working β completed/failed, with history and artifacts
SSE streaming handles long-running tasks β real-time progress updates
Input-required enables multi-turn β agents can ask for clarification mid-task
It's a protocol, not a framework β build with any language/framework, communicate via HTTP
π¦
What's Next?
Episode 82: Webhooks & Event-Driven AI β A2A uses SSE for streaming. But real production systems need more: webhooks, message queues, async pipelines. We'll explore how to build event-driven AI architectures that process triggers, handle backpressure, and scale to millions of events.
β Previous
Ep 80: Structured Output / JSON Mode
Next β
Ep 82: Webhooks & Event-Driven AI
Next: Episode 82 β Webhooks & Event-Driven AI
Request/response works for chatbots. Production AI systems aren't chatbots. They need event-driven architectures.
This is part of a 98-episode series covering AI engineering from tokens to production deployment.