# AI Agents (A2A) (/aiway/a2a)



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**.

<Callout type="info">
  **Part of Aiway.** A2A is one of the two doors into
  [KubeMQ Aiway](/aiway), the AI Agents Fabric. New here? Start with the
  [Aiway overview](/aiway), or follow the end-to-end
  [Aiway tutorial](/aiway/tutorial).
</Callout>

## What is A2A [#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.

<Callout type="info">
  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](/aiway/a2a/guides/building-agents)
  for the current model.
</Callout>

## Why A2A on KubeMQ [#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/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 [#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.

<Mermaid
  chart="`
graph LR
CALLER[&#x22;Caller<br/>JSON-RPC client&#x22;]
A2A[&#x22;A2A gateway<br/>:9090/a2a/{agent_id}&#x22;]
REG[&#x22;Agent registry<br/>+ virtual subscriber&#x22;]
BROKER[&#x22;Message Broker&#x22;]
AGENT[&#x22;Agent server<br/>plain HTTP&#x22;]

CALLER -- &#x22;POST /a2a/{agent_id}&#x22; --> A2A
A2A --> REG
REG -- &#x22;Query<br/>_AGENTS_.agents/ID&#x22; --> BROKER
BROKER -. &#x22;HTTP POST<br/>JSON-RPC 2.0&#x22; .-> AGENT
AGENT -. response .-> BROKER

class CALLER client
class A2A aiway
class REG,BROKER broker
class AGENT external
`"
/>

*The gateway proxies JSON-RPC over the broker; the virtual subscriber calls the agent's HTTP URL.*

## Endpoint surface [#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](/connectors/concepts/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 [#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`.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    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!"}]
          }
        }
      }'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    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!");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    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!")
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    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!");
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    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);
    ```
  </Tab>
</Tabs>

## Supported languages [#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 [#next-steps]

<Cards>
  <Card title="Getting started" href="/aiway/a2a/getting-started" description="Register an agent and send your first message/send in under 10 minutes." />

  <Card title="Agent registry" href="/aiway/a2a/registry" description="Register, list, heartbeat, and deregister agents through the REST API." />

  <Card title="Synchronous messaging" href="/aiway/a2a/sync-messaging" description="message/send, context IDs, custom methods, and header forwarding." />

  <Card title="Streaming (SSE)" href="/aiway/a2a/streaming" description="Proxy long-running tasks over Server-Sent Events with message/stream." />

  <Card title="Building agents" href="/aiway/a2a/guides/building-agents" description="Build a compliant HTTP agent server — no KubeMQ SDK required." />

  <Card title="Configuration" href="/aiway/a2a/configuration" description="A2aConfig fields, timeouts, concurrency caps, and the disable env var." />
</Cards>
