commit 2e1957281b057f952792f511306e27a507f11756 Author: Aneurin Barker Snook Date: Sat Sep 26 15:08:51 2026 +0100 Add Docker image bundling mcpo + webfetch-mcp as HTTP/OpenAPI service Vendors webfetch-mcp (pinned commit, from the code.aneur.in mirror) and installs a pinned mcpo + mcp SDK version on a Python+Node base, wired together via a runtime entrypoint script so MCPO_API_KEY can be supplied at container start rather than baked into the image. Adds a Gitea Actions workflow to build and push on push to main. Closes #1 Co-Authored-By: Claude Sonnet 5 diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..df70734 --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,33 @@ +name: Build and push image + +on: + push: + branches: + - main + +env: + IMAGE: code.aneur.in/cloud/webfetch-mcp-docker + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to code.aneur.in registry + uses: docker/login-action@v3 + with: + registry: code.aneur.in + username: ${{ secrets.PACKAGE_REGISTRY_USER }} + password: ${{ secrets.PACKAGE_REGISTRY_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ env.IMAGE }}:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48f79c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# syntax=docker/dockerfile:1 + +# Pinned revision of https://code.aneur.in/mirrors/webfetch-mcp (mirror of +# https://github.com/manooll/webfetch-mcp). Bump by updating this SHA once +# the mirror has synced past it; verify server.mjs still only reads +# SEARXNG_BASE / DEBUG / DETAILED_LOG from its environment before bumping. +ARG WEBFETCH_MCP_COMMIT=28135553531780c2a35c65e7c972f114f5e1baa4 + +# Pinned mcpo release (PyPI). Bump by checking the new version's CLI flags +# still match what entrypoint.sh assumes (--host/--port/--api-key/--). +ARG MCPO_VERSION=0.0.20 + +# mcpo's own pyproject.toml only requires "mcp>=1.17.0" (unbounded), but its +# lockfile shows it's built/tested against exactly this version. Pinning it +# ourselves avoids pip resolving a newer mcp SDK release whose API mcpo +# doesn't yet support (observed: mcp 2.2.0 renamed an import mcpo 0.0.20 +# depends on, breaking startup). When bumping MCPO_VERSION, check the new +# release's uv.lock for its actual tested "mcp" version and update this too. +ARG MCP_SDK_VERSION=1.17.0 + +FROM python:3.12-slim-bookworm AS vendor +ARG WEBFETCH_MCP_COMMIT +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* +RUN git clone https://code.aneur.in/mirrors/webfetch-mcp.git /src \ + && cd /src \ + && git checkout "${WEBFETCH_MCP_COMMIT}" +RUN mkdir /vendored \ + && cp /src/server.mjs /src/package.json /src/package-lock.json /src/LICENSE /vendored/ + +FROM python:3.12-slim-bookworm +ARG MCPO_VERSION +ARG MCP_SDK_VERSION + +# Node.js 22, matching the version mcpo's own official image is built and +# tested against (webfetch-mcp only requires >=18). +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + gnupg \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir "mcpo==${MCPO_VERSION}" "mcp==${MCP_SDK_VERSION}" + +COPY --from=vendor /vendored /app/webfetch-mcp +WORKDIR /app/webfetch-mcp +RUN npm ci --omit=dev + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 8000 +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..40d2704 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# webfetch-mcp-docker + +A single Docker image exposing [`webfetch-mcp`](https://code.aneur.in/mirrors/webfetch-mcp)'s +`web_search` and `web_fetch` tools over HTTP/OpenAPI, via [`mcpo`](https://github.com/open-webui/mcpo). + +`webfetch-mcp` is a stdio-only MCP server; `mcpo` exposes a wrapped MCP server over +HTTP by spawning it as a local child process. Since that spawn happens in-process +(not over the network) and `webfetch-mcp` has no HTTP/SSE mode of its own, both have +to live in one image — that's what this repo builds. + +## Build + +```sh +docker build -t webfetch-mcp-docker . +``` + +## Run + +```sh +docker run -p 8000:8000 \ + -e MCPO_API_KEY=some-secret \ + -e SEARXNG_BASE=http://searxng.example.internal:8080 \ + webfetch-mcp-docker +``` + +Interactive API docs: `http://localhost:8000/docs` (and `/openapi.json`). These are +served without authentication even when `MCPO_API_KEY` is set — only the generated +tool endpoints (`/web_search`, `/web_fetch`) require it, as +`Authorization: Bearer `. + +## Environment variables + +| Variable | Effect | +|---|---| +| `MCPO_API_KEY` | If set, tool endpoints require `Authorization: Bearer `. If unset, `mcpo` serves unauthenticated (its own default behavior). | +| `MCPO_PORT` | Port `mcpo` listens on inside the container. Default `8000`. | +| `SEARXNG_BASE` | Passed through to `webfetch-mcp`; its own default (`http://localhost:8080`) applies if unset. | +| `DEBUG` | Passed through to `webfetch-mcp`. `"true"` enables debug logging. | +| `DETAILED_LOG` | Passed through to `webfetch-mcp`. Defaults on; set to `"false"` to disable its detailed log file. | + +No explicit plumbing is needed to get these into `webfetch-mcp`: `mcpo` builds the +spawned child process's environment as the union of its own process environment and +any per-server overrides, so anything set on the container's environment is +inherited by the `node server.mjs` child automatically. (Verified against `mcpo`'s +`MCPConnectionManager` source, not assumed.) + +## Known characteristics + +- **No URL filtering / SSRF protection.** `webfetch-mcp`'s `web_fetch` tool fetches + whatever URL it's given, including private/internal addresses. This is a known, + accepted characteristic of `webfetch-mcp` itself — out of scope for this image to + fix. +- **No TLS termination.** This image serves plain HTTP; put a reverse proxy or + firewall in front of it if you need TLS or network restriction. +- `webfetch-mcp` writes a detailed log file next to `server.mjs` inside the image + by default (`DETAILED_LOG` defaults on). That directory needs to stay writable at + runtime — don't run this image with a read-only root filesystem without accounting + for that (e.g. a volume mount, or set `DETAILED_LOG=false`). + +## Pinned revisions + +- `webfetch-mcp`: pinned via `ARG WEBFETCH_MCP_COMMIT` in the `Dockerfile`, vendored + from the Gitea mirror at `https://code.aneur.in/mirrors/webfetch-mcp`, not upstream + GitHub. +- `mcpo`: pinned via `ARG MCPO_VERSION` in the `Dockerfile`, installed from PyPI. + +Bump either by updating the corresponding `ARG` default in the `Dockerfile`. When +bumping `mcpo`, double check its CLI flags still match what `entrypoint.sh` assumes +(`--host`, `--port`, `--api-key`, `--`). + +## CI + +`.gitea/workflows/docker-build.yml` builds and pushes +`code.aneur.in/cloud/webfetch-mcp-docker:latest` on every push to `main`. It +authenticates using the repo secrets `PACKAGE_REGISTRY_USER` and +`PACKAGE_REGISTRY_TOKEN` — the default Gitea Actions token doesn't have +package-registry write permission on this instance, so these must be created +manually under repo Settings → Actions → Secrets, backed by a personal access token +with package-write scope. diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..f12c44e --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +# mcpo's --api-key is a CLI flag, not something it reads from its own +# environment, but the actual value must come from the container's +# environment rather than being baked into the image. This script builds +# the mcpo command line at container start so that can happen. +# +# webfetch-mcp reads SEARXNG_BASE / DEBUG / DETAILED_LOG from its own +# process environment. No passthrough is needed here: mcpo builds the +# spawned child's environment as {**os.environ, **extra}, so anything set +# on this container's environment reaches webfetch-mcp automatically. + +PORT="${MCPO_PORT:-8000}" + +if [ -n "${MCPO_API_KEY:-}" ]; then + exec mcpo --host 0.0.0.0 --port "$PORT" --api-key "$MCPO_API_KEY" -- node /app/webfetch-mcp/server.mjs +else + exec mcpo --host 0.0.0.0 --port "$PORT" -- node /app/webfetch-mcp/server.mjs +fi