Add Docker image bundling mcpo + webfetch-mcp as HTTP/OpenAPI service
Build and push image / build (push) Failing after 42s
Build and push image / build (push) Failing after 42s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
+55
@@ -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"]
|
||||||
@@ -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 <MCPO_API_KEY>`.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `MCPO_API_KEY` | If set, tool endpoints require `Authorization: Bearer <value>`. 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.
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user