Rensei docs
A2A

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 nameSkill IDMCP tool nameLegacy alias
Vercel Opsdeployvercel_ops.deploya2a_vercel_ops_deploy
code-reviewerreviewcode_reviewer.reviewa2a_code_reviewer_review
Linear (prod)create-issuelinear_prod.create-issuea2a_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 responseMCP output
Single DataPart artifactThe part.data object (lossless round-trip)
Single TextPart artifactThe part.text string
Multi-part artifactThe 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:

PhaseWhen
pre-verbBefore the outbound call; Cedar enforcer runs here
post-verb (outcome: completed)After a successful response
post-verb (outcome: failed) + verb-errorOn 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

CodeClassCause
-32201RemoteA2ATimeoutErrorOutbound call exceeded wall-clock timeout
-32202RemoteA2ATransportErrorNetwork failure or non-2xx HTTP response
-32203RemoteA2AInvalidResponseErrorResponse was not valid JSON or missing the JSON-RPC envelope
-32204RemoteA2ATaskFailedErrorRemote agent returned status.state = 'failed'
-32003AuthorizationErrorCedar 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:
        - typescript

See 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/...' })

On this page