Server Guide - ChristianPresley/zig-sdk GitHub Wiki

Server Guide

This page describes the server library: mcp.Server, the request context, the handlers and the options. The server obeys the server part of the specification1.

Applies to: MCP 2026-07-28, Zig 0.16.0, zig-sdk 0.1.0. The Skills and Apps extensions, the authorization extensions and the rate limits: zig-sdk 0.2.0. The regular expressions, the nested header annotations, the checks of input requests and the principal binding: the same release. The rate of progress notifications: the same release. The order of the listen acknowledgment and the lock of a listen stream on the memory link: zig-sdk 0.4.0.

Create a server

mcp.Server.init(gpa, io, options) creates a server. The important options are:

Option Meaning Default
info Name and version of the server. Mandatory.
instructions Text for the model. None.
capabilities Server capabilities. The SDK sets tools, resources and prompts when you register the first item. Empty.
mrtr Which input kinds the server can ask for: elicitation, sampling, sampling_tools, roots. elicitation on, the others off.
request_state sealed_ephemeral seals the multi round-trip state with AES-256-GCM. unprotected sends it as is. sealed_ephemeral.
invalid_args_policy tool_error returns a tool result with isError. rpc_error returns -32602. tool_error.
structured_text_mirror Add a text block with the serialized structuredContent when the handler gave none. On.
allow_unsupported_schema_keywords Ignore a pattern or a patternProperties expression with a regular expression feature that the engine does not support. Off.
tasks Enable the Tasks extension. See Tasks Extension. Off.
skills Enable the Skills extension. See Skills Extension. Off.
apps Advertise the MCP Apps extension. See Apps Extension. Off.
authorization_extensions Advertise the authorization extensions of the authorization server. See Authorization. All off.
cache The ttlMs and the cacheScope of the results: discover for server/discover, lists for the lists, reads for the reads. ttlMs 0 and private.
limits Sizes, counts and durations. See Limits. The defaults in Limits.zig.

Tools

addTool(def, handler) registers a tool. The handler has this signature:

fn (ctx: *mcp.RequestContext, args: Args) anyerror!mcp.Outcome(mcp.CallToolResult)

The SDK derives the input schema from Args at compile time. A pub const json_schema declaration in Args adds descriptions, header annotations and patterns2:

const Args = struct {
    region: []const u8,
    priority: ?i64 = null,
    pub const json_schema = .{
        .description = "Where to run.",
        .fields = .{ .region = .{ .header = "Region" } },
    };
};

addToolJson(def, handler) registers a tool with a JSON Schema text and a handler that receives the raw std.json.Value. The server compiles every schema at registration. The validator supports all keywords of JSON Schema 2020-12. The server rejects remote references, dialects other than 2020-12 and regular expression features that the engine does not support. See JSON Schema Support.

An x-mcp-header annotation can be on a nested property. A chain of properties keywords from the root of the schema must reach that property. An annotation below items, anyOf, $defs or another keyword makes addTool fail with error.InvalidHeaderAnnotation. The annotated property must have the type string, integer or boolean.

The server validates the arguments of every call against the input schema before the handler runs3. A violation follows invalid_args_policy. When a tool declares an output schema, the server validates structuredContent and answers -32603 on a mismatch.

A handler that returns an error gives a tool result with isError: true. error.Canceled and error.OutOfMemory stop the request. ctx.setError sends a JSON-RPC error.

requires_client in the definition names the client capabilities the tool needs. The server answers -32021 before the handler runs when the client did not declare them.

Resources and prompts

  • addResource(def, handler) registers a resource with a fixed URI.
  • addResourceTemplate(def, handler) registers an RFC 6570 template4. The handler receives the matched variables.
  • addPrompt(def, handler) registers a prompt. The handler receives the arguments as a string map.
  • setCompletionHandler(handler) answers completion/complete. The server limits the values to 100. A reference to an unknown or disabled prompt or resource template gets -32602 before the handler runs.

A read of an unknown resource answers -32602 with the URI in the error data5.

The request context

Every handler receives *mcp.RequestContext. It gives you:

  • arena: memory that lives until the server sends the response.
  • io and gpa.
  • checkCancel(): returns error.Canceled when the client canceled the request. On HTTP, a client that closes the connection also cancels the request.
  • progress(value, total, message): sends a progress notification when the request has a progress token. The server sends at most limits.max_progress_rate_per_s (50) notifications per second for each request.
  • principal(): the authorization principal on HTTP, gRPC and WebSocket when the transport checked an access token, else null.
  • log(level, logger, data) and logText(...): send a log message when the server declares capabilities.logging and the client asked for one with logLevel. The server drops a message over rate_limits.log_messages of the caller. Later, it sends one summary with the logger rate_limit and the count.
  • hasClientCapability(path) and requireClientCapability(path).
  • elicitResponse(key), sampleResponse(key), rootsResponse(key) and state(T) for multi round-trip requests.

Multi round-trip requests

A handler that needs input returns .input_required with an InputRequired builder:

fn askName(ctx: *mcp.RequestContext, args: std.json.Value) anyerror!mcp.Outcome(mcp.CallToolResult) {
    if (try ctx.elicitResponse("user_name")) |resp| {
        const name = mcp.json.getString(resp.content.?, "name") orelse "?";
        return .{ .complete = try mcp.CallToolResult.text(ctx.arena, "hello {s}", .{name}) };
    }
    var ir: mcp.InputRequired = .init(ctx.arena);
    try ir.elicitForm("user_name", "What is your name?", try mcp.InputRequired.stringSchema(ctx.arena, "name", null, true));
    try ir.setStateFmt("{{\"round\":{d}}}", .{1});
    return .{ .input_required = ir };
}

The server only sends input kinds that options.mrtr allows and that the client declared6. Only tools/call, resources/read and prompts/get can answer with input_required.

The server checks each input request before it sends it:

  • A URL elicitation must have a valid absolute URL8. InputRequired.elicitUrl returns error.InvalidUrl for another URL. types.isValidUrl has the rule.
  • The messages of a sampling request must obey the tool result rules9. A user message with a tool result has only tool results. Each tool use has a tool result in the next user message. types.checkSamplingMessages has the rules. A violation gets -32603.
  • The includeContext values thisServer and allServers need the client capability sampling.context. Without it, the request gets -32021.

The server binds sealed requestState to the principal of the request: the issuer, the subject and the client id of the token. A state that another principal sends back gets -32602 with the reason invalid_request_state. A task keeps the principal of the request that created it.

Subscriptions

The server answers subscriptions/listen with an acknowledgment first, then with events. It obeys the subset of the filter that the declared capabilities cover. setToolEnabled and setPromptEnabled publish list-changed events. notifyToolsListChanged, notifyPromptsListChanged, notifyResourcesListChanged and notifyResourceUpdated publish an event directly. When the server stops, every stream ends with a result. On stdio, on a Unix socket and on WebSocket, a cancellation notification follows the result.

The acknowledgment is the first message of the stream on all transports10. The server locks the stream before the new subscription becomes visible, and unlocks it after the acknowledgment. An event that the server publishes in that time waits, and then goes out after the acknowledgment. When the acknowledgment does not go out, the stream gets no events and the listen request ends. In version 0.3.0, an event could get to the client before the acknowledgment.

Over the memory link (memory.ClientLink), the callbacks of a listen stream run while the server holds the lock of the stream. This applies to the acknowledgment and to each event. Thus such a callback must not publish an event of the same server, for example with setToolEnabled or notifyToolsListChanged. It also must not wait for a task that publishes an event or ends a listen stream of the same server. The lock is not reentrant, thus a callback that publishes waits without end.

Rate limits

Set the limits in options.limits.rate_limits. All limits are off by default.

options.limits.rate_limits.tool_calls = .{ .count = 10, .burst = 20 };
options.limits.rate_limits.log_messages = .{ .count = 50 };

ToolDef.rate_limit gives one tool its own limit, for example .{ .count = 1, .period = .fromSeconds(60) }. A count of zero removes the limit of each caller for that tool. The server checks the limit before it validates the arguments. rateLimitStats gives the refused tool calls, the dropped log messages and the number of buckets. See Limits and Security.

Errors

Situation Error
Text that is not JSON -32700
A message that is not JSON-RPC -32600
initialize or an unknown method -32601
Missing _meta fields, unknown tool, unknown resource, unknown completion target -32602
Changed or expired requestState, or the state of another principal -32602
A handler failure that is not a tool error -32603
Header and body mismatch on HTTP and gRPC. A header value with a control character other than tab, or with a byte that is not ASCII. -32020
Missing client capability -32021
Unsupported protocol version -32022
A tool call over a rate limit -31429 with data.reason, data.retryAfterMs and data.limit. On HTTP with a JSON response, also status 429 with Retry-After.

The SDK never sends -32002 or -32042. Those codes belong to older revisions7.

Extensions

The server advertises each enabled extension under capabilities.extensions in the server/discover result:

  • The Tasks extension runs a tool call as a task. See Tasks Extension.
  • The Skills extension publishes skills with addSkill and addDynamicSkill. See Skills Extension.
  • The MCP Apps extension gives views with addUiResource and addUiResourceHandler. ToolDef.ui links a tool to its view, and checkUiLinks checks the links. See Apps Extension.

Limits

Limits.zig has every size, count and duration with a documented overflow behavior. Some examples:

  • A stdio line is at most 16 MiB.
  • An HTTP body is at most 4 MiB.
  • A page has 100 items.
  • Request state lives 600 seconds.
  • A request sends at most 50 progress notifications per second.
  • The schema validator evaluates at most 100 000 subschemas per instance.
  • The rate limits of tool calls and log messages are off by default.

References

  1. MCP-SERVER-2026-07-28: Server features overview.
  2. MCP-SEP-2243: Custom headers from tool parameters.
  3. MCP-TOOLS-2026-07-28: Tools.
  4. RFC6570: URI Template.
  5. MCP-RESOURCES-2026-07-28: Resources.
  6. MCP-MRTR-2026-07-28: Multi round-trip requests.
  7. MCP-CHANGELOG-2026-07-28: Key changes of revision 2026-07-28.
  8. MCP-ELICITATION-2026-07-28: Elicitation, the URL mode.
  9. MCP-SAMPLING-2026-07-28: Sampling, the rules for tool uses and tool results.
  10. MCP-SUBSCRIPTIONS-2026-07-28: Subscriptions, the acknowledgment as the first message of the stream.
⚠️ **GitHub.com Fallback** ⚠️