#!/usr/bin/env bash # ============================================================================= # 40-shell-mcp — build and start the NATIVE shell-mcp on this host. # # This replaces the mediabox-mcp SSH proxy that currently runs on arrsstack. # Once this is up and NPM proxy host 42 is repointed to 10.0.1.20:8103, the # proxy container and its SSH key get deleted. The public URL, the LE cert and # the Claude connector entry do not change. # # BUILDS LOCALLY. It does not pull an image. The arrsstack image is arm64 and # would not run here; and there is no registry in this homelab to pull from. # amd64 buildability was verified before this was written — the full Python # dependency tree resolves to prebuilt manylinux x86_64 wheels, so no compiler # is required and the build takes about a minute. # ============================================================================= set -uo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SRC="$REPO_DIR/shell-mcp" DEST="/srv/shell-mcp" ENVFILE="/srv/secrets/stacks/shell-mcp.env" MASTER="/srv/secrets/stacks.env" command -v docker >/dev/null 2>&1 || { echo " [FAIL] docker not installed"; exit 1; } # --- resolve the bearer token ------------------------------------------------ # Reuses the SAME token arrsstack's mediabox-mcp serves today, so the Claude # connector entry survives the cutover untouched. TOKEN="$(grep -E '^MEDIABOX_MCP_BEARER_TOKEN=' "$MASTER" 2>/dev/null | head -1 | cut -d= -f2-)" if [ -z "${TOKEN:-}" ] || [ "$TOKEN" = "FILL_ME" ]; then cat <<'MSG' [SKIP] MEDIABOX_MCP_BEARER_TOKEN is not set in /srv/secrets/stacks.env. Copy the existing value from arrsstack so the connector keeps working: # on arrsstack grep '^MEDIABOX_MCP_BEARER_TOKEN=' /srv/secrets/stacks.env then put it in this host's /srv/secrets/stacks.env and re-run: sudo /srv/mediabox-bootstrap/scripts/40-shell-mcp.sh MSG exit 0 fi # --- stage source ------------------------------------------------------------ install -d -m 0755 "$DEST" install -m 0644 "$SRC/server.py" "$DEST/server.py" install -m 0644 "$SRC/requirements.txt" "$DEST/requirements.txt" install -m 0644 "$SRC/Dockerfile" "$DEST/Dockerfile" install -m 0644 "$SRC/docker-compose.yml" "$DEST/docker-compose.yml" # --- per-stack env slice (same convention as arrsstack) ---------------------- install -d -m 0700 /srv/secrets/stacks umask 077 { printf 'MEDIABOX_MCP_BEARER_TOKEN=%s\n' "$TOKEN" printf 'TZ=%s\n' "$(grep -E '^TZ=' "$MASTER" | head -1 | cut -d= -f2- || echo America/New_York)" } > "$ENVFILE" chmod 600 "$ENVFILE" # --- build & start ----------------------------------------------------------- echo " building shell-mcp for amd64 (local build, no pull)" cd "$DEST" || exit 1 set -a # shellcheck disable=SC1090 . "$ENVFILE" set +a if ! docker compose up -d --build; then echo " [FAIL] build/start failed" exit 1 fi # --- verify ------------------------------------------------------------------ echo " waiting for health endpoint" for i in $(seq 1 30); do if curl -fsS -m 3 "http://127.0.0.1:8103/health" >/dev/null 2>&1; then echo " [ok] /health responding: $(curl -fsS -m 3 http://127.0.0.1:8103/health)" # An unauthenticated /sse MUST be rejected. If this ever returns 200 the # box is publicly shell-able through NPM. code="$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://127.0.0.1:8103/sse" || true)" if [ "$code" = "401" ]; then echo " [ok] /sse rejects unauthenticated requests (401)" else echo " [FAIL] /sse returned $code without a token — DO NOT repoint NPM until fixed" exit 1 fi exit 0 fi sleep 2 done echo " [FAIL] health endpoint never came up" docker compose logs --tail 40 shell-mcp 2>&1 | sed 's/^/ /' exit 1