The Docs MCP Server can run behind a reverse proxy, at the root of its own host (https://docs.example.com) or under a path of a shared host (https://example.com/docs).
Set server.publicUrl to the URL clients use:
npx @arabold/docs-mcp-server@latest --host 127.0.0.1 --public-url https://example.com/docsEvery URL the server advertises derives from it: the MCP endpoint in startup output and the web UI, the authentication metadata, and the URLs the web UI calls. Without it, the server can only describe its bind address, which clients behind a proxy can't reach. On a loopback bind it also refuses API requests from the public host's pages, because it can't know that host is its own.
The server answers every route both with the path prefix and without it. So it works whether the proxy strips /docs before forwarding (the common nginx and Traefik setup) or passes it through. The path's first segment must not be one of the server's own routes (mcp, api, assets, .well-known, sse, messages, oauth) or a root file such as favicon.ico.
Open the web UI under the path, e.g. https://example.com/docs/. The page carries a <base href> for the path, so the UI's requests stay under it.
The server reads nothing from X-Forwarded-* headers. Every URL it advertises comes from server.publicUrl.
- Streaming responses, unbuffered. MCP responses may be event streams. The server marks them
X-Accel-Buffering: no, and turning buffering off for the location makes sure. - WebSocket upgrades on
<path>/api, for the web UI's live updates. - The original
Hostheader, or an upstream address the server answers to. The server refuses host names it doesn't know (see Host Names the Server Answers To). nginx withproxy_set_header Host $hostand Traefik's default both send the public host. - The host-root metadata location, when authentication is enabled and the server lives under a path. RFC 9728 places the metadata at
/.well-known/oauth-protected-resource/docs/mcpon the host root, outside/docs/. MCP clients use the URL from the server's401challenge first, which lives under/docs/. The extra rule is for clients that skip it.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name example.com;
# ssl_certificate ...;
location = /docs {
return 301 /docs/;
}
# Everything under /docs/: prefix stripped by the trailing slash on proxy_pass
location /docs/ {
proxy_pass http://127.0.0.1:6280/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 1h;
}
# RFC 9728 metadata at the host root (only needed with authentication)
location = /.well-known/oauth-protected-resource/docs/mcp {
proxy_pass http://127.0.0.1:6280;
}
}Keep the trailing slash on proxy_pass http://127.0.0.1:6280/;: it strips /docs. Without it nginx forwards the prefix, which also works, since the server accepts both.
File provider (dynamic configuration):
http:
routers:
docs:
rule: "Host(`example.com`) && (Path(`/docs`) || PathPrefix(`/docs/`))"
middlewares: [docs-strip]
service: docs
docs-metadata: # only needed with authentication
rule: "Host(`example.com`) && Path(`/.well-known/oauth-protected-resource/docs/mcp`)"
service: docs
middlewares:
docs-strip:
stripPrefix:
prefixes: ["/docs"]
services:
docs:
loadBalancer:
servers:
- url: "http://docs-mcp:6280"Traefik streams responses and upgrades WebSockets without extra settings. To pass the prefix through instead of stripping it, drop the docs-strip middleware.
With Docker labels, the same routers and middleware become traefik.http.routers.docs.rule, traefik.http.middlewares.docs-strip.stripprefix.prefixes=/docs, and so on. The container runs with --host 0.0.0.0 --public-url https://example.com/docs.
The server's own authentication covers the MCP endpoint only; see Authentication. Put the web UI and /api behind your proxy's sign-in, for example OAuth2-Proxy, Authelia, Traefik forwardAuth, nginx auth_request, or Cloudflare Access.
Exclude the MCP endpoint and its metadata from that sign-in (/docs/mcp, /docs/.well-known/, and the host-root metadata location). MCP clients authenticate there with bearer tokens and can't complete a browser sign-in. The server accepts WebSocket upgrades only at /docs/api and at the web UI root /docs/, where coordinators of earlier releases connect. Keep both behind the sign-in, so it also covers the live-update socket: protecting /docs/api alone leaves the socket open at /docs/.
The MCP endpoint accepts browser requests from loopback origins and from the public URL's origin (see Inbound Requests). A hosted browser-based client, such as a web app calling the server directly, must be listed in server.allowedOrigins:
DOCS_MCP_SERVER_ALLOWED_ORIGINS='["https://inspector.example.com"]'The server answers CORS preflights for allowed origins itself, so the proxy needs no CORS configuration.