You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RequestOptions.log_level: the log level of one request. It replaces Options.log_level of the client. The client also sends it when Options.log_level is null.
RequestOptions.meta: more _meta entries for one request, for example traceparent, tracestate, baggage or a key with a vendor prefix. Before the client sends the request, it checks each key. A key that meta.validateKey refuses, or a key that the SDK owns, gives the new error error.InvalidMeta of Client.RequestError. The client sends nothing then. The client accepts the keys of extensions under io.modelcontextprotocol/. The entries of the SDK always have priority.
meta.sdk_owned_request_keys and meta.isSdkOwnedRequestKey: the request keys that the SDK owns. They are the protocol version, the client information, the client capabilities, the log level, the subscription id and the progress token.
RequestOptions.inline_notifications: the reader task of the transport calls on_progress, on_log and on_notification of the request at once, before it routes the next frame. Thus a notification gets to its callback before the response of another request that the server wrote after it. An example is a notifications/tools/list_changed event of a listen stream and the tools/call result that caused it. The stdio, Unix socket and WebSocket transports obey the option, and the memory link, HTTP and gRPC ignore it. A callback must only copy, serialize and write the data, and must not send a request through the same client. Transport.Exchange.inline_notifications gives the option to the transports.
stdio.Client.SpawnOptions.on_notification and stdio.Client.SpawnOptions.userdata: the callback of the stdio client for each notification without a progress token and without a subscription id. An example is a log message of the server. spawn sets the callback before the reader task starts.
sse.Parser.max_event_bytes (4 MiB): the maximum bytes of the event that the SSE parser reads, across all calls of feed. The bytes are the line that the parser reads with its field name, the data and the event type of the event, and the last event ID. The events and comments before it do not count.
sse.Parser.max_id_bytes (256): the maximum bytes of an event ID. The SSE parser ignores an id field with a longer value.
line_framer.TopLevelScanner: finds the top-level members "id", "method", "result" and "error" of a JSON object in parts, with fixed memory. line_framer.Framer.dropped has the members of the last line that the framer dropped for its length or for its UTF-8. The framer reads the whole line for it, also the part after the limit.
stdio.Client.exitStatus: the exit status of the last server process that ended, as std.process.Child.Term. The client reaps a process after it ends its output, before a restart, and in close and kill. When a process ends its output and no restart is left, the client logs a warning in the scope mcp_stdio with the status.
Client.Diagnostics.http_status: the HTTP status of the last response to a request, also when the request failed. The Streamable HTTP and gRPC transports give it, and the WebSocket transport gives the status of an upgrade that the server refused. Thus a caller can tell a wrong path, for example status 404, from a response that is not valid. Both give error.InvalidResponse.
OAuthClient.Options.ca_bundle: the CA certificates that the OAuth client trusts for its https requests. These are the metadata, registration and token requests, and the authorization request of headless_redirect. Null uses the CA store of the system, as before. Thus a private CA or the CA of a test server works without a change to the CA store of the system. The HTTP client transport can use the same bundle in tls.trust.
auth.common.Fetcher.ca_bundle: the CA bundle of the fetcher. With a bundle, the fetcher sends each request on a new connection of http1.Connection. There, the TLS client of the SDK checks the certificates against the bundle only, at the time of the request. Thus the servers must offer TLS 1.3. The fetcher does not give the bundle to std.http.Client. On Windows, the TLS client of std also accepts a certificate chain that the certificate store of the system trusts.
HttpClient.Options.proxy and OAuthClient.Options.proxy: the HTTP proxy of the client connections, with the new module transport.proxy. The client asks the proxy for a CONNECT tunnel to the server, also for an http URL. The tunnel carries the TLS handshake and the requests, thus the proxy sees only the host and the port. .explicit sets a proxy URL, and .none disables the proxy.
By default, the proxy comes from the environment that the application gives, for example .{ .environment = init.environ_map }. The default environment is null, thus the clients connect directly, as before. An https URL takes the first of https_proxy, HTTPS_PROXY, all_proxy and ALL_PROXY with a value. An http URL takes the first of http_proxy, HTTP_PROXY, all_proxy and ALL_PROXY. When REQUEST_METHOD has a value, the client ignores http_proxy and HTTP_PROXY, because the Proxy header of a request to a web server can set them.
transport.proxy.bypasses: the match of no_proxy and NO_PROXY. * matches each host. A name matches the same host and each host under it, with or without a dot or *. at the start. An IP address matches the same address, and an address with a prefix length, such as 10.0.0.0/8, matches the range. An entry with a port matches only that port. The client does not resolve names. A proxy from the environment never gets a loopback host: localhost, a name under .localhost, an address in 127.0.0.0/8 and ::1.
The user and the password of a proxy URL go to the proxy only in the CONNECT request, in a proxy-authorization header with the Basic scheme. The log lines show only the host and the port of the proxy. The SDK has no TLS to the proxy and no socks5 proxy. A proxy URL that does not use http gives the new error error.InvalidProxy of HttpClient.InitError.
When the proxy refuses the tunnel, the new log scope mcp_proxy has a warning with the proxy, the target and the status. http1.Connection.openThrough and auth.common.Fetcher.fetch then return error.ProxyRefused. The HTTP client transport gives error.WriteFailed, and the client returns error.TransportFailed.
auth.common.Fetcher.proxy: the proxy of the OAuth fetcher. Through a proxy, the TLS client speaks to the server through the tunnel, with the same trust as a direct request. With ca_bundle, that is the TLS client of the SDK. Without it, the TLS client of std uses the CA store of the system. The fetcher does not use the proxy fields of std.http.Client. In Zig 0.16.0, that client sends an https request through a CONNECT tunnel without TLS.
http1.Connection.openThrough and http1.TunnelError: a connection through a CONNECT tunnel. http1.writeRequest and http1.RequestError: the request of Connection.send for another writer. Connection.SendError is now the same error set as RequestError.
http2.Connection.Stream.stopWaits: stops the waits on a stream without a reset. Each wait on the stream then gives error.Canceled when it must block. Use it, not Future.cancel, to stop a task that waits on the stream. In Zig 0.16.0, the condition of std can lose a cancel that comes together with a wake-up. The task then waits until the stream gets a reset or the connection ends.
client.CallStream: the stream of a gRPC call that runs in its own task. The tunnel and typed channels reset the stream with CallStream.stop to stop the call.
util.loopback.connect and util.loopback.listen: a connect to a loopback address and a listen on it, with at most five tries after error.Unexpected or error.AddressUnavailable. On Windows, Zig 0.16.0 gives two errors that can go away as error.Unexpected: a connect to a server with many connections in TIME_WAIT (ADDRESS_ALREADY_EXISTS), and a socket when no local port is free (TOO_MANY_ADDRESSES). On Windows, a refused connect is also error.Unexpected, thus use connect only for a server that listens. The tests of the SDK use these functions for their own sockets.
Changed
Breaking: the client parses each notification of a request into scratch memory and resets that memory after the frame. Thus the method and params arguments of RequestOptions.on_notification and Hooks.on_notification are valid only during the call. Before, they stayed valid until the caller freed the request arena. A callback that keeps a part of them must copy it. The parameters of on_progress and on_log were already valid only during the call.
The request arena does not hold the notifications of the request. Thus a long subscriptions/listen stream does not make the request arena larger. Before, each event added approximately 1900 bytes to the request arena on all transports.
The client applies the progress rate limit before it parses a notification, when the method has no escape sequence. Thus it does not parse a progress notification over limits.max_progress_rate_per_s.
Breaking: the fields on_notification and userdata of stdio.Client are gone. Use the spawn options with the same names. The reader task starts in spawn and read the fields one time for each child process. Thus a callback that the caller set after spawn raced with the reader task. When the reader task was first, it dropped these notifications until a restart, also the log messages of the server.
Breaking: over the memory link, the callback of the acknowledgment of a listen stream runs while the server holds the lock of the stream. Thus the callback must not publish an event of the same server, for example with Server.setToolEnabled. The callbacks of the events already had this rule. Before, the server sent the acknowledgment without the lock. A callback that publishes now waits without end, because the lock of the stream is not reentrant.
Breaking: sse.Parser.feed returns the error set sse.Parser.FeedError, with the new error error.EventTooLarge. The parser returns it when an event gets more than max_event_bytes. The complete events before that event stay available through next. Before, feed returned only error.OutOfMemory, and an event had no limit.
When the server process of stdio.Client ends its output and no restart is left, the reader task closes the input of the process and reaps it, as close does. Before, the process stayed until close. A process that does not exit gets a termination signal after limits.shutdown_grace, and a kill signal after one more grace period. A restart waits for the old process in the same way. Before, a restart waited without a time limit.
Fixed
The doc comments of RequestOptions.on_log, RequestOptions.on_notification and Hooks.on_notification tell which notifications they receive. These callbacks receive only the notifications that the transport routes to a request. Before, the comment of Hooks.on_notification said that it receives the notifications that belong to no request on stdio. The stdio, Unix socket and WebSocket transports route a notification only by its progress token or its subscription id. A notification that has neither goes to the on_notification spawn option of stdio.Client, or to the on_notification option of the Unix socket and WebSocket clients. Log messages of the server are such notifications, also during a request.
The doc comments of the on_notification options of the Unix socket and WebSocket clients tell which notifications they receive. Before, they said that the option receives the notifications that belong to no request in flight. But the option also receives the log messages of a request in flight. The router drops a notification whose progress token or subscription id names no request in flight. The comments also tell that the function runs on the reader task, must not block, and gets arguments that are valid only during the call.
The server sends the acknowledgment of a subscriptions/listen stream before the first event of the stream, on all transports. Before, publish could see a new subscription before its acknowledgment went out. An event at that time then got to the client before the acknowledgment. Now the server locks the stream before the 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.
The memory link (memory.ClientLink) gives the frames of one request to the sink one at a time, as the stdio and HTTP transports do. Before, an event of a listen stream from the task that published it could get to the sink at the same time as another frame of the stream.
The doc comments of memory.zig tell the limits of ClientLink. It runs the handler of a request on the task of the caller, and only the cancel token of the caller ends an exchange. It does not obey Exchange.timeout and Exchange.first_frame_timeout. A callback of a listen stream must not publish an event of the same server. With these limits, you can use ClientLink in production. Before, the comment of the file said that its transports are for tests.
The entry of 0.3.0 for limits.listen_ack_timeout says that all client transports obey it. The memory link does not obey it.
The doc comments of RequestOptions.timeout, limits.request_timeout, limits.listen_ack_timeout and Client.listen tell that the memory link does not obey these limits.
The doc comment of Client.RequestError.InvalidResponse tells that the error also comes from a response that the transport cannot read, or from an HTTP status without a JSON-RPC message. The client does not send the request again after this error.
Over the memory link, a listen stream that the server ends at its shutdown returns its result, as on the other transports. Before, the link returned error.Canceled, because the shutdown fires the cancel token of the caller.
The memory link ends a request with error.InvalidFrame when the sink refuses a frame, as the other transports do. The client then returns error.InvalidResponse and does not send the request again. Before, the link gave error.ReadFailed. Then the client returned error.TransportFailed, or sent the request again when the caller forced a retry.
The HTTP client transport applies max_response_bytes to each event of an SSE response, and not to the sum of the events. Thus keep-alive comments, progress notifications and log messages do not stop a long tool call or a listen stream. Before, the transport ended an SSE response with error.ReadFailed after max_response_bytes in total (4 MiB by default). Then the client returned error.TransportFailed. For example, a tool call with 50 progress notifications each second failed after approximately 11 minutes.
The SSE parser ignores an id field with more than sse.Parser.max_id_bytes. Before, each event got a copy of the last event ID. Thus a server could send one long id line and then many small events. Then one read of the response body used approximately 1000 times its size in memory. The limit of 0.3.0 on the whole stream did not prevent this.
The stdio, Unix socket and WebSocket clients make a request fail with error.InvalidResponse when the router cannot read its response. Before, the router dropped the frame without a log, and the request waited until its timeout. The router finds the request by the top-level "id" member of the frame, also when the member comes after the result or after the length limit of the line. These frames are a line over limits.stdio.max_line_bytes, a line that is not valid UTF-8, and a frame that is not a JSON-RPC message. An error response with a null id makes the request fail when only one request can get it. A frame that the router cannot copy, because no memory is available, also makes its request fail. On the WebSocket client, a message over the limit or a message that is not valid UTF-8 does not get to the router. It closes the connection with the code 1009 or 1007, and the requests fail with error.Closed.
When the router makes a request fail in this way, the stdio, Unix socket and WebSocket clients send notifications/cancelled for the request, because the server can still run it. They also do this when the sink of the request refuses a notification. Only a request that the client wrote and that got no frame from the server can get an error response with a null id. Thus such an error does not end a listen stream after its acknowledgment, or a request after a progress notification.
The client router logs a warning in the scope mcp_router for each frame that it drops, with the reason and the request id. The warning has no other part of the frame. Before, the router logged nothing, also for a line over limits.stdio.max_line_bytes, but the doc comment of the limit told of a warning.
The HTTP client transport ends a request with error.InvalidFrame and logs a warning when an application/json body or one SSE event has more than max_response_bytes. The client then returns error.InvalidResponse and does not send the request again. Before, the transport gave error.ReadFailed. The client took the overflow for a lost stream and sent an idempotent request up to 3 more times, and then returned error.TransportFailed.
The HTTP client transport logs a warning with the status and the content type when a response carries no JSON-RPC message. The warning has at most 100 bytes of the content type, with a question mark for each byte that is not visible ASCII or a space. Thus a server cannot put a line break or an escape sequence into the log. The transport also logs a warning when the server answers with status 401 or 403 and the client has no authorization provider. Before, the transport returned error.HttpStatus without a log.
stdio.Client.close and stdio.Client.kill also end a server process that closed its output but does not exit. Before, close waited for such a process without a time limit.
stdio.Client writes no request to the input of a process after close or a failed restart closed that input. Before, a request could write to the closed pipe at that time. A reap and a signal take the same lock. Thus the client never signals a process ID after the reap, when the system can give the ID to a new process.
stdio.Client.kill also ends a server process that a restart spawns at the same time. Before, kill could find no process while the restart spawned a new one. The new process then waited for its input, and kill waited for the reader task without a time limit.
OAuthClient reads the stored record again before a refresh, and again after the server refuses the refresh token or the client. Thus clients that share one storage, for example in two windows of an application, can use one grant with refresh token rotation. When another client changed the record, the client takes its registration and tokens, and does not write over the record. When the new access token expires soon, the client sends one more refresh with the new refresh token. Before, the client sent its old refresh token, and the server refused it after a rotation. The client then wrote a record without tokens over the newer record, and sent the user to the browser again.
At a challenge that is not a step-up, OAuthClient also reads the stored record again. When another client with the same storage got new tokens, the client tries them before a refresh or the browser.
When a stored record has another client ID than the registration of OAuthClient, the client takes the registration of the record with its tokens. The server issued these tokens to the client ID of the record. When the tokens are for another DPoP key, the client cannot use them. It then keeps its own registration and tokens, and refreshes with its own refresh token. Before, the client kept its own registration when it had one, and sent the tokens of the record with the wrong client ID.
The gRPC clients keep a response that the server sends before it reads the request message. Examples are an authorization challenge, an unknown method and the HTTP status 415. Before, the server reset the stream after such a response. When the reset got to the client before the message, the send failed with error.StreamReset. The transport then lost the response and returned error.ReadFailed, thus the client returned error.TransportFailed or sent an idempotent request again. Now http2.Connection.Stream.sendData and sendHeaders of a client drop the remaining part of the request without an error, and the client reads the response.
A gRPC client drops the request only when the server ended the stream and the stream then got a reset. The ended stream tells that the response is complete. The reset can come from the server or from the client, and its code has no effect. Thus the gRPC clients also keep the response of a server that resets with CANCEL, for example a server of version 0.3.0.
After a complete response, the gRPC server resets a stream with the code NO_ERROR when the request did not end. The client then stops the request and keeps the response (RFC 9113 section 8.1). Before, the server sent CANCEL.
http2.Connection.Stream.cancel wakes each task that waits on the stream, for example in read, waitHeaders, waitEnd, sendData or waitCancelled. The task then sees the reset. Before, a cancel from another task did not wake such a task. It continued to wait until the next frame on the connection or the end of the connection.
After a call, the gRPC server stops the task that watches the stream for a reset with Stream.stopWaits. Before, it canceled the task. When a frame came on the connection at the same time, the cancel could get lost. The handler of the call then waited until the client reset the stream or closed the connection. Until then, the stream counted against max_concurrent_streams.
When a gRPC call stops for its deadline or for a cancel, the tunnel and typed channels reset the stream of the call before they wait for its task. Before, they only canceled the task. When the cancel got lost, the call waited until the server answered or the connection ended.
http2.Connection sends the reset for a header block over max_header_list_size after it unlocks the connection, as on the other paths. Before, the read task held the connection lock while it waited for the write lock. A task that sent the headers of a new stream at the same time held the write lock and waited for the connection lock. Thus the connection stopped.
A stream that gets a cancel from another task before its headers go out gets no id, and sendHeaders gives error.StreamReset. Before, the headers went out after the cancel. The cancel sent no reset, because the stream had no id at that time. Thus the server kept the stream open until the connection ended.
The HPACK decoder reads the complete header block after a field over max_field_size or a header list over max_header_list_size. It applies each change to the dynamic table, adds no more headers and gives the error at the end. Before, it stopped at the error. When the peer added an entry to its table later in the block, a later block of the connection that used the entry failed, and the connection ended with PROTOCOL_ERROR. The encoder of the SDK adds no entries, thus only another peer can cause this. A string length past the end of the block now gives error.Truncated before the size check, because the block is not valid.
http2.Connection frees a stream that the peer opens with a header list over max_header_list_size or with headers that are not valid, after the reset, as for REFUSED_STREAM. Before, nobody closed the stream, and it counted against max_concurrent_streams until the connection ended. After that number of such requests, the connection refused each new stream.
A server http2.Connection ignores a header block on a peer stream that it reset, for example trailers that the client sent before the reset arrived (RFC 9113 section 5.1). The connection remembers the last remembered_resets (64) such streams: the streams that it refused and the streams that Stream.close ended with a reset. It decodes the block to keep the HPACK table in sync. Before, the block ended the connection with PROTOCOL_ERROR. A block on another closed stream, or on a stream id that the client did not open, still ends the connection.