A2A and MCP are often described as complementary, and the one-line summary is accurate: MCP standardises how an agent uses a tool or a data source, and A2A standardises how one agent delegates work to another agent it does not control. The summary stops being useful the moment you build something real. An MCP-only coding assistant wants a remote A2A legal agent; a platform team wants one front door for both. Something must translate, and it has to decide what a task is, who the user is, and what happens when a connection drops halfway through.

This article treats interop as an engineering problem. It covers the four topologies you will meet, a field-by-field mapping between A2A 1.0 and MCP, a bridge that exposes an A2A agent as an MCP tool, how identity and trace context cross the hop, and the specific ways bridges fail. Wire details were checked against A2A 1.0.0 and MCP revisions 2025-11-25 and 2026-07-28; many hosts still speak the older one, so a bridge must handle both.

Advertisement

Two protocols, two jobs

MCP connects a host application (an IDE, a chat client, an agent runtime) to servers that offer tools, resources and prompts. The host's model decides which tool to call; the server executes it and returns content. The server is a capability, not a peer.

A2A connects an agent to another agent that may belong to another team or company. The remote agent publishes an Agent Card at /.well-known/agent-card.json describing its skills, the interfaces it serves (supportedInterfaces, each with a url and a protocolBinding of JSONRPC, GRPC or HTTP+JSON) and its security schemes. The caller sends messages with SendMessage or SendStreamingMessage; the remote agent may answer with a direct message or create a task that moves through TASK_STATE_* states and produces artifacts. The remote agent stays opaque.

So the boundary is ownership: if you control it and your model drives it step by step, it is a tool (MCP); if someone else owns it and you want an outcome, it is an agent (A2A). Interop problems appear when a component and its caller sit on opposite sides of that line.

The four topologies

Almost every deployment is one of four shapes, shown in Figure 1.

Four ways A2A and MCP meet: who is the client, who is the server, where the bridge sitsA. MCP inside an A2A agentA2A clientSendMessageA2A agentMCP hostMCPtoolsB. A2A agent behind an MCP toolMCP hosttools/callBridge servertool -> taskA2A agentremoteC. MCP server published as an A2A agentA2A clientskill requestThin agentskills -> toolsMCPserverD. Dual-protocol gatewayCallersA2A or MCPGatewayauth, map, auditBackendsagents + toolsWhat every bridge must translateidentity (token exchange, never passthrough) | lifecycle (task states) | content (parts and artifacts)idempotency (messageId) | cancellation and TTL | trace context (traceparent)A is the default. B and C each add a hop that owns a state mapping. D centralises B and C for many teams.Pick the fewest translations that meet the requirement; every bridge is a place where state can be lost.
Figure 1. A: an A2A agent uses MCP servers internally, so no translation is needed. B: an MCP host reaches a remote A2A agent through a bridge that presents it as a tool. C: an existing MCP server is published to other organisations as an A2A agent. D: a gateway does B and C for many teams and centralises policy.
  • A. MCP inside an A2A agent. The agent is an A2A server on the outside and an MCP host on the inside. Its tools never appear on the A2A wire. This needs no bridge and should be your default.
  • B. A2A agent behind an MCP tool. An MCP server exposes a tool such as contract_review; calling it sends an A2A message to a remote agent. This is how MCP-only hosts gain access to agents. The bridge owns a lifecycle mapping and is the subject of most of this article.
  • C. MCP server published as an A2A agent. A thin agent wraps a set of MCP tools behind a few coarse skills, usually with a small model or fixed logic choosing which tools to call. Do this when outside callers should get outcomes, not raw tool access.
  • D. Dual-protocol gateway. One service terminates both protocols, authenticates callers, applies policy and routes to backends. It is B and C run as shared infrastructure, with the same mapping rules applied in one place.
Advertisement

Mapping the vocabularies

Both use JSON-RPC 2.0, but almost every concept differs a little. Table 1 is the mapping a topology B bridge implements.

A2A 1.0MCPTranslation note
Agent Card skillstools/list entriesOne tool per skill; write the input schema by hand, because skills carry descriptions and examples, not JSON Schema
SendMessagetools/callTool arguments become message parts; use a stable messageId for retries
Direct message resultOrdinary CallToolResultReturn immediately
task resultTask handle2025-11-25: client opts in per request; 2026-07-28: server returns resultType: "task"
TASK_STATE_INPUT_REQUIREDinput_requiredRemote question becomes an elicitation; answer goes back as a new message with the same taskId
TASK_STATE_AUTH_REQUIREDNo direct equivalentSurface as a URL-mode elicitation or fail with an actionable error
TASK_STATE_REJECTEDfailedKeep the agent's reason in statusMessage
TASK_STATE_CANCELEDcancelledNote the spelling difference
Artifacts with text, data, url or raw partsContent blocks and structuredContentData parts map to structured content; url parts to resource links
contextIdNo equivalentMCP 2026-07-28 has no sessions; store the mapping in the bridge

Two rows deserve attention. MCP's task model changed between revisions. In 2025-11-25, tasks are experimental: the client adds a task field with a ttl to tools/call, then uses tasks/get and a blocking tasks/result. In 2026-07-28, tasks moved to the io.modelcontextprotocol/tasks extension: the client declares the extension in its per-request capabilities, the server decides whether to return a task, tasks/get returns the final result inline once the task is terminal, and input goes back through tasks/update instead of a server-initiated request. tasks/result and tasks/list are gone. Second, nothing in MCP carries A2A's contextId, so the bridge must remember which caller owns which context.

Topology B in code: a tool that delegates to an agent

The sketch below is the core of a bridge server. It uses A2A's JSON-RPC binding, asks the remote agent not to block, and returns either a finished result or a task handle. The MCP framework wiring is omitted; ctx stands for whatever your SDK passes to a tool handler.

import secrets, uuid, httpx

A2A_HEADERS = {"A2A-Version": "1.0", "Content-Type": "application/json"}

async def a2a_rpc(http, url, method, params, token):
    body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method, "params": params}
    r = await http.post(url, json=body,
                        headers={**A2A_HEADERS, "Authorization": f"Bearer {token}"})
    r.raise_for_status()
    reply = r.json()
    if "error" in reply:
        raise RemoteAgentError(reply["error"])
    return reply["result"]

async def contract_review(args, ctx):
    card = await card_cache.get("https://legal.example.com/.well-known/agent-card.json")
    iface = next(i for i in card["supportedInterfaces"] if i["protocolBinding"] == "JSONRPC")
    token = await exchange_token(ctx.user_token, audience=iface["url"])   # never forward ctx.user_token
    result = await a2a_rpc(http, iface["url"], "SendMessage", {
        "message": {
            "messageId": f"mcp-{ctx.principal}-{args['request_key']}",  # stable across retries
            "role": "ROLE_USER",
            "parts": [{"text": args["instructions"]},
                      {"url": args["document_url"], "mediaType": "application/pdf"}],
        },
        "configuration": {"returnImmediately": True,
                          "acceptedOutputModes": ["application/json", "text/plain"]},
    }, token)

    if "message" in result:                              # agent answered without a task
        return complete(parts_to_content(result["message"]["parts"]))

    remote = result["task"]
    local_id = secrets.token_urlsafe(24)                  # unguessable, bound to the caller
    await store.put(local_id, owner=ctx.principal, remote_task=remote["id"],
                    context=remote["contextId"], url=iface["url"])
    if ctx.client_supports_tasks:
        return task_handle(local_id, MAP[remote["status"]["state"]], poll_interval_ms=2000)
    return await wait_bounded(local_id, seconds=25)      # older hosts: block briefly, then fail clearly

The poll handler is the other half. It checks that the caller owns the local task, calls GetTask on the remote agent, and translates the state:

MAP = {
    "TASK_STATE_SUBMITTED": "working",       "TASK_STATE_WORKING": "working",
    "TASK_STATE_INPUT_REQUIRED": "input_required",
    "TASK_STATE_AUTH_REQUIRED": "input_required",   # surfaced as a URL elicitation
    "TASK_STATE_COMPLETED": "completed",
    "TASK_STATE_FAILED": "failed",           "TASK_STATE_REJECTED": "failed",
    "TASK_STATE_CANCELED": "cancelled",
}

async def poll(local_id, ctx):
    rec = await store.get(local_id)
    if rec is None or rec.owner != ctx.principal:
        raise InvalidParams("unknown task")          # same answer for missing and foreign ids
    token = await exchange_token(ctx.user_token, audience=rec.url)
    task = await a2a_rpc(http, rec.url, "GetTask", {"id": rec.remote_task}, token)
    state = MAP.get(task["status"]["state"], "failed")
    out = {"taskId": local_id, "status": state}
    if state == "completed":
        out["result"] = artifacts_to_result(task.get("artifacts", []))
    if state == "input_required":
        out["inputRequests"] = question_from(task["status"].get("message"))
    return out

Unknown states map to failed rather than raising, since newer agents may add states. Where the remote agent supports it, use SubscribeToTask or a push config instead of calling GetTask on every poll.

Identity, authorization and trace context

The most common security mistake in a bridge is token passthrough: taking the bearer token the MCP host sent and forwarding it to the remote agent. MCP's authorization guidance forbids it: the token was issued for the bridge, and forwarding it lets the remote agent replay it elsewhere and hides the bridge from audit logs.

Instead, exchange it. With OAuth 2.0 Token Exchange (RFC 8693), the bridge presents the user's token and receives a new one whose audience is the remote agent's interface URL and whose scopes match the skill. The Agent Card's security schemes tell you what the remote agent accepts: API key, HTTP auth, OAuth 2.0, OpenID Connect or mutual TLS. Keep the user as the subject of the exchanged token so the remote side can apply per-user policy.

Trace context needs the same deliberate handling. MCP 2026-07-28 documents traceparent, tracestate and baggage as _meta keys. On the A2A side over HTTP, send the standard W3C traceparent header. The bridge should start a child span for the remote call, so one trace runs from the user's prompt through the tool call into the remote agent. Filter baggage at organisational boundaries.

Worked example: a contract review from an IDE

A developer asks an IDE assistant, an MCP host on revision 2026-07-28, to check a vendor contract. The host has the bridge's contract_review tool, and the bridge points at a legal agent run by another company.

  1. The model calls contract_review with instructions, a document URL and a request_key. The bridge exchanges the developer's token for one scoped to the legal agent and sends SendMessage with returnImmediately set.
  2. The legal agent creates a task in TASK_STATE_SUBMITTED. The bridge stores it under a fresh local id bound to the developer and returns a task handle in state working. The host hands the model a placeholder result and moves on.
  3. On a later poll the remote task is in TASK_STATE_INPUT_REQUIRED, with a status message asking which jurisdiction applies. The bridge returns input_required with an inputRequests entry carrying an elicitation for that question.
  4. The developer answers "Delaware" and the host sends tasks/update with the answer. The bridge sends a second SendMessage with the same taskId and contextId and a new messageId.
  5. The remote task completes with two artifacts: a data part listing risky clauses and a text summary. The bridge maps the data part to structuredContent and the summary to a text block, and the next tasks/get returns them as the final result.

Had the host spoken 2025-11-25 instead, steps 3 and 4 would run differently. The host would call the blocking tasks/result, and the bridge would send elicitation/create as a server-initiated request tied to the task through the io.modelcontextprotocol/related-task metadata key. The A2A side does not change, so keep mapping and MCP framing in separate modules.

Failure modes

  • Duplicate remote work on retry. Under MCP 2026-07-28 a broken response stream loses the in-flight request, and the client re-issues it with a new request id. If the bridge mints a fresh messageId each time, the remote agent starts a second review. Derive the messageId from a caller-supplied key, and make the remote side deduplicate on it; see A2A idempotency.
  • Orphaned remote tasks. The MCP client cancels or its task TTL expires, but nobody calls CancelTask on the remote agent, and the remote agent keeps working and billing. Propagate cancellation, and sweep stored tasks whose TTL has expired.
  • Context bleed. The bridge caches a contextId per tool rather than per user, so one developer's follow-up lands in another's conversation. Key every stored context by principal.
  • Guessable task ids. Reusing the remote task id as the local id leaks it and lets anyone who learns it poll. Mint unguessable local ids and check ownership on every call.
  • Prompt injection through artifacts. Remote agent output goes straight into your model's context as a tool result. Treat it as untrusted input: strip instructions in data you only meant to display, and never let it trigger privileged tools without confirmation.
  • Version skew. Send A2A-Version explicitly and test against both MCP revisions.

Trade-offs and when not to bridge

Every bridge adds a hop, a store and a mapping that can drift from either specification. If you own both sides, prefer topology A. Bridge only across real ownership boundaries.

Topology C is tempting as a quick way to share tools across companies, but exposing forty fine-grained tools as forty skills recreates a tool API with worse ergonomics. Group them into a few outcome-shaped skills. Topology D pays off once several teams run their own bridges and repeat the same token exchange, audit and mapping code; the MCP gateway design covers the MCP half of that infrastructure.

What to do next

  1. List every place an MCP host needs a remote agent or an outside caller needs your tools, and assign each to topology A, B, C or D. Choose A wherever you own both sides.
  2. For each bridge, write the mapping table for your actual skills and tools, including how you handle AUTH_REQUIRED and REJECTED.
  3. Implement token exchange with audience-scoped tokens, and add a test that fails if the inbound token ever appears in an outbound request.
  4. Store bridged tasks under unguessable local ids keyed by principal, with TTLs and a sweeper that cancels orphaned remote tasks.
  5. Derive messageId from a caller-supplied idempotency key and test a retry after a dropped stream.
  6. Propagate traceparent in both directions and check one end-to-end trace in your tracing backend.
  7. Run the bridge against an MCP 2025-11-25 host and a 2026-07-28 host, and against an A2A agent that returns a direct message, a task, and an input request.
  8. Read the A2A task model, the Agent Card specification and MCP tools to fill in details for your stack.
Key takeaway: A2A and MCP meet in four shapes: MCP inside an A2A agent, an A2A agent behind an MCP tool, an MCP server published as an A2A agent, and a gateway that does both. Default to the first. When you must bridge, the bridge owns real state: a task mapping between TASK_STATE_* values and MCP's task statuses, a per-user contextId store, idempotent messageIds, cancellation propagation, exchanged rather than forwarded tokens, and trace context. Build it to handle MCP 2025-11-25 and 2026-07-28 task models side by side, treat remote artifacts as untrusted input, and keep the mapping in one tested module because both specifications are still moving.