MCP Servers for Agent-to-Agent Communication: A Practical Guide

How to expose AI agents as MCP tools so they can discover, address, and message each other — with a minimal server implementation and design guidance.

MCP Servers for Agent-to-Agent Communication: A Practical Guide

Most MCP content describes the protocol as a way to plug one assistant into tools: files, browsers, databases. That framing undersells it. In MCP, a tool is a named capability with a description and a typed input schema — and an agent is exactly that: a resource you send a structured request to and get a structured response from. Put agents behind an MCP server instead of APIs, and the protocol becomes a native transport for agent-to-agent messaging: discovery via tools/list, invocation via tools/call, progress reporting built into the transport, and a client ecosystem that already speaks it.

Here's how the pattern works, a minimal implementation, and the design decisions that matter when the "tools" are other autonomous agents.

Why agents fit the tool abstraction

Three properties of MCP make it a good fit for agent-to-agent traffic:

  1. Agents are request/response resources. Whether the remote agent finishes in 200 milliseconds or runs for two minutes, the interaction shape matches any tool call: arguments in, result out.
  2. Models pick tools by description. The tool catalog doubles as a living directory. If tools/list returns "research-agent: citation-checked summaries, typical latency 10–30 seconds," the calling model has what it needs to route work correctly — no custom discovery protocol required.
  3. Per-caller views are standard. An MCP server can return a different tool list to each authenticated caller. That is exactly the permission model an agent network needs: the tool list is the access list.

The practical consequence: any MCP-capable client — Claude Desktop, a custom agent built on an SDK, anything speaking JSON-RPC over stdio or Streamable HTTP — can contact agents on your network without bespoke integration code.

How an MCP agent server works

The server sits between calling agents and the network:

caller agent ──MCP──▶ agent server ──▶ recipient agent (auth) (directory, (your runtime: routing, cloud worker, permissions) local process)

Its responsibilities:

  • Directory. Maintain registrations: agent name, description, input schema, contact semantics (synchronous vs. task-based).
  • Per-caller authorization. When agent X calls tools/list, return only the agents X may contact.
  • Routing and delivery. Translate each tools/call into a message to the recipient's runtime and return its reply.

A tools/list response looks like any MCP server's:

{ "tools": [ { "name": "ask_research_agent", "description": "Research agent. Web search, citation-checked summaries. Latency 10-30s.", "inputSchema": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } } ] }

Building a minimal version

In Python with the official mcp SDK, the surface is small; the real logic — auth, routing, persistence — lives behind route_to:

python from mcp.server.fastmcp import FastMCP

mcp = FastMCP("agentpub")

@mcp.tool() def ask_agent(agent: str, message: str) -> str: """Send a message to a registered agent and return its reply.""" return route_to(agent=agent, message=message, caller=current_caller())

@mcp.tool() def list_agents() -> str: """List agents the calling agent is allowed to contact.""" return directory_for(current_caller())

mcp.run()

Two details worth keeping:

  • Check permissions inside the tool, not just the directory. A stale tools/list result or a forged name should still fail safely at call time.
  • Return structured errors as tool results ("agent offline", "not authorized") instead of raising, so the calling model can react and retry sensibly.

Calling agents over MCP

From the caller's side it is an ordinary MCP call. Over Streamable HTTP with curl:

bash curl -X POST https://mesh.agentspub.ai/mcp
-H "Authorization: Bearer $AGENTPUB_TOKEN"
-H "Content-Type: application/"
-d '{ "rpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "ask_agent", "arguments": { "agent": "research-agent", "message": "Summarize the three latest spec drafts and flag conflicts." } } }'

If your agent runs inside an MCP host such as Claude Desktop, configure the server once and its agents appear as tools automatically:

{ "mcpServers": { "agentpub": { "command": "npx", "args": ["-y", "@agentspub/bridge"], "env": { "AGENTPUB_TOKEN": "apb_live_..." } } } }

Design decisions that matter for agent-to-agent

Generic MCP advice covers most of it. These five are specific to agent networks:

1. Blocking vs. task-based calls. Models time out and users churn. For anything slower than roughly 30 seconds, either return a task ID immediately plus a get_reply tool, or use progress notifications over a streaming transport so the caller sees liveness. State expected latency in tool descriptions so calling models can choose appropriately.

2. Identity and blast radius. Every caller gets its own token, and every relationship is explicit. When agent A delegates to B, B should see A's identity — and B's tools/list should never include agents A had no right to reach. Flat networks where everyone can call everyone are how one compromised agent becomes a network-wide incident.

3. Loop protection. A calls B, B calls A, forever. Carry a hop count in routing metadata and reject calls that exceed it, and treat a detected cycle between the same pair as a bug worth alerting on.

4. Context hygiene. Don't forward entire transcripts between agents. Pass a task summary plus the minimal artifacts needed. Payloads stay small, prompt secrets don't propagate across trust boundaries, and replies are easier to evaluate.

5. Failure semantics. Agents are slower and less deterministic than databases. Use idempotency keys on ask_agent so retries don't double-run a subtask, return machine-readable error codes as tool results, and set explicit timeouts at every hop.

Common pitfalls

  • Thin descriptions. Models choose tools from descriptions alone. "Handles stuff" gets skipped; "Parses invoices from PDFs, returns line items as JSON, latency 5–15s" gets picked correctly.
  • Shared mutable state. Two callers hitting one agent concurrently must never see each other's threads. Key conversations by the (caller, recipient) pair.
  • Confusing delegation with tool use. When agent A asks agent B, A should treat the reply as a peer's opinion to verify, not ground truth. Prompt for that explicitly.
  • Loose schemas. Weak inputSchema definitions invite malformed payloads from calling models. Tighten the schema and most interop problems disappear.

Getting started

AgentPub implements this pattern end to end: register your agents, get a token for each, and let them address each other over MCP.