feat(mcp-server): publish a Docker image + /health endpoint#1768
Conversation
Lets clients deploy the MCP server without a Node toolchain (e.g. a Ruby or Python agent stack) — pull the image and run it, config via env vars only. Mirrors the workflow-executor image setup. - add an unauthenticated GET /health liveness probe (Docker HEALTHCHECK, k8s) - multi-stage Dockerfile with an isolated prod-deps install (5-package @ForestAdmin closure: mcp-server + agent-client + forestadmin-client + datasource-toolkit + agent-toolkit), digest-pinned base, non-root user - docker/ assets: deps-manifest generator, closure drift check, dedicated yarn.lock, boot smoke-test, README - docker-publish-mcp-server.yml: PR validate (build + smoke + trivy OS gate, npm libs report-only) and multi-arch push-by-digest to ghcr.io/forestadmin/mcp-server on release - build.yml release job dispatches the image publish on a new mcp-server tag - README/CLAUDE docs No OpenTelemetry (unlike the executor) and no prod auto-deploy: the image is deployed by the client on their own infra. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 new issues
|
| concurrency: | ||
| # Include the dispatch version so two manual runs for different versions from the | ||
| # same branch don't cancel each other mid-release. | ||
| group: docker-publish-mcp-server-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || '' }} |
There was a problem hiding this comment.
🟡 Medium workflows/docker-publish-mcp-server.yml:34
The concurrency group includes github.event.inputs.version, so manual dispatches for different versions run simultaneously. If an older version's build/merge jobs finish after a newer version's, the older run overwrites :latest, :major, and :minor with the stale image — even though its extract-version job correctly computed IS_LATEST=true at startup. The IS_LATEST check only compares against tags that existed when the run started, so a concurrent newer release doesn't prevent the older run from moving the mutable tags. Consider serializing mutable-tag publication across versions (e.g. a shared concurrency group for the merge job) or re-checking the current highest stable tag immediately before pushing the manifest.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @.github/workflows/docker-publish-mcp-server.yml around line 34:
The concurrency group includes `github.event.inputs.version`, so manual dispatches for different versions run simultaneously. If an older version's `build`/`merge` jobs finish after a newer version's, the older run overwrites `:latest`, `:major`, and `:minor` with the stale image — even though its `extract-version` job correctly computed `IS_LATEST=true` at startup. The `IS_LATEST` check only compares against tags that existed when the run started, so a concurrent newer release doesn't prevent the older run from moving the mutable tags. Consider serializing mutable-tag publication across versions (e.g. a shared concurrency group for the `merge` job) or re-checking the current highest stable tag immediately before pushing the manifest.
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (1)
🛟 Help
|
…a timeout The health-poll swallowed `docker exec` stderr, so a container that crashed on boot (the most likely failure — an un-copied @ForestAdmin package) spun the full 30s and was then mislabelled "did not answer within 30s", pointing at a slow boot instead of an instant crash. Probe container liveness each iteration and fail fast with the real reason. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Why
Riipen (Ruby app, Forest v2 agent via the Ruby gem) wants the MCP server but it's Node-only — no "mounted" version on the Ruby agent, and none planned. To remove the friction of running Node next to Ruby, we ship a Docker image they deploy on their own infra. Same approach as
workflow-executor(and Qonto before).What
Mirrors the
workflow-executorimage setup, trimmed of executor-specific parts.GET /health— unauthenticated liveness probe (Docker HEALTHCHECK / k8s), returns200 {"status":"ok"}. Registered beforeallowedMethods(['POST'])so the GET isn't rejected. + test.Dockerfile— 3-stage, digest-pinnednode:22-bookworm-slim, non-rootnodeuser. Isolated prod-deps install over the 5-package@forestadminclosure (mcp-server+agent-client+forestadmin-client+datasource-toolkit+agent-toolkit).EXPOSE 3931, healthcheck on/health.docker/— deps-manifest generator, closure-drift check, dedicateddeps/yarn.lock, boot smoke-test, README.docker-publish-mcp-server.yml— PR validate (closure check + build + smoke-test + trivy OS blocking, npm libs report-only) and, on release, multi-arch (amd64+arm64) push-by-digest toghcr.io/forestadmin/mcp-serverwith mutable:latest/:minor/:major+ immutable:versiontags.build.yml— release job dispatches the image publish on a new@forestadmin/mcp-server@*tag (same pattern as the executor).Differences vs the executor image (intentional)
notify-serverjob → the client deploys the image themselves; no Forest prod auto-deploy.Verification (local)
yarn workspace @forestadmin/mcp-server test→ 609 passing (incl. the new/healthtest);lintclean (0 errors).node packages/mcp-server/docker/check-deps-closure.js→ closure OK (5 packages).docker build -f packages/mcp-server/Dockerfile -t mcp-server:local .→ OK (~274 MB).smoke-test.sh→ passes (boots, answers/health, full module graph loads).GET /health→ 200, OAuth well-known → 200,POST /mcpwithout token → 401.Result
After merge + release, the image is available:
docker pull ghcr.io/forestadmin/mcp-server:latest # or :<version>, :<minor>, :<major> docker run -p 3931:3931 -e FOREST_ENV_SECRET=... -e FOREST_AUTH_SECRET=... ghcr.io/forestadmin/mcp-server:latestRiipen deploys it with just the two Forest secrets — no Node install.
🤖 Generated with Claude Code
Note
Add Docker image publishing and
/healthendpoint tomcp-server@forestadminworkspace dependencies, installs isolated runtime deps, runs as a non-rootnodeuser, and exposes port 3931.GET /healthroute inserver.tsthat returns200 {"status":"ok"}without authentication, used by both the Docker healthcheck and external monitors.latest, major, minor) tags.@forestadmin/mcp-servertags and dispatch the Docker publish workflow automatically.Macroscope summarized 680ecf0.