Expose native MCP endpoint alongside mcpo #2
+15
-2
@@ -18,6 +18,12 @@ ARG MCPO_VERSION=0.0.20
|
|||||||
# release's uv.lock for its actual tested "mcp" version and update this too.
|
# release's uv.lock for its actual tested "mcp" version and update this too.
|
||||||
ARG MCP_SDK_VERSION=1.17.0
|
ARG MCP_SDK_VERSION=1.17.0
|
||||||
|
|
||||||
|
# Pinned supergateway release (npm). It serves a stdio MCP server as a native
|
||||||
|
# streamable-HTTP endpoint. Bump by checking the new version's CLI flags still
|
||||||
|
# match what entrypoint.sh assumes (--stdio/--outputTransport/--port/
|
||||||
|
# --healthEndpoint).
|
||||||
|
ARG SUPERGATEWAY_VERSION=4.1.0
|
||||||
|
|
||||||
FROM python:3.12-slim-bookworm AS vendor
|
FROM python:3.12-slim-bookworm AS vendor
|
||||||
ARG WEBFETCH_MCP_COMMIT
|
ARG WEBFETCH_MCP_COMMIT
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
||||||
@@ -31,19 +37,26 @@ RUN mkdir /vendored \
|
|||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
ARG MCPO_VERSION
|
ARG MCPO_VERSION
|
||||||
ARG MCP_SDK_VERSION
|
ARG MCP_SDK_VERSION
|
||||||
|
ARG SUPERGATEWAY_VERSION
|
||||||
|
|
||||||
|
# tini is PID 1: entrypoint.sh supervises two long-running processes, so
|
||||||
|
# something needs to reap zombies and deliver signals properly.
|
||||||
# Node.js 22, matching the version mcpo's own official image is built and
|
# Node.js 22, matching the version mcpo's own official image is built and
|
||||||
# tested against (webfetch-mcp only requires >=18).
|
# tested against (webfetch-mcp only requires >=18).
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
gnupg \
|
gnupg \
|
||||||
|
tini \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN pip install --no-cache-dir "mcpo==${MCPO_VERSION}" "mcp==${MCP_SDK_VERSION}"
|
RUN pip install --no-cache-dir "mcpo==${MCPO_VERSION}" "mcp==${MCP_SDK_VERSION}"
|
||||||
|
|
||||||
|
RUN npm install -g --omit=dev "supergateway@${SUPERGATEWAY_VERSION}" \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
COPY --from=vendor /vendored /app/webfetch-mcp
|
COPY --from=vendor /vendored /app/webfetch-mcp
|
||||||
WORKDIR /app/webfetch-mcp
|
WORKDIR /app/webfetch-mcp
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
@@ -51,5 +64,5 @@ RUN npm ci --omit=dev
|
|||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000 8080
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["tini", "--", "/entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
# webfetch-mcpo-docker
|
# webfetch-mcpo-docker
|
||||||
|
|
||||||
A single Docker image exposing [`webfetch-mcp`](https://code.aneur.in/mirrors/webfetch-mcp)'s
|
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).
|
`web_search` and `web_fetch` tools in two ways at once:
|
||||||
|
|
||||||
`webfetch-mcp` is a stdio-only MCP server; `mcpo` exposes a wrapped MCP server over
|
| Port | Protocol | For |
|
||||||
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
|
| `8000` | OpenAPI, via [`mcpo`](https://github.com/open-webui/mcpo) | Open WebUI |
|
||||||
to live in one image — that's what this repo builds.
|
| `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
|
## Build
|
||||||
|
|
||||||
@@ -17,7 +22,7 @@ docker build -t webfetch-mcpo-docker .
|
|||||||
## Run
|
## Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -p 8000:8000 \
|
docker run -p 8000:8000 -p 8080:8080 \
|
||||||
-e MCPO_API_KEY=some-secret \
|
-e MCPO_API_KEY=some-secret \
|
||||||
-e SEARXNG_BASE=http://searxng.example.internal:8080 \
|
-e SEARXNG_BASE=http://searxng.example.internal:8080 \
|
||||||
webfetch-mcpo-docker
|
webfetch-mcpo-docker
|
||||||
@@ -28,24 +33,48 @@ served without authentication even when `MCPO_API_KEY` is set — only the gener
|
|||||||
tool endpoints (`/web_search`, `/web_fetch`) require it, as
|
tool endpoints (`/web_search`, `/web_fetch`) require it, as
|
||||||
`Authorization: Bearer <MCPO_API_KEY>`.
|
`Authorization: Bearer <MCPO_API_KEY>`.
|
||||||
|
|
||||||
|
### opencode (or any MCP client)
|
||||||
|
|
||||||
|
In `opencode.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"webfetch": { "type": "remote", "url": "http://<host>:8080/mcp" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `:8080` endpoint has no authentication; `MCPO_API_KEY` applies only to `:8000`.
|
||||||
|
`/healthz` on `:8080` returns `ok`.
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Variable | Effect |
|
| Variable | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `MCPO_API_KEY` | If set, tool endpoints require `Authorization: Bearer <value>`. If unset, `mcpo` serves unauthenticated (its own default behavior). |
|
| `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`. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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
|
No explicit plumbing is needed to get these into `webfetch-mcp`: both `mcpo` and
|
||||||
spawned child process's environment as the union of its own process environment and
|
`supergateway` start the child with the container's environment inherited, so
|
||||||
any per-server overrides, so anything set on the container's environment is
|
anything set on the container's environment reaches `node server.mjs` automatically. (Verified against `mcpo`'s
|
||||||
inherited by the `node server.mjs` child automatically. (Verified against `mcpo`'s
|
|
||||||
`MCPConnectionManager` source, not assumed.)
|
`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
|
## 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
|
- **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,
|
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
|
accepted characteristic of `webfetch-mcp` itself — out of scope for this image to
|
||||||
@@ -64,9 +93,13 @@ inherited by the `node server.mjs` child automatically. (Verified against `mcpo`
|
|||||||
GitHub.
|
GitHub.
|
||||||
- `mcpo`: pinned via `ARG MCPO_VERSION` in the `Dockerfile`, installed from PyPI.
|
- `mcpo`: pinned via `ARG MCPO_VERSION` in the `Dockerfile`, installed from PyPI.
|
||||||
|
|
||||||
Bump either by updating the corresponding `ARG` default in the `Dockerfile`. When
|
- `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
|
bumping `mcpo`, double check its CLI flags still match what `entrypoint.sh` assumes
|
||||||
(`--host`, `--port`, `--api-key`, `--`).
|
(`--host`, `--port`, `--api-key`, `--`); for `supergateway`, `--stdio`,
|
||||||
|
`--outputTransport`, `--port` and `--healthEndpoint`.
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
|
|||||||
+59
-11
@@ -1,20 +1,68 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
# Runs up to two servers in one container, each spawning its own stdio
|
||||||
|
# `node server.mjs` child:
|
||||||
|
#
|
||||||
|
# - mcpo (OpenAPI, for Open WebUI) on $MCPO_PORT.
|
||||||
|
#
|
||||||
|
# - supergateway (native streamable-HTTP MCP, for coding clients) on
|
||||||
|
# $MCP_PORT, path /mcp.
|
||||||
|
#
|
||||||
# mcpo's --api-key is a CLI flag, not something it reads from its own
|
# 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, so the command line is built here at container start.
|
||||||
# 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
|
# webfetch-mcp reads SEARXNG_BASE / DEBUG / DETAILED_LOG from its own
|
||||||
# process environment. No passthrough is needed here: mcpo builds the
|
# process environment. No passthrough is needed here: both mcpo and
|
||||||
# spawned child's environment as {**os.environ, **extra}, so anything set
|
# supergateway start the child with this container's environment inherited.
|
||||||
# on this container's environment reaches webfetch-mcp automatically.
|
|
||||||
|
|
||||||
PORT="${MCPO_PORT:-8000}"
|
MCPO_PORT="${MCPO_PORT:-8000}"
|
||||||
|
MCP_PORT="${MCP_PORT:-8080}"
|
||||||
|
ENABLE_MCPO="${ENABLE_MCPO:-true}"
|
||||||
|
ENABLE_MCP="${ENABLE_MCP:-true}"
|
||||||
|
|
||||||
if [ -n "${MCPO_API_KEY:-}" ]; then
|
pids=""
|
||||||
exec mcpo --host 0.0.0.0 --port "$PORT" --api-key "$MCPO_API_KEY" -- node /app/webfetch-mcp/server.mjs
|
|
||||||
else
|
shutdown() {
|
||||||
exec mcpo --host 0.0.0.0 --port "$PORT" -- node /app/webfetch-mcp/server.mjs
|
# shellcheck disable=SC2086
|
||||||
|
[ -n "$pids" ] && kill -TERM $pids 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap 'shutdown; wait; exit 143' TERM INT
|
||||||
|
|
||||||
|
if [ "$ENABLE_MCP" = "true" ]; then
|
||||||
|
supergateway --stdio "node /app/webfetch-mcp/server.mjs" \
|
||||||
|
--outputTransport streamableHttp --port "$MCP_PORT" \
|
||||||
|
--healthEndpoint /healthz &
|
||||||
|
pids="$pids $!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$ENABLE_MCPO" = "true" ]; then
|
||||||
|
if [ -n "${MCPO_API_KEY:-}" ]; then
|
||||||
|
mcpo --host 0.0.0.0 --port "$MCPO_PORT" --api-key "$MCPO_API_KEY" -- node /app/webfetch-mcp/server.mjs &
|
||||||
|
else
|
||||||
|
mcpo --host 0.0.0.0 --port "$MCPO_PORT" -- node /app/webfetch-mcp/server.mjs &
|
||||||
|
fi
|
||||||
|
pids="$pids $!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$pids" ]; then
|
||||||
|
echo "Both ENABLE_MCPO and ENABLE_MCP are false; nothing to run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit as soon as either server dies, taking the other down with it, so the
|
||||||
|
# orchestrator restarts the whole container rather than leaving it half-up.
|
||||||
|
# POSIX sh has no `wait -n`, so poll instead.
|
||||||
|
while :; do
|
||||||
|
for pid in $pids; do
|
||||||
|
if ! kill -0 "$pid" 2>/dev/null; then
|
||||||
|
status=0
|
||||||
|
wait "$pid" || status=$?
|
||||||
|
echo "Process $pid exited (status $status); stopping container." >&2
|
||||||
|
shutdown
|
||||||
|
wait || true
|
||||||
|
exit "$(( status == 0 ? 1 : status ))"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user