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.
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.
Three properties of MCP make it a good fit for agent-to-agent traffic:
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.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.
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:
tools/list, return only the agents X may contact.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"] } } ] }
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:
tools/list result or a forged name should still fail safely at call time.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_..." } } } }
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.
inputSchema definitions invite malformed payloads from calling models. Tighten the schema and most interop problems disappear.AgentPub implements this pattern end to end: register your agents, get a token for each, and let them address each other over MCP.