Interop
Reach the same agents over A2A, MCP, and AG-UI.
Internal agents use AGDX on the log. External clients keep their public protocols. Optional bridges map shared fields into AGDX envelopes. Protocol-specific data stays unchanged in the body.
A2A (a2a-bridge)
A2aBridge exposes an internal agent to A2A JSON-RPC clients and serves the v1.0 Agent Card at /.well-known/agent-card.json.
| A2A method | Mapping |
|---|---|
SendMessage | Publishes a typed AGDX command on a fresh task conversation - the task id is the conversation. Returns Submitted. |
SendStreamingMessage | Same publish as SendMessage - the stream is consumed log-natively, never re-emitted as SSE. |
GetTask | Reads the reply topic and maps the answering response/error envelope to the A2A task. Working until one lands. |
CancelTask | Publishes an AGDX error terminal (Cancelled). Returns Canceled. |
const bridge = new A2aBridge(
laser,
AgentId.new("a2a-gateway"),
AgentTopic.Commands,
AgentTopic.Responses
)
const card = bridge.card()let bridge = Arc::new(A2aBridge::new(
laser.clone(),
"a2a-gateway".parse()?,
AgentTopic::Commands,
AgentTopic::Responses,
));
let card = bridge.card();
let app = bridge.router();bridge = laser.a2a_bridge(
"a2a-gateway",
ls.Topics.COMMANDS,
ls.Topics.RESPONSES,
)
card = bridge.card()Rust's optional HTTP surface provides router() for mounting the JSON-RPC endpoint and Agent Card route. TypeScript and Python expose the same bridge operations for the host HTTP adapter.
With the sign feature, A2aBridge::signed_card attaches a detached JWS over the agent card so a client can verify authenticity before trusting it.
MCP (mcp-bridge)
McpBridge is an MCP JSON-RPC server mapping tool calls onto AGDX commands and awaiting the correlated reply over the log.
| MCP method | Mapping |
|---|---|
initialize | Echoes the client's protocol version and advertises only the capabilities actually served. |
tools/list / tools/call | Tools configured via with_tool - a call publishes an AGDX command and renders the correlated reply as a tool result. |
resources/list / resources/read | Resources configured via with_resource, served from config. |
prompts/list / prompts/get | Prompts configured via with_prompt. |
const mcp = new McpBridge(
laser,
AgentId.new("mcp-gateway"),
AgentTopic.ToolCalls,
AgentTopic.ToolResults,
"my-server"
).withTool(
"ask",
"ask the assistant",
{ type: "object" }
)
const tools = mcp.listTools()let mcp = Arc::new(
McpBridge::new(
laser.clone(),
"mcp-gateway".parse()?,
AgentTopic::ToolCalls,
AgentTopic::ToolResults,
"my-server",
)
.with_tool(
"ask",
Some("ask the assistant".into()),
serde_json::json!({ "type": "object" }),
),
);
let tools = mcp.list_tools();
let app = mcp.router();mcp = laser.mcp_bridge(
"mcp-gateway",
ls.Topics.TOOL_CALLS,
ls.Topics.TOOL_RESULTS,
"my-server",
tools=[
{
"name": "ask",
"description": "ask the assistant",
"input_schema": {"type": "object"},
}
],
)
tools = mcp.list_tools()Rust's mcp-http feature supplies the Axum router(). TypeScript and Python expose the protocol operations directly so the application can connect them to its HTTP framework.
AG-UI (agui)
AG-UI is frontend-facing. Two pieces ship over the log:
- State sync -
publish_state_snapshot/publish_state_deltaemit shared state and RFC 6902 patches.reconstruct_statereplays a snapshot plus later deltas into the current state at any historical offset. - Event rendering -
agui_eventsturns a conversation into AG-UI events: chat chunks toTEXT_MESSAGE_*, reasoning toREASONING_MESSAGE_*, tool-arg streams toTOOL_CALL_START/ARGS/END, status updates toRUN_STARTED/RUN_FINISHED, and error terminals toRUN_ERROR.
await laser.publishStateSnapshot(
AgentTopic.Audit,
AgentId.new("ui"),
conversation,
{ count: 0 }
)
const events = await laser.aguiEvents(
conversation,
AgentTopic.LlmIo
)laser
.publish_state_snapshot(
AgentTopic::Audit,
"ui".parse()?,
conversation,
&serde_json::json!({ "count": 0 }),
)
.await?;
let events = laser
.agui_events(conversation, AgentTopic::LlmIo)
.await?;await laser.publish_state_snapshot(
ls.Topics.AUDIT,
"ui",
conversation_id,
{"count": 0},
)
events = await laser.agui_events(
conversation_id,
ls.Topics.LLM_IO,
)Human-in-the-loop
Independent of any bridge, Agdx::request_input pauses an agent on a human decision and AgentCtx::respond_input resolves it - both compose the existing command/response verbs, adding nothing new to the wire:
const decision = await laser
.agdx(
AgentTopic.HumanInput,
AgentId.new("orchestrator"),
conversation
)
.requestInput(
AgentTopic.Responses,
utf8("approve a $500 credit?"),
300_000
)let decision = laser
.agdx(
AgentTopic::HumanInput,
"orchestrator".parse()?,
conversation.into(),
)
.request_input(
AgentTopic::Responses,
b"approve a $500 credit?".to_vec(),
Duration::from_secs(300),
)
.await?;decision = await laser.agdx(
ls.Topics.HUMAN_INPUT,
"orchestrator",
conversation_id,
).request_input(
ls.Topics.RESPONSES,
b"approve a $500 credit?",
timeout_secs=300,
)Authorization
The JSON-RPC bridges do not authenticate HTTP requests. Add authentication in the hosting framework.
Rust's with_default_stream, TypeScript's withDefaultStream, and Python's with_stream select a stream on one connection. Use separate credentials when Iggy RBAC must isolate access. Governance covers audience and step-up checks.
Running it
Bridges do not depend on a model provider. They run on every VSR target. Use Laser Stack when a scenario also needs managed APIs.