revert: remove file-based configuration and multi-spec support
CI / Build and push docker image (push) Successful in 52s

This reverts commit 9185b5fa05 while
preserving the build work done afterwards: setuptools-scm versioning with
uv cache keys, the git-enabled Docker builder (full checkout, credentials
excluded), and full-history/tag checkout in CI.

Configuration returns to environment variables with a single OpenAPI
document; .env.example and the env-based Docker/Compose setup are
restored.
This commit is contained in:
woggioni committed 2026-10-22 04:54:47 +00:00
1 parent 0063e7c309
commit 827549c0ec
10 files changed
+326 -743

No files matched your search

+118 -147
View File
@@ -10,148 +10,104 @@
[![Docker](https://img.shields.io/badge/Docker-image-2496ED?logo=docker&logoColor=white&style=flat-square)](https://www.docker.com/)
[![uv](https://img.shields.io/badge/uv-managed-DE5FE9?style=flat-square)](https://docs.astral.sh/uv/)
Turns one or more [OpenAPI](https://spec.openapis.org/oas/latest.html) documents
Turns any [OpenAPI](https://spec.openapis.org/oas/latest.html) document
(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.
server using [FastMCP](https://gofastmcp.com). One MCP tool is generated per
API operation, and requests to the target API are authenticated with a token
supplied via environment variables.
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`).
Everything is configured through environment variables: the spec location, the
transport (stdio or streamable HTTP), the listen host/port, and the API token.
## Quick start
### Docker Compose (HTTP)
```sh
cp xmcp.example.yml xmcp.yml # then edit specs[].spec_url and credentials
cp .env.example .env # then edit OPENAPI_SPEC_URL / API_TOKEN etc.
docker compose up --build
```
The MCP endpoint is served at `http://localhost:8000/mcp`.
### Docker (without Compose)
### Docker (HTTP)
```sh
docker build -t xmcp .
docker run --rm -p 8000:8000 \
-v "$PWD/xmcp.yml:/etc/xmcp/xmcp.yml:ro" \
-e OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json \
xmcp
```
For stdio, mount the config and run the container with an open stdin:
### Docker (stdio)
For stdio the container must run with an open stdin (`-i`); logs go to stderr,
so stdout stays protocol-clean:
```sh
docker run -i --rm \
-v "$PWD/xmcp.yml:/etc/xmcp/xmcp.yml:ro" \
-e MCP_TRANSPORT=stdio \
-e OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json \
xmcp
```
Example MCP client configuration for the stdio container:
```json
{
"mcpServers": {
"petstore": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT=stdio",
"-e", "OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json",
"xmcp:latest"
]
}
}
}
```
### Local development with uv
```sh
uv sync
uv run xmcp --config xmcp.yml
OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json uv run xmcp
# equivalent: uv run python -m xmcp
```
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
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `OPENAPI_SPEC_URL` | yes | – | `http(s)://` URL or local file path of the OpenAPI document (JSON or YAML) |
| `OPENAPI_SPEC_TOKEN` | no | – | Bearer token used only when fetching a remote spec document |
| `API_BASE_URL` | no | spec `servers[0].url` | Overrides the target API base URL |
| `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 |
| `MCP_SERVER_NAME` | no | spec `info.title` | Name advertised by the MCP server |
| `MCP_TOOLS_INCLUDED` | no | – (all tools) | Comma-separated allowlist of tool names to expose |
| `MCP_TOOLS_TAGS_INCLUDED` | no | – (all tags) | Comma-separated allowlist of OpenAPI tags to expose |
| `HTTP_TIMEOUT` | no | `30` | Timeout in seconds for spec fetch and API calls |
| `LOG_LEVEL` | no | `INFO` | Python log level |
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
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. At startup the generated tool list (name, tags and description) is
logged at INFO; set `LOG_LEVEL=WARNING` to suppress it.
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)
### 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>`):
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 \
@@ -161,51 +117,68 @@ curl -X POST http://localhost:8000/mcp \
-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.
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`).
### Tool allowlists
By default every operation in the OpenAPI document is exposed as an MCP tool.
Two environment variables can restrict the exposed set; each accepts a
comma-separated list, and unset or empty disables that filter:
```sh
# Only these operations
MCP_TOOLS_INCLUDED=getPetById,findPetsByStatus
# Only operations tagged "pet" in the OpenAPI document
MCP_TOOLS_TAGS_INCLUDED=pet
```
When both are set, a tool must match both filters (name **and** tag). Entries
that do not match anything are ignored with a startup warning, and the active
selection is reported at startup
(`Tool filters active (names: ...; tags: ...): exposing N of M tool(s): ...`).
### Serving a local spec file
```sh
docker run -i --rm \
-v "$PWD/openapi.json:/spec/openapi.json:ro" \
-e OPENAPI_SPEC_URL=/spec/openapi.json \
xmcp
```
If the document has no absolute `servers[0].url` (for example it only contains
a relative path such as `/v1`), set `API_BASE_URL` to the API root explicitly.
### Base URL resolution
The base URL for generated tools is chosen as follows:
1. `API_BASE_URL`, if set.
2. The first entry of the spec's `servers` list.
3. If that server URL is relative (e.g. `/api/v3`) and the spec was fetched
over HTTP(S), it is resolved against the spec's origin.
4. Otherwise startup fails with a message asking for `API_BASE_URL`.
## 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 API token is attached to every request the generated tools make to the
target API. It is never logged.
- 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` |
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
@@ -228,12 +201,10 @@ development version (for example `0.2.1.dev3+gabc1234`).
## Project layout
```
src/xmcp/config.py configuration loading and validation
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 server construction, mounting, lifecycle
src/xmcp/healthcheck.py container healthcheck
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)
xmcp.example.yml annotated configuration example
```