Repository navigation
Client Guide
This page describes the client library: mcp.Client, the transports, the hooks and the request options. The client obeys the client part of the specification1.
Applies to: MCP 2026-07-28, Zig 0.16.0, zig-sdk 0.1.0.
var client: mcp.Client = .init(gpa, io, .{
.info = .{ .name = "cli", .version = "0.1.0" },
.capabilities = .{ .elicitation = .{}, .roots = .{} },
.hooks = .{ .elicit_form = onForm, .list_roots = onRoots },
});
defer client.deinit();
client.connect(transport);The client stamps _meta on every request: the protocol version, the client info, the client capabilities and a progress token2. Its integer ids increase by one for each request.
| Transport | How to get it |
|---|---|
| In-memory link to a server in the same process | mcp.transport.memory.ClientLink.init(io, gpa, &server).transport() |
| stdio with a child process |
mcp.transport.stdio.Client.spawn(io, gpa, .{ .argv = ... }) then .transport()
|
| Unix domain socket |
mcp.transport.unix.Client.connect(io, gpa, .{ .path = ... }) then .transport(). See Transport Unix Socket. |
| Streamable HTTP and HTTPS |
mcp.transport.HttpClient.init(io, gpa, .{ .url = ... }) then .transport(). For https, tls = .{ .trust = ..., .identity = ... } sets the trust policy and a client certificate. The default is the system trust store. See TLS and Certificates. |
The stdio client demuxes responses by id, and notifications by progress token and subscription id. It sends notifications/cancelled when the timeout of a request expires or when the application cancels it. close() closes the standard input of the process and waits for it. kill() terminates it at once.
The HTTP client opens one connection and sends one POST per request3. HTTPS runs on the SDK TLS client. It mirrors the method, the name or URI, and the annotated parameters into the Mcp-* headers with the base64 sentinel where needed4. It learns x-mcp-header annotations from tools/list and removes tools with invalid annotations from the list. See Header annotations. It gives each SSE event to the caller when the event arrives.
See Authorization for the auth and auth_provider options.
The HTTP client and the gRPC client mirror the tool parameters with an x-mcp-header annotation4. An annotation can be on a nested property. The client reads the value at the property path in the arguments.
The client removes a tool from the tools/list result when an annotation is not valid. For example, a chain of properties keywords from the root must reach the annotated property. An annotation below items, anyOf or $defs is not valid. The client logs a warning with the tool name and the reason. The optional hook tool_headers.on_reject of the transport receives each rejection.
The client mirrors an integer only when it is in the safe range of JavaScript, from -(2^53-1) to 2^53-1. For another integer, the request fails with error.InvalidRequest before the client sends it.
When the server answers a tool call with -32020, the tool schema can have new annotations. The client then reads all pages of tools/list again and sends the tool call one more time. The limit of the pages is limits.max_auto_pages (64).
Every method takes an arena that owns the result:
const tools = try client.listTools(arena, null, .{});
const result = try client.callTool(arena, "add", .{ .a = 2, .b = 3 }, .{ .timeout = .fromSeconds(30) });
const resource = try client.readResource(arena, "file:///readme", .{});
const prompt = try client.getPrompt(arena, "review", .{ .lang = "zig" }, .{});
const raw = try client.requestAs(arena, MyResult, "server/discover", params, .{});RequestOptions has:
-
timeout: the time for the whole request, with every multi round-trip round. Null useslimits.request_timeout(60 s). Progress notifications do not extend the timeout. When the time ends, the client cancels the request and returnserror.Timeout. -
max_total_timeout: the upper limit oftimeout. Null useslimits.max_total_timeout(600 s). A longertimeoutgets this value. -
cancel: a cancel token that stops the request. -
on_progress,on_logandon_notification: callbacks for the notifications on the request stream.on_progressreceives at mostlimits.max_progress_rate_per_s(50) notifications per second. The client drops the other progress notifications. -
allow_input_required: return the rawInputRequiredResult. The client does not call the hooks. -
allow_task: return theCreateTaskResultof the Tasks extension inResponse.task. The client does not wait for the task. -
cache_mode:defaultserves a cached result when the server gave it a lifetime,bypassnever touches the cache,refreshstores the fresh result. The cache is off unlessOptions.cache.enabledis set. See The result cache. -
retry: what to do when the client loses the stream before a response byte arrived.autore-issues idempotent methods with a new id,neverdoes not, andforcere-issues every method. The limit islimits.max_lost_stream_retries(3). -
diagnostics: receives the JSON-RPC error and other findings.
A subscriptions/listen stream has no default timeout. It gets timeout and max_total_timeout only when the caller sets them.
When a tool has an output schema in the listTools result, the client checks the structuredContent of each callTool result against it11. On a mismatch, the client logs a warning and sets diagnostics.structured_content_invalid. The result stays available to the caller.
A JSON-RPC error becomes error.Rpc with the error in diagnostics.rpc_error. When the server rejects the protocol version with -32022 and lists ours as supported, the client retries once5.
With Options.cache.enabled, the client keeps the results of discovery, list and read requests that carry a positive ttlMs12. The cache uses these rules:
- A
privateresult, and a result withoutcacheScope, belongs to one credential. The transport gives the credential: the bearer token on HTTP, or theauthorizationmetadata on gRPC. The cache keeps the SHA-256 digest of the credential, not the credential. - The cache serves a private result only for the same credential. When the credential changes, the cache drops the private results.
- A request with
requestStateorinputResponsesdoes not use the cache. - After an error
-32602for a request with a cursor, the cache drops the cached pages of that list. - A list-changed notification on a listen stream drops the cached lists.
-
connect()andinvalidateCache()drop every cached result.
When a result says input_required, the client runs one hook per input request6. Then it sends the request again with a new id, the answers and the unchanged requestState. The client rejects an input kind it did not declare. The limit is ten rounds.
The hooks are:
-
elicit_form(ctx, params) !ElicitResult: the client validates the accepted content againstrequestedSchema. A mismatch giveserror.HookFailed. -
elicit_url(ctx, params) !ElicitResult: show the URL to the user. The SDK never opens it. Before the hook runs, the client checks that the URL is a valid absolute URL withtypes.isValidUrl13. Another URL giveserror.InvalidResponse. -
sample(ctx, params) !CreateMessageResult: before the hook runs, the client checks the tool result rules of the messages withtypes.checkSamplingMessages14. A violation giveserror.InvalidResponse. -
list_roots(ctx) ![]const Root: every root must be afile://URI.
A client that declares the extension io.modelcontextprotocol/tasks in capabilities.extensions can receive a task instead of a tool result. See Tasks Extension. callTool then polls the task, answers its input requests with the hooks and returns the tool result. The other methods give full control:
switch (try client.callToolOrTask(arena, "slow_compute", .{ .seconds = 5 }, .{})) {
.complete => |result| use(result),
.task => |task| {
const done = try client.awaitTask(arena, task.taskId, .{ .timeout = .fromSeconds(60) });
use(try mcp.Client.taskResult(arena, done));
},
}getTask, updateTask and cancelTask send one request each. awaitTask returns error.Rpc with the task error in the diagnostics when the task failed, and error.TaskCancelled when the server canceled the task.
The client has methods for two more extensions:
- Skills:
listSkills,getSkill,readDirectoryandreadSkillFile.readSkillFileverifies the size, the digest and the frontmatter of each file against the skill entry. See Skills Extension. - Apps: declare the extension with
mcp.protocol.apps.declare. ThentoolUigives the UI metadata of a tool, andreadUiResourcereads its view. See Apps Extension.
listen(arena, filter, options) opens the stream. The on_notification callback receives the acknowledgment and every event. The call returns when the server closes the stream, or with error.Canceled when the cancel token fires.
A lost stream ends the call with error.Closed. Pass .retry = .force to open the stream again with a new id, for example after the stdio client restarted the server process7.
A server can attach icons to Implementation, Tool, Resource, ResourceTemplate, ResourceLink and Prompt. The client treats the icon metadata and the icon bytes as data from a source that it does not trust8. mcp.icons has the rules, and mcp.Client applies them with the client options.
var client: mcp.Client = .init(gpa, io, .{
.info = .{ .name = "cli", .version = "0.1.0" },
.icons = .{ .formats = .{ .svg = true } },
.hooks = .{ .icon_decoder = sanitizeIcon, .userdata = &app },
});
const icon = client.selectIcon(tool.icons, .{ .size = 32, .theme = .dark }, server_url) orelse return;
const image = try client.fetchIcon(arena, icon, .{ .server_url = server_url });
show(image.format, image.bytes);The client obeys these rules:
- The
srcis anhttpsURL or adata:URI9. The client rejects each other scheme, for examplehttp:,javascript:,file:,ftp:andws:, witherror.UnsupportedScheme. AnhttpsURL with user information giveserror.InvalidUri. - An
httpsicon must have the origin of the MCP endpoint10.server_urlgives that origin. The origin includes the scheme, so anhttpendpoint never matches anhttpsicon. Without a server URL, for example on stdio, onlydata:icons and the trusted origins pass. -
Policy.trusted_originsadds other origins.Policy.origins = .anydisables the origin rule.Policy.origins = .noneaccepts onlydata:icons.Policy.allow_data = falserejectsdata:icons. - The fetch sends one
GETrequest for each redirect step, with only the headershost,connection,acceptandaccept-encoding. It sends no cookie and noAuthorizationheader. It sends a client certificate only when thetlsoption offetchIconhas an identity. - A redirect must stay on the origin of the icon URL and on
https. The client follows at mostlimits.http.max_redirect_hops(3) redirects. After that, it giveserror.TooManyRedirects. - The magic bytes set the format. The media type of the response or of the
data:URI applies first. ThemimeTypeof the icon applies when that type is absent or generic. A mismatch giveserror.MimeMismatch, and an unknown format giveserror.UnknownFormat. - PNG and JPEG are on by default and need no decoder. GIF, WebP and SVG are off by default. They pass only when the policy turns them on and the
icon_decoderhook is set. An SVG image can contain script. - The client reads the width and the height from the PNG, JPEG, GIF and WebP header. A value above
limits.icon.max_dimensiongiveserror.DimensionsTooLarge.
The icon_decoder hook receives each checked image, also PNG and JPEG. It can decode, clean or convert the image. It returns an error to reject the image, and the client then gives error.DecoderFailed. The client gives the result of the hook to the caller.
selectIcon ranks the candidates by theme first, then by size, then by their order. For the theme, an equal theme wins over an icon without a theme, and that icon wins over another theme. For the size, an exact size wins. Then an icon with sizes = ["any"] or without sizes, then the smallest larger size, then the largest smaller size. The function uses only the declared mimeType for the format rule. Without an icon_decoder, it removes GIF, WebP and SVG icons.
fetchIcon returns an icons.Image with the format, the bytes and the dimensions. The dimensions are null for SVG. The errors are in icons.Error, for example OriginNotAllowed, RedirectRefused, MimeMismatch, TooLarge and Timeout. The functions mcp.icons.checkSource, decodeData, inspect, select and fetch give the same rules without a client. See Limits and Security for the limits.
- MCP-CLIENT-2026-07-28: Client features overview.
- MCP-SEP-2575: Per-request metadata.
- MCP-HTTP-2026-07-28: Streamable HTTP transport.
- MCP-SEP-2243: HTTP headers derived from the message.
- MCP-VERSIONING-2026-07-28: Version negotiation.
- MCP-SEP-2322: Multi round-trip requests.
- MCP-SUBSCRIPTIONS-2026-07-28: Subscriptions, and a new listen request after a reconnection.
- MCP-BASIC-2026-07-28: Icons, the MIME types and their security considerations.
-
RFC2397: The
dataURL scheme. - RFC6454: The origin of a URI.
- MCP-TOOLS-2026-07-28: Tools, the output schema and structured content.
- MCP-CACHING-2026-07-28: Caching, the scope of a result.
- MCP-ELICITATION-2026-07-28: Elicitation, the URL mode.
- MCP-SAMPLING-2026-07-28: Sampling, the rules for tool uses and tool results.
zig-sdk uses the Apache License 2.0. The wiki uses the project profile of ASD-STE100 Simplified Technical English. The Bibliography lists the sources.
zig-sdk
- Home
- Getting Started
- Concepts
- Server Guide
- Client Guide
- Transports
- Transport stdio
- Transport Streamable HTTP
- Transport gRPC
- Transport Unix Socket
- Transport WebSocket
- TLS and Certificates
- Deploy Reverse Proxy
- Authorization
- Authorization Server
- JSON Schema Support
- Resource Templates
- Tasks Extension
- Skills Extension
- Apps Extension
- Conformance
- Examples
- API Reference
- Interoperability
- Threat Model
- Limits and Security
- Deprecated Features
- Roadmap
- Release Notes
- Versioning
- Contributing
- FAQ
Style and words