A2A, MCP, or Just a Tool? Wiring Agents Together in Late 2026
Most teams end up in the same place. There is a code-review agent that someone built in the spring. There is a research agent that reads the internal docs, a release agent that cuts tags and writes the notes, and a triage agent that lives in the ticket system. Each one works. They are mostly complementary, and a couple of them overlap: three of the four can talk to GitHub. Now someone wants a single front door, one agent you can ask "is PR 412 ready to ship?" that pulls in the others when it needs them.
Every framework offers you three ways to do that. You can pass the other agent in as a tool. You can put it behind an MCP server. Or you can talk to it over A2A, the Agent2Agent protocol. The usual advice is "MCP for tools, A2A for agents". That is true, and it doesn't help, because any agent can be made to look like a tool. The real decision is about something else.
Where things stand in October 2026
Both protocols moved a long way this year, and a lot of what was written about them in 2025 is now out of date.
A2A reached 1.0 in March. It has been a Linux Foundation project since June 2025, and the one-year announcement counts more than 150 supporting organisations, with support built into Amazon Bedrock AgentCore Runtime, Azure AI Foundry and Copilot Studio. The 1.0 specification brings:
- Three bindings: JSON-RPC, gRPC and plain HTTP+JSON.
- Signed Agent Cards: the card at
/.well-known/agent-card.jsoncan carry a JWS signature, so a client can check who published it. - A full task lifecycle: states for
INPUT_REQUIREDandAUTH_REQUIREDas well as working, completed, failed and so on. - An explicit version header. Clients send
A2A-Version, and a request without it is treated as 0.3. Most of the A2A code you'll find in blog posts and old repositories is 0.3, so check which version a sample speaks before you copy it.
MCP shipped its largest revision in July. The 2026-07-28 specification made four changes that matter here:
- The core is stateless. The
initializehandshake and theMcp-Session-Idheader are gone. Every request carries its own version and capabilities, so any request can land on any server instance. - Servers ask questions differently. A server that needs something from the client mid-call, such as a confirmation, now returns
input_requiredand the client retries the call with the answers. This is called Multi Round-Trip Requests, and it replaces the old server-to-client calls. - Long-running calls became Tasks. Tasks moved out of the experimental core into an official extension. A tool call can return a durable task handle instead of a result; the client polls
tasks/get, answers questions throughtasks/update, and cantasks/cancel. - Sampling is deprecated. Sampling let a server borrow the client's model. The spec now says new implementations should not adopt it and should call their model provider directly.
Put those side by side and something interesting has happened. On the question of long-running work, MCP and A2A now have the same shape: a durable handle, a state that means "I need input", polling or a subscription, and cancellation. "MCP can't do long-running agents" is no longer true, as long as your host supports the Tasks extension (support still varies by client; the MCP site keeps a client matrix). And with sampling deprecated, an MCP server that runs an agent has to bring its own model and its own API key. That's the honest version of the pattern anyway: an agent behind a tool.
So the two protocols no longer differ in what they can do. They differ in what the contract is for. An MCP tool is a schema-typed call that the caller's model decides to make. An A2A agent is a party with its own identity, its own published skills and its own task lifecycle, and it is built on the assumption that you don't control it.
The question that decides it
The question isn't "is this thing an agent?" Everything in the opening example is an agent. The question is who owns it, and who else needs to call it?
- Your team owns it and only your orchestrator calls it. Pass it in as a tool, in-process. No network, no protocol, nothing to deploy. This should be the default, and most of your agents should stay here.
- Your team owns it, but more than one host should be able to call it, such as your orchestrator, Claude Code, Cursor, or a colleague's agent. Put it behind an MCP server. Every MCP host can then use it with no work on your part.
- Someone else owns it: another team with its own release schedule, a vendor, a partner company. Use A2A. They publish a card, you authenticate, and their agent runs its tasks on their terms.
There's one more factor that can move an agent down this list: does it need to talk back? A specialist that routinely needs a back-and-forth with its caller ("which environment?", "this needs approval from someone with deploy rights") fits A2A's task model better, even inside one company. A specialist from another team that takes a request and returns an answer can be perfectly happy behind MCP.
In-process: the default
In Strands, passing an agent to another agent's tools list is all it takes. Strands wraps it automatically and uses the agent's name and description as the tool spec:
from strands import Agent
reviewer = Agent(
name="reviewer",
description="Reviews a pull request diff and reports findings.",
system_prompt="You review diffs for correctness bugs.",
tools=[github], # shared GitHub MCP server, read-only credential
callback_handler=None, # only the orchestrator streams to the user
)
orchestrator = Agent(
system_prompt="You decide whether a pull request is ready to ship.",
tools=[reviewer, researcher, release_manager],
)There is nothing to operate, which is the whole appeal. Each specialist gets its own context window, its own model and its own tools, and its context starts clean on every call. The orchestrator never sees the reviewer's tools. That's a feature: the orchestrator's tool list stays short, and its model chooses between outcomes rather than between forty GitHub endpoints.
The cost is that the reviewer receives a single string. It doesn't see the conversation the orchestrator has been having. If the user said "ignore the generated files" three turns ago, the reviewer will only know if the orchestrator remembers to say so. You can't rely on that, so write the wrapper yourself and give it typed arguments:
from pydantic import BaseModel
from strands import tool
class Finding(BaseModel):
path: str
line: int
severity: str
summary: str
class Review(BaseModel):
verdict: str # "ship", "fix first" or "needs a human"
findings: list[Finding]
@tool
def review_pull_request(repo: str, pr_number: int, exclude_paths: list[str], focus: str) -> str:
"""Review a pull request and return findings as JSON.
Use for any request to check, review or assess a PR. Reports only: it never
comments, approves or merges. Pass findings to release_manager for that.
Args:
repo: owner/name of the repository.
pr_number: the pull request number.
exclude_paths: globs the user asked to skip, e.g. generated code.
focus: correctness, security or performance.
"""
result = reviewer(
f"Review {repo}#{pr_number}. Focus: {focus}. Skip: {exclude_paths or 'nothing'}.",
structured_output_model=Review,
)
return result.structured_output.model_dump_json()The arguments make the orchestrator's model fill in what the specialist needs. The structured result means the orchestrator reads a verdict instead of guessing at one. This wrapper is also what makes promotion cheap later: the same arguments become an MCP inputSchema or an A2A data part, and the docstring becomes a tool description or an Agent Card skill.
MCP: when the agent has more than one caller
Promote the reviewer to an MCP server on the day someone says "I want this in my IDE too". The Python SDK (v2 speaks the 2026-07-28 spec) makes the server itself small:
from mcp.server import MCPServer
mcp = MCPServer("pr-reviewer")
@mcp.tool()
def review_pull_request(repo: str, pr_number: int, exclude_paths: list[str], focus: str) -> Review:
"""Review a pull request and return findings. Reports only: never comments, approves or merges."""
result = reviewer(
f"Review {repo}#{pr_number}. Focus: {focus}. Skip: {exclude_paths or 'nothing'}.",
structured_output_model=Review,
)
return result.structured_output
mcp.run(transport="streamable-http", stateless_http=True)Returning the Pydantic model gives callers structured content they can read, not prose. Running stateless means you can put three copies behind a plain load balancer. The reviewer calls its own model with its own credentials; with sampling deprecated, that was always going to be the case.
Three things change once the agent is behind MCP.
- Time becomes your problem. A review that takes four minutes will hit somebody's tool-call timeout. The Tasks extension is the proper answer: the server returns a handle and the host polls. But both sides have to opt in, and host support isn't universal yet. If your callers don't support Tasks, keep the call short, or do what the 2026-07-28 announcement itself suggests: have the tool return an ID that the model passes back to a second tool to collect the result.
- Questions go through the protocol. If the agent needs a confirmation mid-call, it can use elicitation, which now travels over Multi Round-Trip Requests. That's enough for "are you sure?". It is not enough for a real conversation, and if you find yourself chaining elicitations, the agent wants to be an A2A peer.
- Your description is now the routing. Every host's model picks tools from their descriptions, and you no longer know which other tools sit beside yours. That means the "never comments, approves or merges" sentence is doing real work. It tells a model that has three GitHub-shaped tools in front of it which one to use.
A2A: when the agent belongs to someone else
Now suppose the security team runs its own agent for dependency and licence checks. They choose its model, they deploy it every Tuesday, and they want to know who is calling it. That's exactly the situation A2A was designed for. Strands supports both sides. The security team serves theirs:
from strands import Agent
from strands.multiagent.a2a import A2AServer
def create_agent(context_id):
return Agent(
name="dependency-audit",
description="Audits a repository's dependencies for known vulnerabilities and licence conflicts.",
tools=[sbom, advisories],
)
A2AServer(agent_factory=create_agent).serve()The factory is the current API; passing a single shared agent is deprecated, because one A2A server handles many conversations. You consume it like any other agent:
from strands.agent.a2a_agent import A2AAgent
audit = A2AAgent(endpoint="https://agents.security.internal")
result = audit("Audit acme/payments-api at the head of PR 412.")And to give it to the orchestrator you wrap it in a @tool, exactly as in the in-process case. Before you do, know what the wrapping throws away. A2A gives you a task you can watch, a state where the remote agent asks you something, artifacts that arrive over time, and a context that spans several messages. A @tool wrapper that returns str(result) flattens all of that into one string. The A2A project's own guidance on A2A and MCP says this is fine for well-defined skills that behave like stateless tools. If you're doing it for every A2A agent you have, though, you probably didn't need A2A.
As of this month there are also some rough edges in the Strands support to plan around. They're all in the Strands A2A guide:
A2AAgentcan't answer the remote agent's questions. If the remote task goes to input-required, answering it means dropping to the rawa2a-sdkclient, and that's Python only.- No Swarm support. A2A agents can be nodes in a
Graph(Python only) but not in aSwarm, because swarm handoffs are tool-based and A2A doesn't support them yet. context_idis not an authentication boundary. It groups messages into a conversation; it doesn't tell you who sent them. Enforce identity at the transport or gateway, and verify the signature on the Agent Card rather than trusting whatever JSON the URL returned.
Overlapping tools
This is where most multi-agent setups get messy, and none of the protocols helps you. In the opening example, the reviewer, the release agent and the triage agent all talk to GitHub. That overlap causes three different problems.
The orchestrator does the work itself. If the orchestrator has GitHub write tools and a reviewer, it will sometimes skip the reviewer and look at the diff itself, because that's the shortest path to an answer. Then you get a review that didn't use the reviewer's prompt, model or checks. The fix is simple: the orchestrator gets no tools that a specialist already has. It routes; it doesn't do.
Two agents do the same side effect. If the reviewer and the release agent can both comment on a pull request, sooner or later they both will, possibly with contradicting conclusions. Reads can overlap freely. Every write needs exactly one owner. Say so in each agent's description ("reports only; never comments"). Then enforce it with credentials, because a description is a request and a token scope is a rule. The reviewer's GitHub credential can't write. The release agent's can.
Every agent has its own copy of the tool. Three agents with three GitHub integrations means three tokens to rotate, three rate-limit budgets, and three places to look when something comments on a PR at 2 a.m. Put the shared tool in one MCP server, and have every agent that needs it connect as a client with its own scoped credential. The 2026-07-28 spec helps here in small but practical ways. Requests now carry Mcp-Method and Mcp-Name headers, so a gateway can meter and audit per tool without parsing request bodies. And tools/list results can be cached with a TTL in a deterministic order, which keeps each agent's prompt cache stable.
One smaller overlap is worth catching too: two agents fetching the same thing. If the reviewer and the audit agent both pull the same 3,000-line diff, you pay for it twice. Pass identifiers between agents ("PR 412 at commit a3f9c1e") rather than content, and let each specialist fetch exactly what it needs from the shared server. It's cheaper, and it means they're looking at the same commit.
What breaks first
Once agents call agents, the failures look different from single-agent failures. Here is what to expect, roughly in the order you'll hit it.
- Timeouts. A nested agent loop is slow, and hosts set their timeouts with ordinary tools in mind. The first production incident is usually a specialist that takes three minutes behind a call that waits for one. Measure each specialist's p95 before you wire it in, and use tasks (MCP or A2A) for anything that doesn't fit comfortably inside the timeout.
- Cost multiplying quietly. Every hop is a full agent loop with its own system prompt, its own tool definitions and its own conversation. A question that costs one orchestrator turn can cost four specialist loops. Log tokens per agent, not per request, or you'll never find out which one is expensive. Remember that prompt caching is per agent: each specialist needs a stable prefix of its own.
- Confident failures. A tool that fails raises an error. An agent that fails usually returns a fluent, plausible, wrong answer. This is why the
Reviewmodel above has averdictthat includes "needs a human": give every specialist a way to say it couldn't do the job, and make the orchestrator check for it. - Lost traces. When the answer is wrong, you need to see which hop went wrong. Put a trace ID in every call (MCP requests have
_meta, A2A messages havemetadata) and send each agent's spans to the same place. Without that, debugging a four-agent system means reading four logs side by side and lining up timestamps.
A practical layout
For the situation I started with (several mostly complementary agents, a few overlapping tools, one front door), this is what I'd build today:
- One orchestrator with no domain tools. Its job is routing, and its tool list is a short set of specialists.
- Your own specialists in-process, as tools, each behind a hand-written
@toolwrapper with typed arguments and a structured result. Most agents never need to leave this stage. - Promote a specialist to an MCP server when it gets a second caller. Because the wrapper already has a schema, promotion is mostly moving code.
- Use A2A where there's an ownership boundary: another team, a vendor, a partner. Use it fully where the remote agent needs to ask questions or run long tasks, and accept the flattening when it's a one-shot skill.
- Put overlapping tools in shared MCP servers, with a scoped credential per agent and exactly one writer for each side effect.
Notice that the protocols only come up in steps 3 and 4. Getting the boundaries right (who owns which write, what each specialist needs to be told, how it reports failure) matters more than which wire you put between them. If the boundaries are right, you can change the wire later, and you'll find that changing it is the easy part.