Agent Cards
Discover agent capabilities through A2A agent cards served at well-known endpoints — platform card, individual card, skills, and tags.
An agent card is the machine-readable description of an agent's identity and
capabilities. KubeMQ serves agent cards at the standard A2A /.well-known/agent-card.json
endpoints so callers can discover what an agent does before sending it a request.
Overview
Following the A2A protocol convention, KubeMQ exposes two kinds of card:
- Platform card — describes the KubeMQ gateway itself. The
nameis alwayskubemq. It carries no agent-specific skills; it advertises the gateway and the protocol versions it speaks. - Individual card — describes one registered agent. It is the
AgentCardyou supplied at registration, enriched with the server-managedregistered_atandlast_seentimestamps. This is the same representation returned byGET /agents/{agent_id}.
Cards are read-only discovery surfaces. You create and update them through the registry (register / heartbeat); the card endpoints just expose the current state. Both card endpoints are public — no JWT is required to read them, since discovery must work before a caller authenticates.
The two card endpoints
| Endpoint | Returns |
|---|---|
GET /.well-known/agent-card.json | Platform card — the KubeMQ gateway's own card |
GET /a2a/{agent_id}/.well-known/agent-card.json | Individual agent's enriched card |
Requesting a well-known card for a non-existent agent returns HTTP 404.
Platform card
The platform card represents the gateway, not any agent. Its name is always kubemq:
{
"name": "kubemq",
"description": "KubeMQ A2A Gateway",
"version": "latest",
"url": "http://localhost:9090/",
"skills": [],
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"protocolVersions": ["1.0"]
}Individual agent card
An individual card returns the agent's registered fields plus the server-managed timestamps:
{
"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"],
"registered_at": "2026-04-06T10:00:00Z",
"last_seen": "2026-04-06T10:05:00Z"
}Card fields
The AgentCard carries the agent's identity, endpoint, and capabilities. The
required fields are agent_id, name, and url.
| Field | JSON key | Required | Description |
|---|---|---|---|
| Agent ID | agent_id | Yes | Unique identifier — 2–128 chars, lowercase alphanumeric with hyphens, starting and ending with alphanumeric |
| Name | name | Yes | Human-readable name (max 256 chars) |
| Description | description | No | Free-text description (max 2048 chars) |
| Version | version | No | Agent version, e.g. 1.0.0 (max 64 chars) |
| URL | url | Yes | Agent endpoint — an absolute http:// or https:// URL (max 2048 chars) |
| Skills | skills | No | List of AgentSkill entries (see below) |
| Capabilities | capabilities | No | Free-form capability map |
| Default input modes | defaultInputModes | No | e.g. ["text"] |
| Default output modes | defaultOutputModes | No | e.g. ["text"] |
| Protocol versions | protocolVersions | No | Defaults to ["1.0"] if omitted |
| Metadata | metadata | No | Key-value string map |
registered_at and last_seen are server-managed — KubeMQ sets them on
registration and heartbeat. Do not send them when registering; they will be
overwritten.
Skills and tags
Each entry in skills is an AgentSkill. Skills make agents discoverable: the
registry filters by skill tags when listing agents, so well-chosen tags let
callers find the right agent without knowing its ID.
| Field | JSON key | Required | Description |
|---|---|---|---|
| ID | id | Yes | Skill identifier |
| Name | name | Yes | Skill name |
| Description | description | No | What the skill does |
| Tags | tags | No | Tags used for discovery filtering |
To find every agent advertising a tag, pass it to the list endpoint —
GET /agents?skill_tags=echo — see the registry.
Fetch a card
The example below reads an individual agent's card. The curl tab hits the standard well-known endpoint; the language tabs fetch the same card representation via the registry API and print every field.
# Platform card (the gateway itself)
curl http://localhost:9090/.well-known/agent-card.json
# Individual agent card
curl http://localhost:9090/a2a/echo-agent-01/.well-known/agent-card.jsonusing System.Text.Json.Nodes;
const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "full-info-agent-01";
using var client = new HttpClient();
var resp = await client.GetAsync($"{KubeMqUrl}/agents/{AgentId}");
Console.WriteLine($"Status: {(int)resp.StatusCode}");
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
Console.WriteLine("\n--- Agent Card ---");
Console.WriteLine($" agent_id: {data["agent_id"]}");
Console.WriteLine($" name: {data["name"]}");
Console.WriteLine($" description: {data["description"]}");
Console.WriteLine($" version: {data["version"]}");
Console.WriteLine($" url: {data["url"]}");
Console.WriteLine($" defaultInputModes: {data["defaultInputModes"]}");
Console.WriteLine($" defaultOutputModes: {data["defaultOutputModes"]}");
Console.WriteLine($" protocolVersions: {data["protocolVersions"]}");
Console.WriteLine($" registered_at: {data["registered_at"]}");
Console.WriteLine($" last_seen: {data["last_seen"]}");
var skills = data["skills"]?.AsArray() ?? [];
Console.WriteLine($"\n--- Skills ({skills.Count}) ---");
foreach (var skill in skills)
{
Console.WriteLine($" [{skill!["id"]}] {skill["name"]}: {skill["description"]}");
Console.WriteLine($" tags: {skill["tags"]}");
}package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
kubemqURL = "http://localhost:9090"
agentID = "full-info-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.Println("\n--- Agent Card ---")
fmt.Printf(" agent_id: %v\n", data["agent_id"])
fmt.Printf(" name: %v\n", data["name"])
fmt.Printf(" description: %v\n", data["description"])
fmt.Printf(" version: %v\n", data["version"])
fmt.Printf(" url: %v\n", data["url"])
fmt.Printf(" defaultInputModes: %v\n", data["defaultInputModes"])
fmt.Printf(" defaultOutputModes: %v\n", data["defaultOutputModes"])
fmt.Printf(" protocolVersions: %v\n", data["protocolVersions"])
fmt.Printf(" registered_at: %v\n", data["registered_at"])
fmt.Printf(" last_seen: %v\n", data["last_seen"])
skills, _ := data["skills"].([]interface{})
fmt.Printf("\n--- Skills (%d) ---\n", len(skills))
for _, s := range skills {
sk, _ := s.(map[string]interface{})
fmt.Printf(" [%v] %v: %v\n", sk["id"], sk["name"], sk["description"])
fmt.Printf(" tags: %v\n", sk["tags"])
}
}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 = "full-info-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("\n--- Agent Card ---");
System.out.println(" agent_id: " + data.path("agent_id").asText());
System.out.println(" name: " + data.path("name").asText());
System.out.println(" description: " + data.path("description").asText());
System.out.println(" version: " + data.path("version").asText());
System.out.println(" url: " + data.path("url").asText());
System.out.println(" defaultInputModes: " + data.path("defaultInputModes"));
System.out.println(" defaultOutputModes: " + data.path("defaultOutputModes"));
System.out.println(" protocolVersions: " + data.path("protocolVersions"));
System.out.println(" registered_at: " + data.path("registered_at").asText());
System.out.println(" last_seen: " + data.path("last_seen").asText());
var skills = data.path("skills");
System.out.println("\n--- Skills (" + skills.size() + ") ---");
for (var skill : skills) {
System.out.println(" [" + skill.get("id").asText() + "] "
+ skill.get("name").asText() + ": " + skill.get("description").asText());
System.out.println(" tags: " + skill.get("tags"));
}
}
}"""Agent-Info example — retrieves and displays all agent card fields."""
import asyncio
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "full-info-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("\n--- Agent Card ---")
print(f" agent_id: {data.get('agent_id')}")
print(f" name: {data.get('name')}")
print(f" description: {data.get('description')}")
print(f" version: {data.get('version')}")
print(f" url: {data.get('url')}")
print(f" defaultInputModes: {data.get('defaultInputModes')}")
print(f" defaultOutputModes: {data.get('defaultOutputModes')}")
print(f" protocolVersions: {data.get('protocolVersions')}")
print(f" registered_at: {data.get('registered_at')}")
print(f" last_seen: {data.get('last_seen')}")
skills = data.get("skills", [])
print(f"\n--- Skills ({len(skills)}) ---")
for skill in skills:
print(f" [{skill['id']}] {skill['name']}: {skill['description']}")
print(f" tags: {skill.get('tags', [])}")
if __name__ == "__main__":
asyncio.run(main())const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "info-agent-01";
async function main() {
const resp = await fetch(`${KUBEMQ_URL}/agents/${AGENT_ID}`);
const agent = await resp.json();
console.log("=== Agent Card Details ===");
console.log(` agent_id: ${agent.agent_id}`);
console.log(` name: ${agent.name}`);
console.log(` description: ${agent.description}`);
console.log(` version: ${agent.version}`);
console.log(` url: ${agent.url}`);
console.log(` registered_at: ${agent.registered_at}`);
console.log(` last_seen: ${agent.last_seen}`);
console.log(` protocolVersions: ${JSON.stringify(agent.protocolVersions)}`);
console.log(` defaultInputModes: ${JSON.stringify(agent.defaultInputModes)}`);
console.log(` defaultOutputModes:${JSON.stringify(agent.defaultOutputModes)}`);
console.log("\n=== Skills ===");
for (const skill of agent.skills || []) {
console.log(` - ${skill.id}: ${skill.name} (tags: ${(skill.tags || []).join(", ")})`);
console.log(` ${skill.description}`);
}
console.log("\n=== Full JSON ===");
console.log(JSON.stringify(agent, null, 2));
}
main().catch(console.error);The curl tab uses the public /.well-known/agent-card.json endpoint; the language
tabs read the same card via GET /agents/{agent_id} (the registry API), which
returns an identical representation. Both surfaces return the enriched card.
Related
Was this page helpful?
Agent Registry
Register, list, heartbeat, and deregister AI agents through the A2A REST API — agent cards, ownership, TTL liveness, and the MaxAgents limit.
Synchronous Messaging
Send JSON-RPC 2.0 message/send requests to A2A agents — context IDs, custom methods, header forwarding, and concurrent calls.