KubeMQ
AiwayTutorial

5. Orchestrate from an LLM (MCP)

Connect over MCP, discover the agent with agent_list, and invoke it with agent_send — the MCP→A2A bridge in action.

In Step 2 you built and registered research-agent-01, and in Steps 3 and 4 you discovered and invoked it as an A2A caller. Now you'll put an LLM in the driver's seat: an MCP client connects to the same KubeMQ server, discovers your agent, and calls it — all through the Model Context Protocol, with no new glue code on your side. This is the MCP→A2A bridge: the LLM reaches the exact agent you built in Step 2.

This step assumes KubeMQ is running on port 9090 and research-agent-01 is registered and listening (Steps 1 and 2). The MCP connector is enabled by defaultPOST /mcp is already live on the shared HTTP server. For the Python path, install the MCP SDK with pip install mcp.

Connect and list the tools

An MCP client opens a session against POST http://localhost:9090/mcp, negotiates the protocol version (2025-11-25), then calls tools/list. KubeMQ exposes 15 tools11 core messaging tools plus 4 agent-bridge tools (agent_list, agent_info, agent_send, agent_query) that appear when the A2A agent registry is available. The agent-bridge tools are what let the LLM reach your agent.

The MCP session version 2025-11-25 is a different version from the A2A Agent Card's protocolVersions: ["1.0"] you registered in Step 2 — one is the MCP handshake, the other is the agent's A2A card version. They are not interchangeable.

First open a session with initialize, then list the tools with the returned session ID:

# 1. Initialize a session — note the protocol version is the MCP one.
curl -X POST http://localhost:9090/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-11-25",
      "capabilities": {},
      "clientInfo": {"name": "tutorial", "version": "1.0.0"}
    }
  }'

# 2. List the tools (substitute the session ID from the response above).
curl -X POST http://localhost:9090/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Session-Id: <session-id>" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
  }'

The mcp SDK runs the initialize handshake and tracks the session ID for you over the Streamable HTTP transport — you only call tools/list:

import asyncio

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

KUBEMQ_MCP_URL = "http://localhost:9090"


async def main() -> None:
    async with streamablehttp_client(f"{KUBEMQ_MCP_URL}/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools = await session.list_tools()
            names = [t.name for t in tools.tools]
            print(f"{len(names)} tools available")  # 15: 11 core + 4 agent-bridge
            print("agent-bridge tools:", [n for n in names if n.startswith("agent_")])


if __name__ == "__main__":
    asyncio.run(main())

The tool list includes the four agent-bridge tools alongside the 11 core messaging tools:

["agent_list", "agent_info", "agent_send", "agent_query"]

Discover the agent with agent_list

agent_list reads the same A2A registry you queried in Step 3 — but now through an MCP tool call. Pass skill_tags to filter by capability, exactly as before, and the LLM gets back research-agent-01 without ever hard-coding its ID.

curl -X POST http://localhost:9090/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Session-Id: <session-id>" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "agent_list",
      "arguments": {"skill_tags": ["research"]}
    }
  }'
result = await session.call_tool("agent_list", {"skill_tags": ["research"]})

print(f"IsError: {result.isError}")
for content in result.content:
    print(f"Result: {content.text}")

The result text is a JSON array of agent summaries — your agent is in it:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{"type": "text", "text": "[{\"agent_id\":\"research-agent-01\",\"name\":\"Research Agent\",\"skills\":[{\"id\":\"research\",\"name\":\"Research\",\"tags\":[\"research\",\"summarize\"]}]}]"}],
    "isError": false
  }
}

Invoke the agent with agent_send

agent_send wraps your message in an A2A message/send envelope and forwards it through the per-agent Agent Bridge — the same path the direct A2A call took in Step 3, just initiated from MCP. By default the call is blocking: it waits up to timeout_seconds for the agent's reply (default 60, max 300). Pass blocking: false for fire-and-forget, or context_id to thread the message into an existing conversation.

curl -X POST http://localhost:9090/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Session-Id: <session-id>" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tools/call",
    "params": {
      "name": "agent_send",
      "arguments": {
        "agent_id": "research-agent-01",
        "message": "Summarize the history of message queues",
        "timeout_seconds": 60
      }
    }
  }'
result = await session.call_tool("agent_send", {
    "agent_id": "research-agent-01",
    "message": "Summarize the history of message queues",
    "timeout_seconds": 60,
})

print(f"IsError: {result.isError}")
for content in result.content:
    print(f"Result: {content.text}")

The agent's reply comes back in the content text — the same result your Step 2 agent returns from message/send, now delivered to the LLM:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [{"type": "text", "text": "{\"summary\":\"Research summary for: Summarize the history of message queues\",\"sources\":3}"}],
    "isError": false
  }
}

An unknown agent_id returns isError: true with Agent '<id>' not found — a tool-level error in the content block, not a JSON-RPC protocol error. The bridge also adds a gateway timeout buffer on top of your timeout_seconds. See Agent-bridge tools for agent_info, agent_query, and the full argument and error reference.

One fabric, no glue code

Step back and notice what just happened. The LLM discovered your agent by capability and invoked it — and you wrote no integration code to make that possible. The same research-agent-01 you built in Step 2 (a plain-HTTP service, no KubeMQ SDK) is now reachable by an A2A caller and by any MCP-speaking LLM, over the same message broker. That's the Aiway thesis: one fabric, two doors — register once, reach it from both.

To wire this into a real LLM host such as Claude Desktop — a claude_desktop_config.json entry that points at http://localhost:9090/mcp — see MCP Getting started. The host then sees all 15 tools (including the four agent-bridge tools) and can call your agent from a conversation.

Where to go next

You've built the full fabric end-to-end: started KubeMQ, registered a zero-SDK agent, discovered and invoked it, streamed live task events, and orchestrated it from an LLM over MCP. From here, go deeper:

Was this page helpful?

On this page