KubeMQ
AiwayAI Agents (A2A)

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_id and 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 HTTP POST to 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 streamingmessage/send for request/reply, message/stream for 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

MethodPathPurpose
POST/a2a/{agent_id}JSON-RPC 2.0 proxy to the agent (message/send, message/stream, custom methods)
GET/a2a/{agent_id}/streamSSE streaming endpoint
GET/a2a/{agent_id}/.well-known/agent-card.jsonIndividual agent card
GET/.well-known/agent-card.jsonPlatform-level card
POST/agents/registerRegister an agent
POST/agents/heartbeatAgent heartbeat (refresh liveness)
POST/agents/deregisterDeregister an agent
GET/agentsList 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.

LanguageClient
curl / HTTPRaw JSON-RPC over HTTP
C#HttpClient + System.Text.Json
Gonet/http + encoding/json
Javajava.net.http.HttpClient + Jackson
Pythonhttpx (async)
TypeScriptfetch

Next steps

Was this page helpful?

On this page