mcp #
a Model Context Protocol library for zig, server and client, that implements the specification as written and nothing more. no tool registry, no schema generation, no decorators: those belong in fastmcp, which builds on this.
- speaks only the stateless
2026-07-28revision (server/discover, per-request_meta,resultType, caching hints, multi round-trip requests, standard and customMcp-*headers,subscriptions/listen). nothing the revision deprecates is here: noinitialize, sessions, sampling, roots, logging, HTTP+SSE or dynamic client registration. aninitializegets-32022naming the version it should use - transports: stdio over any
std.Io.Reader/std.Io.Writer(pipes, TCP, unix sockets, in memory), and streamable HTTP (SSE or JSON per request, host and origin allowlists, mountable beside your own routes) - requests run concurrently; cancellation, progress, completions, input and output schema validation, middleware, custom methods and notifications, and a pluggable bus for fanning subscriptions out across replicas
- extensions: tasks (SEP-2663), skills (SEP-2640), MCP Apps
- a client (
mcp.Client) over stdio subprocesses, any byte stream or HTTP: typed helpers with auto-pagination, timeouts and cancellation, per-call progress, a response cache that honorsttlMs/cacheScope, result and elicitation validation,subscriptions/listen, and zero round-trip reconnects from a saved discover result - OAuth 2.1 on both sides. client (
mcp.oauth): discovery, client ID metadata documents, PKCE, scope step-up, a loopback browser flow, token storage and refresh, and the auth extensions (client credentials with a secret orprivate_key_jwt, enterprise-managed authorization, workload identity, DPoP). server (mcp.resource_server): bearer verification, protected resource metadata, per-tool scopes, 401/403 step-up - passes the official conformance suite, server and client, every extension scenario included. the only baselined checks are for features removed on purpose
- the python, typescript, go and rust sdks' examples are ported to
examples/ports as self-checking programs (
zig build examples) - interoperates with the typescript, go and rust sdks, client and server, over
HTTP and stdio, extensions included (interop,
interop/run.sh) - std only, no dependencies. zig
0.17.0
usage #
const std = @import("std");
const mcp = @import("mcp");
const Handler = struct {
pub fn listTools(_: *Handler, _: *mcp.Request, _: mcp.schema.ListParams) !mcp.schema.ListToolsResult {
return .{ .tools = &.{.{
.name = "echo",
.inputSchema = .{ .raw =
\\{ "type": "object", "properties": { "text": { "type": "string" } } }
},
}} };
}
pub fn callTool(_: *Handler, req: *mcp.Request, params: mcp.schema.CallToolParams) !mcp.schema.CallToolResult {
if (!std.mem.eql(u8, params.name, "echo")) return req.reject(.invalid_params, "Unknown tool");
const Args = struct { text: []const u8 };
const args = std.json.parseFromValueLeaky(Args, req.arena, params.arguments orelse .null, .{}) catch |err| switch (err) {
error.OutOfMemory => |e| return e, // not the client's fault
else => return req.reject(.invalid_params, "expected {\"text\": string}"),
};
return .{ .content = try req.arena.dupe(mcp.schema.Content, &.{.fromText(args.text)}) };
}
};
pub fn main(init: std.process.Init) !void {
var handler: Handler = .{};
var server: mcp.Server(Handler) = try .init(init.io, init.gpa, &handler, .{
.info = .{ .name = "echo", .version = "0.1.0" },
});
var in_buf: [64 * 1024]u8 = undefined;
var out_buf: [64 * 1024]u8 = undefined;
var stdin = std.Io.File.stdin().readerStreaming(init.io, &in_buf);
var stdout = std.Io.File.stdout().writerStreaming(init.io, &out_buf);
try mcp.stdio.serve(&server, init.io, init.gpa, &stdin.interface, &stdout.interface, .{});
}
over HTTP instead:
try mcp.http.serve(&server, init.io, init.gpa, .{ .port = 8000 });
decoding arguments by hand like this is the price of the slim layer; fastmcp derives it from a function signature.
Server(Handler) reads the handler's pub declarations at compile time and
advertises a capability for each complete pair: listTools + callTool,
listResources + readResource (+ optional listResourceTemplates),
listPrompts + getPrompt. callTool, readResource and getPrompt may
return mcp.schema.Outcome(T) to ask the client for input (elicitation)
through a multi round-trip request. see
examples/server.zig for all three primitives, and
examples/everything.zig for everything the
conformance suite exercises.
the client side is mcp.Client(Handler), where the handler answers what
the server asks for (elicit). it mirrors tool arguments into headers and
runs multi round-trip requests to completion. examples/conformance_client.zig
drives it through every client scenario, auth included.
add it to a project:
zig fetch --save git+https://tangled.org/zzstoatzz.io/mcp
exe.root_module.addImport("mcp", b.dependency("mcp", .{}).module("mcp"));
ownership and failures #
- everything about one message (its bytes, the parsed tree, the
Request, decoded params) lives in a per-message arena. handlers allocate results fromreq.arena; the stdio loop resets it after the response is flushed. anything that must outlive the message needs an owner the handler controls. req.reject(code, message)is a JSON-RPC error the client sees. a tool that ran and failed returns a result withisError = trueinstead, so the model can see it.- any other handler error becomes
-32603 Internal errorand is logged to stderr. running out of memory produces the same error, written without allocating. all allocation happens before the first response byte, so stdout never carries a partial message; an allocation-failure sweep insrc/tests.zigholds that invariant.
performance #
examples/bench.zig sends sequential tools/call
requests over HTTP with this library's client. against mcp-everything
(ReleaseFast) and the python sdk's everything-server, same machine, one
client:
| server | revision | req/s | p50 | p99 | rss |
|---|---|---|---|---|---|
| zig | 2026-07-28 | 11,900 | 73 µs | 238 µs | 1.7 MB |
| python | 2026-07-28 | 570 | 1.6 ms | 3.7 ms | 82 MB |
(measured before the legacy era was removed; not rerun since.)
zig build -Doptimize=ReleaseFast
./zig-out/bin/mcp-everything --port 3001 &
./zig-out/bin/mcp-bench http://127.0.0.1:3001/mcp 5000 2026-07-28
not yet #
- a tool registry, typed arguments and the rest of the DX: that is fastmcp
- websockets (not in the spec; any byte stream works with
stdio.serve)
development #
zig build test --summary all # unit tests
zig build examples # the ported sdk examples, each checking itself
zig build example # run the example server on stdio
zig build conformance # drive the example through the python sdk (needs uv)
zig build conformance-suite # the official conformance suite (needs node)
conformance runs scripts/conformance.py against the
reference python sdk,
pinned to mcp==2.3.0. conformance-suite runs
scripts/conformance-suite.sh, which pins
@modelcontextprotocol/conformance@0.2.0-alpha.12: the server suite against
mcp-everything and the client suite with mcp-conformance-client at
2026-07-28, extensions included. it fails on any FAILURE or WARNING (the
suite's own summary hides warnings), and on any check in
scripts/conformance-baseline.txt (removed
features) that unexpectedly passes.
- Zig 94%
- Rust 2%
- Python 2%
- Go 1%
- Shell 1%