AI Agents (A2A)
A JSON-RPC 2.0 gateway and agent registry that routes requests between AI agents over plain HTTP — no KubeMQ SDK on the agent.
The A2A connector turns KubeMQ into a gateway for AI agents. It implements a
subset of Google's Agent-to-Agent protocol as a transparent JSON-RPC 2.0 proxy: a
caller POSTs to /a2a/{agent_id}, and KubeMQ routes the request to the right agent
and relays the reply back — agents stay plain HTTP servers with zero KubeMQ
dependencies.
Part of Aiway. A2A is one of the two doors into KubeMQ Aiway, the AI Agents Fabric. New here? Start with the Aiway overview, or follow the end-to-end Aiway tutorial.
What is A2A
A2A lets one agent call another through a single, well-known endpoint instead of wiring point-to-point connections between every pair of agents. KubeMQ sits in the middle as the gateway and does the work that would otherwise be repeated in each agent: looking up where a target agent lives, enforcing timeouts and concurrency limits, forwarding the right headers, and proxying Server-Sent Event streams.
Two pieces make this work:
- An agent registry — a REST API where agents announce themselves by
agent_idand HTTP URL. The registry tracks each agent's card (name, skills, version) with TTL-based liveness. - A virtual subscriber (also called the Agent Bridge) — when an agent
registers, KubeMQ spawns an internal subscriber on the internal channel
_AGENTS_.agents/<agent_id>. Incoming JSON-RPC requests arrive over the broker, and the virtual subscriber forwards each one as an HTTPPOSTto the agent's registered URL, then relays the response back. The MCP agent-bridge tools (agent_list/agent_info/agent_send/agent_query) invoke agents through this Agent Bridge — same concept, two layers, not two meanings.
Because the virtual subscriber handles all broker and protobuf translation, an agent is just an HTTP server that speaks JSON-RPC 2.0 — there is no KubeMQ SDK, no protobuf, and no broker knowledge on the agent side.
This is a breaking change from older KubeMQ A2A docs, which described agents that
embedded a KubeMQ SDK. Agents are now registered by absolute http(s):// URL and
require no library. See Building agents
for the current model.
Why A2A on KubeMQ
- No SDK on agents — register a URL; KubeMQ bridges the broker to your agent's HTTP endpoint for you.
- One gateway, many agents — callers always POST to
/a2a/{agent_id}; routing, discovery, and lifecycle are centralized. - Sync and streaming —
message/sendfor request/reply,message/streamfor long-running tasks proxied as SSE. - Method-agnostic proxy — standard A2A methods and any custom JSON-RPC method are forwarded as-is; the agent decides what to handle.
- Built-in guardrails — per-agent concurrency caps, timeout enforcement with a gateway buffer, response-size limits, and selective header forwarding.
Architecture
A caller never connects to an agent directly. The request flows through the A2A gateway, over the broker to the target agent's virtual subscriber, and out as an HTTP POST.
The gateway proxies JSON-RPC over the broker; the virtual subscriber calls the agent's HTTP URL.
Endpoint surface
| Method | Path | Purpose |
|---|---|---|
POST | /a2a/{agent_id} | JSON-RPC 2.0 proxy to the agent (message/send, message/stream, custom methods) |
GET | /a2a/{agent_id}/stream | SSE streaming endpoint |
GET | /a2a/{agent_id}/.well-known/agent-card.json | Individual agent card |
GET | /.well-known/agent-card.json | Platform-level card |
POST | /agents/register | Register an agent |
POST | /agents/heartbeat | Agent heartbeat (refresh liveness) |
POST | /agents/deregister | Deregister an agent |
GET | /agents | List registered agents (optional skill-tag filter) |
GET | /agents/{agent_id} | Get one agent's card |
The A2A connector runs on the shared HTTP server
(port 9090) and is enabled by default — there is no flag to turn it on. To disable
it, set CONNECTORSA2_A_ENABLE=false.
Send a message
A request is a JSON-RPC 2.0 envelope POSTed to /a2a/{agent_id}. The example below
sends message/send to a registered echo-agent-01.
curl -X POST http://localhost:9090/a2a/echo-agent-01 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"parts": [{"text": "Hello, agent!"}]
}
}
}'using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "echo-agent-01";
var payload = new JsonObject
{
["jsonrpc"] = "2.0",
["id"] = 1,
["method"] = "message/send",
["params"] = new JsonObject
{
["message"] = new JsonObject
{
["parts"] = new JsonArray(new JsonObject { ["text"] = "Hello, agent!" })
}
}
};
using var client = new HttpClient();
var resp = await client.PostAsync(
$"{KubeMqUrl}/a2a/{AgentId}",
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
Console.WriteLine($"Status: {(int)resp.StatusCode}");
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
Console.WriteLine(JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
if (data["result"] != null)
Console.WriteLine("\nBasic send completed successfully!");package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
kubemqURL = "http://localhost:9090"
agentID = "echo-agent-01"
)
func main() {
payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": map[string]interface{}{
"message": map[string]interface{}{
"parts": []map[string]interface{}{{"text": "Hello, agent!"}},
},
},
}
data, _ := json.Marshal(payload)
resp, err := http.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(data))
if err != nil {
fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %d\n", resp.StatusCode)
var pretty bytes.Buffer
json.Indent(&pretty, body, "", " ")
fmt.Println(pretty.String())
var result map[string]interface{}
json.Unmarshal(body, &result)
if _, ok := result["result"]; !ok {
fmt.Fprintf(os.Stderr, "Missing 'result' in response\n")
os.Exit(1)
}
fmt.Println("\nBasic send completed successfully!")
}import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
public class Client {
static final String KUBEMQ_URL = "http://localhost:9090";
static final String AGENT_ID = "echo-agent-01";
static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
var payload = Map.of(
"jsonrpc", "2.0",
"id", 1,
"method", "message/send",
"params", Map.of(
"message", Map.of(
"parts", List.of(Map.of("text", "Hello, agent!"))
)
)
);
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/a2a/" + AGENT_ID))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + resp.statusCode());
var data = MAPPER.readTree(resp.body());
System.out.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data));
assert data.has("result");
System.out.println("\nBasic send completed successfully!");
}
}import asyncio
import json
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "echo-agent-01"
async def main() -> None:
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"parts": [{"text": "Hello, agent!"}],
},
},
}
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{KUBEMQ_URL}/a2a/{AGENT_ID}",
json=payload,
)
print(f"Status: {resp.status_code}")
data = resp.json()
print(json.dumps(data, indent=2))
assert "result" in data
print("\nBasic send completed successfully!")
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "echo-agent-01";
async function main() {
const request = {
jsonrpc: "2.0",
id: 1,
method: "message/send",
params: {
message: {
parts: [{ text: "Hello, agent!" }],
},
},
};
const resp = await fetch(`${KUBEMQ_URL}/a2a/${AGENT_ID}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
const data = await resp.json();
console.log("Response:", JSON.stringify(data, null, 2));
if (data.result) {
console.log("\nBasic send completed successfully!");
} else if (data.error) {
console.error("\nError:", data.error.message);
}
}
main().catch(console.error);Supported languages
Every A2A operation ships with a curl example plus client code in five languages, sourced from real working examples.
| Language | Client |
|---|---|
| curl / HTTP | Raw JSON-RPC over HTTP |
| C# | HttpClient + System.Text.Json |
| Go | net/http + encoding/json |
| Java | java.net.http.HttpClient + Jackson |
| Python | httpx (async) |
| TypeScript | fetch |
Next steps
Getting started
Register an agent and send your first message/send in under 10 minutes.
Agent registry
Register, list, heartbeat, and deregister agents through the REST API.
Synchronous messaging
message/send, context IDs, custom methods, and header forwarding.
Streaming (SSE)
Proxy long-running tasks over Server-Sent Events with message/stream.
Building agents
Build a compliant HTTP agent server — no KubeMQ SDK required.
Configuration
A2aConfig fields, timeouts, concurrency caps, and the disable env var.
Was this page helpful?