KubeMQ
AiwayMCPGuides

Channel Resolution

How MCP tools map to KubeMQ channels — naming, the five channel types, the reserved _AGENTS_ prefix, and discovery via channel_list and channel_info.

Every KubeMQ MCP tool acts on a channel — the addressable destination a message is sent to or read from. This guide explains how channels are named, the five channel types each tool family targets, the single reserved prefix you must avoid, and how a model discovers channels at runtime before it acts.

Overview

A channel is just a string. There is no registry to provision and no create step: a channel comes into existence the first time a tool references it, and disappears from discovery when it no longer carries traffic. This makes the MCP surface self-describing — a model can name a channel on the fly (orders.us-west), and the same name resolves consistently across every tool that uses it.

Each tool argument named channel is resolved against one of the five KubeMQ messaging patterns. The tool you call determines the pattern; the channel string determines the destination within it.

How tools map to channels

There is no separate "channel resolution" step in the protocol — the tool name selects the channel type, and the channel argument names the destination. The model never has to declare a type alongside a send; calling queue_send is the declaration that the channel is a queue.

The tool family selects the channel type; the channel argument names the destination.

Channel naming

  • Channels are arbitrary strings — for example my-app.orders, notifications, or user-events.
  • Channels are created implicitly on first use. There is no explicit creation step.
  • Convention: use dot-separated hierarchical names to organize a namespace, such as orders.us-west or events.user.signup.

Because channels are created on demand, a typo creates a new (empty) channel rather than raising an error. Use channel_list to confirm a channel exists and carries traffic before relying on it.

Channel types

Each channel belongs to exactly one of five types — one per messaging pattern — and each type is served by a specific set of tools:

TypeDescriptionAssociated tools
queuesPoint-to-point durable queue channelsqueue_send, queue_receive, queue_peek
eventsEphemeral fire-and-forget pub/sub channelsevents_publish
events_storePersistent pub/sub channels with replayevents_store_publish, events_store_read, events_store_read_latest
commandsRequest/reply command channelscommand_send
queriesRequest/reply query channelsquery_send

The same string can name distinct channels under different types — orders as a queue and orders as an events channel are unrelated. That is why channel_info requires both the channel name and the type.

Only queues and events_store channels are backed by broker monitoring, so channel_list and channel_info report live message statistics for them. The ephemeral types (events, commands, queries) exist only while subscribers are connected and are returned as type descriptions rather than per-channel stats.

Reserved prefix

One prefix is reserved by the broker for the agent bridge and cannot be targeted by the direct messaging tools.

PrefixPurposeRestriction
_AGENTS_.Agent-bridge internal communicationRejected by all direct messaging tools

A channel is reserved when its name begins with the literal prefix _AGENTS_. (the trailing dot is part of the prefix). Passing such a channel to queue_send, events_publish, events_store_publish, command_send, or query_send returns a tool error (isError: true) at the tool layer — the message is never published.

To reach an agent, use the agent-bridge tools agent_send and agent_query instead of addressing _AGENTS_.* channels directly. The bridge manages the reserved channels on your behalf — see Agent-bridge tools.

Discovering channels

Two read-only tools let a model explore the namespace at runtime instead of hard-coding channel names: channel_list enumerates channels (optionally filtered), and channel_info returns metadata for one channel.

Listing and filtering

channel_list returns every known channel, or a subset filtered by type, by a name pattern, or both. The pattern filter is a prefix match"example-" matches example-queue and example-events but not my-example. With no arguments it returns all channels.

# List all channels (no filter)
curl -X POST http://localhost:9090/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{
    "jsonrpc": "2.0",
    "id": 11,
    "method": "tools/call",
    "params": {
      "name": "channel_list",
      "arguments": {}
    }
  }'

# Filter by type:   "arguments": { "type": "queues" }
# Filter by prefix: "arguments": { "pattern": "example-" }
url := os.Getenv("KUBEMQ_MCP_URL")
if url == "" {
    url = "http://localhost:9090"
}

c, err := client.NewStreamableHttpClient(url + "/mcp")
if err != nil {
    log.Fatal(err)
}
defer c.Close()

ctx := context.Background()
if err := c.Start(ctx); err != nil {
    log.Fatal(err)
}

result, err := c.CallTool(ctx, mcp.CallToolRequest{
    Params: mcp.CallToolParams{
        Name:      "channel_list",
        Arguments: map[string]any{},
    },
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Result: %+v\n", result)
async with streamablehttp_client(f"{KUBEMQ_MCP_URL}/mcp") as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()

        # List all channels (no filter)
        result = await session.call_tool("channel_list", {})

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

        # To filter by type, use: {"type": "queues"}
        # To filter by pattern, use: {"pattern": "example-"}
const transport = new StreamableHTTPClientTransport(
  new URL(`${KUBEMQ_MCP_URL}/mcp`)
);
const client = new Client({ name: "kubemq-mcp-ts-example", version: "1.0.0" });
await client.connect(transport);

// List all channels (no filter)
const result = await client.callTool({
  name: "channel_list",
  arguments: {},
});
console.log(JSON.stringify(result, null, 2));

// To filter by type: { type: "queues" }
// To filter by pattern: { pattern: "example-" }

await client.close();
String url = System.getenv().getOrDefault("KUBEMQ_MCP_URL", "http://localhost:9090");
var transport = HttpClientStreamableHttpTransport.builder(url).endpoint("/mcp").build();
var client = McpClient.sync(transport).build();
client.initialize();

// List all channels (no filter)
var result = client.callTool(new CallToolRequest(
    "channel_list",
    Map.of()
));
System.out.println(result);

// To filter by type: Map.of("type", "queues")
// To filter by pattern: Map.of("pattern", "example-")

client.closeGracefully();
var url = Environment.GetEnvironmentVariable("KUBEMQ_MCP_URL") ?? "http://localhost:9090";
var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri($"{url}/mcp") });
await using var client = await McpClientFactory.CreateAsync(transport);

var result = await client.CallToolAsync("channel_list", new Dictionary<string, object>());

Console.WriteLine($"Result: {result}");
val url = System.getenv("KUBEMQ_MCP_URL") ?: "http://localhost:9090"

val httpClient = HttpClient { install(SSE) }
val transport = StreamableHttpClientTransport(client = httpClient, url = "$url/mcp")
val client = Client(clientInfo = Implementation(name = "kubemq-mcp-kotlin-example", version = "1.0.0"))
client.connect(transport)

val result = client.callTool("channel_list", emptyMap())

println("Result: $result")

client.close()
httpClient.close()
url = ENV.fetch("KUBEMQ_MCP_URL", "http://localhost:9090")

client = MCP::Client.new(
  transport: MCP::Transport::StreamableHTTP.new("#{url}/mcp"),
  name: "kubemq-mcp-ruby-example",
  version: "1.0.0"
)
client.initialize_handshake

result = client.call_tool("channel_list", {})

puts "Result: #{result}"

client.close
let url = std::env::var("KUBEMQ_MCP_URL")
    .unwrap_or_else(|_| "http://localhost:9090".to_string());

let transport = StreamableHttpClientTransport::from_uri(format!("{url}/mcp"))?;
let client = ().serve(transport).await?;

let result = client.call_tool("channel_list", json!({})).await?;

println!("Result: {result:#?}");
let url = ProcessInfo.processInfo.environment["KUBEMQ_MCP_URL"] ?? "http://localhost:9090"

let transport = HTTPClientTransport(endpoint: URL(string: "\(url)/mcp")!, streaming: true)
let client = Client(name: "kubemq-mcp-swift-example", version: "1.0.0")
try await client.connect(transport: transport)

let result = try await client.callTool("channel_list", arguments: [:])

print("Result: \(result)")

A successful channel_list returns a JSON array in the standard content[]/isError envelope. An empty array ([]) is a normal result, not an error:

{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"name\":\"example-queue\",\"type\":\"queues\",\"is_active\":true},{\"name\":\"example-events\",\"type\":\"events\",\"is_active\":true}]"
      }
    ],
    "isError": false
  }
}

Inspecting one channel

channel_info confirms a single channel's type and live state. Both the channel name and its type are required — the type scopes the lookup to the correct messaging pattern.

curl -X POST http://localhost:9090/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{
    "jsonrpc": "2.0",
    "id": 12,
    "method": "tools/call",
    "params": {
      "name": "channel_info",
      "arguments": {
        "channel": "example-queue",
        "type": "queues"
      }
    }
  }'
result, err := c.CallTool(ctx, mcp.CallToolRequest{
    Params: mcp.CallToolParams{
        Name: "channel_info",
        Arguments: map[string]any{
            "channel": "example-queue",
            "type":    "queues",
        },
    },
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Result: %+v\n", result)
result = await session.call_tool("channel_info", {
    "channel": "example-queue",
    "type": "queues",
})

print(f"IsError: {result.isError}")
for content in result.content:
    print(f"Result: {content.text}")
const result = await client.callTool({
  name: "channel_info",
  arguments: {
    channel: "example-queue",
    type: "queues",
  },
});
console.log(JSON.stringify(result, null, 2));
var result = client.callTool(new CallToolRequest(
    "channel_info",
    Map.of(
        "channel", "example-queue",
        "type", "queues"
    )
));
System.out.println(result);
var result = await client.CallToolAsync("channel_info", new Dictionary<string, object>
{
    ["channel"] = "example-queue",
    ["type"] = "queues",
});

Console.WriteLine($"Result: {result}");
val result = client.callTool("channel_info", mapOf(
    "channel" to "example-queue",
    "type" to "queues"
))

println("Result: $result")
result = client.call_tool("channel_info", {
  "channel" => "example-queue",
  "type" => "queues",
})

puts "Result: #{result}"
let result = client.call_tool("channel_info", json!({
    "channel": "example-queue",
    "type": "queues"
})).await?;

println!("Result: {result:#?}");
let result = try await client.callTool("channel_info", arguments: [
    "channel": "example-queue",
    "type": "queues",
])

print("Result: \(result)")

For queues and events_store channels the result includes live incoming/outgoing message counts; for the ephemeral types it returns the channel's type and active state:

{
  "jsonrpc": "2.0",
  "id": 12,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"name\":\"example-queue\",\"type\":\"queues\",\"is_active\":true,\"incoming\":5,\"outgoing\":3}"
      }
    ],
    "isError": false
  }
}

Resolution failures

ConditionResult
Channel name begins with _AGENTS_. on a direct messaging toolTool error — isError: true
Invalid type value (not one of the five)JSON-RPC -32602 Invalid Params
channel_info missing channel or typeJSON-RPC -32602 Invalid Params
channel_list with a pattern that matches nothingSuccessful result with an empty array []

For the full JSON-RPC error catalog, see Error codes.

Was this page helpful?

On this page