MCP over ZAP
Model Context Protocol bridge — lifts existing stdio MCP servers onto the ZAP wire.
MCP over ZAP
zap-proto/mcp is a bridge that lifts existing stdio MCP servers (Anthropic Model Context Protocol) onto the ZAP wire. You don't rewrite your tool host; you point the bridge at it and clients call tools over ZAP instead of stdio.
Compared to stdio MCP, you get:
- Mutual KEM authentication — server and client identities are bound to keypairs at the transport. No bearer tokens.
- Network-native — works across hosts without
ssh -Ltricks. - Multiplexed — many concurrent tool calls share one connection without head-of-line blocking.
The actual MCP wire protocol between client and bridge is built on the ZAP
Nodemessage handler API. The example below uses the real exported types from zap-proto/mcp/bridge.go. The bridge wraps existing stdio MCP servers; full client SDK ergonomics will be added once a stable ZAP RPC client lands in the core SDK.
Install
go get github.com/zap-proto/mcpRun a bridge over your stdio MCP servers
package main
import (
"context"
"log"
"github.com/luxfi/zap"
zmcp "github.com/zap-proto/mcp"
)
func main() {
node, err := zap.NewNode(zap.NodeConfig{
Listen: "tcp://0.0.0.0:9100",
})
if err != nil {
log.Fatal(err)
}
bridge := zmcp.NewBridge(node)
if err := bridge.AddServer("fs", "npx", "-y",
"@modelcontextprotocol/server-filesystem", "/tmp"); err != nil {
log.Fatal(err)
}
if err := bridge.AddServer("gh", "npx", "-y",
"@modelcontextprotocol/server-github"); err != nil {
log.Fatal(err)
}
log.Println("MCP bridge listening on tcp://0.0.0.0:9100")
log.Fatal(node.Serve())
}The bridge:
- Spawns each stdio MCP server as a child process
- Discovers its tools via the MCP
tools/listrequest - Assigns each tool a stable
uint32ID - Registers ZAP message handlers (
MsgTypeToolList,MsgTypeToolCall) that route to the right child
Call a tool
From a Go client speaking the bridge's ZAP message types:
// List discovered tools
tools := bridge.GetTools()
for _, t := range tools {
log.Printf("tool %d: %s — %s", t.ID, t.Name, t.Description)
}
// Call a tool by name
result, err := bridge.CallToolByName(ctx, "read_file", map[string]interface{}{
"path": "/etc/hosts",
})CallTool(ctx, toolID, args) calls by stable numeric ID — useful when clients want to avoid round-tripping the name.
ServerConfig
For configuration-driven setup:
bridge.AddServerConfig(zmcp.ServerConfig{
Name: "fs",
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/tmp"},
})Performance
The package comment claims 10-30× over stdio MCP JSON-RPC. The bridge eliminates per-call JSON encoding/decoding by encoding tool calls in ZAP binary frames; numbers are reproducible from the bench in zap-proto/bench once the MCP-specific harness lands. No specific p50/p99 numbers in these docs until the harness produces them.
What's not in the bridge yet
- A first-class non-Go client SDK. Today the bridge speaks ZAP message types directly; a higher-level
mcp.Clientthat wrapszap-proto/gois on the roadmap. - Bidirectional
sampling/createMessage(server → client model calls).
Related
- zap-proto/mcp — bridge implementation
- A2A over ZAP — for agent-to-agent messaging, not tool calls
- Native ZAP RPC — what the bridge is built on