From 59d38921bbbdc943c51265f0dbe76c518a218d69 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:29:57 +0000 Subject: [PATCH] Pin images that exist; add pinned local builds for upstreams without one 11 of 15 pinned images did not exist in their registries. Published images now use real release tags (bifrost v2.3.0, searxng 2026.10.16-0ce72dd2c, docker-android emulator_14.0_v3.8.0-p1, devcontainers/base 2.2.3-bookworm). Upstreams with no usable image get images//Dockerfile on a pinned base, installing a checksum- or commit-verified upstream release, tagged localhost/heretek/: with pull_policy: build. Stdio-only servers are wrapped with a pinned mcp-proxy so every MCP service serves streamable HTTP; compose labels declare each endpoint. radare2-mcp now runs with -R (read-only); the previous -r enabled the dangerous run_* tools. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012AuVk4nEvBXaTjs4G6wdW4 --- AGENTS.md | 19 +++- README.md | 91 +++++++++++++-- compose/profile-android.yml | 34 +++++- compose/profile-appsec.yml | 63 +++++++++-- compose/profile-game.yml | 33 ++++-- compose/profile-research.yml | 24 +++- .../headless-wayland-desktop/Dockerfile | 9 +- devcontainers/polyglot-dev/.devcontainer.json | 2 +- images/adb-mcp/Dockerfile | 33 ++++++ images/adb-mcp/start.sh | 8 ++ images/blender-mcp/Dockerfile | 55 +++++++++ images/computer-use-linux/Dockerfile | 50 +++++++++ images/computer-use-linux/start.sh | 27 +++++ images/computer-use-linux/sway-config | 8 ++ images/ghidra-mcp/Dockerfile | 106 ++++++++++++++++++ images/ghidra-mcp/start.sh | 24 ++++ images/godot-mcp/Dockerfile | 60 ++++++++++ images/maestro-mcp/Dockerfile | 40 +++++++ images/maestro-mcp/start.sh | 8 ++ images/mdb-mcp/Dockerfile | 43 +++++++ images/radare2-mcp/Dockerfile | 39 +++++++ images/rea-mcp/Dockerfile | 32 ++++++ 22 files changed, 765 insertions(+), 43 deletions(-) create mode 100644 images/adb-mcp/Dockerfile create mode 100644 images/adb-mcp/start.sh create mode 100644 images/blender-mcp/Dockerfile create mode 100644 images/computer-use-linux/Dockerfile create mode 100644 images/computer-use-linux/start.sh create mode 100644 images/computer-use-linux/sway-config create mode 100644 images/ghidra-mcp/Dockerfile create mode 100644 images/ghidra-mcp/start.sh create mode 100644 images/godot-mcp/Dockerfile create mode 100644 images/maestro-mcp/Dockerfile create mode 100644 images/maestro-mcp/start.sh create mode 100644 images/mdb-mcp/Dockerfile create mode 100644 images/radare2-mcp/Dockerfile create mode 100644 images/rea-mcp/Dockerfile diff --git a/AGENTS.md b/AGENTS.md index b967f58..a5a7bca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,7 +118,7 @@ No `VERDICT: PASS`, no merge. No exceptions. ├── tsconfig.json ├── docker-compose.yml # Root compose using `include:` for compose/core.yml + 4 specialist profiles ├── compose/ -│ ├── core.yml # Profile `core`: maximhq/bifrost:v1.0.0 (:8080), agentgateway:v1.6.0 (:3000), nats:2.15.1-alpine (:4222) +│ ├── core.yml # Profile `core`: maximhq/bifrost:v2.3.0 (:8080), agentgateway:v1.6.0 (:3000), nats:2.15.1-alpine (:4222) │ ├── bifrost.yaml # 3-Tier LLM Failover config (OpenCode Go -> OpenRouter -> Local Ollama/vLLM) + Session Affinity │ ├── agentgateway.yaml # CEL RBAC rules & MCP target routing │ ├── profile-appsec.yml # Profile `appsec`: bethington/ghidra-mcp-headless:v6.0.0, radareorg/radare2-mcp:1.8.8, morluto/rea:6.5.0, mdb-mcp:1.2.0 @@ -134,14 +134,17 @@ No `VERDICT: PASS`, no merge. No exceptions. │ │ └── pixi.toml # Conda-forge/PyTorch 2.4/CUDA 12.4/CMake/Ninja/OpenCV deduplicated via rattler hardlinks │ └── headless-wayland-desktop/ │ └── Dockerfile # Debian trixie-slim + headless Sway + at-spi2-core + ydotool + agent-sh/computer-use-linux:v0.9.4 -├── quadlets/ # Systemd rootless Podman Quadlets (`StopWhenUnneeded=true` -> 0 MB idle RAM) -│ ├── heretek-android-kvm.container -│ ├── heretek-ghidra-mcp.container -│ └── heretek-blender-mcp.container +├── images/ # Pinned local builds for upstreams with no usable published image +│ └── /Dockerfile # tagged localhost/heretek/: (see README "Images") +├── quadlets/ # Systemd rootless Podman Quadlets, socket-activated (0 MB idle RAM) +│ ├── heretek-android-kvm.container (+ -vnc-proxy / -adb-proxy .socket/.service) +│ ├── heretek-ghidra-mcp.container (+ -proxy.socket/.service) +│ └── heretek-blender-mcp.container (+ -proxy.socket/.service) ├── scripts/ │ ├── heretek-exec.sh # Sub-5ms local worktree wrapper: bwrap + landrun + mise exec (with /dev/kvm & SELinux support) │ ├── heretek-box-ctl.sh # Operator CLI: `up `, `down `, `status`, `verify`, `exec ` │ ├── verify-stack.ts # Deterministic static contract verifier auditing all Compose/Devcontainer/Quadlet/Env files +│ ├── check-images.ts # `make images`: resolves every pinned remote image in its registry │ └── accept/ │ ├── wave-1.sh │ ├── wave-2.sh @@ -156,6 +159,12 @@ No `VERDICT: PASS`, no merge. No exceptions. ## 2. EXACT MODULE SPECIFICATIONS +> **Image pins (2026-10 remediation):** several image tags below never existed +> in their registries. The current pins (real published tags, or pinned local +> builds `localhost/heretek/:` from `images/`) are listed in +> README "Images" and enforced by `make images`. radare2-mcp runs with `-R` +> (read-only); `-r` would enable its dangerous run_* tools. + ### 2.1 Wave 1: Root Compose, `core.yml`, `bifrost.yaml`, Fail-Closed Static Verifier 1. **`docker-compose.yml` & `compose/core.yml`:** diff --git a/README.md b/README.md index 6037859..815d7a9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,58 @@ All host ports bind `127.0.0.1` loopback only. Every image is pinned to an exact version tag. The root `docker-compose.yml` orchestrates the five fragments via `include:`. +### Images + +Published images are pinned to tags that exist in their registry +(`make images` checks this). Upstreams that publish no usable image are +built locally from `images//Dockerfile` and tagged +`localhost/heretek/:`; compose builds them +(`pull_policy: build`) and never pulls those names. Each Dockerfile starts +from a pinned base image and installs a pinned upstream release, verified by +SHA-256/SHA-512 or by checking out the exact upstream commit. + +| Service | Image | Source | +|---|---|---| +| `bifrost` | `ghcr.io/maximhq/bifrost:v2.3.0` | published | +| `agentgateway` | `ghcr.io/agentgateway/agentgateway:v1.6.0` | published | +| `nats` | `nats:2.15.1-alpine` | published | +| `android-kvm` | `budtmo/docker-android:emulator_14.0_v3.8.0-p1` | published | +| `searxng` | `searxng/searxng:2026.10.16-0ce72dd2c` | published | +| `crawl4ai` | `unclecode/crawl4ai:0.9.5` | published | +| `ghidra-mcp` | `localhost/heretek/ghidra-mcp:6.0.0` | bethington/ghidra-mcp v6.0.0 + Ghidra 12.1.2 | +| `radare2-mcp` | `localhost/heretek/radare2-mcp:1.9.0` | r2mcp 1.9.0 + radare2 6.2.4 release .debs | +| `rea-mcp` | `localhost/heretek/rea-mcp:6.5.0` | npm `rea-agents@6.5.0` | +| `mdb-mcp` | `localhost/heretek/mdb-mcp:0.2.0-5d03b25` | smadi0x86/mdb-mcp commit 5d03b25 (upstream has no tags) | +| `adb-mcp` | `localhost/heretek/adb-mcp:0.23.0` | binary from `ghcr.io/iksnerd/adb-mcp:0.23.0` | +| `maestro-mcp` | `localhost/heretek/maestro-mcp:2.11.0` | Maestro CLI cli-2.11.0 release | +| `godot-mcp` | `localhost/heretek/godot-mcp:3.1.0` | tugcantopaloglu/godot-mcp v3.1.0 + Godot 4.3-stable | +| `blender-mcp` | `localhost/heretek/blender-mcp:0.11.1` | sandraschi/blender-mcp v0.11.1 + Blender 4.2.3 LTS | +| `computer-use-linux` | `localhost/heretek/computer-use-linux:0.7.17` | agent-sh/computer-use-linux v0.7.17 release binary | + +Stdio-only MCP servers are wrapped with a pinned `mcp-proxy` (0.13.0) so every +MCP service speaks streamable HTTP at `/mcp` on its container port (crawl4ai: +SSE at `/mcp/sse`). Compose labels (`heretek.mcp.target`, `.transport`, +`.path`) declare those endpoints. + +```sh +make images # resolve every pinned remote image against Docker Hub / GHCR / MCR (network) +``` + +### Agent gateway + +`compose/agentgateway.yaml` is an agentgateway v1.6.0 standalone config +(`agentgateway -f /etc/agentgateway.yaml`). It serves one route per MCP target +at `/mcp/` (`ghidra`, `radare2`, `rea`, `mdb`, `adb`, `maestro`, +`godot`, `blender`, `crawl4ai`, `gui`), each holding exactly one target so +tool names are not prefixed, plus an OpenAI-compatible `/v1` route to Bifrost. +Every request needs `Authorization: Bearer `; +`mdb` and `gui` are admin-only. There is no `core` or `searxng` route yet: +no heretek-core MCP service exists here, and SearXNG is not an MCP server. + +```sh +make validate-gateway AGENTGATEWAY=/path/to/agentgateway # real binary, --validate-only +``` + ## Quickstart ```sh @@ -58,11 +110,21 @@ violation: | `SELINUX_VOLUME_LABELS` | any bind mount missing an SELinux label (`:z`, `:Z`, `:ro,Z`) | | `NO_AGPL_IN_DEV_OR_GAME` | AGPL-licensed or root-escape images | | `ENV_PLACEHOLDER_LEAK` | a real `.env` still carrying template placeholder tokens | +| `SELINUX_CONFINED` | a quadlet that disables SELinux confinement (`SecurityLabelType=spc`, `SecurityLabelDisable=true`, privileged) | +| `QUADLET_SOCKET_ACTIVATION` | a quadlet container that can start on its own or lacks its loopback `.socket` + idle-exiting proxy `.service` | + +`NO_FLOATING_TAGS` also covers local builds (a `build:` service must tag +`localhost/:`) and Dockerfile `FROM` / `COPY --from=` images. +Each rule has a failing fixture tree under `test/fixtures/verify/` +(`bun run scripts/verify-stack.ts --root `), exercised by +`test/verify-stack.test.ts`. ```sh bun run scripts/verify-stack.ts # or: ./scripts/heretek-box-ctl.sh verify make check # verifier + bun test make accept # all three wave acceptance gates +make validate-compose # docker compose config for every profile +make validate-units # systemd-analyze verify on the quadlet sockets/proxies ``` ## Devcontainers @@ -81,18 +143,33 @@ and commit the bumped pins. ## Quadlets -Rootless Podman units for the heavy services, all with -`StopWhenUnneeded=true` so they sit at 0 MB idle RAM until socket-activated: +Rootless Podman units for the heavy services, socket-activated so they use +no memory until something connects: -| Unit | Image | Loopback ports | +- `-proxy.socket` owns the loopback port (the same one compose uses) + and is the only unit you enable (`WantedBy=sockets.target`). +- `-proxy.service` runs `systemd-socket-proxyd --exit-idle-time=15min` + and `Requires=`/`After=` the container unit, so the first connection starts + the container (`Notify=healthy` holds the proxy until it answers). +- `.container` has `StopWhenUnneeded=true` and no `[Install]` section, + so it never starts at login and stops once its proxy exits idle. + +| Container unit | Image | Activation sockets | |---|---|---| -| `quadlets/heretek-android-kvm.container` | `budtmo/docker-android:emulator_14.0` (+ `/dev/kvm` device grant) | `127.0.0.1:6080`, `127.0.0.1:5555` | -| `quadlets/heretek-ghidra-mcp.container` | `bethington/ghidra-mcp-headless:v6.0.0` (token via `~/.config/heretek/ghidra-mcp.env`, mode 600) | `127.0.0.1:8089` | -| `quadlets/heretek-blender-mcp.container` | `ghcr.io/sandraschi/blender-mcp:v1.2.0` | `127.0.0.1:8781` | +| `heretek-android-kvm.container` | `budtmo/docker-android:emulator_14.0_v3.8.0-p1` (+ `AddDevice=/dev/kvm`) | `127.0.0.1:6080` (vnc), `127.0.0.1:5555` (adb) | +| `heretek-ghidra-mcp.container` | `localhost/heretek/ghidra-mcp:6.0.0` (token via `~/.config/heretek/ghidra-mcp.env`, mode 600) | `127.0.0.1:8089` | +| `heretek-blender-mcp.container` | `localhost/heretek/blender-mcp:0.11.1` | `127.0.0.1:8781` | + +All containers keep the default `container_t` SELinux confinement. If an +enforcing host denies the emulator `/dev/kvm`, use +`setsebool -P container_use_devices=true` rather than disabling labels. ```sh +podman build -t localhost/heretek/ghidra-mcp:6.0.0 images/ghidra-mcp # local images first +cp quadlets/*.container ~/.config/containers/systemd/ +cp quadlets/*.socket quadlets/*.service ~/.config/systemd/user/ systemctl --user daemon-reload -systemctl --user enable --now heretek-blender-mcp.service +systemctl --user enable --now heretek-ghidra-mcp-proxy.socket ``` ## heretek-exec and box-ctl exec diff --git a/compose/profile-android.yml b/compose/profile-android.yml index 497cf60..dae8614 100644 --- a/compose/profile-android.yml +++ b/compose/profile-android.yml @@ -8,9 +8,16 @@ # # `android-kvm` passes through /dev/kvm via `devices:` (a device mapping, # not a volume mount — no SELinux label rule applies to it). +# +# android-kvm pins the immutable release tag emulator_14.0_v3.8.0-p1 (the bare +# emulator_14.0 tag is re-pointed on every docker-android release). The two MCP +# bridges have no usable published HTTP image, so they are pinned local builds +# (images//Dockerfile) tagged localhost/heretek/:; +# `pull_policy: build` keeps compose from pulling the local names. Both speak +# MCP over streamable HTTP at /mcp (see the `heretek.mcp.*` labels). services: android-kvm: - image: budtmo/docker-android:emulator_14.0 + image: budtmo/docker-android:emulator_14.0_v3.8.0-p1 user: "1000:1000" restart: unless-stopped profiles: ["android", "all"] @@ -31,7 +38,10 @@ services: retries: 3 adb-mcp: - image: ghcr.io/iksnerd/adb-mcp:v1.4.0 + image: localhost/heretek/adb-mcp:0.23.0 + build: + context: ../images/adb-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["android", "all"] @@ -41,6 +51,10 @@ services: environment: ANDROID_HOST: android-kvm ADB_PORT: "5555" + labels: + heretek.mcp.target: adb + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8770:8770" volumes: @@ -49,23 +63,31 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8770/healthz"] + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8770/status', timeout=4)"] interval: 30s timeout: 5s retries: 3 maestro-mcp: - image: ghcr.io/mobile-dev-inc/maestro:v1.39.8 + image: localhost/heretek/maestro-mcp:2.11.0 + build: + context: ../images/maestro-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["android", "all"] depends_on: android-kvm: condition: service_started - command: ["maestro", "mcp"] + # The image entrypoint attaches adb to android-kvm, then serves + # `maestro mcp` over streamable HTTP via mcp-proxy. environment: ANDROID_HOST: android-kvm ADB_PORT: "5555" + labels: + heretek.mcp.target: maestro + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8771:8771" volumes: @@ -74,7 +96,7 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8771/healthz"] + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8771/status', timeout=4)"] interval: 30s timeout: 5s retries: 3 diff --git a/compose/profile-appsec.yml b/compose/profile-appsec.yml index 77bf8ff..a427737 100644 --- a/compose/profile-appsec.yml +++ b/compose/profile-appsec.yml @@ -9,14 +9,32 @@ # NOTE: `mdb-mcp` carries the SOLE documented capability exception in this # repo — a narrow SYS_PTRACE grant required for debugger attachment. # It still drops all other capabilities and keeps no-new-privileges. +# +# None of these upstreams publishes a usable container image, so each service +# is a pinned local build (images//Dockerfile) tagged +# localhost/heretek/:. `pull_policy: build` keeps +# compose from ever trying to pull the local name from a registry. +# Every service speaks MCP over streamable HTTP at /mcp on its container port; +# the `heretek.mcp.*` labels are what compose/agentgateway.yaml routes to +# (enforced by test/agentgateway-config.test.ts). services: ghidra-mcp: - image: bethington/ghidra-mcp-headless:v6.0.0 + image: localhost/heretek/ghidra-mcp:6.0.0 + build: + context: ../images/ghidra-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["appsec", "all"] environment: GHIDRA_MCP_AUTH_TOKEN: ${GHIDRA_MCP_AUTH_TOKEN:?GHIDRA_MCP_AUTH_TOKEN is mandatory} + # The bridge keeps DNS-rebinding protection on; allow the compose DNS + # name the agent gateway uses as its Host header, plus host loopback. + GHIDRA_MCP_ALLOWED_HOSTS: ghidra-mcp,localhost,127.0.0.1 + labels: + heretek.mcp.target: ghidra + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8089:8089" volumes: @@ -25,19 +43,29 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8089/healthz"] + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8089"] interval: 30s timeout: 5s retries: 3 + start_period: 120s radare2-mcp: - image: ghcr.io/radareorg/radare2-mcp:1.8.8 + image: localhost/heretek/radare2-mcp:1.9.0 + build: + context: ../images/radare2-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["appsec", "all"] # Container-internal listen address (NOT a published host port — the - # published port below is loopback-only). `-r` forces read-only mode. - command: ["r2mcp", "-H", "0.0.0.0:8765", "-r"] + # published port below is loopback-only). `-R` exposes only the read-only + # tool set (`-r` would ENABLE the dangerous run_* tools); `-s /data` + # sandboxes file access to the data volume. + command: ["r2mcp", "-H", "0.0.0.0:8765", "-R", "-s", "/data"] + labels: + heretek.mcp.target: radare2 + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8765:8765" volumes: @@ -46,16 +74,23 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8765/healthz"] + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8765"] interval: 30s timeout: 5s retries: 3 rea-mcp: - image: ghcr.io/morluto/rea:6.5.0 + image: localhost/heretek/rea-mcp:6.5.0 + build: + context: ../images/rea-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["appsec", "all"] + labels: + heretek.mcp.target: rea + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8766:8766" volumes: @@ -64,16 +99,24 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8766/healthz"] + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8766/status', timeout=4)"] interval: 30s timeout: 5s retries: 3 mdb-mcp: - image: ghcr.io/smadi0x86/mdb-mcp:1.2.0 + # Upstream has no release tag; the build pins commit 5d03b25 (version 0.2.0). + image: localhost/heretek/mdb-mcp:0.2.0-5d03b25 + build: + context: ../images/mdb-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["appsec", "all"] + labels: + heretek.mcp.target: mdb + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8767:8767" volumes: @@ -84,7 +127,7 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8767/healthz"] + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8767/status', timeout=4)"] interval: 30s timeout: 5s retries: 3 diff --git a/compose/profile-game.yml b/compose/profile-game.yml index 06e6681..6ef9c23 100644 --- a/compose/profile-game.yml +++ b/compose/profile-game.yml @@ -1,18 +1,30 @@ # compose/profile-game.yml — `game` on-demand domain profile (Wave 2) # -# Headless Godot 4.3 + Blender FastMCP 3.2 MCP servers. Activated on demand via: +# Headless Godot 4.3 + Blender 4.2 LTS FastMCP servers. Activated on demand via: # docker compose --profile game up # (or `--profile all` for everything). Every service is hardened fail-closed: # pinned image, 127.0.0.1 loopback ports only, cap_drop ALL, # no-new-privileges, non-root user, zero socket mounts. +# +# Neither upstream publishes a container image, so both are pinned local +# builds (images//Dockerfile) tagged localhost/heretek/:; +# `pull_policy: build` keeps compose from pulling the local names. Both speak +# MCP over streamable HTTP at /mcp (see the `heretek.mcp.*` labels). services: godot-mcp: - image: ghcr.io/tugcantopaloglu/godot-mcp:v3.1.0 + image: localhost/heretek/godot-mcp:3.1.0 + build: + context: ../images/godot-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["game", "all"] environment: GODOT_HEADLESS: "1" + labels: + heretek.mcp.target: godot + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8780:8780" volumes: @@ -21,20 +33,27 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8780/healthz"] + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8780/status', timeout=4)"] interval: 30s timeout: 5s retries: 3 blender-mcp: - image: ghcr.io/sandraschi/blender-mcp:v1.2.0 + image: localhost/heretek/blender-mcp:0.11.1 + build: + context: ../images/blender-mcp + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["game", "all"] - # Headless Blender background mode serving the FastMCP 3.2 bridge. - command: ["blender", "--background"] + # The image serves the FastMCP server over streamable HTTP; it drives + # Blender 4.2.3 headless (`blender --background`) per operation. environment: BLENDER_HEADLESS: "1" + labels: + heretek.mcp.target: blender + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8781:8781" volumes: @@ -43,7 +62,7 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8781/healthz"] + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8781"] interval: 30s timeout: 5s retries: 3 diff --git a/compose/profile-research.yml b/compose/profile-research.yml index 8664bb9..ad02568 100644 --- a/compose/profile-research.yml +++ b/compose/profile-research.yml @@ -7,9 +7,15 @@ # (or `--profile all` for everything). Every service is hardened fail-closed: # pinned image, 127.0.0.1 loopback ports only, cap_drop ALL, # no-new-privileges, non-root user, zero socket mounts. +# +# searxng and crawl4ai are published images pinned to exact release tags. +# computer-use-linux publishes no image, so it is a pinned local build +# (images/computer-use-linux/Dockerfile) tagged localhost/heretek/...:; +# `pull_policy: build` keeps compose from pulling the local name. +# MCP endpoints are declared with `heretek.mcp.*` labels for the gateway. services: searxng: - image: searxng/searxng:2026.10.14 + image: searxng/searxng:2026.10.16-0ce72dd2c user: "1000:1000" restart: unless-stopped profiles: ["research", "all"] @@ -35,6 +41,11 @@ services: restart: unless-stopped profiles: ["research", "all"] shm_size: "1g" + # crawl4ai's server exposes its MCP tools over SSE at /mcp/sse. + labels: + heretek.mcp.target: crawl4ai + heretek.mcp.transport: sse + heretek.mcp.path: /mcp/sse ports: - "127.0.0.1:11235:11235" volumes: @@ -49,7 +60,10 @@ services: retries: 3 computer-use-linux: - image: ghcr.io/agent-sh/computer-use-linux:v0.9.4 + image: localhost/heretek/computer-use-linux:0.7.17 + build: + context: ../images/computer-use-linux + pull_policy: build user: "1000:1000" restart: unless-stopped profiles: ["research", "all"] @@ -57,6 +71,10 @@ services: environment: WLR_BACKENDS: headless SWAY_HEADLESS: "1" + labels: + heretek.mcp.target: gui + heretek.mcp.transport: streamable-http + heretek.mcp.path: /mcp ports: - "127.0.0.1:8790:8790" volumes: @@ -65,7 +83,7 @@ services: security_opt: - "no-new-privileges:true" healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8790/healthz"] + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8790/status', timeout=4)"] interval: 30s timeout: 5s retries: 3 diff --git a/devcontainers/headless-wayland-desktop/Dockerfile b/devcontainers/headless-wayland-desktop/Dockerfile index 2cd797e..d3ee5a5 100644 --- a/devcontainers/headless-wayland-desktop/Dockerfile +++ b/devcontainers/headless-wayland-desktop/Dockerfile @@ -1,19 +1,20 @@ # devcontainers/headless-wayland-desktop/Dockerfile — headless Sway + AT-SPI2 desktop (Wave 3) # -# Debian trixie-slim base (pinned, never floating) with a headless Sway +# Debian trixie-slim base (pinned to a dated release, never floating) with a headless Sway # compositor, AT-SPI2 accessibility bus, and ydotool for synthetic input. # Pairs at runtime with the pinned computer-use-linux agent image noted below # (see compose/profile-research.yml `computer-use-linux` service), which # consumes this desktop over the headless Wayland socket. # -# Runtime peer (pinned, Apache-2.0/MIT stack — no AGPL tooling): -# ghcr.io/agent-sh/computer-use-linux:v0.9.4 +# Runtime peer (pinned local build of the MIT agent-sh/computer-use-linux +# v0.7.17 release — no AGPL tooling; see images/computer-use-linux/Dockerfile): +# localhost/heretek/computer-use-linux:0.7.17 # # Security posture: non-root `agent` user (uid 1000), no container-socket # mounts, no privileged capabilities required (KVM passthrough, where needed, # is granted by the orchestrator via device mapping, not by this image). -FROM debian:trixie-slim +FROM debian:trixie-20261005-slim ENV DEBIAN_FRONTEND=noninteractive \ WLR_BACKENDS=headless \ diff --git a/devcontainers/polyglot-dev/.devcontainer.json b/devcontainers/polyglot-dev/.devcontainer.json index 1cd2a2a..56b5c5f 100644 --- a/devcontainers/polyglot-dev/.devcontainer.json +++ b/devcontainers/polyglot-dev/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "heretek-polyglot-dev", - "image": "mcr.microsoft.com/devcontainers/base:1.2.2-bookworm", + "image": "mcr.microsoft.com/devcontainers/base:2.2.3-bookworm", "remoteUser": "agent", "containerUser": "agent", "capAdd": [], diff --git a/images/adb-mcp/Dockerfile b/images/adb-mcp/Dockerfile new file mode 100644 index 0000000..2ad6653 --- /dev/null +++ b/images/adb-mcp/Dockerfile @@ -0,0 +1,33 @@ +# images/adb-mcp/Dockerfile — adb-mcp (MIT) with a streamable-HTTP front end +# +# The upstream image ghcr.io/iksnerd/adb-mcp:0.23.0 exists but speaks MCP over +# stdio only, so this build copies its pinned binary next to the Debian adb +# client and a pinned mcp-proxy. Compose tags the result +# localhost/heretek/adb-mcp:0.23.0; the agent gateway reaches it at +# http://adb-mcp:8770/mcp. + +FROM ghcr.io/iksnerd/adb-mcp:0.23.0 AS upstream + +FROM python:3.12.15-slim-trixie + +ARG MCP_PROXY_VERSION=0.13.0 + +COPY --from=upstream /usr/local/bin/adb-mcp /usr/local/bin/adb-mcp +COPY start.sh /usr/local/bin/heretek-adb-mcp + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends adb; \ + rm -rf /var/lib/apt/lists/*; \ + pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \ + chmod 0755 /usr/local/bin/heretek-adb-mcp; \ + useradd -m -u 1000 -s /bin/bash mcp; \ + mkdir -p /data; \ + chown 1000:1000 /data + +ENV HOME=/home/mcp +USER 1000:1000 +WORKDIR /data +EXPOSE 8770 + +CMD ["heretek-adb-mcp"] diff --git a/images/adb-mcp/start.sh b/images/adb-mcp/start.sh new file mode 100644 index 0000000..bc89c26 --- /dev/null +++ b/images/adb-mcp/start.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Attach the local adb server to the emulator, then serve adb-mcp over +# streamable HTTP. A failed connect is not fatal: the emulator may still be +# booting, and adb-mcp reports device errors per tool call. +set -eu +adb start-server +adb connect "${ANDROID_HOST:-android-kvm}:${ADB_PORT:-5555}" || true +exec mcp-proxy --host 0.0.0.0 --port "${MCP_PORT:-8770}" --pass-environment -- adb-mcp diff --git a/images/blender-mcp/Dockerfile b/images/blender-mcp/Dockerfile new file mode 100644 index 0000000..207b195 --- /dev/null +++ b/images/blender-mcp/Dockerfile @@ -0,0 +1,55 @@ +# images/blender-mcp/Dockerfile — pinned local build of sandraschi/blender-mcp (MIT) +# +# Upstream publishes no container image, so this build installs the v0.11.1 +# tagged source (commit verified after checkout) next to Blender 4.2.3 LTS — +# the Blender version upstream's own Dockerfile targets — whose tarball is +# verified against the SHA-256 list blender.org publishes for that release. +# Compose tags the result localhost/heretek/blender-mcp:0.11.1. blender-mcp +# serves streamable HTTP natively (FastMCP), at http://blender-mcp:8781/mcp. + +FROM debian:trixie-20261005-slim AS src + +ARG BLENDER_MCP_TAG=v0.11.1 +ARG BLENDER_MCP_COMMIT=5a9e923f6ae68cfc34a221d31bd1fd5c8aff07f6 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates git; \ + git clone -q --depth 1 --branch "${BLENDER_MCP_TAG}" https://github.com/sandraschi/blender-mcp.git /src; \ + test "$(git -C /src rev-parse HEAD)" = "${BLENDER_MCP_COMMIT}"; \ + rm -rf /src/.git + +FROM python:3.12.15-slim-trixie + +ARG BLENDER_SERIES=4.2 +ARG BLENDER_VERSION=4.2.3 + +COPY --from=src /src /opt/blender-mcp-src + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates curl xz-utils \ + libgl1 libglib2.0-0 libx11-6 libxi6 libxxf86vm1 libxfixes3 libxrender1 libxkbcommon0 libsm6 libice6; \ + cd /tmp; \ + curl -fsSLO "https://download.blender.org/release/Blender${BLENDER_SERIES}/blender-${BLENDER_VERSION}-linux-x64.tar.xz"; \ + curl -fsSLO "https://download.blender.org/release/Blender${BLENDER_SERIES}/blender-${BLENDER_VERSION}.sha256"; \ + grep " blender-${BLENDER_VERSION}-linux-x64.tar.xz\$" "blender-${BLENDER_VERSION}.sha256" | sha256sum -c -; \ + tar -xJf "blender-${BLENDER_VERSION}-linux-x64.tar.xz"; \ + mv "blender-${BLENDER_VERSION}-linux-x64" /opt/blender; \ + rm -f /tmp/blender-*; \ + apt-get purge -y curl xz-utils; \ + apt-get autoremove -y; \ + rm -rf /var/lib/apt/lists/*; \ + pip install --no-cache-dir /opt/blender-mcp-src; \ + useradd -m -u 1000 -s /bin/bash mcp; \ + mkdir -p /data; \ + chown 1000:1000 /data + +ENV HOME=/home/mcp \ + BLENDER_EXECUTABLE=/opt/blender/blender +USER 1000:1000 +WORKDIR /data +EXPOSE 8781 + +CMD ["python", "-c", "from blender_mcp.server import main_http; main_http(host='0.0.0.0', port=8781)"] diff --git a/images/computer-use-linux/Dockerfile b/images/computer-use-linux/Dockerfile new file mode 100644 index 0000000..b7b236f --- /dev/null +++ b/images/computer-use-linux/Dockerfile @@ -0,0 +1,50 @@ +# images/computer-use-linux/Dockerfile — pinned local build of agent-sh/computer-use-linux (MIT) +# +# Upstream publishes no container image, so this build installs the v0.7.17 +# release binary (verified against the release's published SHA-256) on a +# pinned Debian trixie base with a headless Sway compositor and the AT-SPI2 +# accessibility bus. Compose tags the result +# localhost/heretek/computer-use-linux:0.7.17. The stdio MCP server +# (`computer-use-linux mcp`) is wrapped with a pinned mcp-proxy so the agent +# gateway can reach it over streamable HTTP at http://computer-use-linux:8790/mcp. + +FROM debian:trixie-20261005-slim + +ARG CUL_VERSION=0.7.17 +ARG CUL_SHA256=4b9dccfda2e1b593d7ce333f30f88f48dce14c2235efa4f3ef37468e6fbc10b5 +ARG MCP_PROXY_VERSION=0.13.0 + +COPY start.sh /usr/local/bin/heretek-computer-use +COPY sway-config /etc/heretek/sway-config + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates curl \ + sway at-spi2-core dbus dbus-user-session wtype grim fonts-dejavu-core \ + python3 python3-venv; \ + curl -fsSL -o /usr/local/bin/computer-use-linux \ + "https://github.com/agent-sh/computer-use-linux/releases/download/v${CUL_VERSION}/computer-use-linux-x86_64-unknown-linux-gnu"; \ + echo "${CUL_SHA256} /usr/local/bin/computer-use-linux" | sha256sum -c -; \ + chmod 0755 /usr/local/bin/computer-use-linux /usr/local/bin/heretek-computer-use; \ + apt-get purge -y curl; \ + apt-get autoremove -y; \ + rm -rf /var/lib/apt/lists/*; \ + python3 -m venv /opt/mcp-proxy; \ + /opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \ + ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \ + useradd -m -u 1000 -s /bin/bash agent; \ + mkdir -p /data; \ + chown 1000:1000 /data + +ENV HOME=/home/agent \ + XDG_RUNTIME_DIR=/tmp/xdg-runtime \ + WLR_BACKENDS=headless \ + WLR_RENDERER=pixman \ + WLR_LIBINPUT_NO_DEVICES=1 \ + COMPUTER_USE_LINUX_INDICATOR=0 +USER 1000:1000 +WORKDIR /data +EXPOSE 8790 + +CMD ["heretek-computer-use"] diff --git a/images/computer-use-linux/start.sh b/images/computer-use-linux/start.sh new file mode 100644 index 0000000..4e3f089 --- /dev/null +++ b/images/computer-use-linux/start.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Start a private D-Bus session bus and a headless Sway compositor, wait for +# the Wayland socket, then serve `computer-use-linux mcp` over streamable HTTP. +set -eu + +mkdir -p "$XDG_RUNTIME_DIR" +chmod 0700 "$XDG_RUNTIME_DIR" + +DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" +export DBUS_SESSION_BUS_ADDRESS +dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --fork --nopidfile + +sway --config /etc/heretek/sway-config & + +tries=0 +while [ ! -S "$XDG_RUNTIME_DIR/wayland-1" ] && [ ! -S "$XDG_RUNTIME_DIR/wayland-0" ]; do + tries=$((tries + 1)) + if [ "$tries" -gt 100 ]; then + echo "heretek-computer-use: sway did not create a Wayland socket" >&2 + exit 1 + fi + sleep 0.1 +done +if [ -S "$XDG_RUNTIME_DIR/wayland-1" ]; then WAYLAND_DISPLAY=wayland-1; else WAYLAND_DISPLAY=wayland-0; fi +export WAYLAND_DISPLAY + +exec mcp-proxy --host 0.0.0.0 --port "${MCP_PORT:-8790}" --pass-environment -- computer-use-linux mcp diff --git a/images/computer-use-linux/sway-config b/images/computer-use-linux/sway-config new file mode 100644 index 0000000..87a1c57 --- /dev/null +++ b/images/computer-use-linux/sway-config @@ -0,0 +1,8 @@ +# Minimal headless Sway session for computer-use-linux (no input devices, +# software rendering, one virtual output). +output HEADLESS-1 resolution 1920x1080 position 0,0 +default_border none +focus_follows_mouse no + +# AT-SPI2 accessibility bus for the accessibility-tree tools. +exec /usr/libexec/at-spi-bus-launcher --launch-immediately diff --git a/images/ghidra-mcp/Dockerfile b/images/ghidra-mcp/Dockerfile new file mode 100644 index 0000000..70236c1 --- /dev/null +++ b/images/ghidra-mcp/Dockerfile @@ -0,0 +1,106 @@ +# images/ghidra-mcp/Dockerfile — pinned local build of bethington/ghidra-mcp (Apache-2.0) +# +# No image is published for ghidra-mcp v6.0.0, so this build follows the +# upstream docker/Dockerfile recipe from the v6.0.0 tag (commit verified after +# checkout) against the official Ghidra 12.1.2 release (pinned SHA-256; the +# Ghidra version upstream's pom.xml requires). Compose tags the result +# localhost/heretek/ghidra-mcp:6.0.0. +# +# The container runs two processes: the headless Ghidra REST server bound to +# 127.0.0.1:8080 inside the container only, and upstream's MCP bridge serving +# streamable HTTP on 0.0.0.0:8089/mcp for the agent gateway. Both enforce +# GHIDRA_MCP_AUTH_TOKEN. + +FROM debian:trixie-20261005-slim AS src + +ARG GHIDRA_MCP_TAG=v6.0.0 +ARG GHIDRA_MCP_COMMIT=8cd2078e10b9ba28b188cb84ce5b9051a904b995 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates git; \ + git clone -q --depth 1 --branch "${GHIDRA_MCP_TAG}" https://github.com/bethington/ghidra-mcp.git /src; \ + test "$(git -C /src rev-parse HEAD)" = "${GHIDRA_MCP_COMMIT}"; \ + rm -rf /src/.git + +FROM eclipse-temurin:21.0.12.1_1-jdk-noble AS ghidra + +ARG GHIDRA_VERSION=12.1.2 +ARG GHIDRA_DATE=20260605 +ARG GHIDRA_SHA256=b62e81a0390618466c019c60d8c2f796ced2509c4c1aea4a37644a77272cf99d + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates curl unzip; \ + curl -fsSL -o /tmp/ghidra.zip \ + "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip"; \ + echo "${GHIDRA_SHA256} /tmp/ghidra.zip" | sha256sum -c -; \ + unzip -q /tmp/ghidra.zip -d /opt; \ + mv "/opt/ghidra_${GHIDRA_VERSION}_PUBLIC" /opt/ghidra; \ + rm /tmp/ghidra.zip; \ + jython="$(ls /opt/ghidra/Extensions/Ghidra/*_Jython.zip | head -n1)"; \ + mkdir -p /opt/ghidra/Ghidra/Extensions; \ + unzip -q "$jython" -d /opt/ghidra/Ghidra/Extensions/ + +FROM eclipse-temurin:21.0.12.1_1-jdk-noble AS build + +ARG GHIDRA_VERSION=12.1.2 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends maven; \ + rm -rf /var/lib/apt/lists/* + +COPY --from=ghidra /opt/ghidra /opt/ghidra +COPY --from=src /src/pom.xml /build/pom.xml +COPY --from=src /src/src /build/src +COPY --from=src /src/lib /build/lib + +# Install the Ghidra framework JARs the plugin compiles against into the local +# Maven repository at the exact version pom.xml expects (same list as +# upstream's docker/Dockerfile). +RUN set -eux; \ + for jar in \ + Framework/Generic Framework/SoftwareModeling Framework/Project Framework/Docking \ + Framework/Utility Framework/Gui Framework/FileSystem Framework/Help \ + Features/Base Features/Decompiler Framework/DB Framework/Emulation \ + Debug/Debugger-api Debug/Framework-TraceModeling Debug/Debugger-rmi-trace; do \ + name="${jar##*/}"; \ + mvn -q install:install-file -Dfile="/opt/ghidra/Ghidra/${jar}/lib/${name}.jar" \ + -DgroupId=ghidra -DartifactId="${name}" -Dversion="${GHIDRA_VERSION}" -Dpackaging=jar; \ + done; \ + cd /build; \ + mvn -q clean package -P headless -DskipTests + +FROM eclipse-temurin:21.0.12.1_1-jdk-noble + +COPY --from=ghidra /opt/ghidra /opt/ghidra +COPY --from=build /build/target/GhidraMCP-*.jar /app/GhidraMCP.jar +COPY --from=src /src/docker/entrypoint.sh /app/entrypoint.sh +COPY --from=src /src /opt/ghidra-mcp-src +COPY start.sh /usr/local/bin/heretek-ghidra-mcp + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends python3 python3-venv; \ + rm -rf /var/lib/apt/lists/*; \ + sed -i 's/\r$//' /app/entrypoint.sh; \ + chmod 0755 /app/entrypoint.sh /usr/local/bin/heretek-ghidra-mcp; \ + python3 -m venv /opt/ghidra-mcp-bridge; \ + /opt/ghidra-mcp-bridge/bin/pip install --no-cache-dir /opt/ghidra-mcp-src; \ + rm -rf /opt/ghidra-mcp-src; \ + ln -s /opt/ghidra-mcp-bridge/bin/bridge-mcp-ghidra /usr/local/bin/bridge-mcp-ghidra; \ + mkdir -p /data /projects /home/ghidra; \ + chown 1000:1000 /data /projects /home/ghidra + +ENV GHIDRA_HOME=/opt/ghidra \ + HOME=/home/ghidra \ + JAVA_OPTS="-Xmx4g -XX:+UseG1GC" \ + GHIDRA_MCP_BIND_ADDRESS=127.0.0.1 \ + GHIDRA_MCP_PORT=8080 \ + GHIDRA_MCP_URL=http://127.0.0.1:8080 +USER 1000:1000 +WORKDIR /app +EXPOSE 8089 + +CMD ["heretek-ghidra-mcp"] diff --git a/images/ghidra-mcp/start.sh b/images/ghidra-mcp/start.sh new file mode 100644 index 0000000..847a9ae --- /dev/null +++ b/images/ghidra-mcp/start.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Run the headless Ghidra REST server on the container-internal loopback +# (127.0.0.1:${GHIDRA_MCP_PORT}) and expose upstream's MCP bridge over +# streamable HTTP on ${MCP_PORT}/mcp. Fails closed without the auth token. +set -eu + +if [ -z "${GHIDRA_MCP_AUTH_TOKEN:-}" ]; then + echo "heretek-ghidra-mcp: GHIDRA_MCP_AUTH_TOKEN is mandatory" >&2 + exit 1 +fi + +/app/entrypoint.sh & +server_pid=$! +trap 'kill "$server_pid" 2>/dev/null || true' EXIT INT TERM + +bridge-mcp-ghidra --transport streamable-http --mcp-host 0.0.0.0 --mcp-port "${MCP_PORT:-8089}" & +bridge_pid=$! + +# Exit (and let the orchestrator restart us) as soon as either process dies. +while kill -0 "$server_pid" 2>/dev/null && kill -0 "$bridge_pid" 2>/dev/null; do + sleep 2 +done +kill "$server_pid" "$bridge_pid" 2>/dev/null || true +exit 1 diff --git a/images/godot-mcp/Dockerfile b/images/godot-mcp/Dockerfile new file mode 100644 index 0000000..b9d0488 --- /dev/null +++ b/images/godot-mcp/Dockerfile @@ -0,0 +1,60 @@ +# images/godot-mcp/Dockerfile — pinned local build of tugcantopaloglu/godot-mcp (MIT) +# +# Upstream publishes no container image and its npm package lags the v3.1.0 +# tag, so this build compiles the tagged source (commit verified after +# checkout) and pairs it with the official Godot 4.3-stable Linux binary +# (verified against the release's published SHA-512). Compose tags the result +# localhost/heretek/godot-mcp:3.1.0. The stdio MCP server is wrapped with a +# pinned mcp-proxy so the agent gateway can reach it over streamable HTTP at +# http://godot-mcp:8780/mcp. + +FROM node:22.23.3-trixie-slim AS build + +ARG GODOT_MCP_TAG=v3.1.0 +ARG GODOT_MCP_COMMIT=fcbc29e03297900b9a1fcc6c7cbae116fe9d532a + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates git; \ + git clone -q --depth 1 --branch "${GODOT_MCP_TAG}" https://github.com/tugcantopaloglu/godot-mcp.git /src; \ + test "$(git -C /src rev-parse HEAD)" = "${GODOT_MCP_COMMIT}"; \ + cd /src; \ + npm ci --ignore-scripts; \ + npm run build; \ + npm prune --omit=dev + +FROM node:22.23.3-trixie-slim + +ARG GODOT_VERSION=4.3-stable +ARG GODOT_SHA512=fd52bb4ba8acc30ca5accd1c566d470ad7282f891ccc0995dfafabcf92bcf76280ce182bf9d80ebd885f3ed2165d01e1fc3f2928436b15498dfbd98656c2a45a +ARG MCP_PROXY_VERSION=0.13.0 + +COPY --from=build /src/build /opt/godot-mcp/build +COPY --from=build /src/node_modules /opt/godot-mcp/node_modules +COPY --from=build /src/package.json /opt/godot-mcp/package.json + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates curl fontconfig python3 python3-venv unzip; \ + curl -fsSL -o /tmp/godot.zip "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_linux.x86_64.zip"; \ + echo "${GODOT_SHA512} /tmp/godot.zip" | sha512sum -c -; \ + unzip -q /tmp/godot.zip -d /opt/godot; \ + mv "/opt/godot/Godot_v${GODOT_VERSION}_linux.x86_64" /opt/godot/godot; \ + rm /tmp/godot.zip; \ + apt-get purge -y curl unzip; \ + apt-get autoremove -y; \ + rm -rf /var/lib/apt/lists/*; \ + python3 -m venv /opt/mcp-proxy; \ + /opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \ + ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \ + mkdir -p /projects; \ + chown 1000:1000 /projects + +# The node base image already ships the non-root `node` user as uid/gid 1000. +ENV HOME=/home/node \ + GODOT_PATH=/opt/godot/godot +USER 1000:1000 +WORKDIR /projects +EXPOSE 8780 + +CMD ["mcp-proxy", "--host", "0.0.0.0", "--port", "8780", "--pass-environment", "--", "node", "/opt/godot-mcp/build/index.js"] diff --git a/images/maestro-mcp/Dockerfile b/images/maestro-mcp/Dockerfile new file mode 100644 index 0000000..e576cc2 --- /dev/null +++ b/images/maestro-mcp/Dockerfile @@ -0,0 +1,40 @@ +# images/maestro-mcp/Dockerfile — pinned local build of mobile-dev-inc/maestro (Apache-2.0) +# +# Maestro publishes no container image, so compose builds this one as +# localhost/heretek/maestro-mcp:2.11.0. It installs the cli-2.11.0 release +# archive (verified against a pinned SHA-256) on a pinned JRE and wraps the +# stdio `maestro mcp` server with a pinned mcp-proxy so the agent gateway can +# reach it over streamable HTTP at http://maestro-mcp:8771/mcp. + +FROM eclipse-temurin:21.0.12.1_1-jre-noble + +ARG MAESTRO_VERSION=2.11.0 +ARG MAESTRO_SHA256=5384593cb4e7a106489e75a821d157dd43f4e438df6bc308b72e82c685e1283a +ARG MCP_PROXY_VERSION=0.13.0 + +COPY start.sh /usr/local/bin/heretek-maestro-mcp + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends adb ca-certificates curl python3 python3-venv unzip; \ + rm -rf /var/lib/apt/lists/*; \ + curl -fsSL -o /tmp/maestro.zip "https://github.com/mobile-dev-inc/maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip"; \ + echo "${MAESTRO_SHA256} /tmp/maestro.zip" | sha256sum -c -; \ + unzip -q /tmp/maestro.zip -d /opt; \ + rm /tmp/maestro.zip; \ + ln -s /opt/maestro/bin/maestro /usr/local/bin/maestro; \ + python3 -m venv /opt/mcp-proxy; \ + /opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \ + ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \ + chmod 0755 /usr/local/bin/heretek-maestro-mcp; \ + if ! getent passwd 1000 >/dev/null; then useradd -m -u 1000 -s /bin/bash mcp; fi; \ + mkdir -p /data /home/mcp; \ + chown 1000:1000 /data /home/mcp + +ENV HOME=/home/mcp \ + MAESTRO_CLI_NO_ANALYTICS=1 +USER 1000:1000 +WORKDIR /data +EXPOSE 8771 + +CMD ["heretek-maestro-mcp"] diff --git a/images/maestro-mcp/start.sh b/images/maestro-mcp/start.sh new file mode 100644 index 0000000..9929d57 --- /dev/null +++ b/images/maestro-mcp/start.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Attach the local adb server to the emulator, then serve `maestro mcp` over +# streamable HTTP. A failed connect is not fatal: the emulator may still be +# booting, and Maestro reports device errors per tool call. +set -eu +adb start-server +adb connect "${ANDROID_HOST:-android-kvm}:${ADB_PORT:-5555}" || true +exec mcp-proxy --host 0.0.0.0 --port "${MCP_PORT:-8771}" --pass-environment -- maestro mcp diff --git a/images/mdb-mcp/Dockerfile b/images/mdb-mcp/Dockerfile new file mode 100644 index 0000000..f72e3fc --- /dev/null +++ b/images/mdb-mcp/Dockerfile @@ -0,0 +1,43 @@ +# images/mdb-mcp/Dockerfile — pinned local build of smadi0x86/mdb-mcp (GPL-3.0) +# +# Upstream publishes no image, no PyPI package and no release tag, so this +# build pins an exact upstream commit (package version 0.2.0) and verifies it +# after checkout. Compose tags the result localhost/heretek/mdb-mcp:0.2.0-5d03b25. +# GDB is the debugger backend; the compose service grants only SYS_PTRACE. +# The stdio MCP server is wrapped with a pinned mcp-proxy so the agent gateway +# can reach it over streamable HTTP at http://mdb-mcp:8767/mcp. + +FROM debian:trixie-20261005-slim AS src + +ARG MDB_MCP_COMMIT=5d03b25632c572e3640fd1004e9507600ed431d3 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates git; \ + git init -q /src; \ + cd /src; \ + git fetch -q --depth 1 https://github.com/smadi0x86/mdb-mcp.git "${MDB_MCP_COMMIT}"; \ + git checkout -q FETCH_HEAD; \ + test "$(git rev-parse HEAD)" = "${MDB_MCP_COMMIT}"; \ + rm -rf .git + +FROM python:3.12.15-slim-trixie + +ARG MCP_PROXY_VERSION=0.13.0 + +COPY --from=src /src /opt/mdb-mcp-src + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends gdb; \ + rm -rf /var/lib/apt/lists/*; \ + pip install --no-cache-dir /opt/mdb-mcp-src "mcp-proxy==${MCP_PROXY_VERSION}"; \ + useradd -m -u 1000 -s /bin/bash mcp; \ + mkdir -p /data; \ + chown 1000:1000 /data + +USER 1000:1000 +WORKDIR /data +EXPOSE 8767 + +CMD ["mcp-proxy", "--host", "0.0.0.0", "--port", "8767", "--pass-environment", "--", "mdb-mcp"] diff --git a/images/radare2-mcp/Dockerfile b/images/radare2-mcp/Dockerfile new file mode 100644 index 0000000..d25d931 --- /dev/null +++ b/images/radare2-mcp/Dockerfile @@ -0,0 +1,39 @@ +# images/radare2-mcp/Dockerfile — pinned local build of radareorg/radare2-mcp +# +# No container image is published for radare2-mcp, so compose builds this one +# as localhost/heretek/radare2-mcp:1.9.0. It installs the upstream release +# Debian packages (MIT r2mcp 1.9.0, LGPL radare2 6.2.4 — the radare2 version +# the 1.9.0 release was built against), each verified against a pinned SHA-256. +# r2mcp serves MCP over HTTP natively (`-H`), so no stdio bridge is needed. + +FROM debian:trixie-20261005-slim + +ARG R2_VERSION=6.2.4 +ARG R2_SHA256=7019eedc0e0e87d1f53d6b8f5fc62b898567679c5efdce7fbfc557c9e7655e90 +ARG R2MCP_VERSION=1.9.0 +ARG R2MCP_SHA256=d426861a726f7e9bc13a44cc3bbfd8553c51b33fc05f1ff0eda75b433222d9e3 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates curl; \ + cd /tmp; \ + curl -fsSLO "https://github.com/radareorg/radare2/releases/download/${R2_VERSION}/radare2_${R2_VERSION}_amd64.deb"; \ + curl -fsSLO "https://github.com/radareorg/radare2-mcp/releases/download/${R2MCP_VERSION}/r2mcp_${R2MCP_VERSION}_amd64.deb"; \ + echo "${R2_SHA256} radare2_${R2_VERSION}_amd64.deb" | sha256sum -c -; \ + echo "${R2MCP_SHA256} r2mcp_${R2MCP_VERSION}_amd64.deb" | sha256sum -c -; \ + apt-get install -y --no-install-recommends "./radare2_${R2_VERSION}_amd64.deb" "./r2mcp_${R2MCP_VERSION}_amd64.deb"; \ + apt-get purge -y curl; \ + apt-get autoremove -y; \ + rm -rf /var/lib/apt/lists/* /tmp/*.deb; \ + useradd -m -u 1000 -s /bin/bash mcp; \ + mkdir -p /data; \ + chown 1000:1000 /data; \ + r2mcp -v + +USER 1000:1000 +WORKDIR /data +EXPOSE 8765 + +# -R: read-only tool set (never -r, which enables the dangerous run_* tools); +# -s /data: sandbox file access to the data volume. +CMD ["r2mcp", "-H", "0.0.0.0:8765", "-R", "-s", "/data"] diff --git a/images/rea-mcp/Dockerfile b/images/rea-mcp/Dockerfile new file mode 100644 index 0000000..6337901 --- /dev/null +++ b/images/rea-mcp/Dockerfile @@ -0,0 +1,32 @@ +# images/rea-mcp/Dockerfile — pinned local build of morluto/rea (MIT) +# +# No container image is published for REA, so compose builds this one as +# localhost/heretek/rea-mcp:6.5.0. It installs the pinned npm release +# rea-agents@6.5.0 (the package behind the upstream rea-agents-6.5.0 tag) and +# wraps its stdio MCP server (`rea mcp`) with a pinned mcp-proxy so the agent +# gateway can reach it over streamable HTTP at http://rea-mcp:8766/mcp. + +FROM node:22.23.3-trixie-slim + +ARG REA_VERSION=6.5.0 +ARG MCP_PROXY_VERSION=0.13.0 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends python3 python3-venv ca-certificates; \ + rm -rf /var/lib/apt/lists/*; \ + npm install --global --omit=dev "rea-agents@${REA_VERSION}"; \ + npm cache clean --force; \ + python3 -m venv /opt/mcp-proxy; \ + /opt/mcp-proxy/bin/pip install --no-cache-dir "mcp-proxy==${MCP_PROXY_VERSION}"; \ + ln -s /opt/mcp-proxy/bin/mcp-proxy /usr/local/bin/mcp-proxy; \ + mkdir -p /data; \ + chown 1000:1000 /data + +# The node base image already ships the non-root `node` user as uid/gid 1000. +ENV HOME=/home/node +USER 1000:1000 +WORKDIR /data +EXPOSE 8766 + +CMD ["mcp-proxy", "--host", "0.0.0.0", "--port", "8766", "--pass-environment", "--", "rea", "mcp"]