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.
| Field | Type | Default | Description |
|---|---|---|---|
Enable | bool | true | Whether the A2A connector is served. Set to false to disable /a2a/* and the registry. |
AgentTTLSeconds | int | 300 | Liveness window for a registered agent. An agent that does not heartbeat within this window is considered stale. Must be positive. |
DefaultTimeoutSeconds | int | 300 | Timeout applied to an agent request when the caller does not set params.configuration.timeout. Must be positive. |
MaxTimeoutSeconds | int | 3600 | Upper bound for caller-specified timeouts. Larger values are capped to this. Must be >= DefaultTimeoutSeconds. |
MaxAgents | int | 0 | Maximum number of registered agents. 0 means unlimited. Cannot be negative. |
MaxSSEIdleSeconds | int | 300 | Idle 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. |
AgentMaxResponseBytes | int64 | 10485760 | Maximum agent response size in bytes (10 MB). Oversized responses are rejected with -32603. 0 means unlimited. Cannot be negative. |
AgentTLSSkipVerify | bool | false | When agents are registered with https:// URLs, skip TLS certificate verification. Leave false in production. |
AgentMaxConcurrency | int | 100 | Maximum 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:nextThe 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:
| Variable | Config field | Default |
|---|---|---|
CONNECTORSA2_A_ENABLE | Connectors.A2A.Enable | true |
CONNECTORSA2_A_AGENT_TTL_SECONDS | Connectors.A2A.AgentTTLSeconds | 300 |
CONNECTORSA2_A_DEFAULT_TIMEOUT_SECONDS | Connectors.A2A.DefaultTimeoutSeconds | 300 |
CONNECTORSA2_A_MAX_TIMEOUT_SECONDS | Connectors.A2A.MaxTimeoutSeconds | 3600 |
CONNECTORSA2_A_MAX_AGENTS | Connectors.A2A.MaxAgents | 0 |
CONNECTORSA2_A_MAX_SSE_IDLE_SECONDS | Connectors.A2A.MaxSSEIdleSeconds | 300 |
CONNECTORSA2_A_TRUSTED_ORIGINS | Connectors.A2A.TrustedOrigins | auto |
CONNECTORSA2_A_AGENT_MAX_RESPONSE_BYTES | Connectors.A2A.AgentMaxResponseBytes | 10485760 |
CONNECTORSA2_A_AGENT_TLS_SKIP_VERIFY | Connectors.A2A.AgentTLSSkipVerify | false |
CONNECTORSA2_A_AGENT_MAX_CONCURRENCY | Connectors.A2A.AgentMaxConcurrency | 100 |
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:
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: 100The 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.
Related
Was this page helpful?