Agent Registry
Register, list, heartbeat, and deregister AI agents through the A2A REST API — agent cards, ownership, TTL liveness, and the MaxAgents limit.
The agent registry is a REST API where agents announce themselves to KubeMQ by
agent_id and HTTP URL. It is the source of truth for who can be reached over the A2A
gateway: registering an agent spawns its virtual subscriber,
and a TTL-based liveness check removes agents that stop sending heartbeats.
Overview
Every agent that callers can reach through POST /a2a/{agent_id} must first be
registered. A registration is an agent card — agent_id, human-readable name,
absolute url, and an optional list of skills. The registry persists cards in SQLite,
tracks each agent's last_seen time, and replicates state across a cluster so any node
can route to any agent.
The registry exposes five operations as plain HTTP+JSON (not JSON-RPC):
| Operation | Method · Path | Purpose |
|---|---|---|
| Register | POST /agents/register | Add or re-register an agent card |
| List | GET /agents | List agents, optionally filtered by skill tags |
| Get one | GET /agents/{agent_id} | Fetch a single agent's full card |
| Heartbeat | POST /agents/heartbeat | Refresh last_seen to stay alive |
| Deregister | POST /agents/deregister or DELETE /agents/{agent_id} | Remove an agent |
How it works
The registry is a service backed by SQLite. Registering spawns a virtual subscriber and emits a replication event; a background liveness checker sweeps expired agents.
Registration persists the card and spawns a virtual subscriber; the liveness checker prunes agents past their TTL.
The agent card
An agent card describes one agent. agent_id, name, and url are required; the
url must be an absolute http:// or https:// address. Server-managed fields
(registered_at, last_seen) are populated on the response.
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | yes | Unique identifier; 2–128 chars, lowercase alphanumeric and hyphens |
name | string | yes | Human-readable name (max 256 chars) |
url | string | yes | Absolute http(s):// endpoint the gateway POSTs to (max 2048 chars) |
description | string | no | Free-text description (max 2048 chars) |
version | string | no | Agent version (max 64 chars) |
skills | array | no | List of AgentSkill objects (see below) |
defaultInputModes | string[] | no | Default input modes, e.g. ["text"] |
defaultOutputModes | string[] | no | Default output modes, e.g. ["text"] |
protocolVersions | string[] | no | Supported protocol versions; defaults to ["1.0"] |
registered_at | string | server-set | Original registration time (preserved across re-registration) |
last_seen | string | server-set | Last heartbeat or registration time |
Each entry in skills is an AgentSkill: id (required), name (required),
description, and tags (used by the list filter and skill-based
discovery).
{
"agent_id": "echo-agent-01",
"name": "Echo Agent",
"description": "A simple echo agent for testing",
"version": "1.0.0",
"url": "http://localhost:18080/",
"skills": [
{
"id": "echo",
"name": "Echo",
"description": "Echoes back the received message",
"tags": ["test", "echo"]
}
],
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"protocolVersions": ["1.0"]
}Register an agent
POST /agents/register with the agent card as the JSON body. Re-registering the same
agent_id upserts the card and preserves the original registered_at. The response
is the stored card with registered_at and last_seen populated.
When auth is enabled, the registered_by field is set from the JWT principal and is
used for ownership checks. Registration fails with 400 (validation),
403 (ownership conflict), or 409 (the MaxAgents limit was reached).
curl -X POST http://localhost:9090/agents/register \
-H "Content-Type: application/json" \
-d '{
"agent_id": "echo-agent-01",
"name": "Echo Agent",
"description": "A simple echo agent for testing",
"version": "1.0.0",
"url": "http://localhost:18080/",
"skills": [
{"id": "echo", "name": "Echo", "description": "Echoes back the received message", "tags": ["test", "echo"]}
],
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"protocolVersions": ["1.0"]
}'using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
const int AgentPort = 18080;
var card = new JsonObject
{
["agent_id"] = "echo-agent-01",
["name"] = "Echo Agent",
["description"] = "A simple echo agent for testing",
["version"] = "1.0.0",
["url"] = $"http://localhost:{AgentPort}/",
["skills"] = new JsonArray(new JsonObject
{
["id"] = "echo", ["name"] = "Echo",
["description"] = "Echoes back the received message",
["tags"] = new JsonArray("test", "echo")
}),
["defaultInputModes"] = new JsonArray("text"),
["defaultOutputModes"] = new JsonArray("text"),
["protocolVersions"] = new JsonArray("1.0")
};
using var client = new HttpClient();
var resp = await client.PostAsync(
$"{KubeMqUrl}/agents/register",
new StringContent(card.ToJsonString(), Encoding.UTF8, "application/json"));
Console.WriteLine($"Registered: {(int)resp.StatusCode}");
var body = await resp.Content.ReadAsStringAsync();
Console.WriteLine(JsonSerializer.Serialize(JsonNode.Parse(body), new JsonSerializerOptions { WriteIndented = true }));package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
kubemqURL = "http://localhost:9090"
agentID = "echo-agent-01"
agentPort = 18080
)
func main() {
card := map[string]interface{}{
"agent_id": agentID,
"name": "Echo Agent",
"description": "A simple echo agent for testing",
"version": "1.0.0",
"url": fmt.Sprintf("http://localhost:%d/", agentPort),
"skills": []map[string]interface{}{
{
"id": "echo",
"name": "Echo",
"description": "Echoes back the received message",
"tags": []string{"test", "echo"},
},
},
"defaultInputModes": []string{"text"},
"defaultOutputModes": []string{"text"},
"protocolVersions": []string{"1.0"},
}
data, _ := json.Marshal(card)
resp, err := http.Post(kubemqURL+"/agents/register", "application/json", bytes.NewReader(data))
if err != nil {
fmt.Fprintf(os.Stderr, "Registration failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Registered: %d\n", resp.StatusCode)
var pretty bytes.Buffer
json.Indent(&pretty, body, "", " ")
fmt.Println(pretty.String())
}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 int AGENT_PORT = 18080;
static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
var card = Map.of(
"agent_id", "echo-agent-01",
"name", "Echo Agent",
"description", "A simple echo agent for testing",
"version", "1.0.0",
"url", "http://localhost:" + AGENT_PORT + "/",
"skills", List.of(Map.of(
"id", "echo", "name", "Echo",
"description", "Echoes back the received message",
"tags", List.of("test", "echo"))),
"defaultInputModes", List.of("text"),
"defaultOutputModes", List.of("text"),
"protocolVersions", List.of("1.0")
);
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/agents/register"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(card)))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("Registered: " + resp.statusCode());
var data = MAPPER.readTree(resp.body());
System.out.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data));
}
}import asyncio
import json
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_PORT = 18080
async def main() -> None:
card = {
"agent_id": "echo-agent-01",
"name": "Echo Agent",
"description": "A simple echo agent for testing",
"version": "1.0.0",
"url": f"http://localhost:{AGENT_PORT}/",
"skills": [
{
"id": "echo",
"name": "Echo",
"description": "Echoes back the received message",
"tags": ["test", "echo"],
}
],
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"protocolVersions": ["1.0"],
}
async with httpx.AsyncClient() as client:
resp = await client.post(f"{KUBEMQ_URL}/agents/register", json=card)
print(f"Registered: {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
const AGENT_PORT = 18080;
async function main() {
const card = {
agent_id: "echo-agent-01",
name: "Echo Agent",
description: "A simple echo agent for testing",
version: "1.0.0",
url: `http://localhost:${AGENT_PORT}/`,
skills: [
{
id: "echo",
name: "Echo",
description: "Echoes back the received message",
tags: ["test", "echo"],
},
],
defaultInputModes: ["text"],
defaultOutputModes: ["text"],
protocolVersions: ["1.0"],
};
const resp = await fetch(`${KUBEMQ_URL}/agents/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(card),
});
const data = await resp.json();
console.log("Registered:", JSON.stringify(data, null, 2));
}
main().catch(console.error);List agents
GET /agents returns a bare JSON array [<AgentCard>, ...]. Add ?skill_tags=tag1,tag2
(comma-separated) to filter by skill tags, and ?limit=N to cap the page size. Skill-tag
filtering is applied in memory after fetching, enabling skill-based discovery.
# All agents
curl http://localhost:9090/agents
# Filter by skill tags
curl "http://localhost:9090/agents?skill_tags=echo"using System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
using var client = new HttpClient();
Console.WriteLine("=== All Agents ===");
var resp = await client.GetAsync($"{KubeMqUrl}/agents");
var agentsRoot = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
var agents = agentsRoot is JsonArray arr ? arr : agentsRoot["agents"]!.AsArray();
foreach (var agent in agents)
{
var skills = agent!["skills"]?.AsArray().Select(s => s!["id"]!.GetValue<string>()).ToList() ?? [];
Console.WriteLine($" {agent["agent_id"]}: skills=[{string.Join(", ", skills)}]");
}
Console.WriteLine($"\nTotal agents: {agents.Count}");
Console.WriteLine("\n=== Filter by skill_tags=echo ===");
resp = await client.GetAsync($"{KubeMqUrl}/agents?skill_tags=echo");
var echoRoot = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
var filtered = echoRoot is JsonArray echoArr ? echoArr : echoRoot["agents"]!.AsArray();
foreach (var agent in filtered)
Console.WriteLine($" {agent!["agent_id"]}");package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const kubemqURL = "http://localhost:9090"
func listAgents(url string, label string) {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// GET /agents returns a bare JSON array: [<AgentCard>, ...]
var agents []map[string]interface{}
if err := json.Unmarshal(body, &agents); err != nil {
// Fallback for a {"agents":[...]} wrapper, if ever present.
var wrapper map[string]interface{}
json.Unmarshal(body, &wrapper)
if raw, ok := wrapper["agents"].([]interface{}); ok {
for _, a := range raw {
if m, ok := a.(map[string]interface{}); ok {
agents = append(agents, m)
}
}
}
}
fmt.Printf("=== %s ===\n", label)
for _, agent := range agents {
fmt.Printf(" %s\n", agent["agent_id"])
}
fmt.Printf("\nTotal: %d\n\n", len(agents))
}
func main() {
listAgents(kubemqURL+"/agents", "All Agents")
listAgents(kubemqURL+"/agents?skill_tags=echo", "Filter by skill_tags=echo")
}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;
public class Client {
static final String KUBEMQ_URL = "http://localhost:9090";
static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
System.out.println("=== All Agents ===");
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/agents"))
.GET().build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
var root = MAPPER.readTree(resp.body());
var agents = root.isArray() ? root : root.get("agents");
for (var agent : agents) {
System.out.println(" " + agent.get("agent_id").asText());
}
System.out.println("\nTotal agents: " + agents.size());
}
}import asyncio
import httpx
KUBEMQ_URL = "http://localhost:9090"
async def main() -> None:
async with httpx.AsyncClient() as client:
print("=== All Agents ===")
resp = await client.get(f"{KUBEMQ_URL}/agents")
data = resp.json()
agents = data.get("agents", data) if isinstance(data, dict) else data
for agent in agents:
skills = [s["id"] for s in agent.get("skills", [])]
print(f" {agent['agent_id']}: skills={skills}")
print(f"\nTotal agents: {len(agents)}")
print("\n=== Filter by skill_tags=echo ===")
resp = await client.get(f"{KUBEMQ_URL}/agents", params={"skill_tags": "echo"})
data = resp.json()
filtered = data.get("agents", data) if isinstance(data, dict) else data
for agent in filtered:
print(f" {agent['agent_id']}")
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
async function main() {
console.log("=== List all agents ===");
const allResp = await fetch(`${KUBEMQ_URL}/agents`);
const allData = await allResp.json();
const allAgents = Array.isArray(allData) ? allData : (allData.agents || []);
console.log(`Found ${allAgents.length} agent(s):`);
for (const agent of allAgents) {
const skillIds = (agent.skills || []).map((s: { id?: string }) => s.id).filter(Boolean);
console.log(` - ${agent.agent_id} (skills: ${skillIds.join(", ") || "none"})`);
}
console.log("\n=== Filter by skill_tags=echo ===");
const echoResp = await fetch(`${KUBEMQ_URL}/agents?skill_tags=echo`);
const echoData = await echoResp.json();
const echoAgents = Array.isArray(echoData) ? echoData : (echoData.agents || []);
for (const agent of echoAgents) {
console.log(` - ${agent.agent_id}`);
}
}
main().catch(console.error);Get one agent
GET /agents/{agent_id} returns the full agent card, or 404 if the agent is not
registered. Use it to inspect server-managed fields like registered_at and last_seen.
curl http://localhost:9090/agents/echo-agent-01using System.Text.Json;
using System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "echo-agent-01";
using var client = new HttpClient();
var resp = await client.GetAsync($"{KubeMqUrl}/agents/{AgentId}");
Console.WriteLine($"Status: {(int)resp.StatusCode}");
var data = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
Console.WriteLine($" agent_id: {data["agent_id"]}");
Console.WriteLine($" name: {data["name"]}");
Console.WriteLine($" url: {data["url"]}");
Console.WriteLine($" registered_at: {data["registered_at"]}");
Console.WriteLine($" last_seen: {data["last_seen"]}");package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
kubemqURL = "http://localhost:9090"
agentID = "echo-agent-01"
)
func main() {
resp, err := http.Get(kubemqURL + "/agents/" + agentID)
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 data map[string]interface{}
json.Unmarshal(body, &data)
fmt.Printf(" agent_id: %v\n", data["agent_id"])
fmt.Printf(" name: %v\n", data["name"])
fmt.Printf(" url: %v\n", data["url"])
fmt.Printf(" registered_at: %v\n", data["registered_at"])
fmt.Printf(" last_seen: %v\n", data["last_seen"])
}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;
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 client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/agents/" + AGENT_ID))
.GET().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 asyncio
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "echo-agent-01"
async def main() -> None:
async with httpx.AsyncClient() as client:
resp = await client.get(f"{KUBEMQ_URL}/agents/{AGENT_ID}")
print(f"Status: {resp.status_code}")
data = resp.json()
print(f" agent_id: {data.get('agent_id')}")
print(f" name: {data.get('name')}")
print(f" url: {data.get('url')}")
print(f" registered_at: {data.get('registered_at')}")
print(f" last_seen: {data.get('last_seen')}")
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "echo-agent-01";
async function main() {
const resp = await fetch(`${KUBEMQ_URL}/agents/${AGENT_ID}`);
const agent = await resp.json();
console.log(` agent_id: ${agent.agent_id}`);
console.log(` name: ${agent.name}`);
console.log(` url: ${agent.url}`);
console.log(` registered_at: ${agent.registered_at}`);
console.log(` last_seen: ${agent.last_seen}`);
}
main().catch(console.error);Heartbeat
POST /agents/heartbeat with {"agent_id": "..."} refreshes the agent's last_seen
time. An agent must heartbeat (or re-register) within its TTL to
avoid being expired. When auth is enabled, the ownership check applies. The response is
{"ok": true}; heartbeating an unregistered agent returns an error.
curl -X POST http://localhost:9090/agents/heartbeat \
-H "Content-Type: application/json" \
-d '{"agent_id": "echo-agent-01"}'using System.Text;
using System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "echo-agent-01";
using var client = new HttpClient();
var body = new JsonObject { ["agent_id"] = AgentId };
var resp = await client.PostAsync(
$"{KubeMqUrl}/agents/heartbeat",
new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json"));
var data = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
Console.WriteLine($"Heartbeat: status={(int)resp.StatusCode} last_seen={data["last_seen"]}");package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
kubemqURL = "http://localhost:9090"
agentID = "echo-agent-01"
)
func main() {
body, _ := json.Marshal(map[string]string{"agent_id": agentID})
resp, err := http.Post(kubemqURL+"/agents/heartbeat", "application/json", bytes.NewReader(body))
if err != nil {
fmt.Fprintf(os.Stderr, "Heartbeat failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(raw, &data)
fmt.Printf("Heartbeat: status=%d last_seen=%v\n", resp.StatusCode, data["last_seen"])
}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.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 client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/agents/heartbeat"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
MAPPER.writeValueAsString(Map.of("agent_id", AGENT_ID))))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
var data = MAPPER.readTree(resp.body());
System.out.println("Heartbeat: status=" + resp.statusCode()
+ " last_seen=" + data.path("last_seen").asText());
}
}import asyncio
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "echo-agent-01"
async def main() -> None:
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{KUBEMQ_URL}/agents/heartbeat",
json={"agent_id": AGENT_ID},
)
data = resp.json()
print(f"Heartbeat: status={resp.status_code} last_seen={data.get('last_seen')}")
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "echo-agent-01";
async function main() {
const resp = await fetch(`${KUBEMQ_URL}/agents/heartbeat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: AGENT_ID }),
});
const data = await resp.json();
console.log(`Heartbeat: status=${resp.status}, last_seen=${data.last_seen}`);
}
main().catch(console.error);Deregister
Remove an agent with either POST /agents/deregister (body {"agent_id": "..."}) or
DELETE /agents/{agent_id}. Deregistering deletes the card, stops the agent's virtual
subscriber, and drains in-flight requests. Both methods return {"ok": true}; when auth
is enabled, the ownership check applies.
# Deregister via POST
curl -X POST http://localhost:9090/agents/deregister \
-H "Content-Type: application/json" \
-d '{"agent_id": "echo-agent-01"}'
# Or via DELETE
curl -X DELETE http://localhost:9090/agents/echo-agent-01using System.Text;
using System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "echo-agent-01";
using var client = new HttpClient();
// Deregister via POST
var body = new JsonObject { ["agent_id"] = AgentId };
var postResp = await client.PostAsync(
$"{KubeMqUrl}/agents/deregister",
new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json"));
Console.WriteLine($"POST deregister: {(int)postResp.StatusCode}");
// Or via DELETE
var delResp = await client.DeleteAsync($"{KubeMqUrl}/agents/{AgentId}");
Console.WriteLine($"DELETE: {(int)delResp.StatusCode}");package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const (
kubemqURL = "http://localhost:9090"
agentID = "echo-agent-01"
)
func main() {
// Deregister via POST
body, _ := json.Marshal(map[string]string{"agent_id": agentID})
resp, err := http.Post(kubemqURL+"/agents/deregister", "application/json", bytes.NewReader(body))
if err != nil {
fmt.Fprintf(os.Stderr, "Deregister POST failed: %v\n", err)
os.Exit(1)
}
resp.Body.Close()
fmt.Printf("POST /agents/deregister: %d\n", resp.StatusCode)
// Or via DELETE
req, _ := http.NewRequest(http.MethodDelete, kubemqURL+"/agents/"+agentID, nil)
resp, err = http.DefaultClient.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "DELETE failed: %v\n", err)
os.Exit(1)
}
resp.Body.Close()
fmt.Printf("DELETE /agents/%s: %d\n", agentID, resp.StatusCode)
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Client {
static final String KUBEMQ_URL = "http://localhost:9090";
static final String AGENT_ID = "echo-agent-01";
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
// Deregister via POST
var postReq = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/agents/deregister"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
"{\"agent_id\": \"" + AGENT_ID + "\"}"))
.build();
var postResp = client.send(postReq, HttpResponse.BodyHandlers.ofString());
System.out.println("POST deregister: " + postResp.statusCode());
// Or via DELETE
var delReq = HttpRequest.newBuilder()
.uri(URI.create(KUBEMQ_URL + "/agents/" + AGENT_ID))
.DELETE()
.build();
var delResp = client.send(delReq, HttpResponse.BodyHandlers.ofString());
System.out.println("DELETE: " + delResp.statusCode());
}
}import asyncio
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "echo-agent-01"
async def main() -> None:
async with httpx.AsyncClient() as client:
# Deregister via POST
resp = await client.post(
f"{KUBEMQ_URL}/agents/deregister",
json={"agent_id": AGENT_ID},
)
print(f"POST /agents/deregister: {resp.status_code}")
# Or via DELETE
resp = await client.delete(f"{KUBEMQ_URL}/agents/{AGENT_ID}")
print(f"DELETE /agents/{AGENT_ID}: {resp.status_code}")
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "echo-agent-01";
async function main() {
// Deregister via POST
const postResp = await fetch(`${KUBEMQ_URL}/agents/deregister`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: AGENT_ID }),
});
console.log(`POST deregister status: ${postResp.status}`);
// Or via DELETE
const delResp = await fetch(`${KUBEMQ_URL}/agents/${AGENT_ID}`, {
method: "DELETE",
});
console.log(`DELETE status: ${delResp.status}`);
}
main().catch(console.error);TTL and liveness
The registry expires agents that go silent. A background liveness checker runs every
60 seconds and deletes any agent whose last_seen is older than AgentTTLSeconds
(default 300 — five minutes). Expiring an agent also removes its virtual subscriber and
emits a deregister replication event.
To stay registered, an agent must heartbeat (or re-register) within the TTL
window. A safe interval is well under AgentTTLSeconds — for the default 300s TTL, a
heartbeat every 60–120 seconds gives plenty of margin.
Tune the window with AgentTTLSeconds. See
Configuration for the full A2aConfig field set
and the disable env var.
Ownership
When authentication is enabled, the registry records the JWT principal that registered
each agent in registered_by. Heartbeat and deregister then enforce an ownership
check: only the registering principal may refresh or remove the agent. Cross-principal
re-registration is rejected with 403 (ownership conflict).
The check fails closed: if auth is enabled and an agent's registered_by is blank,
no principal can modify or delete it. See
Authentication for the auth model and
X-KubeMQ-Caller-ID propagation.
MaxAgents limit
MaxAgents caps the total number of registered agents. The default is 0, meaning
unlimited. When a positive limit is set and reached, new registrations are rejected
with 409 (conflict) — re-registering an existing agent still succeeds, since it does not
grow the count.
Response and status codes
| Status | Operation | Meaning |
|---|---|---|
200 | register / heartbeat / get / list | Success |
400 | register / heartbeat | Validation error (bad card, missing agent_id) |
403 | register / heartbeat / deregister | Ownership conflict (auth enabled) |
404 | get / deregister | Agent not found |
409 | register | MaxAgents limit reached |
Related
Agent cards
The well-known agent-card endpoints and the platform vs individual card.
How it works
Virtual subscribers, internal channels, and registry replication across a cluster.
Configuration
A2aConfig fields including AgentTTLSeconds, MaxAgents, and the disable env var.
Reference
Endpoint, JSON-RPC, AgentCard schema, and metrics reference tables.
Was this page helpful?