ZAP Protocol
Protocols

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 -L tricks.
  • 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 Node message 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/mcp

Run 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:

  1. Spawns each stdio MCP server as a child process
  2. Discovers its tools via the MCP tools/list request
  3. Assigns each tool a stable uint32 ID
  4. 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.Client that wraps zap-proto/go is on the roadmap.
  • Bidirectional sampling/createMessage (server → client model calls).

On this page