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.
211 lines
8.5 KiB
Markdown
211 lines
8.5 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 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). One MCP tool is generated per
|
||
API operation, and requests to the target API are authenticated with a token
|
||
supplied via environment variables.
|
||
|
||
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 .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 (HTTP)
|
||
|
||
```sh
|
||
docker build -t xmcp .
|
||
docker run --rm -p 8000:8000 \
|
||
-e OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json \
|
||
xmcp
|
||
```
|
||
|
||
### 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 \
|
||
-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
|
||
OPENAPI_SPEC_URL=https://petstore3.swagger.io/api/v3/openapi.json uv run xmcp
|
||
# equivalent: uv run python -m xmcp
|
||
```
|
||
|
||
## Configuration
|
||
|
||
| 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 |
|
||
|
||
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.
|
||
|
||
### 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`).
|
||
|
||
### 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 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 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
|
||
|
||
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`.
|
||
|
||
The Python package version is derived from the pushed `release/*` tag with
|
||
[setuptools-scm](https://setuptools-scm.readthedocs.io): `release/0.2.0`
|
||
produces package version `0.2.0`. Builds from untagged commits carry a PEP 440
|
||
development version (for example `0.2.1.dev3+gabc1234`).
|
||
|
||
## Project layout
|
||
|
||
```
|
||
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)
|
||
```
|