Both Docker stages now use python:3.14-alpine3.24. Since there is no combined uv image tag for python3.14-alpine3.24, the static uv binary is copied from ghcr.io/astral-sh/uv:0.13.1 into the builder stage. Requires Python 3.14, refreshes uv.lock, and updates the README badges.
8.2 KiB
xmcp
Turns any OpenAPI document (JSON or YAML) into a ready-to-run MCP server using FastMCP. 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)
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)
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:
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:
{
"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
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>):
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:
# 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
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:
API_BASE_URL, if set.- The first entry of the spec's
serverslist. - 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. - 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-Keyheader 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:
git tag release/0.1.0
git push origin release/0.1.0
This publishes gitea.woggioni.net/woggioni/xmcp:0.1.0 and
gitea.woggioni.net/woggioni/xmcp:latest.
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)