Files
zeekayandHanzo Dev 896036e87d feat(go): MCP over ZAP, the sibling of zap-proto/http
The schema has described this wire since the repo opened and TypeScript has
spoken it since v1.0.0, but nothing in Go did — so every Go service reaching a
tool server still went over HTTP+SSE or stdio. MCP therefore sat BESIDE the ZAP
stack rather than on it, and the properties the wire already provides (PQ
confidentiality, mutual auth, zero-copy parse) had to be re-argued per
deployment.

Built as the sibling of zap-proto/http: same seams (Server/ListenAndServe/
Serve, Dial/Transport/Do), same offset discipline in the codec, same
relationship to the ZAP core. Neither module depends on the other, which is
why each carries its own framing — a protocol that had to import a sibling to
find its frame boundaries would make the wire's shape a property of whichever
sibling defined it first.

One value, two projections. MCP is JSON-RPC 2.0 and ZAP is binary, so the
obvious shape is two message types kept in step by hand. Instead there is one
Frame with two renderings: Marshal for the wire, json.Marshal for JSON-RPC. A
server answers frames and never learns which door one came in — which is what
lets a handler serve ZAP natively and have HTTP be an adapter over it rather
than a peer of it.

The schema's Payload union is encoded the way the sibling encodes its frame
type: the tag rides the message header flags, so a reader dispatches on the
envelope without decoding the body. The three arms share one object shape
because they overlap almost entirely and an unused field is a zeroed slot that
costs no bytes.

Two gates worth naming, both mutation-checked:

  - TestCodec_MatchesBuilder pins the encoder against the reference zap.Builder
    path byte for byte. Without it the hand-rolled offsets are only checked
    against themselves, and a decoder agreeing with a wrong encoder passes
    every round trip here while being unreadable by every other language.
  - TestServe_CloseBeforeServe covers a real deadlock this found: a Close that
    beat the accept loop to the lock saw no listener, closed nothing, and left
    Serve blocked in Accept forever. Serve now refuses to start once closed, so
    either half of the window stops the server.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 18:33:12 -07:00

4.3 KiB

@zap-proto/mcp

Docs: Model Context Protocol over ZAP · part of the ZAP Protocol

Model Context Protocol over ZAP — mutually authenticated, non-repudiable AI agent ↔ tool communication.

zap-proto.io · Spec · Paper · Discord

@zap-proto/mcp layers AI agent ↔ tool semantics over the ZAP wire layer. Post-quantum confidentiality, mutual authentication, and zero-copy parse come from the wire (@zap-proto/zap); this package adds only the AI agent ↔ tool message shape: clients, servers, tool calls, and the transport that carries them.

Install

pnpm add @zap-proto/mcp

@zap-proto/zap (the ZAP wire layer) is a dependency and is installed automatically.

Usage

// MCP server
import { ZapServer } from "@zap-proto/mcp/server";

const server = new ZapServer({ port: 9999 });
server.tool("search", "Search the web", {}, async (params) => ({
  content: [{ type: "text", text: "results" }],
}));
await server.start();
// Browser extension client
import { ExtensionClient } from "@zap-proto/mcp/browser";

const client = new ExtensionClient({ browser: "chrome", version: "1.0.0" });
const mcps = await client.discover();
const result = await client.callTool("search", { query: "hello" });

Go

go get github.com/zap-proto/mcp

github.com/zap-proto/mcp is the sibling of zap-proto/http: same seams, same relationship to the ZAP core, neither depending on the other.

// server — no HTTP listener anywhere in the path
srv := &zapmcp.Server{Network: "unix", Addr: "/run/zip/tools.sock", Handler: h}
srv.ListenAndServe()

func h(ctx context.Context, f *zapmcp.Frame) *zapmcp.Frame {
        if f.Method != "tools/call" {
                return f.Fail(zapmcp.CodeMethod, "method not found: "+f.Method)
        }
        return f.Answer(result)
}

// client
c := zapmcp.Dial("unix", "/run/zip/tools.sock")
out, err := c.Call(ctx, "tools/call", args)

One value, two projections

MCP is JSON-RPC 2.0; ZAP is binary. Rather than two message types kept in step by hand there is one — Frame — and two renderings of it:

zapmcp.Marshal(f) the ZAP wire (schema/zap_mcp.zap)
json.Marshal(f) the JSON-RPC 2.0 message

A server answers frames and never learns which door a frame came in, which is what lets one handler serve ZAP natively and have HTTP be an adapter over it rather than a peer of it.

Layering

Layer Package Owns
Application @zap-proto/mcp (TS) · github.com/zap-proto/mcp (Go) MCP message shape — tools, calls, agent ↔ tool semantics
Wire @zap-proto/zap ZAP zero-copy wire codec + Level-1 RPC primitives

By the composability theorem, @zap-proto/mcp inherits ZAP's PQ confidentiality and mutual auth from the wire — no MCP-specific PQ analysis required.

Why

Property MCP-over-HTTP+SSE / stdio @zap-proto/mcp
Confidentiality TLS (classical) X-Wing hybrid PQ (default)
Authentication bearer / TLS cert KEM keypair at transport
Wire encoding text or per-protocol binary ZAP wire, zero-copy
Identity binding DNS / cert chain zap-rns keypair
Future-quantum classical only hybrid by construction

Sub-protocol family

  • zap-http — HTTP request/response over ZAP
  • zap-ws — multi-stream pubsub
  • zap-fix — FIX 4.4 / 5.0 trading channel
  • zap-rns — KEM-bound service naming
  • zap-mcp — Model Context Protocol over ZAP
  • zap-acp — Agent Communication Protocol
  • zap-a2a — Google Agent2Agent over ZAP

License

MIT OR Apache-2.0