A2A MCP Bridge
A2A-as-MCP-tools: `<agentSlug>.<skillId>` bridge.
The A2A MCP Bridge exposes every registered external A2A agent as a set of MCP tools. Once an agent is registered in the A2A Registry, any Claude session running against your project can call its skills via the standard MCP tool surface - no special wiring required in the workflow. The bridge handles translation, authorization via Cedar, and outcome tracking.
How it works
When registerA2AAgentsAsMcpTools runs for your org, it walks every agent_cards row and registers one MCP tool per skill. The tool name uses the plugin-namespaced format: <agentSlug>.<skillId>. An additional legacy alias a2a_<agentSlug>_<skillId> is also registered for back-compat.
Tool naming
Agent names are slugified (lowercased, non-alphanumerics replaced with _) to form the namespace prefix:
| Agent name | Skill ID | MCP tool name | Legacy alias |
|---|---|---|---|
Vercel Ops | deploy | vercel_ops.deploy | a2a_vercel_ops_deploy |
code-reviewer | review | code_reviewer.review | a2a_code_reviewer_review |
Linear (prod) | create-issue | linear_prod.create-issue | a2a_linear_prod_create_issue |
Both names are registered and dispatched through the same handler. Hook events (Cedar, audit, load tracking) always report the canonical plugin-namespaced name as the verb, even when the legacy alias was called.
Input schema
If the remote agent's AgentCard.skill includes an inputSchema (JSON Schema), the bridge uses it verbatim as the MCP tool's input schema. MCP clients with schema validation will enforce it before the call reaches the bridge.
When no inputSchema is present, the tool accepts any object (additionalProperties: true). The remote agent performs its own input validation.
{
"name": "vercel_ops.deploy",
"description": "Invokes the deploy skill on remote A2A agent Vercel Ops",
"inputSchema": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"branch": { "type": "string" }
},
"required": ["projectId"]
}
}Wire protocol
The bridge issues a JSON-RPC 2.0 message/send request to the remote agent's base URL:
POST https://agents.example.com/vercel-ops
Content-Type: application/json
Authorization: Bearer <resolved-token>
{
"jsonrpc": "2.0",
"id": 1717281600000,
"method": "message/send",
"params": {
"message": {
"skill": { "skillId": "deploy" },
"parts": [
{
"kind": "data",
"data": { "projectId": "proj_abc", "branch": "main" }
}
]
},
"correlationId": "a2a-<agentId>-deploy-...",
"taskId": "a2a-<agentId>-deploy-..."
}
}The correlationId and taskId are both set to the session ID so audit chains trace cleanly across the hop.
Response translation
The bridge maps the message/send JSON-RPC response back to an MCP tool output:
| Remote response | MCP output |
|---|---|
Single DataPart artifact | The part.data object (lossless round-trip) |
Single TextPart artifact | The part.text string |
| Multi-part artifact | The full artifact (parts array preserved) |
status.state = 'failed' | RemoteA2ATaskFailedError thrown (maps to MCP error) |
Timeout
The default wall-clock timeout is 30 seconds. Override it per-org with the timeoutMs option when calling registerA2AAgentsAsMcpTools. On timeout, a RemoteA2ATimeoutError (code -32201) is thrown and surfaced as an MCP error so the calling agent can decide to fall back.
Cedar authorization
Every tool invocation emits a pre-verb event on the Layer-6 hook bus before the outbound HTTP call. The Cedar policy engine subscribes to these events. If any forbid policy matches or no permit policy matches for the (caller, a2a_invoke, target) triple, the event's context.denied flag is set to true and the bridge throws AuthorizationError (code -32003) without ever reaching the wire.
To configure which agents may invoke which skills, see A2A Policies.
Hook bus events
The bridge emits three event phases on the verb bus:
| Phase | When |
|---|---|
pre-verb | Before the outbound call; Cedar enforcer runs here |
post-verb (outcome: completed) | After a successful response |
post-verb (outcome: failed) + verb-error | On transport error, timeout, or task failure |
Each event carries: verb, legacyAlias, orgId, agentCardId, agentUrl, skillId, sessionId, args, emittedAt. The post-verb event additionally carries durationMs and outcome. The verb-error event carries error: { code, message }.
Subscribers on the hook bus use these events to write a2a_task_instances rows (load tracking), audit records, and cost events.
Dynamic registration
registerA2AAgentsAsMcpTools is called per-org inside createRenseiMcpServerForOrg. Re-running it after a new agent is registered picks up new tools idempotently - tools that are already registered (by name) are skipped without error.
unreachable agents are skipped at registration time to avoid registering tools that will always return a transport error. When the agent recovers and is refreshed to healthy, re-running registration picks it up.
Error codes
| Code | Class | Cause |
|---|---|---|
-32201 | RemoteA2ATimeoutError | Outbound call exceeded wall-clock timeout |
-32202 | RemoteA2ATransportError | Network failure or non-2xx HTTP response |
-32203 | RemoteA2AInvalidResponseError | Response was not valid JSON or missing the JSON-RPC envelope |
-32204 | RemoteA2ATaskFailedError | Remote agent returned status.state = 'failed' |
-32003 | AuthorizationError | Cedar denied the invocation |
Example: calling a registered agent from a workflow node
In a workflow, use the agent.invoke node and select the agent and skill:
steps:
- id: run-code-review
type: action
nodeId: agent.invoke
config:
agentCardId: "{{ nodes.agent-ref.agentCardId }}"
skillId: review
args:
prUrl: "{{ $trigger.prUrl }}"Or let the routing layer choose the best available agent for the work type using agent.dispatch:
steps:
- id: dispatch-qa
type: action
nodeId: agent.dispatch
config:
workType: qa
description: Run integration tests for the new feature
requiredSkills:
- typescriptSee A2A Routing for how the routing layer selects among registered agents.
Testing the bridge
For integration tests, inject a stub fetcher and an in-memory verb bus:
import {
buildA2ASkillTool,
createInMemoryVerbBus,
} from '@/lib/a2a/a2a-as-mcp-tools'
const bus = createInMemoryVerbBus()
let lastEvent: RemoteA2AVerbEvent | undefined
bus.subscribe((e) => { lastEvent = e })
const tool = buildA2ASkillTool({
agentRow: { id: 'agent-1', orgId: 'org-1', agentUrl: 'https://example.com' },
card: mockCard,
skill: mockSkill,
verbBus: bus,
fetcher: async () => ({ status: 200, bodyText: JSON.stringify(mockResult) }),
})
const result = await tool.handler({ prUrl: 'https://github.com/...' })Related pages
- A2A Registry - register agents to make them available
- A2A Policies - Cedar policy management for MCP invocations
- A2A Dispatches - audit log for all bridge invocations
- A2A Routing - how
agent.dispatchselects the agent to call - Cedar Policies - platform-wide Cedar policy engine