diff --git a/README.md b/README.md index bf88bf9..aeaf5fb 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ at `.` — **no host ports**, ingress is Traefik-only). | 🔗 n8n | Workflow automation tool | | 📄 Paperless-ngx | Document management — scan, index, and archive your documents | | 📅 Postiz | Open-source social media scheduling tool with AI features | +| 🧠 Proxmox MCP | MCP server for Proxmox VE — manage nodes, VMs, containers, snapshots and backups from AI agents | | 🐧 Ubuntu Webtop | A full Ubuntu desktop environment in your browser | | 📈 Uptime Kuma | Self-hosted uptime monitoring and status pages | | 🔐 Vaultwarden | Self-hosted, Bitwarden-compatible password manager | diff --git a/icons/README.md b/icons/README.md index ea41517..daf2c48 100644 --- a/icons/README.md +++ b/icons/README.md @@ -41,3 +41,6 @@ the apps they represent. SVGs were sourced from community icon sets: 2. Set `"icon": "https://raw.githubusercontent.com/try-hola/apps/main/icons/.svg"` in `src//src/manifest.json`. 3. Regenerate the index: `./bin/build-catalog.sh`. +- `proxmox-mcp.svg`: upstream's own logo from + [RekklesNA/ProxmoxMCP-Plus](https://github.com/RekklesNA/ProxmoxMCP-Plus/blob/main/docs/assets/logo-proxmoxmcp-plus-400.png) + (MIT). A 400² PNG in an SVG wrapper (~75KB), the same approach as `remo.svg`. diff --git a/icons/proxmox-mcp.svg b/icons/proxmox-mcp.svg new file mode 100644 index 0000000..b343544 --- /dev/null +++ b/icons/proxmox-mcp.svg @@ -0,0 +1 @@ + diff --git a/src/proxmox-mcp/README.md b/src/proxmox-mcp/README.md new file mode 100644 index 0000000..36420c2 --- /dev/null +++ b/src/proxmox-mcp/README.md @@ -0,0 +1,83 @@ +# proxmox-mcp + +[ProxmoxMCP-Plus](https://github.com/RekklesNA/ProxmoxMCP-Plus) — an MCP server for +Proxmox VE (nodes, VMs, containers, snapshots, backups, task logs) — packaged for Hola +so an AI agent can manage your hypervisor through one authenticated endpoint. + +## Layout (Hola app package format) + +``` +src/proxmox-mcp/ +├── package.json # name + version + OCI annotations +└── src/ + ├── compose.yaml # one service, MCP Streamable HTTP mode, no host ports + └── manifest.json # Hola defaults (ingress port, wizard fields, auth) +``` + +## How it is exposed + +- **Endpoint:** `https://proxmox-mcp./mcp` (MCP Streamable HTTP). + Traefik is the only ingress; the container publishes no host port. +- **Auth:** the manifest declares `forward-auth` with `bypassPaths: ["/mcp"]` — the same + pattern `remo` uses for its setup API. `/mcp` is exempted from the interactive Authentik + login because the app protects it with its **own** bearer credential (`MCP_API_KEY`, + generated at install); everything else on the host stays behind SSO. MCP clients are not + browsers, so a cookie-based forward-auth gate would only lock them out. +- **Client config:** point your MCP client at the URL above with + `Authorization: Bearer `; the key is on the deployment's configuration tab. + +## What it can do — and the blast radius + +This app hands whoever holds `MCP_API_KEY` control of your hypervisor, so it ships +defensively: + +- `MCP_TOOL_DENYLIST` defaults to `delete_vm,delete_container,delete_snapshot,rollback_snapshot`. + Clear it in the wizard (advanced) to expose everything. +- Guest command execution (`execute_vm_command`, `execute_container_command`) is gated + by the server's command policy, which defaults to deny-all + ([upstream docs](https://github.com/RekklesNA/ProxmoxMCP-Plus/blob/main/docs/container-command-execution.md)). + Two advanced wizard fields expose it: `COMMAND_POLICY_MODE` (`deny_all` | `allowlist` | + `audit_only`) and `COMMAND_POLICY_ALLOW_PATTERNS` (comma-separated regexes). An + agent that provisions VMs typically needs exactly one allowlisted command — appending + an SSH key to a freshly cloned guest's `authorized_keys` — since the MCP tool set can + clone, start, stop, snapshot and delete VMs but has no tool to write cloud-init + settings (`sshkeys`, `ciuser`, `ipconfig0`) on the clone. +- Use a **dedicated Proxmox user + API token** with only the ACLs the exposed tools need + (`PVEAuditor` for read-only use; add `PVEVMAdmin` on the pools you want managed). + Do not point it at `root@pam`. +- DNS-rebinding protection is on, allowing only the public host Traefik serves. + +## Configuration + +**Self-signed Proxmox certificate?** Turn *Verify Proxmox TLS certificate* off **and** +*Allow unverified TLS (dev mode)* on. The upstream server refuses to start with +verification off unless its dev-mode flag is set (verified: the container exits with +"Insecure TLS configuration blocked" otherwise). Dev mode gates only that check. + +All settings are wizard fields (`manifest.defaultEnv`): the Proxmox host, API user, +token name and secret are required; the MCP key is generated; port, TLS verification +and the tool denylist are advanced. The image's `PROXMOX_MCP_CONFIG` file is never +mounted, so the server reads exactly these environment variables. + +## State + +None worth keeping: the optional job store is an SQLite file inside the container's +own working directory and is recreated on restart. No data volume, so nothing to back +up (`accepts` is intentionally empty). + +## Pre-release: fork image + +Version `0.1.0-beta.1` (channel `beta`) runs the image built from the +[pofallon/ProxmoxMCP-Plus](https://github.com/pofallon/ProxmoxMCP-Plus) fork at +`v0.5.15-hola.1`: upstream v0.5.15 plus the VM provisioning tools proposed upstream in +[RekklesNA/ProxmoxMCP-Plus#127](https://github.com/RekklesNA/ProxmoxMCP-Plus/pull/127) +(`update_vm_config`, `get_vm_ip_addresses`, `get_next_vmid`), which an agent needs to take a +cloud-init template clone to a reachable guest. The stable version returns to the upstream +image once a release includes them. + +## Aggregation (later) + +Hola's planned aggregated MCP gateway (try-hola/hola spec 002) will front every +MCP-capable app behind one endpoint with Authentik OAuth. This package already speaks +the transport that gateway requires, so joining it will be a manifest `mcp` block, not a +repackage. diff --git a/src/proxmox-mcp/package.json b/src/proxmox-mcp/package.json new file mode 100644 index 0000000..ffedfc9 --- /dev/null +++ b/src/proxmox-mcp/package.json @@ -0,0 +1,13 @@ +{ + "name": "proxmox-mcp", + "version": "0.1.0-beta.1", + "description": "Proxmox MCP — MCP server for Proxmox VE, so AI agents can manage nodes, VMs, containers, snapshots and backups (Hola app package)", + "license": "MIT", + "oci": { + "annotations": { + "org.opencontainers.image.title": "proxmox-mcp", + "org.opencontainers.image.description": "MCP server for Proxmox VE — manage nodes, VMs, containers, snapshots and backups from AI agents", + "org.opencontainers.image.version": "${npm_package_version}" + } + } +} diff --git a/src/proxmox-mcp/src/compose.yaml b/src/proxmox-mcp/src/compose.yaml new file mode 100644 index 0000000..0f4ca2f --- /dev/null +++ b/src/proxmox-mcp/src/compose.yaml @@ -0,0 +1,59 @@ +services: + proxmox-mcp: + # ProxmoxMCP-Plus (MIT) — https://github.com/RekklesNA/ProxmoxMCP-Plus + # Multi-arch (linux/amd64, linux/arm64); runs as the unprivileged `proxmoxmcp` user. + # + # PRE-RELEASE (channel `beta`): built from the pofallon/ProxmoxMCP-Plus fork at + # v0.5.15-hola.1 = upstream v0.5.15 + the VM provisioning tools proposed in + # RekklesNA/ProxmoxMCP-Plus#127 (update_vm_config, get_vm_ip_addresses, + # get_next_vmid). Switch back to the upstream image once a release includes them. + image: ghcr.io/pofallon/proxmoxmcp-plus:v0.5.15-hola.1@sha256:d36d2fdba3e7dd5f503ef869f85197f9fea97e442b0c5ed17d7826c5623a7aff + environment: + # Native MCP Streamable HTTP mode (the image defaults to its OpenAPI/REST + # bridge on 8811, which Hola does not expose). The MCP endpoint is /mcp. + PROXMOX_MCP_MODE: mcp-http + MCP_HOST: 0.0.0.0 + MCP_PORT: "8000" + MCP_TRANSPORT: STREAMABLE_HTTP + # Proxmox VE connection — all from the install wizard (manifest defaultEnv). + # The image's PROXMOX_MCP_CONFIG points at a config file that is never + # mounted here, so the loader falls back to these variables. + PROXMOX_HOST: "${PROXMOX_HOST}" + PROXMOX_PORT: "${PROXMOX_PORT}" + PROXMOX_USER: "${PROXMOX_USER}" + PROXMOX_TOKEN_NAME: "${PROXMOX_TOKEN_NAME}" + PROXMOX_TOKEN_VALUE: "${PROXMOX_TOKEN_VALUE}" + PROXMOX_VERIFY_SSL: "${PROXMOX_VERIFY_SSL}" + # Upstream blocks verify_ssl=false unless dev_mode is on; the wizard exposes + # both so an operator with a self-signed Proxmox cert can flip them together. + PROXMOX_DEV_MODE: "${PROXMOX_DEV_MODE}" + # Inbound bearer credential every MCP client must present. Generated by + # Hola at install (manifest defaultEnv). /mcp is exempted from Authentik's + # forward-auth precisely because this key protects it. + MCP_API_KEY: "${MCP_API_KEY}" + # Tools hidden from every client (comma-separated). Defaults to the + # destructive ones; clear it in the wizard to expose everything. + MCP_TOOL_DENYLIST: "${MCP_TOOL_DENYLIST}" + # Guest-agent command execution policy (deny_all | allowlist | audit_only) + # and the allowlist patterns; both from the wizard's advanced fields. + COMMAND_POLICY_MODE: "${COMMAND_POLICY_MODE}" + COMMAND_POLICY_ALLOW_PATTERNS: "${COMMAND_POLICY_ALLOW_PATTERNS}" + # DNS-rebinding protection: only the public host Traefik serves, plus + # loopback for the container healthcheck below. Each host is listed both + # bare and with `:*` — the MCP SDK matches a `host:*` pattern only when + # the Host header carries a port, and Traefik forwards the bare host + # (verified: bare-host-only allowlists answer 421 behind a proxy). + MCP_DNS_REBINDING_PROTECTION: "true" + MCP_ALLOWED_HOSTS: "${HOLA_APP_HOST},${HOLA_APP_HOST}:*,127.0.0.1,127.0.0.1:*,localhost,localhost:*" + # Browser origin, for a UI client; non-browser MCP clients send no Origin + # header and pass regardless. + MCP_ALLOWED_ORIGINS: "https://${HOLA_APP_HOST}" + healthcheck: + # The MCP transport has no unauthenticated health route; any HTTP answer + # on /mcp (a 401 without the bearer key) proves the server is up, so this + # deliberately does NOT use `curl -f`. + test: ["CMD", "curl", "-sS", "-o", "/dev/null", "http://127.0.0.1:8000/mcp"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 15s diff --git a/src/proxmox-mcp/src/manifest.json b/src/proxmox-mcp/src/manifest.json new file mode 100644 index 0000000..a25814b --- /dev/null +++ b/src/proxmox-mcp/src/manifest.json @@ -0,0 +1,164 @@ +{ + "name": "proxmox-mcp", + "version": "0.1.0-beta.1", + "title": "Proxmox MCP", + "description": "MCP server for Proxmox VE — let AI agents manage nodes, VMs, containers, snapshots and backups over one authenticated endpoint", + "icon": "https://raw.githubusercontent.com/try-hola/apps/main/icons/proxmox-mcp.svg", + "category": "developer-tools", + "tags": [ + "mcp", + "proxmox", + "ai", + "virtualization" + ], + "ingress": { + "service": "proxmox-mcp", + "port": 8000 + }, + "defaultEnv": [ + { + "key": "PROXMOX_HOST", + "value": "", + "isSecret": false, + "type": "string", + "label": "Proxmox host", + "placeholder": "pve.example.internal", + "description": "Hostname or IP of the Proxmox VE API this server talks to. Must be reachable from the Hola host's Docker network.", + "required": true + }, + { + "key": "PROXMOX_USER", + "value": "", + "isSecret": false, + "type": "string", + "label": "Proxmox API user", + "placeholder": "mcp@pve", + "description": "The user the API token belongs to, in Proxmox's user@realm form (e.g. mcp@pve). Prefer a dedicated user with only the privileges the tools you expose need, not root@pam.", + "required": true + }, + { + "key": "PROXMOX_TOKEN_NAME", + "value": "", + "isSecret": false, + "type": "string", + "label": "API token name", + "placeholder": "hola-mcp", + "description": "Name of the API token created for that user (Datacenter → Permissions → API Tokens). With 'Privilege Separation' on, grant the token's own ACLs.", + "required": true + }, + { + "key": "PROXMOX_TOKEN_VALUE", + "value": "", + "isSecret": true, + "type": "string", + "label": "API token secret", + "description": "The token's secret value (the UUID shown once when the token is created). Stored as a secret; never returned by the API.", + "required": true + }, + { + "key": "MCP_API_KEY", + "value": "", + "isSecret": true, + "type": "string", + "label": "MCP bearer key", + "description": "The credential every MCP client must send as `Authorization: Bearer ` to https:///mcp. Generated automatically; copy it from this deployment's configuration into your client. Anyone holding it can drive Proxmox through the tools you expose.", + "required": true, + "generate": { + "kind": "hex", + "length": 32 + } + }, + { + "key": "MCP_TOOL_DENYLIST", + "value": "delete_vm,delete_container,delete_snapshot,rollback_snapshot", + "isSecret": false, + "type": "string", + "label": "Hidden tools", + "description": "Comma-separated MCP tool names that are never exposed to clients. Defaults to the destructive ones; clear it to expose every tool. Guest command execution (execute_vm_command / execute_container_command) is additionally gated by the server's own command policy, which defaults to deny-all.", + "advanced": true + }, + { + "key": "COMMAND_POLICY_MODE", + "value": "deny_all", + "isSecret": false, + "type": "enum", + "options": [ + { + "value": "deny_all", + "label": "Deny all", + "description": "execute_vm_command / execute_container_command never run anything." + }, + { + "value": "allowlist", + "label": "Allowlist", + "description": "Only commands matching one of the allow patterns run." + }, + { + "value": "audit_only", + "label": "Audit only", + "description": "Everything runs; each command is logged. Only for a trusted, private endpoint." + } + ], + "label": "Guest command policy", + "description": "Governs the guest-agent command tools. Leave at Deny all unless an agent must run commands inside guests (e.g. injecting an SSH key into a freshly cloned VM); then use Allowlist with narrow patterns.", + "advanced": true + }, + { + "key": "COMMAND_POLICY_ALLOW_PATTERNS", + "value": "", + "isSecret": false, + "type": "string", + "label": "Allowed command patterns", + "description": "Comma-separated regular expressions a guest command must match when the policy is Allowlist. Keep them narrow — e.g. ^mkdir -p /home/hola/\\.ssh$,^tee -a /home/hola/\\.ssh/authorized_keys$.", + "placeholder": "^tee -a /home/hola/\\.ssh/authorized_keys$", + "advanced": true + }, + { + "key": "PROXMOX_PORT", + "value": "8006", + "isSecret": false, + "type": "port", + "label": "Proxmox API port", + "description": "Port of the Proxmox VE API (8006 by default).", + "advanced": true + }, + { + "key": "PROXMOX_VERIFY_SSL", + "value": "true", + "isSecret": false, + "type": "boolean", + "label": "Verify Proxmox TLS certificate", + "description": "Turn off only for a Proxmox host that still uses its self-signed certificate. Upstream refuses to start with verification off unless 'Allow unverified TLS (dev mode)' below is also on — flip both together.", + "advanced": true + }, + { + "key": "PROXMOX_DEV_MODE", + "value": "false", + "isSecret": false, + "type": "boolean", + "label": "Allow unverified TLS (dev mode)", + "description": "Must be on whenever 'Verify Proxmox TLS certificate' is off: the server's config check blocks unverified TLS otherwise and the container exits at startup. It has no other effect. Leave off when verification is on.", + "advanced": true + } + ], + "defaults": { + "ports": [ + { + "container": 8000, + "protocol": "tcp" + } + ], + "volumes": [] + }, + "auth": { + "mode": "forward-auth", + "forwardAuth": { + "bypassPaths": [ + "/mcp" + ] + } + }, + "upgrade": { + "upgradeNotesUrl": "https://github.com/RekklesNA/ProxmoxMCP-Plus/releases" + } +}