KubeMQ
AiwayAI Agents (A2A)

Configuration

Configure the A2A connector — enable model, timeouts, agent TTL, concurrency, and response limits via YAML, environment variables, or Docker.

The A2A connector is enabled by default on the shared HTTP server — start kubemq-server and /a2a/* is live with no flag to set. Configuration tunes agent liveness, request timeouts, concurrency, and response limits; you only set values that differ from the defaults below.

Overview

A2A settings live under connectors.a2a in the kubemq-server configuration (the Go A2aConfig struct). Every field has a working default, so a minimal deployment needs no A2A configuration at all. You can override any field through a YAML/TOML config file, an environment variable, or a Docker -e flag — they map one-to-one.

The connector listens on the shared HTTP server's port (9090 by default, inherited from Rest.Port) and is on unless you disable it. There is no =true flag — older KubeMQ builds were off-by-default, but current builds ship all three connectors enabled.

Config fields

Defaults are taken verbatim from the A2aConfig struct in kubemq-server.

FieldTypeDefaultDescription
EnablebooltrueWhether the A2A connector is served. Set to false to disable /a2a/* and the registry.
AgentTTLSecondsint300Liveness window for a registered agent. An agent that does not heartbeat within this window is considered stale. Must be positive.
DefaultTimeoutSecondsint300Timeout applied to an agent request when the caller does not set params.configuration.timeout. Must be positive.
MaxTimeoutSecondsint3600Upper bound for caller-specified timeouts. Larger values are capped to this. Must be >= DefaultTimeoutSeconds.
MaxAgentsint0Maximum number of registered agents. 0 means unlimited. Cannot be negative.
MaxSSEIdleSecondsint300Idle timeout for an SSE stream. When it fires, a task.error with code -32001 is sent and the agent is asked to cancel. Must be positive.
TrustedOrigins[]string["auto"]Origins allowed by Origin validation. auto matches localhost and the bind address; * allows all. See Authentication.
AgentMaxResponseBytesint6410485760Maximum agent response size in bytes (10 MB). Oversized responses are rejected with -32603. 0 means unlimited. Cannot be negative.
AgentTLSSkipVerifyboolfalseWhen agents are registered with https:// URLs, skip TLS certificate verification. Leave false in production.
AgentMaxConcurrencyint100Maximum simultaneous in-flight requests per agent. The 101st concurrent request is rejected with -32603. A non-positive value resets to 100.

Validation. kubemq-server rejects the configuration at startup if AgentTTLSeconds, DefaultTimeoutSeconds, or MaxSSEIdleSeconds is not positive, if MaxTimeoutSeconds < DefaultTimeoutSeconds, or if MaxAgents / AgentMaxResponseBytes is negative.

Enable / disable

The A2A connector is enabled by default. To disable it, set its enable variable to false:

docker run -d -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY -e CONNECTORSA2_A_ENABLE=false europe-docker.pkg.dev/kubemq/images/kubemq:next

The enable variable name is irregular by design. Connectors.A2A.Enable becomes CONNECTORSA2_A_ENABLE — the 2-to-A boundary inside A2A splits into A2_A. kubemq-server derives every env var by snake-casing the dotted config path with two regexes, stripping dots, and uppercasing. Do not "fix" the name to CONNECTORS_A2A_ENABLE; that string is not bound and has no effect. The full algorithm and the matching MCP/CE names are documented in Shared HTTP server.

Environment variables

Each field maps to one environment variable via the same transform. These are the A2A variables in full:

VariableConfig fieldDefault
CONNECTORSA2_A_ENABLEConnectors.A2A.Enabletrue
CONNECTORSA2_A_AGENT_TTL_SECONDSConnectors.A2A.AgentTTLSeconds300
CONNECTORSA2_A_DEFAULT_TIMEOUT_SECONDSConnectors.A2A.DefaultTimeoutSeconds300
CONNECTORSA2_A_MAX_TIMEOUT_SECONDSConnectors.A2A.MaxTimeoutSeconds3600
CONNECTORSA2_A_MAX_AGENTSConnectors.A2A.MaxAgents0
CONNECTORSA2_A_MAX_SSE_IDLE_SECONDSConnectors.A2A.MaxSSEIdleSeconds300
CONNECTORSA2_A_TRUSTED_ORIGINSConnectors.A2A.TrustedOriginsauto
CONNECTORSA2_A_AGENT_MAX_RESPONSE_BYTESConnectors.A2A.AgentMaxResponseBytes10485760
CONNECTORSA2_A_AGENT_TLS_SKIP_VERIFYConnectors.A2A.AgentTLSSkipVerifyfalse
CONNECTORSA2_A_AGENT_MAX_CONCURRENCYConnectors.A2A.AgentMaxConcurrency100

Configuration file

kubemq-server loads a YAML or TOML file (auto-detected) from the --config flag or the CONFIG environment variable. The A2A connector sits under connectors.a2a, alongside the shared http block. This example pins every A2A field to its default and shows the shared HTTP/CORS context:

config.yaml
connectors:
  rest:
    enable: true
    port: "9090"
  http:
    readtimeout: 60
    bodylimit: "100M"
    cors:
      alloworigins: ["*"]
      allowmethods: ["GET", "POST", "DELETE", "OPTIONS"]
      allowheaders: ["Authorization", "Content-Type", "MCP-Protocol-Version", "MCP-Session-Id", "Last-Event-ID", "Accept"]
  a2a:
    enable: true
    agentttlseconds: 300
    defaulttimeoutseconds: 300
    maxtimeoutseconds: 3600
    maxagents: 0
    maxsseidleseconds: 300
    trustedorigins: ["auto"]
    agentmaxresponsebytes: 10485760
    agenttlsskipverify: false
    agentmaxconcurrency: 100

The shared HTTP server inherits its port from connectors.rest.port. To run the connectors on a separate port from REST, set connectors.http.port explicitly. See Shared HTTP server for the port-inheritance and middleware details.

Tuning the limits

The concurrency and response-size caps are the two limits you are most likely to hit under load. The .kb examples exercise both against a live server — the curl call below shows the wire behavior, and the language tabs run the full client that fires past the limit and reports the rejection.

Per-agent concurrency

AgentMaxConcurrency (default 100) bounds in-flight requests per agent. The 101st concurrent request to a single agent is rejected with JSON-RPC error -32603 ("internal error") while the first 100 succeed.

# Each of these is a normal request; firing more than AgentMaxConcurrency
# of them at once against one agent makes the overflow return -32603.
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" }] } }
  }'
// .kb/integration-a2a/examples/csharp/limits/concurrency-limit/Client.cs
using System.Net.Http.Json;

var kubemqUrl = Environment.GetEnvironmentVariable("KUBEMQ_URL") ?? "http://localhost:9090";
var agentId = "concurrency-agent-01";
const int total = 101; // limit is 100; 1 request will be rejected

using var http = new HttpClient();

async Task<(bool ok, int? code)> SendOne(int i)
{
    var payload = new
    {
        jsonrpc = "2.0",
        id = i,
        method = "message/send",
        @params = new { message = new { parts = new[] { new { text = $"req-{i}" } } } }
    };
    var resp = await http.PostAsJsonAsync($"{kubemqUrl}/a2a/{agentId}", payload);
    var body = await resp.Content.ReadFromJsonAsync<JsonRpcResponse>();
    if (body?.Error is not null)
        return (false, body.Error.Code);
    return (true, null);
}

var results = await Task.WhenAll(Enumerable.Range(0, total).Select(SendOne));
var rejected = results.Count(r => r.code == -32603);
Console.WriteLine($"Successes: {results.Count(r => r.ok)}  Rejected (-32603): {rejected}");
// .kb/integration-a2a/examples/go/limits/concurrency-limit/client.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "sync"
    "sync/atomic"
)

func main() {
    kubemqURL := os.Getenv("KUBEMQ_URL")
    if kubemqURL == "" {
        kubemqURL = "http://localhost:9090"
    }
    agentID := "concurrency-agent-01"
    const total = 101 // limit is 100; 1 request is rejected

    var success, rejected int64
    var wg sync.WaitGroup
    for i := 0; i < total; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            body, _ := json.Marshal(map[string]any{
                "jsonrpc": "2.0", "id": i, "method": "message/send",
                "params": map[string]any{"message": map[string]any{
                    "parts": []map[string]string{{"text": fmt.Sprintf("req-%d", i)}}}},
            })
            resp, err := http.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(body))
            if err != nil {
                return
            }
            defer resp.Body.Close()
            var out struct {
                Error *struct {
                    Code int `json:"code"`
                } `json:"error"`
            }
            _ = json.NewDecoder(resp.Body).Decode(&out)
            if out.Error != nil && out.Error.Code == -32603 {
                atomic.AddInt64(&rejected, 1)
            } else {
                atomic.AddInt64(&success, 1)
            }
        }(i)
    }
    wg.Wait()
    fmt.Printf("Successes: %d  Rejected (-32603): %d\n", success, rejected)
}
// .kb/integration-a2a/examples/java/limits/concurrency-limit/Client.java
import java.net.URI;
import java.net.http.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class Client {
    public static void main(String[] args) throws Exception {
        String kubemqUrl = System.getenv().getOrDefault("KUBEMQ_URL", "http://localhost:9090");
        String agentId = "concurrency-agent-01";
        int total = 101; // limit is 100; 1 request is rejected

        HttpClient http = HttpClient.newHttpClient();
        AtomicInteger success = new AtomicInteger();
        AtomicInteger rejected = new AtomicInteger();
        ExecutorService pool = Executors.newFixedThreadPool(total);

        var tasks = IntStream.range(0, total).<Callable<Void>>mapToObj(i -> () -> {
            String payload = """
                {"jsonrpc":"2.0","id":%d,"method":"message/send",
                 "params":{"message":{"parts":[{"text":"req-%d"}]}}}""".formatted(i, i);
            HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(kubemqUrl + "/a2a/" + agentId))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(payload))
                .build();
            String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
            if (body.contains("-32603")) rejected.incrementAndGet();
            else success.incrementAndGet();
            return null;
        }).toList();

        pool.invokeAll(tasks);
        pool.shutdown();
        System.out.printf("Successes: %d  Rejected (-32603): %d%n", success.get(), rejected.get());
    }
}
# .kb/integration-a2a/examples/python/limits/concurrency-limit/client.py
import asyncio
import os

import httpx

KUBEMQ_URL = os.getenv("KUBEMQ_URL", "http://localhost:9090")
AGENT_ID = "concurrency-agent-01"
TOTAL = 101  # limit is 100; 1 request is rejected


async def send_one(client: httpx.AsyncClient, i: int) -> int | None:
    payload = {
        "jsonrpc": "2.0",
        "id": i,
        "method": "message/send",
        "params": {"message": {"parts": [{"text": f"req-{i}"}]}},
    }
    resp = await client.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload)
    error = resp.json().get("error")
    return error["code"] if error else None


async def main() -> None:
    async with httpx.AsyncClient(timeout=30) as client:
        codes = await asyncio.gather(*(send_one(client, i) for i in range(TOTAL)))
    rejected = sum(1 for c in codes if c == -32603)
    successes = sum(1 for c in codes if c is None)
    print(f"Successes: {successes}  Rejected (-32603): {rejected}")


if __name__ == "__main__":
    asyncio.run(main())
// .kb/integration-a2a/examples/typescript/limits/concurrency-limit/client.ts
const kubemqUrl = process.env.KUBEMQ_URL ?? "http://localhost:9090";
const agentId = "concurrency-agent-01";
const total = 101; // limit is 100; 1 request is rejected

async function sendOne(i: number): Promise<number | null> {
  const resp = await fetch(`${kubemqUrl}/a2a/${agentId}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: i,
      method: "message/send",
      params: { message: { parts: [{ text: `req-${i}` }] } },
    }),
  });
  const body = (await resp.json()) as { error?: { code: number } };
  return body.error?.code ?? null;
}

const codes = await Promise.all(
  Array.from({ length: total }, (_, i) => sendOne(i)),
);
const rejected = codes.filter((c) => c === -32603).length;
const successes = codes.filter((c) => c === null).length;
console.log(`Successes: ${successes}  Rejected (-32603): ${rejected}`);

Response size

AgentMaxResponseBytes (default 10485760, 10 MB) caps the size of an agent's reply. A larger response is rejected before it reaches the caller, who receives -32603 with the message internal error: response too large.

# A normal request; the cap is hit only when the agent's reply exceeds
# AgentMaxResponseBytes, in which case the response carries error -32603.
curl -X POST http://localhost:9090/a2a/oversize-agent-01 \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": { "message": { "parts": [{ "text": "return a big payload" }] } }
  }'
// .kb/integration-a2a/examples/csharp/limits/response-size/Client.cs
using System.Net.Http.Json;

var kubemqUrl = Environment.GetEnvironmentVariable("KUBEMQ_URL") ?? "http://localhost:9090";
var agentId = "oversize-agent-01";

using var http = new HttpClient();
var payload = new
{
    jsonrpc = "2.0",
    id = 1,
    method = "message/send",
    @params = new { message = new { parts = new[] { new { text = "return a big payload" } } } }
};

var resp = await http.PostAsJsonAsync($"{kubemqUrl}/a2a/{agentId}", payload);
var body = await resp.Content.ReadFromJsonAsync<JsonRpcResponse>();
if (body?.Error is not null)
    Console.WriteLine($"Error code: {body.Error.Code}  Message: {body.Error.Message}");
else
    Console.WriteLine("Response within the size limit.");
// .kb/integration-a2a/examples/go/limits/response-size/client.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    kubemqURL := os.Getenv("KUBEMQ_URL")
    if kubemqURL == "" {
        kubemqURL = "http://localhost:9090"
    }
    agentID := "oversize-agent-01"

    body, _ := json.Marshal(map[string]any{
        "jsonrpc": "2.0", "id": 1, "method": "message/send",
        "params": map[string]any{"message": map[string]any{
            "parts": []map[string]string{{"text": "return a big payload"}}}},
    })
    resp, err := http.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(body))
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var out struct {
        Error *struct {
            Code    int    `json:"code"`
            Message string `json:"message"`
        } `json:"error"`
    }
    _ = json.NewDecoder(resp.Body).Decode(&out)
    if out.Error != nil {
        fmt.Printf("Error code: %d  Message: %s\n", out.Error.Code, out.Error.Message)
    } else {
        fmt.Println("Response within the size limit.")
    }
}
// .kb/integration-a2a/examples/java/limits/response-size/Client.java
import java.net.URI;
import java.net.http.*;

public class Client {
    public static void main(String[] args) throws Exception {
        String kubemqUrl = System.getenv().getOrDefault("KUBEMQ_URL", "http://localhost:9090");
        String agentId = "oversize-agent-01";

        String payload = """
            {"jsonrpc":"2.0","id":1,"method":"message/send",
             "params":{"message":{"parts":[{"text":"return a big payload"}]}}}""";

        HttpClient http = HttpClient.newHttpClient();
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(kubemqUrl + "/a2a/" + agentId))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();

        String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
        if (body.contains("-32603"))
            System.out.println("Error code: -32603  (response too large)");
        else
            System.out.println("Response within the size limit.");
    }
}
# .kb/integration-a2a/examples/python/limits/response-size/client.py
import os

import httpx

KUBEMQ_URL = os.getenv("KUBEMQ_URL", "http://localhost:9090")
AGENT_ID = "oversize-agent-01"


def main() -> None:
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "message/send",
        "params": {"message": {"parts": [{"text": "return a big payload"}]}},
    }
    resp = httpx.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload, timeout=30)
    error = resp.json().get("error")
    if error:
        print(f"Error code: {error['code']}  Message: {error['message']}")
    else:
        print("Response within the size limit.")


if __name__ == "__main__":
    main()
// .kb/integration-a2a/examples/typescript/limits/response-size/client.ts
const kubemqUrl = process.env.KUBEMQ_URL ?? "http://localhost:9090";
const agentId = "oversize-agent-01";

const resp = await fetch(`${kubemqUrl}/a2a/${agentId}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "message/send",
    params: { message: { parts: [{ text: "return a big payload" }] } },
  }),
});

const body = (await resp.json()) as {
  error?: { code: number; message: string };
};
if (body.error) {
  console.log(`Error code: ${body.error.code}  Message: ${body.error.message}`);
} else {
  console.log("Response within the size limit.");
}

Timeouts compound. The gateway adds GatewayTimeoutBuffer (10s) on top of the effective request timeout before timing out the proxied call to the agent, so the agent always has a slightly longer window than the caller-facing timeout. See Concurrency & limits.

Was this page helpful?

On this page