KubeMQ
AiwayAI Agents (A2A)

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 cardagent_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):

OperationMethod · PathPurpose
RegisterPOST /agents/registerAdd or re-register an agent card
ListGET /agentsList agents, optionally filtered by skill tags
Get oneGET /agents/{agent_id}Fetch a single agent's full card
HeartbeatPOST /agents/heartbeatRefresh last_seen to stay alive
DeregisterPOST /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.

FieldTypeRequiredDescription
agent_idstringyesUnique identifier; 2–128 chars, lowercase alphanumeric and hyphens
namestringyesHuman-readable name (max 256 chars)
urlstringyesAbsolute http(s):// endpoint the gateway POSTs to (max 2048 chars)
descriptionstringnoFree-text description (max 2048 chars)
versionstringnoAgent version (max 64 chars)
skillsarraynoList of AgentSkill objects (see below)
defaultInputModesstring[]noDefault input modes, e.g. ["text"]
defaultOutputModesstring[]noDefault output modes, e.g. ["text"]
protocolVersionsstring[]noSupported protocol versions; defaults to ["1.0"]
registered_atstringserver-setOriginal registration time (preserved across re-registration)
last_seenstringserver-setLast 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-01
using 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-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();

// 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

StatusOperationMeaning
200register / heartbeat / get / listSuccess
400register / heartbeatValidation error (bad card, missing agent_id)
403register / heartbeat / deregisterOwnership conflict (auth enabled)
404get / deregisterAgent not found
409registerMaxAgents limit reached

Was this page helpful?

On this page