Skip to content

Deploy Reverse Proxy

Christian Presley edited this page Oct 1, 2026 · 4 revisions

Deploy Reverse Proxy

This page tells you how to put the Streamable HTTP server of the SDK behind a reverse proxy, such as nginx or Caddy. The proxy terminates TLS and forwards each request to the SDK on the loopback interface. Three settings are important: the Host and Origin checks, the buffering of server-sent events and the time limits. The examples are a start for your own configuration. They are not a complete configuration.

Applies to: MCP 2026-07-28, Zig 0.16.0, zig-sdk 0.1.0. The cancellation at a disconnect, the default request timeout, the rate limits, DPoP and the WebSocket transport: zig-sdk 0.2.0.

The layout

The SDK server binds to 127.0.0.1 and serves plain HTTP. The proxy accepts HTTPS on the public address and forwards the requests to the SDK. Only the proxy can connect to the SDK port.

The SDK can also serve HTTPS itself with the tls option. Then you do not need a proxy. See TLS and Certificates.

Host and Origin checks

The SDK server compares the Host header and the Origin header with two lists before it reads the body1. These checks prevent DNS rebinding attacks. A request that fails a check gets 403.

  • allowed_hosts: when this list is empty, the server accepts the loopback names and the bound address. When the list has names, the server accepts only these names. The port does not count.
  • allowed_origins: when this list is empty, the server accepts loopback origins only. A request without an Origin header passes this check.

The proxy changes what the SDK sees:

Proxy Host at the SDK What to configure
nginx with its defaults The address in proxy_pass, such as 127.0.0.1:3000 Nothing for Host, when allowed_hosts is empty.
nginx with proxy_set_header Host $host The public name Add the public name to allowed_hosts.
Caddy with its defaults The public name Add the public name to allowed_hosts.

Both proxies forward the Origin header of a browser without a change. When browser clients use the server, add their exact origins to allowed_origins, such as https://app.example.com. Do not add a wildcard.

var http_server: mcp.transport.http.Server = .init(io, gpa, &server, .{
    .address = "127.0.0.1",
    .port = 3000,
    .allowed_hosts = &.{"mcp.example.com"},
    .allowed_origins = &.{"https://app.example.com"},
});

Server-sent events

A proxy that keeps a response in its buffer holds the events of a stream. The client then gets progress notifications late, and a listen stream seems to stop. The SDK sends two headers on each SSE response to prevent this:

  • x-accel-buffering: no tells nginx not to keep this response in its buffer.
  • cache-control: no-cache, no-transform tells caches and proxies not to store or change the stream.

Caddy sends each part of a text/event-stream response at once. For nginx, we recommend that you also set proxy_buffering off on the MCP location.

Time limits

A listen stream can stay open for hours. When the keepalive option is on, the SDK sends an SSE comment every limits.http.sse_keepalive (15 s) on each listen stream. Set the read timeout of the proxy above this interval.

A tool call without notifications sends no bytes until its response. The SDK client cancels a request without a timeout after limits.request_timeout (60 s). A timeout can be up to limits.max_total_timeout (600 s). Set the read timeout of the proxy above the longest time that you permit for a tool.

When the proxy closes the connection to the SDK, for example at its read timeout, the SDK cancels the request.

The SDK closes a keep-alive connection without a new request after limits.http.idle_timeout (60 s). Set the keep-alive time of the proxy for its connections to the SDK below this value. Else the proxy can send a request on a connection that the SDK closes at the same time.

Other settings

  • The body limit of the SDK is limits.http.max_body_bytes (4 MiB). The default body limit of nginx is 1 MiB. Set client_max_body_size 4m.
  • The proxy must forward the Authorization, DPoP, MCP-Protocol-Version, Mcp-Method, Mcp-Name and Mcp-Param-* headers without a change. Both proxies forward them by default.
  • With the auth option, the SDK serves the protected resource metadata at /.well-known/oauth-protected-resource/mcp2. The proxy must forward this path too. The resource and resource_metadata_url fields of ResourceServer must use the public URL, not the loopback URL. With ResourceServer.dpop, the htu claim of each DPoP proof3 must be equal to resource.
  • The SDK does not read X-Forwarded-For or Forwarded. Log the client address in the proxy.
  • The rate limits use the IP address of the caller when the request has no principal. Behind a proxy, all these callers have the address of the proxy and share one bucket. See Limits and Security.
  • The gRPC transport needs a proxy that forwards HTTP/2 from end to end, such as nginx with grpc_pass. See Transport gRPC.
  • The WebSocket transport needs a proxy that forwards the upgrade request. With nginx, set proxy_http_version 1.1 and forward the Upgrade and Connection headers. See Transport WebSocket.

An nginx example

server {
    listen 443 ssl;
    http2 on;
    server_name mcp.example.com;
    ssl_certificate     /etc/ssl/mcp.example.com.crt;
    ssl_certificate_key /etc/ssl/mcp.example.com.key;
    ssl_protocols TLSv1.3;

    location = /mcp {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        client_max_body_size 4m;
    }

    location = /.well-known/oauth-protected-resource/mcp {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
    }
}

The example sends the public name in Host. Thus add mcp.example.com to allowed_hosts, as in the Zig example above.

A Caddy example

mcp.example.com {
    reverse_proxy /mcp 127.0.0.1:3000
    reverse_proxy /.well-known/oauth-protected-resource/mcp 127.0.0.1:3000
}

Caddy gets a certificate for the name and terminates TLS. Its default read timeout has no limit. With Caddy, add mcp.example.com to allowed_hosts.

References

  1. MCP-HTTP-2026-07-28: Streamable HTTP transport, with the security warning about the Origin header.
  2. RFC9728: OAuth 2.0 Protected Resource Metadata.
  3. RFC9449: OAuth 2.0 DPoP.

Clone this wiki locally