crates-docs 1.0.0

High-performance Rust crate documentation query MCP server, supports Stdio/HTTP/SSE transport and OAuth authentication
Documentation
# Caddyfile for crates-docs: TLS termination + X-API-Key -> Bearer translation.
#
# What it does:
#   - Terminates HTTPS (automatic certificates).
#   - Rejects requests that carry no `X-API-Key` (except /health) at the edge.
#   - Rewrites `X-API-Key: <k>` into `Authorization: Bearer <k>` so the
#     crates-docs in-process auth layer enforces the key cryptographically.
#   - Forwards to the loopback-only backend on 127.0.0.1:8080.
#
# Run with:   caddy run --config ./Caddyfile
#
# Replace `docs.example.com` with your domain for a public Let's Encrypt cert.
# For local testing use `localhost` (Caddy issues an internal-CA cert; pass
# `curl -k` or trust the Caddy root CA).

docs.example.com {
	# /health stays open so monitoring needs no key. Forwarded as-is; the
	# backend health route bypasses the auth middleware too.
	handle /health {
		reverse_proxy 127.0.0.1:8080
	}

	# Reject requests that carry no X-API-Key before they ever reach the
	# backend (fail fast at the edge). Note: this matcher fires only when the
	# header is *absent*; a present-but-empty `X-API-Key:` falls through and is
	# rejected by the backend's token check (which refuses empty keys). Either
	# way the request is denied — the backend re-verifies every key, so this
	# edge check is defense in depth, not the only gate.
	@no_api_key {
		not header X-Api-Key *
	}
	handle @no_api_key {
		respond "Missing X-API-Key header" 401
	}

	# Everything else (/mcp, /sse, /messages): translate the header and proxy.
	handle {
		reverse_proxy 127.0.0.1:8080 {
			# X-API-Key -> Authorization: Bearer for the in-process check.
			header_up Authorization "Bearer {http.request.header.X-Api-Key}"
			# Don't forward the raw key header upstream once translated.
			header_up -X-Api-Key

			# SSE: Caddy streams responses and does not buffer by default, so
			# `/sse` works without extra flush settings. Allow long-lived
			# streams instead of cutting them at the default timeout.
			transport http {
				read_timeout 3600s
			}
		}
	}
}