Synchronous Messaging
Send JSON-RPC 2.0 message/send requests to A2A agents — context IDs, custom methods, header forwarding, and concurrent calls.
Synchronous A2A messaging is a request/reply call: a caller POSTs a JSON-RPC 2.0
request to POST /a2a/<agent_id>, the gateway routes it to the target agent through
its virtual subscriber, and the agent's reply comes back on the same HTTP response —
no SSE, no polling.
Overview
message/send is the workhorse of the A2A connector. You address an agent by ID in
the URL, put a standard JSON-RPC 2.0 envelope in the body, and read the reply
synchronously. The gateway is method-agnostic: it forwards whatever method you
send — message/send, tasks/get, tasks/cancel, or your own custom/action — and
returns the agent's response as-is. Use synchronous messaging whenever you want a
single answer back in one round-trip; switch to
streaming when the agent produces incremental
output over time.
How it works
A synchronous call travels from the caller through the gateway and the agent's virtual subscriber to the plain HTTP agent, then the reply retraces the path.
The gateway proxies one request to one agent and relays the reply on the same HTTP response.
The gateway validates the agent ID and Content-Type, reads the timeout from
params.configuration.timeout (falling back to DefaultTimeoutSeconds, capped at
MaxTimeoutSeconds), packs the caller's X-* headers, and issues a Query on
_AGENTS_.agents/<agent_id>. The agent's virtual subscriber unpacks the request,
POSTs it to the agent's registered URL, and relays the response.
Basic message/send
Send a text message and read the synchronous reply. The agent receives the full JSON-RPC envelope and returns its result.
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!"}]
}
}
}'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!");const (
kubemqURL = "http://localhost:9090"
agentID = "echo-agent-01"
)
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)
fmt.Println(string(body))static final String KUBEMQ_URL = "http://localhost:9090";
static final String AGENT_ID = "echo-agent-01";
static final ObjectMapper MAPPER = new ObjectMapper();
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));import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "echo-agent-01"
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 dataconst KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "echo-agent-01";
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));Context IDs
Set params.contextId to correlate a sequence of messages with the same agent — for
conversation threading or session management. The gateway forwards it to the agent,
which echoes it back so callers can confirm the correlation.
curl -X POST http://localhost:9090/a2a/context-agent-01 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {"parts": [{"text": "Track this request"}]},
"contextId": "ctx-001"
}
}'var payload = new JsonObject
{
["jsonrpc"] = "2.0",
["id"] = 1,
["method"] = "message/send",
["params"] = new JsonObject
{
["message"] = new JsonObject
{
["parts"] = new JsonArray(new JsonObject { ["text"] = "Track this request" })
},
["contextId"] = "ctx-001"
}
};
using var client = new HttpClient();
var resp = await client.PostAsync(
$"{KubeMqUrl}/a2a/context-agent-01",
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
var returnedCtx = data["result"]?["contextId"]?.GetValue<string>();
Console.WriteLine($"Received contextId: {returnedCtx}");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": "Track this request"}}},
"contextId": "ctx-001",
},
}
data, _ := json.Marshal(payload)
resp, _ := http.Post("http://localhost:9090/a2a/context-agent-01", "application/json", bytes.NewReader(data))
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
if r, ok := result["result"].(map[string]interface{}); ok {
fmt.Printf("Received contextId: %v\n", r["contextId"])
}var payload = Map.of(
"jsonrpc", "2.0",
"id", 1,
"method", "message/send",
"params", Map.of(
"message", Map.of("parts", List.of(Map.of("text", "Track this request"))),
"contextId", "ctx-001"
)
);
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/a2a/context-agent-01"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
var data = MAPPER.readTree(resp.body());
var returnedCtx = data.path("result").path("contextId").asText(null);
System.out.println("Received contextId: " + returnedCtx);payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {"parts": [{"text": "Track this request"}]},
"contextId": "ctx-001",
},
}
async with httpx.AsyncClient() as client:
resp = await client.post("http://localhost:9090/a2a/context-agent-01", json=payload)
data = resp.json()
returned_ctx = data.get("result", {}).get("contextId")
print(f"Received contextId: {returned_ctx}")const contextId = "ctx-001";
const request = {
jsonrpc: "2.0",
id: 1,
method: "message/send",
params: {
message: { parts: [{ text: "Track this request" }] },
contextId,
},
};
const resp = await fetch(`${KUBEMQ_URL}/a2a/context-agent-01`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
const data = await resp.json();
const echoedCtx = data.result?.contextId;
console.log(`Echoed contextId: ${echoedCtx}`);Custom methods
The gateway forwards any JSON-RPC method name to the agent unchanged — standard A2A
methods like tasks/get and tasks/cancel, or your own custom/action. KubeMQ does
not interpret the method; the agent decides how to handle it.
curl -X POST http://localhost:9090/a2a/custom-method-agent-01 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "custom/action",
"params": {"data": "custom-payload"}
}'var payload = new JsonObject
{
["jsonrpc"] = "2.0",
["id"] = 1,
["method"] = "custom/action",
["params"] = new JsonObject { ["data"] = "custom-payload" }
};
using var client = new HttpClient();
var resp = await client.PostAsync(
$"{KubeMqUrl}/a2a/custom-method-agent-01",
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
var handledMethod = data["result"]?["handled_method"]?.GetValue<string>();
Console.WriteLine($"Handled method: {handledMethod}");payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": "custom/action",
"params": map[string]interface{}{"data": "custom-payload"},
}
data, _ := json.Marshal(payload)
resp, _ := http.Post("http://localhost:9090/a2a/custom-method-agent-01", "application/json", bytes.NewReader(data))
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
r, _ := result["result"].(map[string]interface{})
fmt.Printf("Handled method: %v\n", r["handled_method"])var payload = Map.of(
"jsonrpc", "2.0",
"id", 1,
"method", "custom/action",
"params", Map.of("data", "custom-payload")
);
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/a2a/custom-method-agent-01"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
var data = MAPPER.readTree(resp.body());
System.out.println("Handled method: " + data.path("result").path("handled_method").asText());payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "custom/action",
"params": {"data": "custom-payload"},
}
async with httpx.AsyncClient() as client:
resp = await client.post("http://localhost:9090/a2a/custom-method-agent-01", json=payload)
data = resp.json()
result = data.get("result", {})
print(f"Handled method: {result.get('handled_method')}")async function sendRpc(method: string, params: unknown) {
const request = { jsonrpc: "2.0", id: Date.now(), method, params };
const resp = await fetch(`${KUBEMQ_URL}/a2a/custom-method-agent-01`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
return resp.json();
}
const custom = await sendRpc("custom/action", { data: "custom-payload" });
console.log("Response:", JSON.stringify(custom, null, 2));Header forwarding
Caller request headers prefixed with X- are forwarded to the agent; hop-by-hop and
sensitive headers (Authorization, Cookie, X-Forwarded-For, and others) are
stripped. The gateway always injects X-KubeMQ-Caller-ID so the agent knows which
client originated the call.
curl -X POST http://localhost:9090/a2a/header-agent-01 \
-H "Content-Type: application/json" \
-H "X-Custom-Header: my-custom-value" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {"parts": [{"text": "Check my headers"}]}
}
}'var payload = new JsonObject
{
["jsonrpc"] = "2.0",
["id"] = 1,
["method"] = "message/send",
["params"] = new JsonObject
{
["message"] = new JsonObject
{
["parts"] = new JsonArray(new JsonObject { ["text"] = "Check my headers" })
}
}
};
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, $"{KubeMqUrl}/a2a/header-agent-01")
{
Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json")
};
request.Headers.Add("X-Custom-Header", "my-custom-value");
var resp = await client.SendAsync(request);
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
var received = data["result"]?["received_headers"];
Console.WriteLine($"Forwarded headers: {received?.ToJsonString()}");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": "Check my headers"}},
},
},
}
data, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "http://localhost:9090/a2a/header-agent-01", bytes.NewReader(data))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Custom-Header", "my-custom-value")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
received, _ := result["result"].(map[string]interface{})["received_headers"].(map[string]interface{})
fmt.Printf("Forwarded headers: %v\n", received)var payload = Map.of(
"jsonrpc", "2.0",
"id", 1,
"method", "message/send",
"params", Map.of(
"message", Map.of("parts", List.of(Map.of("text", "Check my headers")))
)
);
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/a2a/header-agent-01"))
.header("Content-Type", "application/json")
.header("X-Custom-Header", "my-custom-value")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
var data = MAPPER.readTree(resp.body());
System.out.println("Forwarded headers: " + data.path("result").path("received_headers"));payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {"parts": [{"text": "Check my headers"}]},
},
}
async with httpx.AsyncClient() as client:
resp = await client.post(
"http://localhost:9090/a2a/header-agent-01",
json=payload,
headers={"X-Custom-Header": "my-custom-value"},
)
data = resp.json()
received = data.get("result", {}).get("received_headers", {})
print(f"Forwarded headers: {received}")const request = {
jsonrpc: "2.0",
id: 1,
method: "message/send",
params: {
message: { parts: [{ text: "Check my headers" }] },
},
};
const resp = await fetch(`${KUBEMQ_URL}/a2a/header-agent-01`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "test-value-123",
},
body: JSON.stringify(request),
});
const data = await resp.json();
const headers = data.result?.received_headers || {};
console.log(`X-Custom-Header: ${headers["x-custom-header"] || "(not forwarded)"}`);
console.log(`X-KubeMQ-Caller-ID: ${headers["x-kubemq-caller-id"] || "(not present)"}`);Concurrent requests
Multiple simultaneous calls to the same agent are processed in parallel up to the
per-agent concurrency cap (AgentMaxConcurrency, default 100). Requests beyond the
cap are rejected immediately with a "server busy" error instead of being queued — see
Concurrency & limits.
# Fire 20 requests in parallel against the same agent
for i in $(seq 1 20); do
curl -s -X POST http://localhost:9090/a2a/concurrent-agent-01 \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"id\":$i,\"method\":\"message/send\",\"params\":{\"message\":{\"parts\":[{\"text\":\"Request #$i\"}]}}}" &
done
waitconst int NumRequests = 20;
async Task<JsonNode?> SendRequest(HttpClient httpClient, int requestId)
{
var payload = new JsonObject
{
["jsonrpc"] = "2.0",
["id"] = requestId,
["method"] = "message/send",
["params"] = new JsonObject
{
["message"] = new JsonObject
{
["parts"] = new JsonArray(new JsonObject { ["text"] = $"Request #{requestId}" })
}
}
};
var resp = await httpClient.PostAsync(
$"{KubeMqUrl}/a2a/concurrent-agent-01",
new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
var body = await resp.Content.ReadAsStringAsync();
return JsonNode.Parse(body);
}
using var client = new HttpClient();
var tasks = Enumerable.Range(1, NumRequests).Select(i => SendRequest(client, i)).ToArray();
var results = await Task.WhenAll(tasks);
var successes = results.Count(r => r?["result"] != null);
Console.WriteLine($"Successes: {successes} / {NumRequests}");const numRequests = 20
results := make(chan map[string]interface{}, numRequests)
var wg sync.WaitGroup
for i := 1; i <= numRequests; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
payload := map[string]interface{}{
"jsonrpc": "2.0", "id": id, "method": "message/send",
"params": map[string]interface{}{
"message": map[string]interface{}{
"parts": []map[string]interface{}{{"text": fmt.Sprintf("Request #%d", id)}},
},
},
}
data, _ := json.Marshal(payload)
resp, err := http.Post("http://localhost:9090/a2a/concurrent-agent-01", "application/json", bytes.NewReader(data))
if err != nil {
results <- nil
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var r map[string]interface{}
json.Unmarshal(body, &r)
results <- r
}(i)
}
wg.Wait()
close(results)
successes := 0
for r := range results {
if r != nil {
if _, ok := r["result"]; ok {
successes++
}
}
}
fmt.Printf("Successes: %d / %d\n", successes, numRequests)static final int NUM_REQUESTS = 20;
var client = HttpClient.newHttpClient();
List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
for (int i = 1; i <= NUM_REQUESTS; i++) {
var payload = Map.of(
"jsonrpc", "2.0", "id", i, "method", "message/send",
"params", Map.of(
"message", Map.of("parts", List.of(Map.of("text", "Request #" + i)))
)
);
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/a2a/concurrent-agent-01"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();
futures.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
int successes = 0;
for (var future : futures) {
var data = MAPPER.readTree(future.get().body());
if (data.has("result")) successes++;
}
System.out.println("Successes: " + successes + " / " + NUM_REQUESTS);NUM_REQUESTS = 20
async def send_request(client: httpx.AsyncClient, request_id: int) -> dict:
payload = {
"jsonrpc": "2.0",
"id": request_id,
"method": "message/send",
"params": {
"message": {"parts": [{"text": f"Request #{request_id}"}]},
},
}
resp = await client.post("http://localhost:9090/a2a/concurrent-agent-01", json=payload)
return resp.json()
async with httpx.AsyncClient() as client:
tasks = [send_request(client, i) for i in range(1, NUM_REQUESTS + 1)]
results = await asyncio.gather(*tasks, return_exceptions=True)
successes = sum(1 for r in results if isinstance(r, dict) and "result" in r)
print(f"Successes: {successes} / {NUM_REQUESTS}")const NUM_REQUESTS = 20;
async function sendRequest(id: number): Promise<boolean> {
const request = {
jsonrpc: "2.0",
id,
method: "message/send",
params: { message: { parts: [{ text: `Request ${id}` }] } },
};
try {
const resp = await fetch(`${KUBEMQ_URL}/a2a/concurrent-agent-01`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
const data = await resp.json();
return !!data.result;
} catch {
return false;
}
}
const promises = Array.from({ length: NUM_REQUESTS }, (_, i) => sendRequest(i + 1));
const results = await Promise.all(promises);
const succeeded = results.filter((r) => r).length;
console.log(`Succeeded: ${succeeded} / ${NUM_REQUESTS}`);Parameters
The body is a standard JSON-RPC 2.0 request. Method-specific fields live under
params.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
jsonrpc | string | yes | — | Must be "2.0". |
id | string | number | yes | — | Request identifier; echoed back on the response. |
method | string | yes | — | Any JSON-RPC method name, e.g. message/send, tasks/get, or a custom one. Empty method is rejected with -32600. |
params.message.parts | array | for message/send | — | Message content parts, each with a text (or other typed) field. |
params.contextId | string | no | — | Correlation ID forwarded to the agent for conversation threading. |
params.configuration.timeout | number | no | DefaultTimeoutSeconds (300) | Per-request timeout in seconds, capped at MaxTimeoutSeconds (3600). |
The URL path segment <agent_id> selects the target agent and is validated against
the registry before routing.
Response
On success the gateway returns the agent's JSON-RPC result verbatim (the echo agent
mirrors the request under result.echo):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"echo": {
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": { "parts": [{ "text": "Hello, agent!" }] }
}
}
}
}A response carrying an error object signals a problem. KubeMQ distinguishes two
failure classes:
- Transport errors — the agent never processed the request (unreachable, timeout,
502/503/504, or an oversized response). Internally these surface asExecuted: false; safe to retry. - Application errors — the agent processed the request and returned a JSON-RPC
error in its response body (
Executed: true). Retrying without changing the request usually will not help.
A timeout, for example, returns:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "agent timeout: echo-agent-01"
}
}See Error handling for the full code list and retry guidance.
Related
Streaming (SSE)
Use message/stream when the agent emits incremental task events over time.
Error handling
A2A error codes and how to tell transport failures from application errors.
Concurrency & limits
Per-agent concurrency cap, response-size limit, and the timeout chain.
Reference
Endpoint, method, and error-code tables plus the AgentCard schema.
Was this page helpful?
Agent Cards
Discover agent capabilities through A2A agent cards served at well-known endpoints — platform card, individual card, skills, and tags.
Streaming (SSE)
Stream long-running agent tasks over Server-Sent Events with message/stream — task envelopes, keepalive, idle timeout, and client-disconnect cancellation.