Limits and Security - ChristianPresley/zig-sdk GitHub Wiki

Limits and Security

Most size, count and time limits of the SDK live in mcp.Limits, with a default and one documented overflow behavior. The gRPC transport, the TLS code and the authorization server have their own limits. A server gets its limits from Server.Options.limits. A client gets them from Client.Options.limits. This page lists them with the security defaults of the transports. See Threat Model for the reasons.

Applies to: zig-sdk 0.1.0. The rate limits, the Unix socket, WebSocket, the skills, the icons, the regular expressions, the typed gRPC messages and the authorization server: zig-sdk 0.2.0. The checks of the URI template limits: the same release. The HTTP time limits, the JSON depth limit, the filter size limit and listen_ack_timeout: zig-sdk 0.3.0. The limits of the HTTP client for each SSE event, the proxy and the remembered resets of HTTP/2: zig-sdk 0.4.0.

Messages and requests

Limit Default On overflow
json_max_depth 64 -32700 with a null id. The message object is level 1. The server checks the depth before the parse, without recursion. A value above 200 counts as 200. On the client, the request fails with error.InvalidResponse.
max_in_flight_requests 256 The reader waits. On gRPC: REFUSED_STREAM.
max_listen_subscriptions 1024 -32603
max_resource_subscription_uris 1024 -32603
max_filter_bytes 64 KiB -32603. The server counts the notifications filter of subscriptions/listen as compact JSON.
max_progress_rate_per_s 50 per second for each request The server does not send the notification. The client does not give it to on_progress. The client applies the limit before it parses the notification, when the method has no escape sequence.
request_timeout 60 s The timeout of a client request without RequestOptions.timeout. The client cancels the request with error.Timeout. A listen stream has no default timeout. The memory link (memory.ClientLink) does not obey it.
max_total_timeout 600 s The upper limit of a request timeout without RequestOptions.max_total_timeout. The client cancels the request with error.Timeout. Progress does not extend a timeout.
shutdown_grace 2 s The stdio client escalates from stdin close to SIGTERM to SIGKILL. It waits for the exit of the process in the same steps at close, before a restart and after the server ends its output. At a shutdown, the HTTP server cancels a busy connection after this time.
max_lost_stream_retries 3 The client does not re-issue the lost request again.
listen_ack_timeout 10 s The client cancels a listen stream without its acknowledgment and returns error.Timeout. Zero disables the limit. Before 0.3.0, the field was http.listen_ack_timeout. The memory link does not obey it.
request_state_ttl 600 s The server rejects the sealed request state with -32602.
page_size 100 The server divides lists into pages.
max_auto_pages 64 The client stops an automatic list refresh, for example the tools/list refresh after -32020.
completion_max_values 100 The server sends the first values and hasMore.
mrtr_max_rounds_client 10 error.TooManyRounds

Rate limits

Limit Default On overflow
rate_limits.tool_calls Off -31429 with data.retryAfterMs. On HTTP with a JSON response, also status 429 with Retry-After.
rate_limits.tool_calls_total Off The same as tool_calls. The call does not use a token of its caller.
rate_limits.log_messages Off The server drops the message and counts it. Before the next message that the limit admits, the server sends one summary with the count.
rate_limits.max_callers 4096 The server forgets the bucket that it used least recently.

Each limit is a token bucket with count tokens in each period and at most burst tokens. The caller of a request is the principal: the issuer, the subject and the client of the token. Without a principal, the caller is the IP address of the client on HTTP, gRPC and WebSocket. All IPv6 addresses of one /64 network are one caller. On stdio and on a Unix socket, the caller is the connection.

ToolDef.rate_limit gives one tool its own bucket for each caller. Server.rateLimitStats gives the counters.

The specification defines no error code for a rate limit, and it reserves the codes from -32000 to -32099. Thus the SDK uses -31429. Behind a reverse proxy, all clients without a token have the address of the proxy and share one bucket. Use authorization in this configuration.

stdio

Limit Default On overflow
stdio.max_line_bytes 16 MiB The server drops the line and logs a warning. On the client, the request with the top-level "id" of the line fails with error.InvalidResponse. The client then sends notifications/cancelled for the request.
stdio.read_buffer 64 KiB For a longer line, the reader uses a buffer that grows.

Unix socket

Limit Default On overflow
unix_socket.max_connections 64 The server closes the new connection at once.

The Unix socket transport also uses the stdio limits for each line and max_in_flight_requests for each connection. A socket path must fit into the sockaddr_un structure of the target, unix.max_path_len. A longer path gives error.NameTooLong. The path of the directory of the socket plus 14 bytes must also fit, for the private directory of bind. See Transport Unix Socket.

WebSocket

Limit Default On overflow
websocket.max_message_bytes 4 MiB Close with 1009.
websocket.max_frame_bytes 4 MiB Close with 1009.
websocket.max_connections 256 The server closes the new connection at once.
websocket.max_in_flight_requests 256 for each connection The error -32603. The connection stays open.
websocket.ping_interval 30 s A ping when no frame came for this time.
websocket.idle_timeout 90 s Close with 1001.
websocket.handshake_timeout 10 s The server closes the connection without a response.

The upgrade request obeys http.max_head_bytes. The server checks Origin and Host against loopback names by default, and it checks the access token at the upgrade. It closes the connection with 1008 when the token expires. See Transport WebSocket.

Streamable HTTP

Limit Default On overflow
http.max_body_bytes 4 MiB HTTP 413
http.max_head_bytes 16 KiB HTTP 431
http.max_connections 256 The accept loop waits.
http.idle_timeout 60 s A keep-alive connection without a new request closes without a response. A body that does not arrive in this time after its head gets HTTP 408.
http.head_timeout 10 s HTTP 408, and the connection closes. The time starts at the first byte of the head. For the first request of a connection, it starts at the accept and includes the TLS handshake.
http.sse_keepalive 15 s A comment line keeps the stream open.
http.max_redirect_hops 3 The icon fetcher gives error.TooManyRedirects.

The HTTP server binds 127.0.0.1 by default and validates Origin and Host against loopback names. It answers GET and DELETE with 405. It requires a bearer token when the auth option is set. reuse_address is off. When a client closes the connection, the server cancels the request and closes the connection.

Zero disables http.idle_timeout or http.head_timeout. At a shutdown, the server closes a connection that waits for a request at once.

The HTTP client transport and its SSE parser have their own limits:

Limit Default On overflow
HttpClient.Options.max_response_bytes 4 MiB One application/json body or one SSE event. The transport logs a warning, and the request fails with error.InvalidResponse. The client does not send the request again. An SSE response has no limit of its total size.
sse.Parser.max_event_bytes 4 MiB feed returns error.EventTooLarge. The bytes are the line that the parser reads, the data, the event type and the last event ID. The HTTP client transport sets the limit to max_response_bytes.
sse.Parser.max_id_bytes 256 bytes The parser ignores the id field.

Proxy

The HTTP client transport and the OAuth client connect directly by default. They use a proxy only when the application gives an environment or a proxy URL in the option proxy. Through a proxy, they ask for a CONNECT tunnel to the server. The tunnel carries the TLS handshake and the requests. Thus for an https URL, the proxy sees only the host and the port of the server.

  • The user and the password of a proxy URL go to the proxy only in the CONNECT request. They are in a proxy-authorization header with the Basic scheme.
  • The log lines show only the host and the port of the proxy. For a proxy URL that the client cannot use, the warning names the source of the URL, but not its value.
  • A proxy from the environment never gets a loopback host. The client does not resolve names for this check or for the no_proxy match.
  • For an http URL, the client ignores http_proxy and HTTP_PROXY when REQUEST_METHOD has a value. The Proxy header of a request to a web server can set these variables.
  • The SDK has no TLS to the proxy and no socks5 proxy. Another proxy URL gives error.InvalidProxy.

See Transport Streamable HTTP and Authorization.

gRPC

Limit Default On overflow
Concurrent streams max_in_flight_requests REFUSED_STREAM
Frame size 16384 octets FRAME_SIZE_ERROR
Header list 8 KiB ENHANCE_YOUR_CALM
Header block 32 KiB Connection error
Message 4 MiB RESOURCE_EXHAUSTED
Window per stream 1 MiB The peer waits.
Window per connection 4 MiB The peer waits.
Typed message nesting typed_limits.max_depth (64) INVALID_ARGUMENT
Typed message elements typed_limits.max_elements (262144) INVALID_ARGUMENT
Peer streams with a reset that a server connection remembers http2.Connection.remembered_resets (64) A header block on an older reset stream is a connection error PROTOCOL_ERROR.

A stream that the connection refuses for its headers does not count against the concurrent streams after the reset. After a complete response, the server resets a stream with NO_ERROR when the request did not end. The client then keeps the response. After a size error, the HPACK decoder reads the rest of the header block to keep the dynamic table in sync. See Transport gRPC.

JSON Schema

Limit Default On overflow
schema.max_depth 32 The compiler rejects the schema, or the instance is invalid.
schema.max_subschemas 4096 The server rejects the schema at registration.
schema.max_ref_hops 64 The instance is invalid.
schema.max_errors 32 The report stops at the limit.
schema.eval_budget 100000 The instance is invalid.
schema.max_pattern_bytes 4096 The server rejects the schema at registration.
schema.max_regex_states 4096 The server rejects the schema at registration.

schema.eval_budget also counts the sets of evaluated locations for unevaluatedProperties and unevaluatedItems, one unit for each 64 locations. schema.max_regex_states counts the instructions of one compiled regular expression. The match time is linear in the input length multiplied by this size. See JSON Schema Support.

Skills

Limit Default On overflow
skills.max_files 512 The server rejects the skill at registration with error.TooManySkillFiles. The client rejects the entry.
skills.max_bytes 16 MiB The server rejects the skill at registration with error.SkillTooLarge. The client rejects the entry.

The defaults are the limits of the Skills extension. A host must accept skills up to these limits, so the client uses the larger of its value and the limit of the extension. The server applies the value that the application sets. See Skills Extension.

Icons

Limit Default On overflow
icon.max_bytes 1 MiB error.TooLarge. The fetcher stops the read at the limit.
icon.max_dimension 4096 pixels error.DimensionsTooLarge
icon.timeout 10 s error.Timeout. The time includes the redirects. A data: URI has no timeout.

The icon fetcher also uses http.max_redirect_hops. See the icons section of the Client Guide.

URI templates

Limit Default On overflow
uri_template.max_template_bytes 64 KiB The registration fails with error.InvalidUriTemplate. Version 0.1.0 did not check this limit.
uri_template.max_expressions 256 The registration fails with error.InvalidUriTemplate.
uri_template.max_uri_bytes 64 KiB A longer URI matches no template. resources/read gives the error for an unknown resource. Version 0.1.0 did not check this limit.

Authorization server

Limit Default On overflow
limits.max_body_bytes 64 KiB HTTP 413 with invalid_request
limits.max_parameters 64 HTTP 400 with invalid_request
limits.max_parameter_bytes 8 KiB HTTP 400 with invalid_request
limits.max_scopes 64 invalid_scope
limits.max_state_bytes 1024 bytes The server redirects with invalid_request.
memory_limits.max_clients 10000 The store removes the expired records. When no record expired, it refuses the new record with error.StoreFull.
memory_limits.max_codes 10000 The same as max_clients.
memory_limits.max_refresh_tokens 100000 The same as max_clients.
memory_limits.max_jti 100000 The same as max_clients.

AuthorizationServer.Options has the fields limits and memory_limits. The memory_limits apply to the MemoryStore. See Authorization Server.

TLS

Limit Value On overflow
Handshake message 16 KiB on the server. On the client, 84 KiB: 64 KiB for the certificate chain, 16 KiB for an OCSP response and 4 KiB. decode_error alert
Certificate chain 8 certificates, 64 KiB bad_certificate alert
Plaintext compatibility records 4 unexpected_message alert
Records per key 2^24 The keys rotate with KeyUpdate.
HelloRetryRequest cookie 8 KiB on the client illegal_parameter alert
Client options ALPN names of 1 to 255 bytes, at most 1024 bytes of ALPN names, 16 cipher suites and 16 groups error.TlsInvalidOptions before the client sends a message

Security defaults in short

  • The server seals the request state of multi round-trip requests with AES-256-GCM and binds it to the method, the name and the arguments. With authorization, it also binds the state to the issuer, the subject and the client id of the token.
  • The server validates tool arguments against the input schema before the handler runs.
  • The rate limits of tool calls and log messages are off. We recommend that a server on a network sets rate_limits.tool_calls.
  • The server rejects a control character other than tab and a byte that is not ASCII in the raw values of Mcp-Method, Mcp-Name and Mcp-Param-*.
  • The client never opens a URL from a URL elicitation itself. It gives the URL to the application. It refuses a URL that is not a valid absolute URL.
  • The client validates a form answer against requestedSchema, and the messages of a sampling request against the tool result rules.
  • The client cache serves a private result only for the credential that got it.
  • The authorization clients refuse http metadata, registration, authorization and token endpoints unless allow_http is set. A redirect_uri must be https or a loopback http address.
  • The OAuth client sends pre-registered credentials only to the authorization server of their issuer.
  • The client verifies server certificates against a trust policy. The policy no_verification exists for tests only.
  • The HTTP client, the WebSocket client and the authorization clients refuse a request head with CR, LF or another control character. They also refuse such a URL, and an access token that is not a token68 value. A value from a peer thus cannot add a header.
  • The HTTP client and the OAuth client connect directly unless the application gives a proxy. The credentials of a proxy URL go only to the proxy, and the log lines show only its host and port.
  • With ca_bundle, the OAuth client trusts only the CA certificates of the bundle, also on Windows.
  • The client router logs a dropped frame with only the reason, the request id and the error code of an error response. The HTTP client writes no control character of a header of the server to the log.
  • A key of RequestOptions.meta cannot replace a _meta key that the SDK owns. Such a key gives error.InvalidMeta, and the client sends nothing.
  • The HPACK encoder never indexes the authorization metadata.
  • The client accepts icons only from https URLs on the origin of the server and from data: URIs. It fetches them without cookies and without an Authorization header. GIF, WebP and SVG are off by default.
  • The JSON Schema regular expressions run in linear time. A pattern cannot make the validator run for an exponential time.
  • The WebSocket server checks Origin and Host as the HTTP server does, and closes a connection when its access token expires.
  • The Unix socket server sets the mode 0600 on its socket file (on Windows, an access control list for the owner only) and never replaces a file that is not a socket.
  • The Unix socket server refuses a directory where other accounts can add, delete or rename entries. It moves the socket to its path only after it applies the protection.
  • An RSA key verifies each signature before the signature goes out.
  • The client verifies each skill file against the size and the digest of the skill entry.