# webfetch-mcpo-docker A single Docker image exposing [`webfetch-mcp`](https://code.aneur.in/mirrors/webfetch-mcp)'s `web_search` and `web_fetch` tools in two ways at once: | Port | Protocol | For | |---|---|---| | `8000` | OpenAPI, via [`mcpo`](https://github.com/open-webui/mcpo) | Open WebUI | | `8080` | Native MCP (streamable HTTP) at `/mcp`, via [`supergateway`](https://github.com/supercorp-ai/supergateway) | Coding agents (opencode etc.) | `webfetch-mcp` is a stdio-only MCP server with no HTTP mode of its own. `mcpo` and `supergateway` each expose a wrapped stdio server over HTTP by spawning it as a local child process, so they have to live in the same image as `webfetch-mcp`. Each spawns its own `node server.mjs`. ## Build ```sh docker build -t webfetch-mcpo-docker . ``` ## Run ```sh docker run -p 8000:8000 -p 8080:8080 \ -e MCPO_API_KEY=some-secret \ -e SEARXNG_BASE=http://searxng.example.internal:8080 \ webfetch-mcpo-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 `. ### opencode (or any MCP client) In `opencode.json`: ```json { "mcp": { "webfetch": { "type": "remote", "url": "http://:8080/mcp" } } } ``` The `:8080` endpoint has no authentication; `MCPO_API_KEY` applies only to `:8000`. `/healthz` on `:8080` returns `ok`. ## 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`. | | `MCP_PORT` | Port the native MCP endpoint listens on. Default `8080`. | | `ENABLE_MCPO` / `ENABLE_MCP` | Set either to `false` to run only the other side. Both default `true`. | | `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`: both `mcpo` and `supergateway` start the child with the container's environment inherited, so anything set on the container's environment reaches `node server.mjs` automatically. (Verified against `mcpo`'s `MCPConnectionManager` source, not assumed.) ## Process supervision `entrypoint.sh` runs both servers under `tini`. If either one exits, the other is stopped and the container exits non-zero, so the orchestrator restarts the whole thing rather than leaving it half up. ## Known characteristics - **The `:8080` endpoint is unauthenticated.** Intended for a trusted LAN only. - **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. - `supergateway`: pinned via `ARG SUPERGATEWAY_VERSION` in the `Dockerfile`, installed from npm. Bump any of them 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`, `--`); for `supergateway`, `--stdio`, `--outputTransport`, `--port` and `--healthEndpoint`. ## CI `.gitea/workflows/docker-build.yml` builds and pushes `code.aneur.in/cloud/webfetch-mcpo-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.