
MCP + Pilot: Tools and a Network for AI Agents
The Model Context Protocol (MCP) gives agents tools: database queries, API calls, file access, web search. Pilot Protocol gives agents a network: addresses, encrypted tunnels, trust management, messaging and pub/sub. Neither replaces the other. An agent that can query a database but cannot share the results with a peer agent is half an agent. An agent that can reach peers but cannot access tools is the other half. This article shows you how to build the complete picture.
MCP has crossed 97 million monthly SDK downloads. Thousands of MCP servers exist for everything from GitHub to Postgres to Slack. What none of them provide is peer communication. An MCP-equipped agent can gather data from any source, but delivering that data to another agent still requires building your own transport, discovery, and encryption layer. That is exactly what Pilot provides.
The Two Halves of Agent Infrastructure
Think of agent infrastructure as two orthogonal capabilities:
Vertical: Tools (MCP). An agent reaches down into systems. It queries databases, calls APIs, reads files, executes code. MCP standardizes this with a client-server protocol: the agent runs an MCP client, connects to MCP servers (each wrapping a tool), and invokes tools through a structured JSON-RPC interface. The agent talks to systems.
Horizontal: Network (Pilot). An agent reaches across to peers. It finds other agents, establishes trust, opens encrypted connections, sends messages and requests, subscribes to events. Pilot standardizes this with virtual addresses, UDP tunnels, and a port-based service model. The agent talks to agents.
Neither layer knows about the other. MCP does not care how your agent communicates with peers. Pilot does not care what tools your agent uses. This separation is a feature: you can upgrade your tools without changing your network layer, and vice versa. But at the application level, you need both.
The key insight: MCP gives an agent eyes and hands (it can see data, it can act on systems). Pilot gives an agent a voice and ears (it can tell peers what it found, it can hear what peers need). A fully capable agent needs all four.
Architecture: Same Process, Two Capabilities
The integration is architecturally simple. A single agent process runs two clients:
- MCP Client - connects to one or more MCP servers (database, API, file system, etc.) via stdio or HTTP transport.
- Pilot Driver - connects to the local Pilot daemon via IPC socket (
/tmp/pilot.sock) for peer networking.
The agent's application logic coordinates between them. When the agent needs data, it calls MCP. When it needs to share data, delegate work, or respond to a peer, it calls Pilot. The two clients are independent: they run on different protocols, connect to different endpoints, and have different lifecycle management.
// Conceptual architecture (Go)
type Agent struct {
mcp *mcpclient.Client // MCP: tool access
pilot *driver.Driver // Pilot: peer networking
llm *llmclient.Client // LLM: reasoning
}
// The agent loop: accept a request, gather data, reason, reply
func (a *Agent) Run() {
ln, _ := a.pilot.Listen(1000)
for {
conn, _ := ln.Accept()
req := readRequest(conn)
// 1. Use MCP to gather data
data := a.mcp.CallTool("database_query", req.Params)
// 2. Use LLM to reason about the data
result := a.llm.Complete(req.Prompt + data)
// 3. Use Pilot to deliver the result on the same connection
conn.Write([]byte(result))
conn.Close()
}
}
The Pilot daemon runs as a separate process (started with pilotctl daemon start). The MCP servers run as separate processes too (started by the MCP client). Your agent process is the coordinator that ties them together.
Example: Research Agent with MCP + Pilot
Let us build a concrete example. A research agent that:
- Receives a research request from a peer via Pilot
- Uses MCP to query a database and fetch web content
- Produces a summary using an LLM
- Returns the summary to the requesting agent via Pilot
- Publishes a "research.completed" event so other interested agents are notified
Go Implementation
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/pilot-protocol/common/driver"
"github.com/pilot-protocol/common/protocol"
"github.com/pilot-protocol/eventstream"
)
func main() {
// Connect to the local Pilot daemon
drv, err := driver.Connect("/tmp/pilot.sock")
if err != nil {
log.Fatal(err)
}
defer drv.Close()
// Accept research requests on port 1000
ln, err := drv.Listen(1000)
if err != nil {
log.Fatal(err)
}
// Completion events go to a hub node's event stream (port 1002).
// Other agents follow them with: pilotctl subscribe research-hub research.completed
hub, err := drv.ResolveHostname("research-hub")
if err != nil {
log.Fatal(err)
}
hubAddr, err := protocol.ParseAddr(hub["address"].(string))
if err != nil {
log.Fatal(err)
}
events, err := eventstream.Subscribe(drv, hubAddr, "research.completed")
if err != nil {
log.Fatal(err)
}
defer events.Close()
log.Println("Research agent online, waiting for requests...")
for {
// Accept an incoming research request via Pilot
conn, err := ln.Accept()
if err != nil {
log.Printf("accept error: %v", err)
continue
}
buf := make([]byte, 64*1024)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
continue
}
var req map[string]string
if err := json.Unmarshal(buf[:n], &req); err != nil {
conn.Close()
continue
}
log.Printf("Received request from %s", conn.RemoteAddr())
// Use MCP to query the database
queryResult := callMCPTool("database", "query", map[string]string{
"sql": fmt.Sprintf(
"SELECT title, abstract FROM papers WHERE topic LIKE '%%%s%%' LIMIT 10",
req["topic"],
),
})
// Use MCP to fetch web content for additional context
webResult := callMCPTool("web-search", "search", map[string]string{
"query": req["topic"] + " recent developments",
})
// Combine sources and produce summary (LLM call omitted for clarity)
summary := fmt.Sprintf(
"Research summary for: %s\n\nDatabase:\n%s\n\nWeb: %s",
req["topic"], queryResult, webResult,
)
// Return results to the requester on the same connection
if _, err := conn.Write([]byte(summary)); err != nil {
log.Printf("write error: %v", err)
}
conn.Close()
// Publish an event so other agents know the research is done
eventData, _ := json.Marshal(map[string]string{"topic": req["topic"]})
if err := events.Publish("research.completed", eventData); err != nil {
log.Printf("publish error: %v", err)
}
}
}
// callMCPTool invokes a tool through your MCP client
// (for example the official MCP Go SDK). Left as a stub here.
func callMCPTool(server, tool string, params map[string]string) string {
return ""
}
The flow is straightforward: Pilot handles the peer communication (accepting the request, returning the result, publishing the event), while MCP handles the tool access (database query, web search). The agent's logic is the glue between them.
Python Implementation
For Python agents, the pattern uses the Pilot Python SDK (pip install pilotprotocol) for Pilot integration and the MCP Python SDK for tool access:
import asyncio
import json
from pilotprotocol import Driver
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def research_agent():
# Connect to the local Pilot daemon and accept requests on port 1000
pilot = Driver()
listener = pilot.listen(1000)
# Connect to MCP database server
server_params = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-postgres",
"postgresql://localhost/research"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as mcp:
await mcp.initialize()
while True:
# Wait for a request (accept blocks, so run it off the event loop)
conn = await asyncio.to_thread(listener.accept)
req = json.loads(conn.read(65536))
topic = req["topic"]
# Use MCP to query the database
db_result = await mcp.call_tool("query", {
"sql": f"SELECT title, abstract FROM papers "
f"WHERE topic LIKE '%{topic}%' LIMIT 10"
})
# Produce summary
summary = f"Found {len(db_result.content)} papers on {topic}\n"
for row in db_result.content:
summary += f"- {row.text}\n"
# Return results on the same connection
conn.write(summary.encode())
conn.close()
# Publish a completion event on the hub's event stream
event = json.dumps({
"topic": topic,
"papers": str(len(db_result.content))
})
pilot.publish_event("research-hub", "research.completed",
event.encode())
The Python SDK calls the same Go driver as the Go example, through a shared library that ships in the wheel. If you would rather not add a dependency, the same steps work by shelling out to pilotctl: pilotctl send <addr> 1000 --data '...' to make a request and pilotctl publish research-hub research.completed --data '...' for the event.
What MCP + Pilot Enables
With both capabilities in a single agent, several patterns become natural:
Data Pipeline Across Agents
Agent A uses MCP to query a database. It publishes a "data.ready" event via Pilot's event stream. Agent B subscribes to the "data.ready" topic. When it receives the notification, it asks Agent A for the dataset, and Agent A sends it with pilotctl send-file over data exchange (port 1001). Agent B uses MCP to write the processed results to a different database. The event stream runs inside a Pilot daemon, so there is no separate message broker to deploy.
Tool Delegation
Not every agent has access to every tool. Agent A has MCP access to a proprietary API. Agent B needs data from that API but does not have credentials. Agent B sends a request to Agent A via Pilot: "query the API for X." Agent A executes the query via MCP and returns the results via Pilot. The API credentials never leave Agent A's machine. Trust between Agent A and Agent B is managed by Pilot's handshake system, not by sharing API keys.
Monitoring and Alerting
A monitoring agent uses MCP to connect to logging and metrics systems. When it detects an anomaly, it publishes an event via Pilot's event stream. Response agents subscribe to anomaly events and take action: one agent uses MCP to roll back a deployment, another uses MCP to page the on-call engineer via Slack, a third uses MCP to capture diagnostics. The monitoring agent does not need to know which response agents exist or what tools they have. It publishes the event; the network handles the rest.
Every MCP Agent Could Benefit from a Pilot Address
Today, MCP agents are islands. They connect to tools, they process data, they produce results. But they have no standardized way to find each other, establish trust, or exchange those results. Each MCP deployment reinvents peer communication with ad-hoc HTTP endpoints, message queues, or shared databases.
A Pilot address gives an MCP agent an identity on a peer network. It can be discovered by other agents (or stay invisible, if private). It can receive requests, publish events, and exchange data. It can do all of this over encrypted tunnels that traverse NAT without changes to your existing infrastructure - the only requirement is a registry and beacon, which you can use publicly (the backbone Vulture Labs runs at no charge) or self-host.
The integration cost is minimal: start a Pilot daemon alongside your MCP agent, add a few pilotctl calls (or driver API calls in Go) to your agent loop, and your MCP agent becomes a networked MCP agent. The tools still work the same way. The LLM still works the same way. You just added the ability to collaborate with peers.
MCP handles the vertical. Pilot handles the horizontal. Together, they are the full stack for agent infrastructure. For more integration patterns, see Building A2A Agent Cards Over Pilot Tunnels and Connecting MCP Servers to Agents Across Any Network. For the technical details of the Pilot driver API, check the documentation.
Add a Network to Your MCP Agent
Install Pilot, start the daemon, and give your agent a peer address in under 5 minutes.
View on GitHub