# 5. Orchestrate from an LLM (MCP) (/aiway/tutorial/orchestrate-from-an-llm)



In [Step 2](/aiway/tutorial/build-and-register-an-agent) you built and registered
`research-agent-01`, and in Steps [3](/aiway/tutorial/discover-and-invoke) and
[4](/aiway/tutorial/stream-live-results) 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.

<Callout type="info">
  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 default** — `POST /mcp`
  is already live on the shared HTTP server. For the Python path, install the MCP SDK with
  `pip install mcp`.
</Callout>

<Steps>
  <Step>
    ## Connect and list the tools [#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 tools** —
    **11 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.

    <Callout type="info">
      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](/aiway/tutorial/build-and-register-an-agent) — one is the MCP handshake,
      the other is the agent's A2A card version. They are not interchangeable.
    </Callout>

    <Tabs groupId="language" items="['curl','Python']">
      <Tab value="curl">
        First open a session with `initialize`, then list the tools with the returned session ID:

        ```bash
        # 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"
          }'
        ```
      </Tab>

      <Tab value="Python">
        The `mcp` SDK runs the `initialize` handshake and tracks the session ID for you over the
        Streamable HTTP transport — you only call `tools/list`:

        ```python
        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())
        ```
      </Tab>
    </Tabs>

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

    ```json
    ["agent_list", "agent_info", "agent_send", "agent_query"]
    ```
  </Step>

  <Step>
    ## Discover the agent with agent\_list [#discover-the-agent-with-agent_list]

    `agent_list` reads the same A2A registry you queried in
    [Step 3](/aiway/tutorial/discover-and-invoke) — 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.

    <Tabs groupId="language" items="['curl','Python']">
      <Tab value="curl">
        ```bash
        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"]}
            }
          }'
        ```
      </Tab>

      <Tab value="Python">
        ```python
        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}")
        ```
      </Tab>
    </Tabs>

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

    ```json
    {
      "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
      }
    }
    ```
  </Step>

  <Step>
    ## Invoke the agent with agent\_send [#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](/aiway/a2a/architecture) — the same path the direct A2A
    call took in [Step 3](/aiway/tutorial/discover-and-invoke), just initiated from MCP.
    By default the call is **blocking**: it waits up to `timeout_seconds` for the agent's
    reply (&#x2A;*default `60`, max `300`**). Pass `blocking: false` for fire-and-forget, or
    `context_id` to thread the message into an existing conversation.

    <Tabs groupId="language" items="['curl','Python']">
      <Tab value="curl">
        ```bash
        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
              }
            }
          }'
        ```
      </Tab>

      <Tab value="Python">
        ```python
        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}")
        ```
      </Tab>
    </Tabs>

    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:

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

    <Callout type="info">
      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](/aiway/mcp/tools/agent-bridge) for `agent_info`, `agent_query`,
      and the full argument and error reference.
    </Callout>
  </Step>
</Steps>

## One fabric, no glue code [#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](/aiway/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 [#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:

<Cards>
  <Card title="Aiway overview" href="/aiway" description="The big picture: an AI Agents Fabric, the two doors in, and one fabric across three planes." />

  <Card title="Use cases" href="/aiway/use-cases" description="Where Aiway is uniquely strong — zero-SDK onboarding, LLM orchestration, streaming, and capability routing." />

  <Card title="A2A reference" href="/aiway/a2a/reference" description="The full agent gateway API: registration, discovery, message/send, and message/stream." />

  <Card title="MCP endpoints" href="/aiway/mcp/reference/endpoints" description="The MCP HTTP surface: initialize, tools/list, tools/call, and session handling." />
</Cards>
