Agent-to-Agent (A2A) and MCP: How They Fit Together
The Hook: Two Protocols, One Confusing Acronym Soup
If you've spent any time building with AI agents in the last year, you've probably run into two acronyms that sound suspiciously similar: A2A and MCP. Both showed up around the same time, both are described as "protocols for agents," and both get thrown around in the same breath in conference talks and Twitter threads. It's easy to assume they're competitors, or worse, that you have to pick one.
They're not competitors. They solve two completely different problems, and once you see the distinction clearly, the whole "multi-agent architecture" conversation stops being mysterious. MCP (Model Context Protocol) standardizes how a single agent talks to tools, files, databases, and APIs. A2A (Agent-to-Agent protocol) standardizes how one agent talks to *another agent* — a peer, not a tool. One is about giving an agent hands. The other is about letting agents delegate work to each other without knowing each other's internals.
This article breaks down what each protocol actually does, where the boundary between them sits, and how you'd architect a real system — say, a trip-planning assistant or an internal support bot — that uses both. By the end you should be able to look at any "multi-agent" diagram and immediately say "that arrow is MCP" or "that arrow is A2A."
What Problem MCP Solves
Before MCP, every AI application that wanted to connect a language model to external data or tools had to write custom glue code. Want your chatbot to query a Postgres database? Write a custom function-calling integration. Want it to read files from Google Drive? Another custom integration. Every vendor, every tool, every data source meant another one-off adapter, and none of that code was reusable across projects.
MCP, introduced by Anthropic in late 2024, standardizes this. It defines a client-server protocol where an MCP server exposes a set of capabilities — tools (functions the model can call), resources (data the model can read), and prompts (reusable templates) — and an MCP client (usually embedded inside an agent or an application like Claude Desktop or Claude Code) discovers and invokes those capabilities over a consistent JSON-RPC-based wire format.
The mental model: think of MCP as a USB-C port for AI applications. Before USB-C, every device had its own charging cable. USB-C didn't make devices smarter, it made *connecting* them predictable. MCP does the same thing for agents and tools — it doesn't make the model smarter, it makes plugging in a new capability a matter of pointing at a server rather than writing bespoke integration code.
A minimal MCP tool definition looks like this in Python using the official SDK:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""Return a short-range weather forecast for a city."""
# In a real server this would call a weather API
return f"Forecast for {city}: sunny, {days}-day outlook stable."
if __name__ == "__main__":
mcp.run(transport="stdio")Any MCP-compatible client — Claude, an IDE agent, a custom LangGraph pipeline — can now discover get_forecast and call it, without the client author knowing anything about how the weather server is implemented. That's the whole point: decoupling the *agent's reasoning loop* from the *tool's implementation*.
What Problem A2A Solves
Now consider a different scenario. You've built an agent that's excellent at planning trips — it knows how to reason about itineraries, budgets, and preferences. Someone else on your team, or at a different company entirely, has built an agent that's excellent at checking real-time flight availability and another that specializes in hotel negotiations. You don't want to rebuild their logic inside your trip-planning agent. You want your agent to delegate a sub-task to their agent, get a result back, and continue reasoning.
This is a fundamentally different integration problem than "call a tool." A tool call is stateless and synchronous in spirit — you invoke a function, you get a return value. But delegating to another *agent* often means:
- The task might take a long time (minutes, hours) and needs asynchronous status updates.
- The remote agent might need to ask clarifying questions back before it can proceed.
- The remote agent has its own internal reasoning, memory, and possibly its own sub-agents or tools — it's opaque by design, not just by accident.
- You need to know what the other agent is even capable of before you send it anything.
A2A, originally introduced by Google and later contributed to the Linux Foundation as an open, vendor-neutral standard, addresses exactly this. It defines how one agent (a "client agent") discovers another agent's capabilities via an Agent Card (a JSON document describing what the agent can do, its authentication requirements, and its endpoint), and then exchanges Tasks with it over a structured protocol that supports long-running work, streaming updates, and multi-turn clarification — all without either side needing to see the other's internal prompts, tools, or model choice.
An Agent Card is deliberately simple and descriptive, something like:
{
"name": "FlightAvailabilityAgent",
"description": "Checks live flight availability and pricing across carriers",
"url": "https://flights.example.com/a2a",
"version": "1.2.0",
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"skills": [
{
"id": "search-flights",
"name": "Search Flights",
"description": "Search for flights given origin, destination, and dates"
}
]
}A client agent fetches this card, decides the remote agent is a good fit for a sub-task, and then sends it a Task — essentially "here's what I need, here's the context, let me know when you're done or if you need more from me." The remote agent replies with structured artifacts (results) and status updates. Crucially, the client agent never needs to know *how* FlightAvailabilityAgent gets its answers — whether it uses MCP internally, calls a legacy SOAP API, or runs its own fine-tuned model is entirely its business.
Why Both Showed Up Around the Same Time
It's worth pausing on why the industry needed two separate specs instead of one. Early "agent frameworks" tried to handle both problems with a single abstraction — usually "tools" for everything, including other agents. That works for demos but breaks down fast in production for a simple reason: tools and agents have different failure modes and different lifecycles.
A tool call fails in predictable ways — bad arguments, a timeout, a 500 from the backend. You retry, you validate the schema, you move on. An agent-to-agent interaction fails in messier ways — the remote agent might genuinely disagree with the premise of the task, might need to ask a follow-up question that changes the shape of the work, or might take an unpredictable amount of time because it's doing its own multi-step reasoning underneath. If you model "call another agent" the same way you model "call a function," you end up either blocking synchronously on something that might take ten minutes, or hacking in polling and callbacks yourself, on every project, the same way teams hacked in custom tool-calling before MCP existed.
That's the real reason two specs emerged instead of one: the underlying interaction shapes are genuinely different, and trying to flatten them into a single abstraction just pushes the complexity back onto whoever's building the system, which is exactly the problem both protocols were created to get rid of.
The Key Distinction: Vertical vs Horizontal
Here's the framing that makes this click for most engineers: MCP is a vertical integration, A2A is a horizontal one.
- MCP goes *down* — from an agent into the tools, data, and systems it needs to act on the world. The things on the other end of an MCP connection are not autonomous; they don't reason, they don't have their own goals, they just expose capabilities and execute them when asked.
- A2A goes *across* — from one autonomous agent to a peer autonomous agent. Both sides of an A2A connection can reason, plan, and potentially even reject or renegotiate a task. Neither side controls the other; they collaborate.
Think about a company org chart. MCP is like an employee using a company's internal tools — a spreadsheet, a CRM, a database dashboard. A2A is like that employee picking up the phone and asking someone in a different department, at a different company even, to handle a piece of the work. You wouldn't describe "using Excel" and "asking a contractor to do a task" as the same kind of relationship, even though both help you get work done. That's exactly the MCP/A2A split.
This is also why the two protocols have different design priorities:
- MCP optimizes for capability discovery and structured invocation — the agent needs to know exactly what parameters a tool takes and what shape the response will be, because the model is going to reason over that response directly.
- A2A optimizes for opacity and long-running collaboration — the calling agent doesn't need or want to know the internal reasoning of the remote agent, and the interaction might span multiple back-and-forth turns instead of a single request/response.
How They Compose in a Real Architecture
The two protocols aren't just compatible, they're designed to be used together, and most non-trivial agent systems will end up using both simultaneously. Consider a customer support system with three agents:
- Orchestrator Agent — the front door. Talks to the end user, decides which specialist to route to.
- Billing Agent — handles refunds, invoices, subscription changes.
- Technical Support Agent — handles bug reports, diagnostics, log analysis.
Internally, each of these agents uses MCP servers to actually get work done. The Billing Agent might connect to an MCP server that exposes tools like lookup_invoice, issue_refund, and update_subscription, backed by your Stripe or Razorpay integration. The Technical Support Agent might connect to an MCP server exposing search_logs, check_service_status, and create_jira_ticket.
But the Orchestrator Agent doesn't talk to those MCP servers directly, and arguably shouldn't — it doesn't need issue_refund in its own tool list, and giving it that power would violate least-privilege. Instead, the Orchestrator talks to the Billing Agent and Technical Support Agent via A2A. It fetches their Agent Cards, sees that Billing can handle "refund-related" skills and Technical Support can handle "diagnostic" skills, and delegates accordingly. Each specialist agent then uses its own MCP connections internally to actually execute the work, and reports back a result artifact over A2A.
Visually, the layering looks like this:
[ User ]
|
v
[ Orchestrator Agent ]
| (A2A: delegate task, get status/result)
|------------------> [ Billing Agent ] --(MCP)--> [ Stripe/Razorpay tools ]
|------------------> [ Tech Support Agent ] --(MCP)--> [ Logs, Jira tools ]This layering gives you clean security boundaries. The Orchestrator's blast radius if compromised is small — it can only ask other agents to do things within their declared skills, not directly call issue_refund. Each specialist agent's MCP tool access is scoped tightly to what it actually needs. That's a much better security posture than one giant agent with every tool bolted onto it.
What Happens Inside the Specialist Agent
It's worth zooming into one box from that diagram to make the composition less abstract. Say the Technical Support Agent receives an A2A task like "customer reports the export feature is timing out." Internally, that agent runs its own reasoning loop, and during that loop it reaches for MCP tools the same way any single agent would:
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
async def diagnose_issue(customer_report: str):
server_params = StdioServerParameters(
command="python",
args=["log_search_server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
# The agent's reasoning loop decides which tool fits
result = await session.call_tool(
"search_logs",
arguments={"query": "export timeout", "window": "24h"}
)
return resultFrom the Technical Support Agent's perspective, this MCP call is a completely ordinary part of its own internal reasoning — no different from how a single-agent chatbot would use MCP. The A2A layer only shows up at the edges: the task came in over A2A, and once diagnose_issue produces a finding, the agent packages that finding as an A2A artifact and sends it back to the Orchestrator. The two protocols never overlap in scope inside this one component; A2A owns the inbound and outbound edges, MCP owns everything the agent does with its own tools in between.
This is the pattern worth internalizing: every agent in a multi-agent system is potentially both an A2A server (from the caller's perspective) and an MCP client (from its own tools' perspective) at the same time. Nesting works cleanly precisely because neither protocol tries to reach past its own boundary — A2A doesn't care how the specialist gets its answer, and MCP doesn't care who asked the agent to look.
A Concrete Code Example: Delegating a Task
Let's make the A2A side concrete with a simplified client interaction. Most A2A implementations follow this rough shape, whether you're using Google's reference SDK or a compatible library:
import httpx
class A2AClient:
def __init__(self, agent_url: str):
self.agent_url = agent_url
self.agent_card = None
def discover(self):
# Fetch the well-known Agent Card describing capabilities
resp = httpx.get(f"{self.agent_url}/.well-known/agent.json")
self.agent_card = resp.json()
return self.agent_card
def send_task(self, message: str, context: dict):
payload = {
"message": {
"role": "user",
"parts": [{"type": "text", "text": message}]
},
"metadata": context
}
resp = httpx.post(f"{self.agent_url}/tasks/send", json=payload)
return resp.json()
# Orchestrator delegating a refund request to the Billing Agent
billing = A2AClient("https://billing.internal.example.com/a2a")
card = billing.discover()
if "issue-refund" in [s["id"] for s in card["skills"]]:
result = billing.send_task(
message="Customer requests a refund for order #48213, duplicate charge.",
context={"user_id": "u_9021", "order_id": "48213"}
)
print(result["status"], result.get("artifacts"))Notice what's absent here: nothing in this code knows that the Billing Agent internally calls an MCP tool named issue_refund. From the Orchestrator's point of view, it just sent a task and got a result. The Billing Agent could rewrite its entire internal tool stack — swap Stripe for a different payment processor, change its MCP server implementation, even change its underlying model — and the Orchestrator's code above would never need to change. That's the decoupling A2A buys you, mirroring what MCP buys you one layer down.
Common Misconceptions Worth Clearing Up
"A2A replaces MCP." No. They were never solving the same problem. You still need MCP (or something like it) for any agent that has to touch real tools and data. A2A just adds a layer above that for agent-to-agent delegation.
"You need A2A to have multiple agents." Not true. Plenty of legitimate multi-agent systems today use a single orchestrating process where a "supervisor" pattern calls sub-agents as ordinary function calls or even as MCP tools themselves (an MCP server can absolutely wrap an entire agent as a single "black box" tool). A2A becomes valuable specifically when the agents are independently deployed, potentially owned by different teams or companies, need asynchronous long-running task semantics, or need capability discovery before you've hardcoded the integration. If your two agents live in the same repo and you control both, a direct function call or a shared MCP server might be simpler and you shouldn't reach for A2A just because it's the trendier acronym.
"MCP is only for local tools." MCP supports both local transports (stdio, common for CLI tools like Claude Code) and remote transports (HTTP with Server-Sent Events, or the newer Streamable HTTP transport), so an MCP server can absolutely live on a remote host, be shared across teams, and serve many clients. The local/remote distinction is a transport detail, not a defining feature of the protocol.
"An agent can't be both an MCP server and an A2A participant." It can be both at once. A well-designed specialist agent might expose an Agent Card and speak A2A to accept delegated tasks from an orchestrator, while simultaneously acting as an MCP client to reach its own backend tools. Nothing prevents a single service from wearing both hats depending on which direction the conversation is going.
Security and Trust Considerations
Because A2A explicitly enables cross-organizational agent communication, it puts more emphasis on things MCP mostly leaves to the deployer: authentication schemes are declared right in the Agent Card (OAuth2, API keys, mutual TLS, etc.), and task payloads are expected to carry enough context for an unfamiliar agent to act correctly and safely. When you're delegating a task to an agent you don't operate, you're trusting it with whatever information you include in that task — treat that boundary the same way you'd treat any third-party API integration: minimize what you share, scope credentials tightly, and validate the artifacts you get back before acting on them.
MCP has an analogous concern one layer down: an MCP server that exposes a run_shell_command tool to a language model is handing that model real, unsandboxed power, and prompt injection through untrusted tool outputs is a documented risk. The rule of thumb across both protocols is the same one that governs all agentic systems — every hop where autonomy changes hands (model to tool, agent to agent) is a place where you should ask "what's the worst this could be asked to do, and have I limited its ability to do it?"
Where This Is Headed
Both protocols are still young and moving fast. MCP's ecosystem has grown quickly — official servers exist for databases, cloud providers, version control, and countless SaaS products, plus a long tail of community servers. A2A, since being handed to the Linux Foundation as a neutral home, is picking up support from an increasing number of agent frameworks and platforms that want their agents to be able to interoperate with agents built on entirely different stacks.
The practical implication for anyone building agent systems today: don't treat this as an either/or decision. Learn MCP first if you haven't, because almost every agent you build will need it to do anything useful in the world. Add A2A when your architecture actually grows multiple independent, autonomous agents that need to hand work to each other — especially across team or organizational boundaries where you want the callee to remain a black box. Trying to force A2A-style delegation semantics onto a simple in-process tool call adds needless complexity, and trying to force MCP's tool-calling model onto a genuinely autonomous peer agent will fight you the whole way, because you'll keep needing things — clarifying turns, long-running status, capability negotiation — that MCP was never designed to express.
If you want to go deeper on the implementation side rather than just the conceptual model, that's exactly the ground we cover in Building & Integrating MCP Servers — where you'll build real MCP servers from scratch, wire them into agent workflows, and see firsthand where the natural seams are for layering A2A-style delegation on top.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.