MQTT.Agent

3. MCP over MQTT

This section defines how to carry Anthropic's Model Context Protocol (MCP) over MQTT 5. The semantics (tools as named capabilities with JSON Schema arguments and structured results) are unchanged from MCP. What changes is the transport: tool servers become network services that any agent on the broker can call, can be replicated for horizontal scale via shared subscriptions, and inherit broker-side identity binding for free.

This profile is complementary to MCP-over-stdio and MCP-over-HTTP+SSE. Use stdio when the tool runs as a subprocess co-located with its caller. Use this profile when tool servers are network services callable from multiple hosts, multiple agents, or both.

3.1 Topic shapes

Tool servers and callers MUST use the following topic patterns.

TopicDirectionQoSRetainedPurpose
{ns}/mcp/servers/{server_id}/cardpublish (server)1yesServer card listing tools served by this instance.
{ns}/mcp/tools/{tool_id}/cardpublish (server)1yesTool card with schema, description, auth metadata.
{ns}/mcp/tools/{tool_id}/callpublish (caller), subscribe (server)1noTool invocation request.
$share/{group}/{ns}/mcp/tools/{tool_id}/callsubscribe (server replica)1n/aShared-subscription form for load-balanced replicas (see Replicas and load balancing below).
{ns}/mcp/clients/{client_id}/responsespublish (server), subscribe (caller)1noPer-caller response inbox.
{ns}/mcp/tools/{tool_id}/stream/{call_id}publish (server), subscribe (caller)0 or 1noStreaming partials for long-running tools (see Streaming partials below).
{ns}/mcp/servers/+/cardsubscribe (caller)1n/aWildcard server enumeration.
{ns}/mcp/tools/+/cardsubscribe (caller)1n/aWildcard tool enumeration.

{client_id} is the caller's identifier, typically the same value as their agent_id claim (Substrate, identity claims), so an agent acting as both A2A peer and MCP client uses one identity throughout.

3.2 Call and response

A tool call is a PUBLISH to {ns}/mcp/tools/{tool_id}/call with qos=1. The payload MUST be a JSON object with the following shape:

{
  "call_id": "call_lr8xab7g",
  "arguments": { "query": "example-value" },
  "client": "agent-a",
  "timestamp": "2026-05-07T10:00:05.123Z"
}

Required fields: call_id, arguments, client, timestamp. Optional field: response_topic, used as a fallback when MQTT 5 properties are not available.

Callers SHOULD use MQTT 5 properties on the call PUBLISH:

  • Response Topic MUST be set to the caller's response inbox {ns}/mcp/clients/{client_id}/responses.
  • Correlation Data MUST be a binary representation of call_id (UTF-8 bytes are RECOMMENDED).
  • Content Type MAY be set to application/vnd.mqtt-agent.tool-call+json; bare application/json is fully conformant.

Servers MUST publish the response to either the topic indicated by Response Topic (preferred) or, if that property is absent, to the topic given in the response_topic payload field. If neither is supplied, the server SHOULD publish to {ns}/mcp/clients/{client}/responses using the client field from the request. Servers MUST echo the received Correlation Data on their response PUBLISH so the caller can match it to the originating call without parsing.

Response shape:

{
  "call_id": "call_lr8xab7g",
  "status": "ok",
  "result": { "items": [] },
  "elapsed_ms": 187
}

On failure:

{
  "call_id": "call_lr8xab7g",
  "status": "error",
  "error": {
    "type": "tool_error",
    "message": "Database unavailable",
    "code": "E_DB_TIMEOUT"
  },
  "elapsed_ms": 30000
}

status MUST be one of "ok" or "error". The error.type field, when present, MUST be one of: invalid_arguments, unauthorized, tool_error, timeout, unavailable. Implementations MAY define additional error types under reverse-DNS prefixes (e.g. com.example.rate_limited); consumers MUST tolerate unknown error types and SHOULD fall back to displaying error.message.

Callers SHOULD apply a per-call timeout (30 seconds is a reasonable default) and SHOULD treat the absence of a response within the timeout as a timeout error, distinct from a server-side timeout response.

3.3 Replicas and load balancing

A tool MAY be served by multiple server instances concurrently. Each replica subscribes using an MQTT 5 shared subscription:

$share/{group}/{ns}/mcp/tools/{tool_id}/call

The {group} segment names the load-balancing group. Implementations SHOULD use the convention mcp-tool-{tool_id} so replicas of the same tool share a group automatically. The broker delivers each call to exactly one subscriber within the group; the dispatch policy (round-robin, hash-by-client, etc.) is broker-implementation-specific.

Retained tool cards (see Discovery below) SHOULD be published by exactly one replica to avoid card flapping. The simplest workable convention is "first writer wins": each replica attempts to publish its card on start; if a different replica already published an online card recently, the second replica's card overwrites it but the content is identical. A more elaborate leader-election scheme is OUT OF SCOPE for v0.1.

Because shared-subscription dispatch is at-most-once-per-group, callers MUST design tool semantics to be safe under at-most-once delivery from any single replica's perspective. QoS 1 redelivery within a single replica is still possible; tool handlers SHOULD therefore be idempotent for the same call_id.

3.4 Discovery

Tool discovery follows the same exact-vs-wildcard pattern as agent discovery (A2A, agent cards and discovery):

  • Exact lookup. Subscribe to {ns}/mcp/tools/{tool_id}/card, receive the retained card, unsubscribe. Always works.
  • Wildcard enumeration. Subscribe to {ns}/mcp/tools/+/card (or {ns}/mcp/servers/+/card for server-level enumeration) and collect retained cards. Subject to broker wildcard policy (Substrate, wildcards and discovery).

The tool card schema:

{
  "mqtt_agent_version": "0.1",
  "version": "1",
  "tool": "tool-1",
  "server": "example-server",
  "namespace": "myapp",
  "description": "Performs example operation.",
  "input_schema": {
    "type": "object",
    "properties": {
      "param": { "type": "string" }
    },
    "additionalProperties": false
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "items": { "type": "array", "items": { "type": "object" } }
    }
  },
  "supports_streaming": false,
  "requires_auth": true,
  "allowed_callers": ["agent-a", "agent-b"],
  "version_info": { "sdk": "0.1.0", "tool": "1.0.0" },
  "status": "online",
  "last_seen": "2026-05-07T10:00:00.000Z"
}

Required fields: mqtt_agent_version (see Substrate, common card fields), version, tool, server, namespace, description, input_schema, supports_streaming, requires_auth, status, last_seen. The input_schema MUST be a valid JSON Schema and SHOULD be the same schema accepted by the MCP-over-stdio reference for the same tool, so a tool can be exposed identically over both transports. Optional fields: output_schema, allowed_callers, version_info. The allowed_callers field is informational; actual enforcement is at the broker (see Substrate Section 1.7 Card content trust).

The card content type MAY be application/vnd.mqtt-agent.tool-card+json; bare application/json is fully conformant. Server cards MAY use application/vnd.mqtt-agent.server-card+json and have the shape:

{
  "mqtt_agent_version": "0.1",
  "version": "1",
  "server": "example-server",
  "namespace": "myapp",
  "tools": ["tool-1", "tool-2", "tool-3"],
  "version_info": { "sdk": "0.1.0" },
  "status": "online",
  "last_seen": "2026-05-07T10:00:00.000Z"
}

3.5 MQTT 5 properties on calls and responses

Summary, for clarity:

PropertyOn call PUBLISHOn response PUBLISH
Response TopicMUST be setMUST NOT be set
Correlation DataMUST be set, SHOULD be UTF-8 bytes of call_idMUST be set, MUST equal the value received on the call
Content TypeMAY be application/vnd.mqtt-agent.tool-call+json; bare application/json is conformantMAY be application/vnd.mqtt-agent.tool-result+json; bare application/json is conformant
Message Expiry IntervalMAY be set to the caller's timeout in secondsn/a
User PropertiesMAY carry application metadata (trace IDs, request IDs)MAY carry application metadata; SHOULD echo any user properties the caller marked as propagate

3.6 Streaming partials

A tool whose card declares "supports_streaming": true MAY emit zero or more intermediate updates on {ns}/mcp/tools/{tool_id}/stream/{call_id} before publishing the final response on the caller's response topic. Stream messages MAY use QoS 0 to favor latency over reliability.

Stream payload:

{
  "call_id": "call_lr8xab7g",
  "seq": 3,
  "type": "partial",
  "content": "Processing 15 of 42 items..."
}

type MUST be one of "partial" or "done". The final response on the response inbox is still authoritative; type: "done" is informational and signals the caller MAY unsubscribe from the stream topic. Callers MUST tolerate out-of-order seq values (rare under QoS 1, possible under QoS 0).

3.7 Relationship to MCP-over-stdio (non-normative)

This profile is a transport binding for MCP, not a fork. The same tool implementation MAY be exposed over stdio and over MQTT simultaneously. Specifically:

  • Tool schemas (input_schema, output_schema) are the same JSON Schema documents used by the MCP-over-stdio reference; tools should not need to ship a different schema for MQTT.
  • Argument and result shapes (arguments, result) are the same JSON values; the MQTT envelope wraps them in a transport-specific frame (call_id, client, timestamp, elapsed_ms).
  • Resources, prompts, and other MCP concepts beyond tool calls are not addressed by v0.1 of this specification; future minor versions MAY define MQTT topic shapes for them following the same pattern.

When to use which transport:

  • stdio: single caller, subprocess co-located with caller, no need to share the tool with other processes. Lowest transport overhead; single-caller only.
  • MQTT: multiple callers across hosts, need for replicas / load balancing, need for broker-enforced authorization, or shared infrastructure with A2A messaging.

Feedback

Found an issue with the spec? Have a proposal or an RFC comment? The public spec repository is being prepared. In the meantime, send feedback to efi@cloudsignal.io.