Lab 055: A2A + MCP Full Stack β Agent Interoperability CapstoneΒΆ
The Three Agentic Protocols
This capstone covers A2A + MCP. The third protocol β AG-UI (agentβuser interaction) β is covered in Lab 077.
What You'll LearnΒΆ
- How A2A + MCP work together in a full-stack multi-agent architecture
- Analyze a travel planner system with 3 specialized agents using delegation traces
- Distinguish A2A calls (agent-to-agent delegation) from MCP calls (agent-to-tool access)
- Perform token cost analysis across a distributed agent system
- Understand error handling and retry patterns in multi-agent workflows
- Apply design principles for building production-grade agent architectures
IntroductionΒΆ
A2A and MCP are complementary protocols that serve different roles in a multi-agent system:
| Protocol | Role | Example |
|---|---|---|
| A2A | Agent-to-agent task delegation | Coordinator asks FlightAgent to find flights |
| MCP | Agent-to-tool access | FlightAgent calls a booking API via MCP server |
In this capstone lab, you'll analyze a travel planner system that uses both protocols. The Coordinator agent receives a customer request and delegates sub-tasks to specialized agents via A2A. Each specialized agent then uses MCP to access its back-end tools and APIs.
The ArchitectureΒΆ
Customer Request
β
βΌ
ββββββββββββββββββββ
β Coordinator β
β Agent β
ββββββββ¬ββββββββββββ
β A2A
βββββββββββββββββΌββββββββββββββββ
βΌ βΌ βΌ
βββββββββββββββ βββββββββββββββ ββββββββββββββββ
β FlightAgent β β HotelAgent β β Itinerary β
β β β β β Agent β
ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬ββββββββ
MCP β MCP β MCP β
βΌ βΌ βΌ
βββββββββββββββ βββββββββββββββ ββββββββββββββββ
β booking_api β β booking_api β β maps_api β
β pricing_api β β reviews_api β β calendar_api β
β payment_api β β β β weather_api β
βββββββββββββββ βββββββββββββββ ββββββββββββββββ
The delegation traces dataset (delegation_traces.csv) captures 20 events from a complete travel planning session β 8 A2A calls between agents and 12 MCP calls from agents to tools.
Why Two Protocols?
A2A handles the social layer β agents discovering, negotiating, and delegating tasks to peers. MCP handles the tool layer β agents accessing databases, APIs, and external services. Separating these concerns enables independent scaling, security boundaries, and protocol evolution.
PrerequisitesΒΆ
| Requirement | Why |
|---|---|
| Python 3.10+ | Analyze delegation traces |
pandas library |
DataFrame operations on trace data |
π¦ Supporting FilesΒΆ
Download these files before starting the lab
Save all files to a lab-055/ folder in your working directory.
| File | Description | Download |
|---|---|---|
broken_delegation.py |
Bug-fix exercise (3 bugs + self-tests) | π₯ Download |
delegation_traces.csv |
Dataset | π₯ Download |
Step 1: Understanding the ArchitectureΒΆ
Before diving into data, understand what each component does:
Agent RolesΒΆ
| Agent | A2A Role | MCP Tools |
|---|---|---|
| Coordinator | Receives customer request, delegates to specialists | None (orchestration only) |
| FlightAgent | Finds and books flights | booking_api, pricing_api, payment_api |
| HotelAgent | Finds and books hotels | booking_api, reviews_api |
| ItineraryAgent | Plans and updates itineraries | maps_api, calendar_api, weather_api |
Call FlowΒΆ
- Customer sends a travel request to the Coordinator
- Coordinator uses A2A to delegate sub-tasks (find flights, find hotels, plan itinerary)
- Each specialist uses MCP to call its back-end tools
- Results flow back through A2A to the Coordinator
- Coordinator assembles the final response
Protocol BoundariesΒΆ
| Boundary | Protocol | Auth |
|---|---|---|
| Customer β Coordinator | HTTP/API | API key |
| Coordinator β Specialists | A2A | OAuth 2.0 |
| Specialists β Tools | MCP | Service-to-service tokens |
OAuth Across A2A Boundaries
When the Coordinator delegates to FlightAgent via A2A, it must pass an OAuth token scoped to the customer's permissions. The FlightAgent then uses a separate service token for its MCP calls to the booking API. This two-layer auth model prevents privilege escalation.
Step 2: Load and Explore Delegation TracesΒΆ
Load the trace data containing all 20 events from the travel planning session:
import pandas as pd
traces = pd.read_csv("lab-055/delegation_traces.csv")
print(f"Total events: {len(traces)}")
print(f"Unique request IDs: {traces['request_id'].nunique()}")
print(f"Protocols: {traces['protocol'].unique().tolist()}")
print(f"Statuses: {traces['status'].unique().tolist()}")
print(f"\nFirst 5 events:")
print(traces.head().to_string(index=False))
Expected output:
Total events: 20
Unique request IDs: 8
Protocols: ['A2A', 'MCP']
Statuses: ['OK', 'ERROR']
First 5 events:
request_id source_agent target_agent protocol action duration_ms tokens_used status
R001 Coordinator FlightAgent A2A find_flights 2500 450 OK
R001 FlightAgent booking_api MCP search_flights 1800 0 OK
R001 FlightAgent pricing_api MCP get_prices 600 0 OK
R002 Coordinator HotelAgent A2A find_hotels 3200 520 OK
R002 HotelAgent booking_api MCP search_hotels 2400 0 OK
Step 3: Analyze A2A vs MCP Call PatternsΒΆ
Separate the traces by protocol to understand the delegation structure:
3a β Call Counts by ProtocolΒΆ
a2a_calls = traces[traces["protocol"] == "A2A"]
mcp_calls = traces[traces["protocol"] == "MCP"]
print(f"A2A calls (agent β agent): {len(a2a_calls)}")
print(f"MCP calls (agent β tool): {len(mcp_calls)}")
print(f"Total calls: {len(traces)}")
Expected output:
3b β A2A Delegation BreakdownΒΆ
print("A2A Delegations (Coordinator β Specialists):")
print(a2a_calls[["request_id", "source_agent", "target_agent", "action", "status"]].to_string(index=False))
Expected output:
A2A Delegations (Coordinator β Specialists):
request_id source_agent target_agent action status
R001 Coordinator FlightAgent find_flights OK
R002 Coordinator HotelAgent find_hotels OK
R003 Coordinator ItineraryAgent plan_itinerary OK
R004 Coordinator FlightAgent book_flight OK
R005 Coordinator HotelAgent book_hotel OK
R006 Coordinator FlightAgent find_flights ERROR
R007 Coordinator ItineraryAgent update_itinerary OK
R008 Coordinator HotelAgent cancel_hotel OK
3c β MCP Tool Usage by AgentΒΆ
print("MCP tool calls per agent:")
print(mcp_calls.groupby("source_agent")["action"].count().to_string())
print(f"\nUnique MCP tools used: {mcp_calls['target_agent'].nunique()}")
Expected output:
MCP tool calls per agent:
source_agent
FlightAgent 5
HotelAgent 3
ItineraryAgent 4
Unique MCP tools used: 7
3d β Error AnalysisΒΆ
errors = traces[traces["status"] == "ERROR"]
print(f"Total errors: {len(errors)}")
print(f"\nFailed events:")
print(errors[["request_id", "source_agent", "target_agent", "protocol", "action"]].to_string(index=False))
Expected output:
Total errors: 2
Failed events:
request_id source_agent target_agent protocol action
R006 Coordinator FlightAgent A2A find_flights
R006 FlightAgent booking_api MCP search_flights
Cascading Failures
Notice that R006 has errors in both the A2A call and the MCP call. When the booking_api MCP tool fails, the FlightAgent cannot complete the A2A task β the error cascades upward. Production systems need retry logic and circuit breakers at both protocol boundaries.
Step 4: Token Cost AnalysisΒΆ
A2A calls consume LLM tokens (agents reason about tasks), while MCP calls are typically token-free (direct API calls):
total_tokens = traces["tokens_used"].sum()
a2a_tokens = a2a_calls["tokens_used"].sum()
mcp_tokens = mcp_calls["tokens_used"].sum()
print(f"Total tokens consumed: {total_tokens}")
print(f" A2A tokens: {a2a_tokens} ({a2a_tokens/total_tokens*100:.0f}%)")
print(f" MCP tokens: {mcp_tokens} ({mcp_tokens/total_tokens*100:.0f}%)")
print(f"\nTokens per A2A call:")
print(a2a_calls[["request_id", "action", "tokens_used"]].to_string(index=False))
Expected output:
Total tokens consumed: 3330
A2A tokens: 3330 (100%)
MCP tokens: 0 (0%)
Tokens per A2A call:
request_id action tokens_used
R001 find_flights 450
R002 find_hotels 520
R003 plan_itinerary 680
R004 book_flight 380
R005 book_hotel 350
R006 find_flights 460
R007 update_itinerary 290
R008 cancel_hotel 200
Cost BreakdownΒΆ
COST_PER_1K_TOKENS = 0.005 # example: GPT-4o-mini pricing
cost = total_tokens / 1000 * COST_PER_1K_TOKENS
print(f"Estimated cost at ${COST_PER_1K_TOKENS}/1K tokens: ${cost:.4f}")
print(f"Average tokens per A2A call: {a2a_tokens / len(a2a_calls):.0f}")
print(f"Most expensive call: {a2a_calls.loc[a2a_calls['tokens_used'].idxmax(), 'action']} "
f"({a2a_calls['tokens_used'].max()} tokens)")
Step 5: Error Handling and Retry PatternsΒΆ
Analyze how errors propagate and design retry strategies:
5a β Error Rate by ProtocolΒΆ
for protocol in ["A2A", "MCP"]:
subset = traces[traces["protocol"] == protocol]
error_count = (subset["status"] == "ERROR").sum()
total = len(subset)
print(f"{protocol}: {error_count}/{total} errors ({error_count/total*100:.0f}%)")
Expected output:
5b β Latency AnalysisΒΆ
print("Average latency by protocol:")
print(traces.groupby("protocol")["duration_ms"].mean().to_string())
print(f"\nSlowest call: {traces.loc[traces['duration_ms'].idxmax(), 'action']} "
f"({traces['duration_ms'].max()} ms)")
print(f"Fastest call: {traces.loc[traces['duration_ms'].idxmin(), 'action']} "
f"({traces['duration_ms'].min()} ms)")
5c β Retry Design PatternsΒΆ
| Pattern | A2A Layer | MCP Layer |
|---|---|---|
| Retry | Retry the full A2A task with exponential backoff | Retry the specific tool call |
| Fallback | Route to an alternative agent | Use a backup API endpoint |
| Circuit Breaker | Stop delegating to a failing agent | Stop calling a failing tool |
| Timeout | Set per-task timeout in A2A request | Set per-call timeout in MCP |
| Idempotency | Include idempotency key in task ID | Include in tool call params |
Step 6: Design PrinciplesΒΆ
Based on this analysis, here are the key principles for building A2A + MCP systems:
| Principle | Description |
|---|---|
| Separation of Concerns | A2A for delegation, MCP for tool access β don't mix them |
| Token Awareness | Only A2A calls consume LLM tokens; optimize agent prompts |
| Auth Boundaries | Separate OAuth scopes for A2A (user context) and MCP (service context) |
| Error Isolation | Handle errors at each protocol boundary independently |
| Observability | Trace both A2A and MCP calls with correlated request IDs |
| Idempotency | Design all operations to be safely retryable |
π Bug-Fix ExerciseΒΆ
The file lab-055/broken_delegation.py has 3 bugs in the trace analysis functions. Can you find and fix them all?
Run the self-tests to see which ones fail:
You should see 3 failed tests. Each test corresponds to one bug:
| Test | What it checks | Hint |
|---|---|---|
| Test 1 | A2A call count | Should filter by protocol == "A2A", not count all rows |
| Test 2 | Average latency | Should include ALL requests (including errors), not just OK |
| Test 3 | Success rate | Should divide by total request count, not unique agent count |
Fix all 3 bugs, then re-run. When you see π All 3 tests passed, you're done!
π§ Knowledge CheckΒΆ
Q1 (Multiple Choice): What is the key difference between A2A and MCP?
- A) A2A is faster than MCP
- B) A2A handles agent-to-agent delegation; MCP handles agent-to-tool access
- C) A2A uses REST; MCP uses GraphQL
- D) A2A is for cloud agents; MCP is for local agents
β Reveal Answer
Correct: B) A2A handles agent-to-agent delegation; MCP handles agent-to-tool access
A2A (Agent-to-Agent) is a peer-to-peer protocol for agents to discover each other and delegate tasks. MCP (Model Context Protocol) is a client-server protocol for agents to access tools, databases, and APIs. They are complementary β a multi-agent system typically uses both.
Q2 (Multiple Choice): Why does the architecture use separate protocols for agent communication and tool access?
- A) To reduce the total number of API calls
- B) Because agents and tools use different programming languages
- C) To enable independent scaling, security boundaries, and protocol evolution
- D) Because A2A is proprietary and MCP is open source
β Reveal Answer
Correct: C) To enable independent scaling, security boundaries, and protocol evolution
Separating agent-to-agent communication (A2A) from agent-to-tool access (MCP) lets you scale each layer independently, enforce different auth scopes at each boundary (user-context OAuth for A2A, service tokens for MCP), and evolve the protocols without breaking the other layer.
Q3 (Run the Lab): How many A2A calls are in the delegation traces?
Filter π₯ delegation_traces.csv by protocol == "A2A" and count the rows.
β Reveal Answer
8
There are 8 A2A calls β one for each request from the Coordinator to a specialist agent (R001βR008). The remaining 12 events are MCP calls from specialists to their back-end tools.
Q4 (Run the Lab): How many MCP calls are in the delegation traces?
Filter delegation_traces.csv by protocol == "MCP" and count the rows.
β Reveal Answer
12
There are 12 MCP calls β FlightAgent makes 5 (search, pricing, booking, payment, search-retry), HotelAgent makes 3 (search, booking, booking), and ItineraryAgent makes 4 (directions, availability, forecast, update).
Q5 (Run the Lab): What is the total number of tokens consumed across all events?
Sum the tokens_used column in delegation_traces.csv.
β Reveal Answer
3330
Only A2A calls consume tokens (LLM reasoning). The 8 A2A calls use: 450 + 520 + 680 + 380 + 350 + 460 + 290 + 200 = 3330 tokens. All 12 MCP calls use 0 tokens (direct API calls).
SummaryΒΆ
| Topic | What You Learned |
|---|---|
| Architecture | A2A for agent delegation + MCP for tool access in a unified system |
| Travel Planner | Coordinator β FlightAgent / HotelAgent / ItineraryAgent |
| Call Patterns | 8 A2A delegations triggering 12 MCP tool calls |
| Token Costs | Only A2A calls consume LLM tokens (3330 total) |
| Error Handling | Cascading failures across protocol boundaries; retry patterns |
| Design Principles | Separation of concerns, auth boundaries, observability |