3. Discover & invoke
Find your agent by capability and call it synchronously with message/send.
In Step 2 you registered the
research-agent-01 agent with a research skill. Now you'll act as a caller: first
discover the agent by its capability (no need to know its ID up front), then invoke it
synchronously and read the result back in a single round-trip.
This step assumes KubeMQ is running on port 9090 and research-agent-01 is already
registered (Steps 1 and 2). Verify with curl http://localhost:9090/agents — the agent
should appear in the roster.
Discover by capability
Callers don't have to hard-code agent IDs. The registry lets you find agents by skill
tag: GET /agents?skill_tags=research returns every registered agent that advertises
the research skill as a bare JSON array of agent cards. This is capability-based
discovery — ask for what you need done, get back who can do it.
curl "http://localhost:9090/agents?skill_tags=research"import asyncio
import httpx
KUBEMQ_URL = "http://localhost:9090"
async def main() -> None:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{KUBEMQ_URL}/agents",
params={"skill_tags": "research"},
)
agents = resp.json()
for agent in agents:
skills = [s["id"] for s in agent.get("skills", [])]
print(f" {agent['agent_id']}: skills={skills}, url={agent['url']}")
if __name__ == "__main__":
asyncio.run(main())The response includes research-agent-01 — its agent_id, url, and advertised
skills:
[
{
"agent_id": "research-agent-01",
"name": "Research Agent",
"url": "http://localhost:18080/",
"skills": [
{
"id": "research",
"name": "Research",
"description": "Searches sources and returns a summary.",
"tags": ["research", "summarize"]
}
],
"protocolVersions": ["1.0"]
}
]skill_tags accepts a comma-separated list (?skill_tags=research,summarize) and is
matched in memory after the roster is fetched. See the
Agent registry for the full list/filter API.
Invoke synchronously
Once you know the agent's ID, invoke it by POSTing a JSON-RPC 2.0 message/send envelope
to POST /a2a/research-agent-01. The gateway routes the request to the agent through its
Agent Bridge (the per-agent virtual subscriber) and
relays the agent's reply on the same HTTP response — no SSE, no polling.
curl -X POST http://localhost:9090/a2a/research-agent-01 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"parts": [{"text": "Summarize the history of message queues"}]
}
}
}'import asyncio
import json
import httpx
KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "research-agent-01"
async def main() -> None:
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"parts": [{"text": "Summarize the history of message queues"}],
},
},
}
async with httpx.AsyncClient() as client:
resp = await client.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload)
data = resp.json()
print(json.dumps(data, indent=2))
if "error" in data:
print(f"\nAgent returned an error: {data['error']['message']}")
else:
print("\nGot a synchronous result from the agent.")
if __name__ == "__main__":
asyncio.run(main())The agent's JSON-RPC result comes back verbatim under result:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"summary": "Research summary for: Summarize the history of message queues",
"sources": 3
}
}Header forwarding & error handling
A couple of behaviors are worth knowing before you build real callers:
-
Header forwarding — any request header you send prefixed with
X-is forwarded to the agent (hop-by-hop and sensitive headers likeAuthorizationandCookieare stripped), and the gateway always injectsX-KubeMQ-Caller-IDso the agent knows who originated the call. Use this to pass tracing or tenant context through to your agent. -
Transport vs application errors — when a response carries an
errorobject, KubeMQ distinguishes two classes:- Transport errors — the agent never processed the request (unreachable, timeout,
502/503/504, or an oversized reply). Internally these surface asExecuted: false, and they are safe to retry. - Application errors — the agent processed the request and chose to return a
JSON-RPC error (
Executed: true). Retrying the same request usually won't help — fix the request instead.
Tell them apart so your retry logic only re-sends calls that never reached the agent.
- Transport errors — the agent never processed the request (unreachable, timeout,
For the full message/send envelope (context IDs, custom methods), header-forwarding
rules, and the complete error-code list, see
Synchronous messaging and the
Agent registry.
Next step
You've discovered and invoked your agent in one round-trip. Next, switch from a single reply to a live stream of task events as the agent works.
Was this page helpful?