Configuration moves from environment variables to a YAML/JSON file,
located via --config, XMCP_CONFIG, or /etc/xmcp/xmcp.yml. String values
support ${VAR} interpolation for secrets.
One or more OpenAPI documents can be configured as specs[]; multiple
documents are mounted under unique namespaces and served on a single
transport, on one HTTP port. Tool filters, per-request API keys and
per-spec base URL/auth overrides live in the same file.
The Docker image no longer sets MCP_* defaults and expects the config
mounted at /etc/xmcp/xmcp.yml; the healthcheck reads the config.
235 lines
9.2 KiB
Markdown
235 lines
9.2 KiB
Markdown
# xmcp
|
|
|
|
[](https://gitea.woggioni.net/woggioni/xmcp/commits/branch/main)
|
|
[](https://gitea.woggioni.net/woggioni/xmcp)
|
|
[](https://gitea.woggioni.net/woggioni/xmcp)
|
|
|
|
[](https://www.python.org/)
|
|
[](https://gofastmcp.com)
|
|
[](https://alpinelinux.org/)
|
|
[](https://www.docker.com/)
|
|
[](https://docs.astral.sh/uv/)
|
|
|
|
Turns one or more [OpenAPI](https://spec.openapis.org/oas/latest.html) documents
|
|
(JSON or YAML) into a ready-to-run [MCP](https://modelcontextprotocol.io)
|
|
server using [FastMCP](https://gofastmcp.com). Each API operation becomes an MCP
|
|
tool; when several documents are configured they are mounted under a shared
|
|
server and served on a single transport.
|
|
|
|
All configuration lives in a YAML file (JSON is also accepted). Only the
|
|
optional path to that file can come from the environment (`XMCP_CONFIG`).
|
|
|
|
## Quick start
|
|
|
|
```sh
|
|
cp xmcp.example.yml xmcp.yml # then edit specs[].spec_url and credentials
|
|
docker compose up --build
|
|
```
|
|
|
|
The MCP endpoint is served at `http://localhost:8000/mcp`.
|
|
|
|
### Docker (without Compose)
|
|
|
|
```sh
|
|
docker build -t xmcp .
|
|
docker run --rm -p 8000:8000 \
|
|
-v "$PWD/xmcp.yml:/etc/xmcp/xmcp.yml:ro" \
|
|
xmcp
|
|
```
|
|
|
|
For stdio, mount the config and run the container with an open stdin:
|
|
|
|
```sh
|
|
docker run -i --rm \
|
|
-v "$PWD/xmcp.yml:/etc/xmcp/xmcp.yml:ro" \
|
|
xmcp
|
|
```
|
|
|
|
### Local development with uv
|
|
|
|
```sh
|
|
uv sync
|
|
uv run xmcp --config xmcp.yml
|
|
```
|
|
|
|
The configuration file is located, in order, via:
|
|
|
|
1. `--config PATH`
|
|
2. the `XMCP_CONFIG` environment variable
|
|
3. the default `/etc/xmcp/xmcp.yml`
|
|
|
|
## Configuration
|
|
|
|
```yaml
|
|
server:
|
|
name: xmcp # optional; single-spec default is the spec title
|
|
transport: http # stdio | http
|
|
host: 0.0.0.0
|
|
port: 8000
|
|
log_level: INFO
|
|
http_timeout: 30
|
|
client_api_key_header: X-API-Key # "" disables per-request key forwarding
|
|
|
|
api: # defaults applied to every OpenAPI document
|
|
# base_url: https://api.example.com
|
|
# token: ${API_TOKEN}
|
|
auth_header: Authorization
|
|
auth_scheme: Bearer # "" sends the raw token
|
|
|
|
tools: # optional global filters
|
|
include: [getPetById] # namespaced names in multi-spec mode
|
|
tags_include: [pet] # OpenAPI operation tags
|
|
|
|
specs:
|
|
- spec_url: https://petstore3.swagger.io/api/v3/openapi.json
|
|
api:
|
|
base_url: https://petstore3.swagger.io/api/v3
|
|
```
|
|
|
|
| Key | Description |
|
|
| --- | --- |
|
|
| `server.name` | Name advertised by the parent server (multi-spec default: `xmcp`) |
|
|
| `server.transport` | `stdio` or `http` (default `stdio`) |
|
|
| `server.host`, `server.port` | HTTP listen address (defaults `0.0.0.0`, `8000`) |
|
|
| `server.log_level` | Python log level (default `INFO`) |
|
|
| `server.http_timeout` | Timeout in seconds for spec fetch and API calls (default `30`) |
|
|
| `server.client_api_key_header` | Header carrying a per-request API key (default `X-API-Key`, empty disables) |
|
|
| `api.base_url`, `api.token` | Defaults for every spec's target API |
|
|
| `api.auth_header`, `api.auth_scheme` | Default credential header and scheme |
|
|
| `tools.include` | Allowlist of tool names (namespaced in multi-spec mode) |
|
|
| `tools.tags_include` | Allowlist of OpenAPI tags |
|
|
| `specs[].namespace` | Tool-name prefix; required when more than one spec is configured |
|
|
| `specs[].spec_url` | HTTP(S) URL or local file path of the document |
|
|
| `specs[].spec_token` | Bearer token used only to fetch the document |
|
|
| `specs[].server_name` | MCP server name for this document |
|
|
| `specs[].api.*` | Per-spec overrides of the `api` defaults |
|
|
|
|
Notes:
|
|
|
|
- `${VAR}` in any string value is replaced with the environment variable. A
|
|
missing variable is a startup error; the error names the exact key.
|
|
- Unset or empty `tools` filters expose everything. `tools.include` and
|
|
`tools.tags_include` combine with AND semantics; entries that match nothing
|
|
are reported with a startup warning.
|
|
- Unknown keys, invalid values, duplicate namespaces and empty `specs` fail
|
|
fast with a clear message.
|
|
- Relative local paths in `spec_url` resolve against the config file's
|
|
directory, which keeps mounted deployments predictable.
|
|
- The generated tool list (name, tags, description) is logged at startup at
|
|
INFO; set `server.log_level: WARNING` to suppress it.
|
|
|
|
## Multiple OpenAPI documents
|
|
|
|
Give each entry a unique namespace:
|
|
|
|
```yaml
|
|
tools:
|
|
include: [pet_getPetById, billing_listInvoices]
|
|
tags_include: [pet, billing]
|
|
|
|
specs:
|
|
- namespace: pet
|
|
spec_url: https://petstore3.swagger.io/api/v3/openapi.json
|
|
api:
|
|
base_url: https://petstore3.swagger.io/api/v3
|
|
token: ${PETSTORE_TOKEN}
|
|
- namespace: billing
|
|
spec_url: ./specs/billing.yaml
|
|
api:
|
|
base_url: https://billing.example.com
|
|
token: ${BILLING_TOKEN}
|
|
auth_header: X-API-Key
|
|
auth_scheme: ""
|
|
```
|
|
|
|
Tools are exposed as `pet_getPetById`, `billing_listInvoices`, and so on;
|
|
resources and prompts are namespaced too. A single document without a namespace
|
|
keeps its original, unprefixed tool names.
|
|
|
|
## Per-client API keys (HTTP transport)
|
|
|
|
When serving over HTTP, clients can send their own API key on each request
|
|
instead of using the configured `api.token`. The value is treated as a raw
|
|
token and forwarded using each spec's `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 `server.client_api_key_header`; an empty value disables the
|
|
mapping. When mapped to a differently named target header, the source header is
|
|
removed from the outgoing API request so the credential is not sent twice. In
|
|
multi-spec mode the same request header is used for every mounted API, each
|
|
formatting it according to its own settings.
|
|
|
|
Note: FastMCP independently passes other incoming HTTP headers through to the
|
|
API request, except credential headers such as `Authorization` and `Cookie`.
|
|
|
|
## Security notes
|
|
|
|
- The configuration file may contain API tokens. Keep it out of version
|
|
control (it is gitignored), mount it read-only, and restrict its permissions.
|
|
- `${VAR}` interpolation keeps secrets out of the file (e.g. `token: ${API_TOKEN}`).
|
|
- The MCP HTTP endpoint (`/mcp`) itself is **not** authenticated. Run it on a
|
|
private network or behind a reverse proxy / gateway that enforces
|
|
authentication, and terminate TLS there so client keys are not sent in clear
|
|
text.
|
|
- Per-request API keys are forwarded only to the target API and are never
|
|
logged.
|
|
|
|
## Migrating from environment variables
|
|
|
|
Configuration is now file-based. Legacy environment variables are ignored (a
|
|
startup warning lists any that are set); only `XMCP_CONFIG` (the config file
|
|
path) is still read.
|
|
|
|
| Previous environment variable | Configuration key |
|
|
| --- | --- |
|
|
| `OPENAPI_SPEC_URL` | `specs[].spec_url` |
|
|
| `OPENAPI_SPEC_TOKEN` | `specs[].spec_token` |
|
|
| `API_BASE_URL` | `api.base_url` / `specs[].api.base_url` |
|
|
| `API_TOKEN` | `api.token` / `specs[].api.token` |
|
|
| `API_AUTH_HEADER` | `api.auth_header` |
|
|
| `API_AUTH_SCHEME` | `api.auth_scheme` |
|
|
| `MCP_API_KEY_HEADER` | `server.client_api_key_header` |
|
|
| `MCP_TRANSPORT` | `server.transport` |
|
|
| `MCP_HOST` | `server.host` |
|
|
| `MCP_PORT` | `server.port` |
|
|
| `MCP_SERVER_NAME` | `server.name` |
|
|
| `MCP_TOOLS_INCLUDED` | `tools.include` |
|
|
| `MCP_TOOLS_TAGS_INCLUDED` | `tools.tags_include` |
|
|
| `HTTP_TIMEOUT` | `server.http_timeout` |
|
|
| `LOG_LEVEL` | `server.log_level` |
|
|
|
|
## Releasing
|
|
|
|
Pushing a `release/*` tag triggers the Gitea Actions pipeline, which builds the
|
|
Docker image and pushes it to the Gitea container registry:
|
|
|
|
```sh
|
|
git tag release/0.2.0
|
|
git push origin release/0.2.0
|
|
```
|
|
|
|
This publishes `gitea.woggioni.net/woggioni/xmcp:0.2.0` and
|
|
`gitea.woggioni.net/woggioni/xmcp:latest`.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
src/xmcp/config.py configuration loading 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 server construction, mounting, lifecycle
|
|
src/xmcp/healthcheck.py container healthcheck
|
|
Dockerfile multi-stage build (uv builder -> alpine runtime, non-root)
|
|
.gitea/workflows/build.yaml release pipeline (tag push -> docker build & push)
|
|
xmcp.example.yml annotated configuration example
|
|
```
|