From 77159ce1cdbb2702718549ed464ce48adecabda8 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 26 May 2026 15:13:14 +0200 Subject: [PATCH 1/3] feat(scripts): add LXC/bare-metal MCP install script Adds scripts/lxc-mcp-install.sh for Proxmox LXC and other non-Docker hosts. Creates a homelable-mcp systemd service, prompts for MCP_API_KEY / MCP_SERVICE_KEY (auto-generated on Enter), and skips prompts when mcp/.env already exists so user edits are preserved. Closes #132 --- README.md | 4 + scripts/lxc-mcp-install.sh | 153 +++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100755 scripts/lxc-mcp-install.sh diff --git a/README.md b/README.md index 8b5ada9..592376c 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,10 @@ docker compose up -d mcp # MCP server is now listening on http://:8001 ``` +> **Proxmox LXC / bare-metal (no Docker):** run `sudo bash scripts/lxc-mcp-install.sh`. +> Installs a `homelable-mcp` systemd service, prompts for `MCP_API_KEY` / `MCP_SERVICE_KEY` +> (auto-generated if you press Enter), and skips prompts if `mcp/.env` already exists. + **3. Configure your AI client:** **Claude Code** — run this command in your terminal: diff --git a/scripts/lxc-mcp-install.sh b/scripts/lxc-mcp-install.sh new file mode 100755 index 0000000..957c5a4 --- /dev/null +++ b/scripts/lxc-mcp-install.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# Install / enable the Homelable MCP server as a systemd service on a +# Proxmox LXC (or any Debian/Ubuntu host) where Docker is NOT used. +# +# Idempotent: re-running is safe. If mcp/.env already exists, the script +# keeps it untouched and only refreshes the venv + systemd unit. +# +# Usage (run as root inside the LXC): +# bash scripts/lxc-mcp-install.sh +# INSTALL_DIR=/srv/homelable bash scripts/lxc-mcp-install.sh +set -euo pipefail + +INSTALL_DIR="${INSTALL_DIR:-/opt/homelable}" +SERVICE_USER="${SERVICE_USER:-homelable}" +SERVICE_NAME="homelable-mcp" +MCP_PORT="${MCP_PORT:-8001}" +DEFAULT_BACKEND_URL="http://127.0.0.1:8000" + +log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; } +fail() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; } + +# --- preflight --------------------------------------------------------------- +[[ $EUID -eq 0 ]] || fail "Run as root (sudo bash $0)." + +MCP_DIR="$INSTALL_DIR/mcp" +[[ -d "$MCP_DIR" ]] || fail "Not found: $MCP_DIR — clone the repo to $INSTALL_DIR first, or set INSTALL_DIR=..." +[[ -f "$MCP_DIR/requirements.txt" ]] || fail "Missing $MCP_DIR/requirements.txt — repo layout unexpected." + +if ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${MCP_PORT}$"; then + warn "Port $MCP_PORT already in use. If it's a previous $SERVICE_NAME instance this is fine; otherwise abort and free the port." +fi + +# --- packages ---------------------------------------------------------------- +log "Installing OS dependencies (python3-venv, curl)" +apt-get update -qq +DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-venv python3-pip curl >/dev/null + +# --- service user ------------------------------------------------------------ +if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then + log "Creating service user '$SERVICE_USER'" + useradd --system --home "$INSTALL_DIR" --shell /usr/sbin/nologin "$SERVICE_USER" +fi + +# --- .env -------------------------------------------------------------------- +ENV_FILE="$MCP_DIR/.env" +if [[ -f "$ENV_FILE" ]]; then + log ".env already present at $ENV_FILE — keeping user values (prompts skipped)" +else + [[ -f "$MCP_DIR/.env.example" ]] || fail "Missing $MCP_DIR/.env.example" + log "No .env found — generating one (press Enter to accept defaults)" + + gen_key() { python3 -c "import secrets;print('$1' + secrets.token_hex(24))"; } + default_api_key="$(gen_key mcp_sk_)" + default_svc_key="$(gen_key svc_)" + + read -rp "MCP_API_KEY (client → MCP) [default: auto-generate]: " api_key + api_key="${api_key:-$default_api_key}" + + read -rp "MCP_SERVICE_KEY (MCP → backend, must match backend .env) [default: auto-generate]: " svc_key + svc_key="${svc_key:-$default_svc_key}" + + read -rp "BACKEND_URL [$DEFAULT_BACKEND_URL]: " backend_url + backend_url="${backend_url:-$DEFAULT_BACKEND_URL}" + + umask 077 + cat >"$ENV_FILE" <"$UNIT" </dev/null 2>&1; then + ok=1; break + fi + sleep 1 +done +if [[ "$ok" -ne 1 ]]; then + warn "MCP did not respond on /health within 10s. Check: journalctl -u $SERVICE_NAME -n 50" +else + log "MCP server is up." +fi + +# --- summary ----------------------------------------------------------------- +LXC_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" +API_KEY_VALUE="$(grep -E '^MCP_API_KEY=' "$ENV_FILE" | cut -d= -f2-)" + +cat <}:${MCP_PORT}/mcp + Env file: $ENV_FILE + Logs: journalctl -u $SERVICE_NAME -f + +Claude Code client setup: + claude mcp add --transport sse homelable http://${LXC_IP:-}:${MCP_PORT}/mcp \\ + --header "X-API-Key: $API_KEY_VALUE" +---------------------------------------------------------------- +EOF From fc765fa25500847de4ef673901c5e2c4a7bed3b1 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Wed, 27 May 2026 21:32:55 +0200 Subject: [PATCH 2/3] ci(docker): publish homelable-mcp image to GHCR Add mcp matrix entry in docker-publish workflow so the MCP server image is built and pushed alongside backend/frontend. Also wire the prebuilt compose file so users can pull and run it directly. Closes #162 --- .github/workflows/docker-publish.yml | 11 +++++++++-- docker-compose.prebuilt.yml | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 600ae67..17c817f 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -16,14 +16,21 @@ jobs: matrix: include: - image: ghcr.io/pouzor/homelable-backend + context: . dockerfile: Dockerfile.backend build_args: "" - image: ghcr.io/pouzor/homelable-frontend + context: . dockerfile: Dockerfile.frontend build_args: "" - image: ghcr.io/pouzor/homelable-frontend-standalone + context: . dockerfile: Dockerfile.frontend build_args: "VITE_STANDALONE=true" + - image: ghcr.io/pouzor/homelable-mcp + context: ./mcp + dockerfile: Dockerfile.mcp + build_args: "" steps: - uses: actions/checkout@v4 @@ -55,8 +62,8 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 with: - context: . - file: ${{ matrix.dockerfile }} + context: ${{ matrix.context }} + file: ${{ matrix.context }}/${{ matrix.dockerfile }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/docker-compose.prebuilt.yml b/docker-compose.prebuilt.yml index 6a9d777..c62ae23 100644 --- a/docker-compose.prebuilt.yml +++ b/docker-compose.prebuilt.yml @@ -24,6 +24,20 @@ services: networks: - homelable + mcp: + image: ghcr.io/pouzor/homelable-mcp:latest + restart: unless-stopped + ports: + - "8001:8001" + env_file: + - .env + environment: + BACKEND_URL: "http://backend:8000" + depends_on: + - backend + networks: + - homelable + volumes: backend_data: From 529c75a175056ab0df247d4b25f26ef2a0d43724 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Wed, 27 May 2026 22:27:54 +0200 Subject: [PATCH 3/3] feat(scripts): lxc-mcp-install env-var overrides + repo clone fallback - All prompted values overridable via env vars (MCP_API_KEY, MCP_SERVICE_KEY, BACKEND_URL, INSTALL_DIR, etc.). - Clone the repo into INSTALL_DIR if it isn't already present, so the script can be fetched and run directly inside a fresh LXC created by the community-scripts/ProxmoxVE helper (no manual git clone first). - README: clarify the Proxmox flow (community-scripts creates the LXC, user runs this script inside it). --- README.md | 4 +- scripts/lxc-mcp-install.sh | 83 +++++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 592376c..553b632 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,9 @@ docker compose up -d mcp # MCP server is now listening on http://:8001 ``` -> **Proxmox LXC / bare-metal (no Docker):** run `sudo bash scripts/lxc-mcp-install.sh`. +> **Proxmox LXC / bare-metal (no Docker):** create the LXC via +> [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE) (or any +> Debian/Ubuntu LXC), then inside it run `sudo bash scripts/lxc-mcp-install.sh`. > Installs a `homelable-mcp` systemd service, prompts for `MCP_API_KEY` / `MCP_SERVICE_KEY` > (auto-generated if you press Enter), and skips prompts if `mcp/.env` already exists. diff --git a/scripts/lxc-mcp-install.sh b/scripts/lxc-mcp-install.sh index 957c5a4..3e05838 100755 --- a/scripts/lxc-mcp-install.sh +++ b/scripts/lxc-mcp-install.sh @@ -1,16 +1,27 @@ #!/bin/bash -# Install / enable the Homelable MCP server as a systemd service on a -# Proxmox LXC (or any Debian/Ubuntu host) where Docker is NOT used. +# Install / enable the Homelable MCP server as a systemd service. +# +# Run interactively as root, inside an LXC or any Debian/Ubuntu host. +# Typical Proxmox VE flow: create the LXC via the community-scripts/ProxmoxVE +# helper, then run this script inside that LXC. # # Idempotent: re-running is safe. If mcp/.env already exists, the script # keeps it untouched and only refreshes the venv + systemd unit. # -# Usage (run as root inside the LXC): -# bash scripts/lxc-mcp-install.sh -# INSTALL_DIR=/srv/homelable bash scripts/lxc-mcp-install.sh +# Optional env vars (override defaults / skip the matching prompt): +# INSTALL_DIR repo root (default: /opt/homelable) +# REPO_URL clone URL if $INSTALL_DIR is empty (default: https://github.com/Pouzor/homelable.git) +# REPO_REF branch/tag/commit when cloning (default: main) +# SERVICE_USER systemd User= (default: homelable) +# MCP_PORT listen port (default: 8001) +# MCP_API_KEY client → MCP key (default: prompt, auto-gen on empty) +# MCP_SERVICE_KEY MCP → backend key (default: prompt, auto-gen on empty; must match backend .env) +# BACKEND_URL backend base URL (default: http://127.0.0.1:8000) set -euo pipefail INSTALL_DIR="${INSTALL_DIR:-/opt/homelable}" +REPO_URL="${REPO_URL:-https://github.com/Pouzor/homelable.git}" +REPO_REF="${REPO_REF:-main}" SERVICE_USER="${SERVICE_USER:-homelable}" SERVICE_NAME="homelable-mcp" MCP_PORT="${MCP_PORT:-8001}" @@ -20,48 +31,57 @@ log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; } -# --- preflight --------------------------------------------------------------- [[ $EUID -eq 0 ]] || fail "Run as root (sudo bash $0)." +log "Installing OS dependencies (git, python3-venv, curl)" +apt-get update -qq +DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + git python3 python3-venv python3-pip curl iproute2 >/dev/null + MCP_DIR="$INSTALL_DIR/mcp" -[[ -d "$MCP_DIR" ]] || fail "Not found: $MCP_DIR — clone the repo to $INSTALL_DIR first, or set INSTALL_DIR=..." +if [[ ! -d "$MCP_DIR" ]]; then + log "Cloning $REPO_URL ($REPO_REF) → $INSTALL_DIR" + mkdir -p "$(dirname "$INSTALL_DIR")" + git clone --depth 1 --branch "$REPO_REF" "$REPO_URL" "$INSTALL_DIR" +fi [[ -f "$MCP_DIR/requirements.txt" ]] || fail "Missing $MCP_DIR/requirements.txt — repo layout unexpected." if ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${MCP_PORT}$"; then warn "Port $MCP_PORT already in use. If it's a previous $SERVICE_NAME instance this is fine; otherwise abort and free the port." fi -# --- packages ---------------------------------------------------------------- -log "Installing OS dependencies (python3-venv, curl)" -apt-get update -qq -DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-venv python3-pip curl >/dev/null - -# --- service user ------------------------------------------------------------ if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then log "Creating service user '$SERVICE_USER'" useradd --system --home "$INSTALL_DIR" --shell /usr/sbin/nologin "$SERVICE_USER" fi -# --- .env -------------------------------------------------------------------- ENV_FILE="$MCP_DIR/.env" +gen_key() { python3 -c "import secrets;print('$1' + secrets.token_hex(24))"; } + if [[ -f "$ENV_FILE" ]]; then - log ".env already present at $ENV_FILE — keeping user values (prompts skipped)" + log ".env already present at $ENV_FILE — keeping existing values" else [[ -f "$MCP_DIR/.env.example" ]] || fail "Missing $MCP_DIR/.env.example" log "No .env found — generating one (press Enter to accept defaults)" - gen_key() { python3 -c "import secrets;print('$1' + secrets.token_hex(24))"; } - default_api_key="$(gen_key mcp_sk_)" - default_svc_key="$(gen_key svc_)" + api_key="${MCP_API_KEY:-}" + svc_key="${MCP_SERVICE_KEY:-}" + backend_url="${BACKEND_URL:-}" - read -rp "MCP_API_KEY (client → MCP) [default: auto-generate]: " api_key - api_key="${api_key:-$default_api_key}" - - read -rp "MCP_SERVICE_KEY (MCP → backend, must match backend .env) [default: auto-generate]: " svc_key - svc_key="${svc_key:-$default_svc_key}" - - read -rp "BACKEND_URL [$DEFAULT_BACKEND_URL]: " backend_url - backend_url="${backend_url:-$DEFAULT_BACKEND_URL}" + if [[ -z "$api_key" ]]; then + default_api_key="$(gen_key mcp_sk_)" + read -rp "MCP_API_KEY (client → MCP) [default: auto-generate]: " api_key + api_key="${api_key:-$default_api_key}" + fi + if [[ -z "$svc_key" ]]; then + default_svc_key="$(gen_key svc_)" + read -rp "MCP_SERVICE_KEY (MCP → backend, must match backend .env) [default: auto-generate]: " svc_key + svc_key="${svc_key:-$default_svc_key}" + fi + if [[ -z "$backend_url" ]]; then + read -rp "BACKEND_URL [$DEFAULT_BACKEND_URL]: " backend_url + backend_url="${backend_url:-$DEFAULT_BACKEND_URL}" + fi umask 077 cat >"$ENV_FILE" <"$UNIT" <