woggioni 63c5497a83 build: let uv track git version changes; copy full checkout in Docker
Add uv cache-keys for the commit and tags so the editable install is
refreshed when the git state changes, and copy the complete checkout
(with .git) into the builder so setuptools-scm sees a clean worktree.
Previously the partial copy made git describe report a dirty tree and
tagged Docker builds got a development version suffix.
2026-10-22 04:05:31 +00:00

xmcp

Last commit Stars Forks

Python 3.14 FastMCP 4 Alpine 3.24 Docker uv

Turns one or more OpenAPI documents (JSON or YAML) into a ready-to-run MCP server using FastMCP. 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

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)

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:

docker run -i --rm \
  -v "$PWD/xmcp.yml:/etc/xmcp/xmcp.yml:ro" \
  xmcp

Local development with uv

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

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:

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>):

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:

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: 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        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
S
Description
MCP server generated from an OpenAPI specification with FastMCP; uv + Docker
Readme
340 KiB
Languages
Python 90.4%
Dockerfile 9.6%