MCP¶
An MCP server is a normal toolset. You connect a client to a transport, and
its tools join the agent like any other tool. ZigAI implements MCP
2026-07-28: requests are stateless and self-describing; there is no
initialize handshake or protocol session.
Streamable HTTP¶
var mcp_http = zigai.mcp.StreamableHttpTransport.init(
init.io,
http.transport(),
"https://example.com/mcp",
);
var mcp_client = zigai.mcp.Client{ .transport = mcp_http.transport() };
const remote_tools = mcp_client.toolset();
const agent = zigai.Agent{
.model = model,
.toolsets = &.{remote_tools},
.io = init.io,
};
Streamable HTTP admits up to 64 requests at once. Set
StreamableHttpOptions.max_in_flight with initWithOptions to match the
upstream server's capacity; excess requests wait at the transport boundary.
Request-scoped SSE uses bounded, standard multi-line data: framing. Event
callbacks run as each complete event arrives; the final correlated response
is returned normally. Event IDs and retry hints are ignored because MCP
2026-07-28 has no session resume. HTTP transports without line streaming fall
back to the same bounded buffered parser.
Stdio¶
For a local server, start it over stdio and use the same client:
var stdio = try zigai.mcp.StdioTransport.init(
init.io,
&.{ "my-mcp-server", "--config", "server.json" },
);
defer stdio.deinit();
var mcp_client = zigai.mcp.Client{ .transport = stdio.transport() };
Use initWithOptions to discard child diagnostics, change the graceful
shutdown window, or tighten the pending-request bound. Stdio is serialized;
calls beyond that bound fail with McpStdioBackpressure instead of growing
an unbounded queue. Closing the transport closes stdin first, then
force-kills and reaps a child that does not exit within the grace period.
Client capabilities¶
Optional client behavior must be advertised on every request. Build the
standard fields with ClientCapabilities; the returned JSON is owned by the
caller and stays borrowed by the client:
const capabilities_json = try (zigai.mcp.ClientCapabilities{
.roots = true,
.sampling = .{ .context = true, .tools = true },
.elicitation = .{ .form = true },
}).stringifyAlloc(allocator);
defer allocator.free(capabilities_json);
mcp_client.capabilities_json = capabilities_json;
The raw capabilities_json field remains the forward-compatible escape
hatch. Multi-round-trip input that was not advertised is rejected.
max_round_trips and max_pages bound retries and tool discovery.
Requests, subscriptions, and input¶
The client handles discovery, every core request, pagination, SSE
subscriptions, cancellation, and multi-round-trip sampling, roots, and
elicitation through an InputHandler. Its borrowed InputRequest carries a
typed kind, key, and validated request JSON, so handlers do not dispatch on
method strings. InputResponse builds validated elicitation, roots, or
sampling JSON while preserving the callback's explicit caller-owned byte
lifetime. Tool arguments marked with x-mcp-header are mirrored for
Streamable HTTP.
The typed helpers cover tools, prompts, resources, completion, discovery,
and subscriptions; SubscriptionFilter selects list and resource updates
without hand-written JSON, while PromptRequest and CompletionRequest
model their standardized parameters. Cancellation accepts the protocol's
integer-or-string RequestId; RequestOptions.metadata adds a progress
token and per-request logging opt-in.
listenWithRecovery can reissue a fresh stateless subscription after a
classified transport interruption. Its retry count, delay, deadline, and
cancellation are explicit; event callbacks may observe duplicates across
attempts.
Tasks¶
Tasks have typed helpers; capability negotiation and the Mcp-Name task
route are applied automatically:
var task = try mcp_client.getTask(allocator, "task-1");
defer task.deinit();
try mcp_client.updateTask(allocator, .{
.task_id = "task-1",
.input_responses_json = "{\"approval\":{\"action\":\"accept\"}}",
});
try mcp_client.cancelTask(allocator, "task-1");
var terminal = try mcp_client.waitTask(allocator, "task-1", .{ .io = io });
defer terminal.deinit();
Task results own a single arena. waitTask follows server polling hints,
handles input, and cooperatively cancels when its deadline or poll budget
ends. Task subscriptions use SubscriptionFilter.task_ids and
mcp.tasks.parseNotification.
Durable tasks¶
Add a store when tasks must survive a process restart:
var task_file = zigai.mcp.task_store.FileStore.init(
io,
std.Io.Dir.cwd(),
".zigai/mcp-tasks.json",
);
mcp_client.task_store = task_file.store();
var resumed = try mcp_client.resumeTasks(allocator, .{ .io = io });
defer resumed.deinit();
Tool-created tasks are tracked automatically. Pending input is saved before
it is sent, so recovery replays the response instead of asking twice. The
file is atomically replaced and owner-only on POSIX; use a custom
task_store.Store when responses need encrypted storage. If initial
persistence fails, ZigAI attempts to cancel the newly created remote task.
Extensions¶
Extension settings stay as owned JSON:
const settings = try zigai.mcp.extensionSettings(
allocator,
capabilities_json,
"io.modelcontextprotocol/tasks",
);
defer if (settings) |json| allocator.free(json);
Servers¶
zigai.mcp.Server provides the matching transport-neutral server dispatcher,
automatic server/discover, protocol and HTTP header validation, extension
dispatch, result metadata, and a stdio serving loop. HTTP hosts pass their
request headers and TLS state to Server.handle.
Protected endpoints use mcp.auth.ClientPolicy and mcp.auth.ServerPolicy;
browser-facing hosts add a separate mcp.auth.DeploymentPolicy for exact
Origin and Host checks. Bearer tokens never enter MCP JSON or handler
parameters, and refresh/step-up retries are bounded. Core handlers run only
when the matching capability appears in the server's capabilities_json.
mcp.Notification produces validated, caller-owned JSON-RPC for server
progress, cancellation, logging, updates, and subscription acknowledgements.
See the MCP conformance matrix for message coverage, compatibility boundaries, validation, and ownership. The security guide covers OAuth discovery, token callbacks, response headers, and deployment.