A2A → MCP Bridge

Exposes a remote A2A agent as an MCP server, so any MCP client can call it without knowing A2A exists.

  MCP client ──MCP over stdio──→ this bridge ──A2A JSON-RPC──→ remote agent
             ←──tool result─────            ←──task───────────

This is the reverse of MCP Agent, which is an agent that uses MCP tools. This one makes an A2A agent be one — and it is the direction that reaches further, because an MCP client needs no A2A library, no A2A concept and no new code to call a remote agent. It needs a command in its server list:

{ "command": "a2a-mcp-bridge", "args": ["http://agent.internal:8080"] }

One bridge process fronts one agent, because that is how MCP clients are configured anyway.

How much of A2A survives the crossing

A2AMCPBridged
agent card skillstools/list✅ one tool per skill, the agent's own descriptions
Completedtool result
Failed, Rejected, Canceledtool result with isError
long-running taskSEP-2663 task, polled with tasks/get
task status messageMCP task status message✅ at poll granularity
CancelTasktasks/cancel✅ cooperative on both sides
InputRequiredCallToolResponse::InputRequired❌ reported, not bridged

MCP gained long-running tasks in the 2026-07-28 specification (SEP-2663), and that is what makes the task rows possible at all. Before it, a long-running A2A task had nowhere to go but a blocked request.

Three asymmetries worth knowing before you copy this

A2A skills carry no argument schema. An MCP tool has one; an A2A skill has a description, tags and examples, because A2A's calling convention is fixed — you send a Message. Every published tool therefore takes one string, and the skill's description is what tells a caller's model which to use. Inventing a schema per skill would publish a contract the agent never agreed to.

A2A has no skill selector. SendMessage has no field naming a skill, so every tool sends to the same agent and the choice is advisory. The chosen tool travels in the message metadata under a2a-mcp-bridge/skill, which an agent may route on or ignore.

input-required is reported, not bridged. MCP has a counterpart, and wiring it up needs the bridge to hold an A2A task id across MCP calls and translate input_responses into an A2A continuation message — real work with a real chance of being subtly wrong, where a half-built version would strand tasks with no way to answer them. A paused task comes back as an error whose text says what the agent is waiting for, so a caller can tell "waiting" from "broken".

Running

a2a-mcp-bridge http://127.0.0.1:8080      # or set A2A_AGENT_URL

# or, to see the whole path at once:
cargo run -p a2a-mcp-bridge --bin bridge-demo

bridge-demo stands up a sample A2A agent, spawns the bridge against it as a real child process, and acts as an MCP client. No model and no network — the bridge is protocol only, so the demo is deterministic.

What the demo run shows

The verbatim transcript is in the example's README. Three things it settles:

  • The skill hint arrives. The sample agent echoes back the metadata the bridge sent, so the "advisory hint" above is demonstrated rather than asserted.
  • A2A progress reaches the MCP caller. The agent's own task status message crosses two protocols and lands in tasks/get.
  • It arrives coarsened. The agent emits three steps 120 ms apart; the caller sees the last one, because the bridge polls A2A every 250 ms. That is the stated cost of polling rather than streaming, visible in the output rather than discovered later.

Why polling: an agent card may not advertise streaming at all, and MCP's own task model is polling, so poll-to-poll keeps one clock instead of translating between two.

Tests

Fifteen, in two layers. The mapping is pure, so its awkward cases are asserted on values — a skill id that is not a legal tool name, two that collide (refused, naming both), a card with no skills, and each task state's rendering including the input-required gap. The bridge needs a real agent, so those tests start one on localhost and connect a real MCP client over an in-process pipe, covering discovery, a returned artifact, the skill hint arriving, a failed A2A task becoming an isError result without killing the session, and both refusals-to-start.

They need no model and no network beyond localhost, and run under cargo test --workspace. The child-process spawn is what bridge-demo exercises; no CI job runs the demo.

Failure semantics

ConditionResult
A2A task completestool result with the artifact text
A2A task fails, is rejected or cancelledtool result with isError and the agent's reason
A2A task pauses on input-requiredisError, saying what the agent is waiting for
A2A agent unreachable at startupthe bridge refuses to start
Agent card advertises no skillsthe bridge refuses to start
Two skill ids collide as tool namesthe bridge refuses to start, naming both
A2A task never settlescancelled after 5 minutes, reported as an error
Non-text artifact partsdropped, with a line saying how many

Refusing to start is deliberate in three of those rows: an MCP server that came up with an empty or wrong tool list is indistinguishable, to its caller, from an agent with nothing to offer.

See examples/mcp-bridge/README.md for the full walkthrough and the verbatim transcript.