crates-docs 1.0.0

High-performance Rust crate documentation query MCP server, supports Stdio/HTTP/SSE transport and OAuth authentication
Documentation
# Reverse proxy: TLS + `X-API-Key` for crates-docs

The server's **in-process** authentication validates an API key sent as a
bearer token — `Authorization: Bearer <key>` — and it speaks **plain HTTP**
(no TLS). That is enough on a trusted/loopback network, but for anything exposed
you usually want two more things:

1. **TLS** so traffic can't be read or tampered with on the wire.
2. The ability to keep sending the key in the familiar **`X-API-Key`** header
   (many clients and the project's own config default to that name).

A small reverse proxy in front of the server provides both. It terminates TLS
and rewrites `X-API-Key: <key>` into `Authorization: Bearer <key>` so the
server's built-in check still runs.

```
                      TLS (HTTPS)                      plain HTTP, localhost
   ┌────────┐   X-API-Key: <key>     ┌───────────┐   Authorization: Bearer <key>   ┌──────────────┐
   │ client │ ─────────────────────► │  proxy    │ ──────────────────────────────► │  crates-docs │
   │        │ ◄───────────────────── │ (TLS term)│ ◄────────────────────────────── │ 127.0.0.1:8080│
   └────────┘                        └───────────┘                                 └──────────────┘
                                   edge key check (opt.)        in-process Argon2 verify (source of truth)
```

**Defense in depth.** The proxy may reject requests with no key at the edge
(fail fast), but the server remains the cryptographic source of truth: it
re-verifies every key against its stored Argon2 hash in constant time. If the
proxy is ever misconfigured or bypassed, the server still rejects bad keys.

> **Why not just send `X-API-Key` to the server directly?** The MCP SDK's auth
> middleware reads **only** the `Authorization: Bearer` header — it cannot be
> told to read `X-API-Key`. The proxy bridges that gap. (See the note in
> `src/server/auth/api_key_provider.rs`.)

---

## 1. Configure the backend (crates-docs)

Bind to **loopback only** so the server is reachable solely through the proxy,
and enable API-key auth.

Generate a key (prints the plain-text key once and its Argon2 hash):

```bash
crates-docs generate-api-key
```

Store the **hash** in your config and keep the **plain-text key** for clients:

```toml
# config.toml
[server]
host = "127.0.0.1"      # loopback: only the local proxy can reach it
port = 8080
transport_mode = "http" # or "sse" / "hybrid"

[auth.api_key]
enabled = true                       # runtime on/off switch (restart to apply)
keys = ["$argon2id$v=19$m=...$..."]  # the HASH from generate-api-key
```

Run it:

```bash
crates-docs serve --config config.toml
```

You should see a startup log line confirming **`API key authentication is
ENFORCED`**. (Auth is compiled in by default; flipping `enabled` and restarting
turns enforcement on/off — no rebuild needed.)

Quick local check that enforcement is on (before adding the proxy):

```bash
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health                 # 200 (open)
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8080/mcp             # 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
     -H "Authorization: Bearer <plain-key>" http://127.0.0.1:8080/mcp                  # not 401
```

## 2. Put a proxy in front

Pick one:

- **[`Caddyfile`]./Caddyfile** — automatic HTTPS (Let's Encrypt for a real
  domain, internal CA for `localhost`). Simplest to run.
- **[`nginx.conf`]./nginx.conf** — bring your own certificate.

Both: terminate TLS on `:443`, require `X-API-Key` on everything except
`/health`, translate it to `Authorization: Bearer`, and forward to
`127.0.0.1:8080`. Endpoints forwarded: `/mcp` (Streamable HTTP), `/sse` +
`/messages` (SSE), `/health` (open).

## 3. Verify end to end

```bash
# Missing key → rejected at the edge.
curl -sk -o /dev/null -w '%{http_code}\n' -X POST https://docs.example.com/mcp          # 401

# Valid key in X-API-Key → translated to Bearer, accepted by the backend.
curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
     -H "X-API-Key: <plain-key>" \
     -H "Accept: application/json, text/event-stream" \
     https://docs.example.com/mcp                                                       # not 401

# Health stays open for monitoring.
curl -sk -o /dev/null -w '%{http_code}\n' https://docs.example.com/health               # 200
```

## Notes

- **Rate-limit at the edge for public exposure.** Each key check runs an
  intentionally CPU/memory-hard Argon2 verification, and the server does not cap
  request rate itself. Without an edge limit, an attacker can force expensive
  work with a flood of bad keys. Add a rate limit at the proxy (nginx
  `limit_req`; Caddy's `rate_limit` handler or a WAF) when the server faces
  untrusted networks — the edge `X-API-Key` check above already rejects
  keyless requests cheaply before they reach the backend.
- **Key rotation / revocation** is not hot-reloaded: update `keys` and
  **restart** the server (a deliberate safeguard so removed keys stop working
  immediately and predictably).
- **MCP `initialize`** clients should send `Accept: application/json,
  text/event-stream`.
- **SSE** needs response buffering disabled and long read timeouts; both proxy
  configs already set this.
- Keep `host = "127.0.0.1"`. Binding the backend to `0.0.0.0` while running a
  proxy would let clients reach it directly over plain HTTP and skip TLS.