woggioni 3ec12439b4 feat: add optional tools allowlist via MCP_TOOLS_INCLUDED
When MCP_TOOLS_INCLUDED is set to a comma-separated list of tool names,
only those tools are exposed (tools/list and calls); unset or empty
exposes every generated tool. Unknown names are ignored with a startup
warning and the active selection is logged at startup.
2026-10-19 07:24:41 +00:00
2026-10-19 04:26:25 +00:00

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
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. With LOG_LEVEL=DEBUG, the list of generated MCP tools (name and description) is logged at startup.

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 allowlist

By default every operation in the OpenAPI document is exposed as an MCP tool. Set MCP_TOOLS_INCLUDED to a comma-separated list of tool names to expose only those:

MCP_TOOLS_INCLUDED=getPetById,findPetsByStatus

Unset or empty exposes all tools. Names that do not match a generated tool are ignored with a startup warning, and the active selection is reported at startup (Tools allowlist active: 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:

  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:

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