Why A2A exists next to MCP
Mapki already treats MCP as the vertical plane: an agent calling tools, resources, and prompts through a gateway with allowlists and audit. A2A is the horizontal plane. It is how an opaque agent discovers another opaque agent, authenticates, and hands off a trackable unit of work without reading the remote system prompt, memory, or tool graph.
Google announced Agent2Agent in April 2025. The project moved to the Linux Foundation in June 2025. On 27 August 2026 the project announced acceptance as a Growth Stage project at the Agentic AI Foundation, the same neutral home that hosts MCP. A technical steering committee with representatives from AWS, Cisco, Google, IBM Research, Microsoft, Salesforce, SAP, and ServiceNow maintains the spec. Version 1.0 was the production cut: signed Agent Cards, stricter OAuth 2.0 and PKCE, explicit multi-tenancy, and a versioning policy. v0.3 clients can still be advertised beside v1.0. Treat any library that still speaks only tasks/send as pre-1.0.
The practical rule is boundary, not fashion. If one team owns every agent and they share a process, an in-process handoff is cheaper. A2A pays for itself when the callee is another team, vendor, or tenant, and you need a durable task id, cancellation, and an auth scheme declared by the callee rather than hard-coded by the caller.
The objects that actually cross the wire
- Agent Card: JSON metadata at
/.well-known/agent-card.json. Identity, skills, interfaces, and security schemes. Serve it withContent-Type: application/json, aCache-Controlmax-age, and anETag. After authentication, clients may fetch an extended card. - Task: The unit of work. Terminal states are completed, failed, canceled, and rejected. Interrupted states are input-required and auth-required. In-progress states include submitted and working.
- Message: One turn. Role is user or agent. Parts carry text, structured data, or files.
- Artifact: Named output attached to a task. Poll or stream until the artifact you contracted for is present.
- Extension: Optional capability beyond the core spec. Ignore unknown extensions; do not fail the card parse.
Normative objects live in spec/a2a.proto in the A2A repository. Generated JSON is a convenience artifact, not the source of truth. Bindings are JSON-RPC 2.0, gRPC, and HTTP+JSON. Operations are the same across bindings. Production transports are HTTPS or TLS. The spec requires authorization checks on every operation, scoped to what the caller may see.
Method names moved. Current JSON-RPC uses message/send and message/stream. Some 1.0 servers also accept PascalCase SendMessage and GetTask when the client sends A2A-Version: 1.0. Pin the version you implement. Do not mix card examples from 2025 blog posts with a 1.0 server.
Agent Card you can actually publish
{
"name": "Returns Agent",
"description": "Opens returns for orders placed in the last 30 days.",
"version": "1.2.0",
"url": "https://agents.example.com/a2a/returns",
"capabilities": { "streaming": true, "pushNotifications": true },
"securitySchemes": {
"oauth": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://idp.example.com/authorize",
"tokenUrl": "https://idp.example.com/token",
"scopes": { "returns:write": "Open a return case" }
}
}
}
},
"security": [{ "oauth": ["returns:write"] }],
"skills": [
{
"id": "start-return",
"name": "Start a return",
"description": "Requires orderId, sku, reason. Asks for refundMethod if missing.",
"tags": ["returns", "orders"],
"inputModes": ["text/plain", "application/json"],
"outputModes": ["text/plain", "application/json"]
}
]
}
Signed cards are the v1.0 control against card forgery. A JWS over a canonically serialized card lets the caller verify the card was issued by the domain owner. Without that check, a caller that trusts DNS plus TLS still accepts a card swapped by a compromised publisher. Verify the signature before you cache the card. Cache the verified card, not the raw fetch.
Task lifecycle and the blocking default
message/send is blocking unless configuration.returnImmediately (or the proto return_immediately) is set. Blocking waits until a terminal state or an interrupt (input-required, auth-required). That is the wrong default for anything that calls a model. Set return-immediately, persist the task id, and resume from GetTask, SubscribeToTask, or a push notification.
States you must handle:
submittedthenworkingwhile the remote agent runs.input-requiredwhen a field is missing. Continue on the same task id. A new task loses context.auth-requiredwhen the remote agent needs a step-up. Do not retry with the same token.completedwith artifacts. Read the contracted artifact name, not the first text part.failed,canceled,rejected. Terminal. Further messages on that task are an error.
{
"jsonrpc": "2.0",
"id": "req-1",
"method": "message/send",
"params": {
"message": {
"messageId": "msg-2c81",
"role": "user",
"parts": [
{ "kind": "text", "text": "Start a return for the boots in this order." },
{
"kind": "data",
"data": { "orderId": "A-1001", "sku": "BOOT-42-BLK", "reason": "wrong_size" },
"mediaType": "application/json"
}
]
},
"configuration": { "returnImmediately": true }
}
}
Long work uses message/stream. The server emits SSE task, status, and artifact events and closes the stream on a terminal state. The last event sets final to true. Push notifications exist for callers that cannot hold a stream: register a webhook, then treat delivery as at-least-once and idempotent on task id plus event id.
Hop cost, measured
On 2 October 2026, Sinha, Das, Babu, and Palleti posted arXiv:2610.04053, The Cost of a Hop: Benchmarking NLIP and A2A. They split latency into message creation, connection, and send across three machines. For lightweight coordination, NLIP was 8.4–9.6× faster than the baseline A2A SDK on two environments and about 4× on a third. The gap is connection setup. With connection caching, A2A closed to 2.75× on one machine and near parity on faster hardware. End to end, once LLM inference dominates, the protocols are near parity.
That measurement decides the architecture:
- Component Name: Connection pool. Keep A2A sessions warm. A fresh TLS plus JSON-RPC handshake per skill call is the tax the paper measured.
- Component Name: Workload filter. Use A2A for delegated tasks that last seconds or minutes. Do not use it as a chatty function bus between two agents in the same pod.
- Component Name: Inference dominance. If the remote skill calls a model, protocol overhead disappears. Optimize the model call and the retry policy, not the framing.
Where MCP stays in the stack
MCP is how the returns agent reads the order system. A2A is how a customer agent asks the returns agent to open a case. Putting order-system credentials on the A2A card is a boundary violation. The remote agent holds its own MCP gateway, short-lived tokens, and tool allowlist. The caller sees skills and artifacts, not tools.
Reddit threads in r/mcp and r/muvon keep reaching the same split: MCP for tools, A2A for cross-boundary tasks, ACP when an editor needs to host an agent. Bridges that translate send_message aliases into A2A JSON-RPC are useful at the edge. They are not a substitute for declaring auth on the card. Comments on those threads also match the adoption data: MCP is everywhere; A2A shows up where a task lifecycle and a signed card are the requirement, mostly enterprise and cross-vendor.
Minimal Python client
import json
import uuid
import httpx
CARD = "https://agents.example.com/.well-known/agent-card.json"
def send_return(token: str, order_id: str, sku: str) -> str:
card = httpx.get(CARD, timeout=5).json()
endpoint = card["url"]
body = {
"jsonrpc": "2.0",
"id": str(uuid.uuid4()),
"method": "message/send",
"params": {
"message": {
"messageId": str(uuid.uuid4()),
"role": "user",
"parts": [
{"kind": "text", "text": "Start a return."},
{"kind": "data", "mediaType": "application/json",
"data": {"orderId": order_id, "sku": sku, "reason": "wrong_size",
"refundMethod": "store_credit"}},
],
},
"configuration": {"returnImmediately": True},
},
}
resp = httpx.post(
endpoint,
json=body,
headers={"Authorization": f"Bearer {token}", "A2A-Version": "1.0"},
timeout=30,
)
resp.raise_for_status()
task = resp.json()["result"]
return task["id"] if "id" in task else task["task"]["id"]
Poll with GetTask or tasks/get until the state is terminal. On input-required, send the next message with the same task id. Log gen_ai.operation.name spans around the hop if you already emit OpenTelemetry GenAI conventions, and attach the A2A task id as a span attribute so a gateway trace and an agent trace join.
Production checklist
1. Publish the card at the well-known path. Sign it. Set cache headers.
- Declare OAuth 2.0 or OIDC on the card. Reject static API keys for cross-organization callers.
- Default to return-immediately. Stream or push for anything past a few seconds.
- Persist task id, context id, and idempotency key. Retries must not open a second case.
- Pool connections. The October hop-cost paper is a connection-setup result, not a model result.
6. Keep tools behind the callee’s MCP gateway. A2A artifacts are the contract.
- Version the card and the protocol header together. Advertise v0.3 only if you still have those clients.
References & Community Insights
- A2A specification: https://github.com/a2aproject/A2A/blob/main/docs/specification.md
- Hop-cost benchmark (2 Oct 2026): https://arxiv.org/abs/2610.04053
- Protocol status checked 7 Oct 2026: https://www.swfte.com/what-is-a2a-protocol
- JSON-RPC samples: https://a2aprotocol.ai/docs/guide/a2a-sample-methods-and-json-responses
- a2a-cli card and send lesson: https://github.com/a2aproject/a2a-cli
- r/muvon on MCP, A2A, and ACP as layers: https://www.reddit.com/r/muvon/comments/1wpnri1/a2a_acp_and_mcp_the_three_protocols_every_ai/
- r/mcp on bridging both protocols: https://www.reddit.com/r/mcp/comments/1s6k9ue/got_tired_of_the_a2a_vs_mcp_debate_how_about_both/
- Architecture walkthrough:
Want to implement this in your business?
Mapki designs bespoke AI agents, custom workflow automations, and tool-agnostic integrations tailored specifically to your existing ERP, CRM, and databases.

