CI / Build and push docker image (push) Successful in 49s
The generated tool list is now logged at INFO (default) and each line includes the tool's OpenAPI tags alongside its name. The list is only skipped when LOG_LEVEL is above INFO.
206 lines
8.2 KiB
Markdown
206 lines
8.2 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.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)
|
||
```
|