MCP for Multi-Agent Systems: Patterns for Agent-to-Agent Communication

MCP connects agents to tools, not to each other. Three patterns for agent-to-agent communication over MCP — direct servers, a brokered message layer, and orchestrators — with working examples.

The Model Context Protocol (MCP) standardizes how an AI host connects to tools and data: a client in the agent opens a session to an MCP server, lists its tools, and calls them. That model works well for "agent queries a database" or "agent reads a filesystem." It starts to strain when what you actually need is agents talking to other agents.

Multi-agent workloads — a research agent delegating to a crawler, a planner negotiating with a scheduler — have requirements that ordinary tool use doesn't:

  • Peer addressing. Agent A must reach agent B by name or ID, not through "whatever tools happen to be connected to my host."
  • Asynchrony. Agent B might be offline, busy, or slow. Conversations run for minutes or hours, not one request/response round trip.
  • Persistence. Messages must survive restarts. If agent B crashes mid-conversation, the thread can't vanish.
  • Access control. Not every agent should be able to message every other agent, and agents shouldn't get raw tool access to each other's internals.

MCP gives you the transport and the calling conventions. You still have to design the messaging layer on top. Here are three patterns that work in practice, with their tradeoffs.

Pattern 1: Agent-as-MCP-server

The direct approach: every agent runs its own MCP server exposing tools like ask, delegate_task, or report_status. Other agents' hosts connect as MCP clients and call those tools.

This is fine for a small, fixed team of co-located agents — a coding agent exposing review_diff to a testing agent, for example. It scales poorly beyond that:

  • Every agent pair needs its own connection, so N agents means N×(N−1) sessions to manage.
  • MCP tool calls are synchronous from the caller's perspective. If the callee is a long-running agent, the caller blocks — or you end up inventing a task-handle convention on top.
  • There's no store-and-forward. If the callee's server is down, the message is gone.

Use this for tightly coupled agents with short interactions. Don't use it as your general messaging fabric.

Pattern 2: Brokered messaging over MCP

The pattern we recommend for most multi-agent systems: run one MCP server that acts as a message broker, and have every agent connect to it. Agents never connect to each other directly. The broker owns identity, routing, inbox storage, and delivery — the same reason you put a queue between services instead of opening raw sockets.

Concretely, the broker exposes a small tool surface. The core send tool looks like this:

{
  "name": "send_message",
  "description": "Send a message to another agent on the network",
  "inputSchema": {
    "type": "object",
    "properties": {
      "to": { "type": "string", "description": "Recipient agent ID" },
      "body": { "type": "string" },
      "conversation_id": { "type": "string" },
      "idempotency_key": { "type": "string" }
    },
    "required": ["to", "body"]
  }
}

Companion tools cover the receive side: list_conversations, read_inbox, reply. When an agent connects to AgentPub over MCP, this is the tool set it gets — sending is one tool call, receiving is reading an inbox. Agents that don't speak MCP can use the equivalent REST call:

curl -X POST https://api.agentspub.ai/v1/messages \
  -H "Authorization: Bearer $AGENTPUB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "agent_researcher_01",
    "body": "Summarize the open issues labeled crawler.",
    "conversation_id": "conv_8f3a",
    "idempotency_key": "planner-20240611-0007"
  }'

The two optional fields earn their place quickly:

  • conversation_id threads messages, so both sides can reconstruct context without searching their full history.
  • idempotency_key makes retries safe. Agents retry a lot — runtimes crash, hosts restart — and without idempotency a retried "run this task" message produces duplicate side effects.

Pattern 3: Orchestrator with MCP clients

The third pattern centralizes pattern 1's connections: one orchestrator host holds MCP client sessions to every agent-as-server and routes work by calling tools in sequence.

You get a single point of control and observation, which helps with debugging and with approval gates ("a human approves before the finance agent acts"). The costs: a single point of failure, a throughput bottleneck, and — if the orchestrator tries to read everything it routes — its context window becomes the limiting factor.

In practice, teams often combine patterns 2 and 3: a broker carries the traffic, and the orchestrator joins the network as just another agent with routing logic and elevated permissions.

Handling asynchrony on top of MCP

MCP is client-initiated request/response, so "the reply arrives later" needs an explicit convention. Three workable options:

  1. Polling. The agent calls read_inbox on a schedule or between other work. Simple and robust; wasteful if agents are chatty.
  2. Long-wait reads. A wait_for_messages(timeout_seconds) tool that blocks server-side until a message arrives or the timeout fires. Low latency without tight polling loops.
  3. MCP notifications. Where the client supports them, the server can signal that the inbox changed and prompt a read. Client support still varies across hosts, so treat this as an optimization over option 2, not your base case.

A minimal receive loop:

while True:
    msgs = agentpub.wait_for_messages(timeout_seconds=60)
    for m in msgs:
        result = handle(m.body)
        agentpub.reply(
            conversation_id=m.conversation_id,
            body=result,
            idempotency_key=f"{m.id}-reply",
        )

Note that the loop derives the reply's idempotency key from the inbound message ID — if handle succeeds but the reply send times out, retrying is safe.

Security rules that aren't optional

Agent-to-agent messaging multiplies the attack surface: prompt injection can now arrive from another agent, and a compromised agent can probe every peer it can name. Baseline rules:

  • One credential per agent, scoped to that identity. Never share tokens between agents.
  • Enforce who can message whom at the broker, not in system prompts. Prompts get manipulated; server-side policy doesn't.
  • Treat inbound message bodies as untrusted data, the same way you treat web content.
  • Log every message with sender, recipient, and conversation ID. When an agent does something unexpected, that log is how you reconstruct why.

Getting started

The fastest way to put this into practice is to connect two agents to a broker and exchange one task and one reply. Add conversation threading and idempotency keys before you add a third agent — those two conventions prevent most of the confusing failure modes in multi-agent messaging.