feat: accept per-client API keys from an HTTP header

When serving over HTTP, the key sent in the MCP_API_KEY_HEADER header
(default X-API-Key) is used as the API credential for that request and
takes precedence over the static API_TOKEN. The key is formatted with
API_AUTH_HEADER/API_AUTH_SCHEME and the source header is stripped from
the outgoing API request. Implemented as an httpx2 auth flow over
FastMCP's request context, so concurrent clients stay isolated.
This commit is contained in:
2026-10-19 06:12:58 +00:00
parent 8ee7b07b4b
commit 363a1bee69
6 changed files with 115 additions and 4 deletions
+31
View File
@@ -77,6 +77,7 @@ OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json uv run xmcp
| `API_TOKEN` | no | – | Credential sent to the target API on every tool call |
| `API_AUTH_HEADER` | no | `Authorization` | Header used to carry the token (e.g. `X-API-Key`) |
| `API_AUTH_SCHEME` | no | `Bearer` | Scheme prefix; set to an empty value to send the raw token |
| `MCP_API_KEY_HEADER` | no | `X-API-Key` | HTTP header treated as the per-request API key (empty disables the mapping) |
| `MCP_TRANSPORT` | no | `stdio` (`http` in the image) | `stdio` or `http` (`streamable-http` is an alias) |
| `MCP_HOST` | no | `0.0.0.0` | Listen host for the HTTP transport |
| `MCP_PORT` | no | `8000` | Listen port for the HTTP transport |
@@ -88,6 +89,32 @@ Authentication examples: for a bearer token leave the defaults; for an API key
header use `API_AUTH_HEADER=X-API-Key` with `API_AUTH_SCHEME` set to an empty
string.
### Per-client API keys (HTTP transport)
When serving over HTTP, clients can send their own API key on each request
instead of sharing the server's `API_TOKEN`. The value is treated as a raw
token and forwarded to the target API using `API_AUTH_HEADER` /
`API_AUTH_SCHEME` (by default `Authorization: Bearer <key>`):
```sh
curl -X POST http://localhost:8000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'X-API-Key: sk-client-secret' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",...}'
```
Precedence: the request header wins, then `API_TOKEN`, then no authentication.
The header name is configurable with `MCP_API_KEY_HEADER`; set it to an empty
value to disable the mapping. When the key is mapped to a differently named
target header, the source header is removed from the outgoing API request so
the credential is not sent twice.
Note: FastMCP independently passes other incoming HTTP headers through to the
API request, except credential headers such as `Authorization` and `Cookie`.
MCP clients that support custom transport headers should send the key on every
request (at least on `tools/call`).
### Serving a local spec file
```sh
@@ -117,6 +144,9 @@ The base URL for generated tools is chosen as follows:
- The MCP HTTP endpoint (`/mcp`) itself is **not** authenticated. Run it on a
private network or behind a reverse proxy / gateway that enforces
authentication before exposing it publicly.
- Per-request API keys (the `X-API-Key` header by default) are forwarded only to
the target API and are never logged. Terminate TLS in front of the MCP
endpoint so client keys are not sent in clear text.
## Releasing
@@ -136,6 +166,7 @@ This publishes `gitea.woggioni.net/woggioni/xmcp:0.1.0` and
```
src/xmcp/config.py env var parsing and validation
src/xmcp/spec.py spec loading, base URL resolution, API client
src/xmcp/auth.py per-request API key forwarding (HTTP transport)
src/xmcp/server.py FastMCP server construction and entrypoint
Dockerfile multi-stage build (uv builder -> alpine runtime, non-root)
.gitea/workflows/build.yaml release pipeline (tag push -> docker build & push)