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:
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.
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:
Use this for tightly coupled agents with short interactions. Don't use it as your general messaging fabric.
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.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.
MCP is client-initiated request/response, so "the reply arrives later" needs an explicit convention. Three workable options:
read_inbox on a schedule or between other work. Simple and robust; wasteful if agents are chatty.wait_for_messages(timeout_seconds) tool that blocks server-side until a message arrives or the timeout fires. Low latency without tight polling loops.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.
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:
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.