A2A vs MCP: agents over the wire, tools over the wire¶
AIMU speaks two cross-process protocols, and they are complementary, not competing:
| MCP (Model Context Protocol) | A2A (Agent2Agent) | |
|---|---|---|
| Exposes | Tools: typed function calls | Agents: opaque, autonomous units of work |
| Unit of work | One stateless call → one result | A task with a lifecycle (submit, work, complete) |
| Discovery | list_tools() |
An agent card at /.well-known/agent-card.json |
| AIMU surface | aimu.tools.MCPClient / python -m aimu.tools.mcp |
aimu.agents.a2a.RemoteAgent / serve_a2a() |
| The other side knows | Your function names + JSON schemas | Only that an agent exists and what it claims to do |
Use MCP when you want a remote process to contribute capabilities (a search tool, a database query) that your model decides how to orchestrate. Use A2A when the remote process is itself an agent (it has its own model, its own loop, its own judgment) and you want to delegate a whole task to it and get an answer back.
Both are optional adapters, not core abstractions¶
A2A lives behind the a2a extra (pip install 'aimu[a2a]') and adapts entirely at the boundary. Nothing about A2A leaks into Runner, Agent, or the workflow classes:
- Plain data. A2A
Message/Task/AgentCardtypes exist only insideaimu/agents/a2a/.runner.run()still takes astrand returns astr. The adapters inaimu/agents/a2a/_card.pyconvert at the edge, the same roleaimu.tools.mcp_formatplays for MCP. - Composability through uniform interfaces. This is the strongest fit. A remote A2A agent is wrapped as
RemoteAgent, which is aRunner. So it drops intoChain/Router/Parallel/OrchestratorAgent.assemble(workers=[...])with no special-casing, and, via theRunner.as_tool()method, into any localAgent's tool list:
from aimu.agents import Agent, Chain, RemoteAgent
remote = RemoteAgent.connect("http://localhost:9000")
# As a workflow step:
pipeline = Chain(agents=[local_planner, remote])
# As a tool an autonomous agent can call:
orchestrator = Agent(client, tools=[remote.as_tool()])
This is the exact analog of MCPClient.as_tools(), lifted from the tool level to the agent level.
- Progressive disclosure / direct paths. One obvious entry point per direction: RemoteAgent.connect(url) to consume, serve_a2a(runner) to expose. They sit beside the MCP equivalents on the same rung of the ladder.
- Failures are apparent. A2AConnectionError is raised at connection time (and on a failed call), with the underlying cause chained via raise ... from exc, mirroring MCPConnectionError.
Sync and async, mirroring the MCP split¶
aimu.agents.a2a.RemoteAgent (sync) drives the async a2a-sdk client through an anyio blocking portal, the same mechanism aimu.tools.MCPClient uses, so synchronous code never has to touch await. aimu.aio.a2a.RemoteAgent (async) uses the SDK client natively (no portal), exactly as aimu.aio.MCPClient uses FastMCP's async Client directly. Real incremental streaming (message/stream → StreamChunks) is available on the async surface; the sync surface returns the full response as a single GENERATING chunk.
Known limitations / notes¶
- Streaming is approximate. The map from A2A SSE status/artifact events to AIMU
StreamChunkphases is lossy; the bundled server emits a single final message rather than incremental task-status updates. RemoteAgent.messagesis best-effort: it records the local(user, assistant)exchange, not the remote agent's internal transcript, which A2A keeps opaque by design.- SDK version pin. AIMU pins
a2a-sdkto the0.3.xline, whose pydantic-native API matches the broader A2A ecosystem and reference samples. The1.xline is a protobuf/gRPC transport rework with a still-churning surface; migrating to it is a tracked follow-up, not a blocker for interop today.