Initial commit: Custom Proxmox MCP with SSE wrapper
All checks were successful
Build and Push Proxmox MCP Docker Image / build (push) Successful in 8s

This commit is contained in:
Ben
2025-12-14 18:07:57 +00:00
commit eaa88a7cf9
11 changed files with 443 additions and 0 deletions

24
.env.example Normal file
View File

@@ -0,0 +1,24 @@
# Proxmox MCP Server - Environment Variables
# Copy this file to .env and fill in your values
# --- Proxmox API Configuration ---
# Base URL of your Proxmox VE instance (without https://)
PROXMOX_URL=pve.local.example.io:8006
# Proxmox API User (e.g., "root@pam", "user@pve", "proxmox-mcp@pam")
PROXMOX_USER=proxmox-mcp@pam
# --- Token Authentication (Recommended) ---
# Token ID (the name after the !, e.g., if "user@pam!mytoken" then use "mytoken")
PROXMOX_TOKEN_ID=token
# Token Secret Value (the long UUID-like string)
PROXMOX_PASSWORD=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# --- OR Password Authentication ---
# If not using tokens, set PROXMOX_PASSWORD to the user's password
# and leave PROXMOX_TOKEN_ID empty
# --- SSL Verification ---
# Set to 'true' in production, 'false' for self-signed certs in homelabs
PROXMOX_VERIFY_SSL=false

View File

@@ -0,0 +1,30 @@
name: Build and Push Proxmox MCP Docker Image
on:
push:
branches:
- main
- master
jobs:
build:
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v2
with:
registry: gitea.ext.ben.io
username: ${{ gitea.actor }}
password: ${{ secrets.CR_PAT }} # Using CR_PAT secret
- name: Build and Push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: gitea.ext.ben.io/${{ gitea.repository }}:latest

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Environment files with credentials
.env
.env.local
.env.*.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
# IDE
.vscode/
.idea/
# OS
.DS_Store

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# Stage 1: Builder for dependencies
FROM python:3.11-slim-bullseye AS builder
WORKDIR /app
# Install uv
RUN pip install uv
# Copy only dependency files first to leverage Docker cache
COPY pyproject.toml ./
# Install dependencies with uv
# Use 'uv pip install --system' to install directly into the system python environment
# This avoids creating a virtualenv inside the Docker build stage
RUN uv pip install --system .
# Stage 2: Final image
FROM python:3.11-slim-bullseye
WORKDIR /app
# Copy installed dependencies from builder
# We copy the entire site-packages directory
COPY --from=builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
# Also copy uv itself (optional, but good for debugging)
COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv
# And the bin directory for executables like 'uvicorn'
COPY --from=builder /usr/local/bin/ /usr/local/bin/
# Copy application code
COPY server.py ./
# Expose the port Uvicorn will listen on
EXPOSE 8000
# Run the Uvicorn server
CMD ["python", "server.py"]

71
IMPLEMENTATION.md Normal file
View File

@@ -0,0 +1,71 @@
# Custom Proxmox MCP Server Implementation
## Architecture Overview
This project implements a **Model Context Protocol (MCP)** server for Proxmox VE using a "Hybrid" design pattern. It combines specific, high-value tools with raw API access to ensure both usability and comprehensive coverage.
### Core Components
1. **Transport:**
* **Protocol:** Server-Sent Events (SSE) over HTTP.
* **Framework:** `mcp` (official Python SDK) using `FastMCP`.
* **Server:** `uvicorn` (ASGI server).
* **Host Binding:** `0.0.0.0:8000` (exposed for Docker).
2. **Proxmox Integration:**
* **Client Library:** `proxmoxer` (Python wrapper for Proxmox API).
* **Authentication:** API Token (`user@pam!token_id`) or Username/Password.
* **SSL:** Configurable verification (default `false` for homelabs).
3. **Deployment:**
* **Container:** Docker (multi-stage build with `uv`).
* **Registry:** Gitea Container Registry (`gitea.ext.ben.io`).
* **Orchestration:** Portainer (Docker Compose stack).
* **CI/CD:** Gitea Actions (build & push on commit).
## The Hybrid Tool Strategy
Instead of wrapping every Proxmox API endpoint (of which there are hundreds), we expose two layers of tools:
### Layer 1: Curated Tools (High Frequency)
These tools simplify common tasks for the LLM.
* `list_nodes()`: Returns cluster node status.
* `get_cluster_resources()`: Returns all VMs, LXCs, and storage.
### Layer 2: Raw Access (The "Escape Hatch")
* `proxmox_api_call(path, method, data)`: A generic tool that allows the LLM to construct *any* API call supported by `proxmoxer`.
* Example: `path="nodes/pve1/qemu/100/status/start", method="POST"`
* **Benefit:** Zero maintenance. If Proxmox adds a feature, the LLM can use it immediately without code changes.
## Current Implementation Details
### `server.py`
* Uses `mcp.server.fastmcp.FastMCP` to define the server and tools.
* Connects to Proxmox using `proxmoxer.ProxmoxAPI`.
* Exposes `mcp.sse_app` to `uvicorn` for execution.
### Challenges & Workarounds
* **Host Header Validation:** The `mcp` SDK enforces strict Host header checks by default. We are currently configuring `FastMCP` settings to allow external access (e.g., `proxmox-mcp.ext.ben.io`).
* **Dependency Management:** We use `uv` for fast, reliable builds in Docker.
* **API Client:** `proxmoxer` requires `requests` (added to `pyproject.toml`).
## Next Steps
1. Resolve the `Invalid Host header` error by correctly configuring `mcp.settings.allowed_hosts` or middleware.
2. Deploy the final image to Portainer.
3. Connect Gemini CLI via `https://proxmox-mcp.ext.ben.io/sse`.
## Local Testing Challenges
A significant challenge in this development environment is the **lack of direct local testing capabilities**. Due to the nature of operating within a sandboxed agent and the separation between the development VM (where this agent runs) and the target Docker/Portainer host, the standard iterative development loop (code -> test -> debug) is heavily impacted:
* **No Direct `uvicorn` Execution:** I cannot directly run the Python server locally to quickly test changes.
* **Remote Docker Environment:** Each code change requires:
1. Committing and pushing to Gitea.
2. Waiting for Gitea Actions to build and push the Docker image.
3. Redeploying the Docker stack on the Portainer host.
4. Analyzing remote container logs for feedback.
* **Limited Debugging:** The inability to attach a debugger or inspect live execution locally forces a heavy reliance on log analysis.
* **Slow Feedback Loop:** This multi-step remote process introduces significant delays, prolonging the time it takes to identify and fix issues (as evidenced by the numerous iterations for `portainer-mcp` and `proxmox-mcp-custom`).
This necessitates careful reasoning, documentation review, and a systematic approach to debugging, often leading to more turns than would be typical in a local development setup.

36
Makefile Normal file
View File

@@ -0,0 +1,36 @@
.PHONY: build dev logs stop test-sse clean
# Build the Docker image locally
build:
docker compose -f docker-compose.dev.yml build
# Start the development server
dev:
docker compose -f docker-compose.dev.yml up -d
@echo "Server starting at http://localhost:8001"
@echo "Use 'make logs' to view output"
# View container logs
logs:
docker compose -f docker-compose.dev.yml logs -f
# Stop the development server
stop:
docker compose -f docker-compose.dev.yml down
# Test SSE endpoint (5 second timeout)
test-sse:
@echo "Testing SSE endpoint (5s timeout)..."
@curl -N --max-time 5 http://localhost:8001/sse 2>/dev/null || echo "\n[Timeout - this is expected for SSE]"
# Test root endpoint
test-root:
@curl -s http://localhost:8001/ | head -20
# Full rebuild (no cache)
rebuild:
docker compose -f docker-compose.dev.yml build --no-cache
# Clean up containers and images
clean:
docker compose -f docker-compose.dev.yml down --rmi local -v

69
README.md Normal file
View File

@@ -0,0 +1,69 @@
# Custom Proxmox MCP Server
A robust, maintenance-free Model Context Protocol (MCP) server for Proxmox VE, built with Python and `proxmoxer`.
## Philosophy: The Hybrid Approach
Most MCP servers suffer from "feature rot" where the author implements 10 tools (`start_vm`, `stop_vm`) but misses 500 others. This project takes a hybrid approach:
1. **Core Tools:** A small set of high-value tools for discovery and context (e.g., `list_nodes`, `get_cluster_resources`).
2. **Raw API Access:** A single powerful tool `proxmox_api` that allows the LLM to call *any* Proxmox API endpoint dynamically. This ensures 100% API coverage without writing wrappers for every function.
## Tech Stack
* **Language:** Python 3.11+
* **MCP SDK:** `mcp` (Official Python SDK) with `FastMCP` (if available) or standard server implementation.
* **Proxmox Client:** `proxmoxer` (Community standard, stable).
* **Transport:** SSE (Server-Sent Events) for Docker/Remote compatibility.
* **Deployment:** Docker (built via Gitea Actions).
## Tools
### `list_nodes`
Returns a list of all nodes in the cluster with their status.
### `get_resources`
Returns a summary of all resources (VMs, LXC containers, storage) across the cluster.
### `proxmox_api`
Executes a raw API call to Proxmox.
* `service`: Service path (e.g., `nodes/pve1/qemu`).
* `method`: HTTP Method (GET, POST, PUT, DELETE).
* `data`: Optional JSON payload.
## Configuration
Required Environment Variables:
* `PROXMOX_URL`: Base URL (e.g., `https://192.168.1.1:8006`).
* `PROXMOX_USER`: User (e.g., `root@pam`).
* `PROXMOX_PASSWORD`: Password or Token Secret.
* `PROXMOX_TOKEN_ID`: (Optional) Token ID if using tokens (e.g., `mcp-token`).
* `PROXMOX_VERIFY_SSL`: `true` or `false` (Default: `false` for homelabs).
## Local Development
Prerequisites: Docker
```bash
# 1. Copy and configure credentials
cp .env.example .env
# Edit .env with your Proxmox credentials
# 2. Build and start the server
make build
make dev
# 3. View logs
make logs
# 4. Test SSE endpoint (5s timeout)
make test-sse
# 5. Stop when done
make stop
```
## Build & Deploy
This project is built using Gitea Actions and pushed to the internal registry.
Deploy using Portainer.

11
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
proxmox-mcp-dev:
build:
context: .
dockerfile: Dockerfile
container_name: proxmox-mcp-dev
env_file:
- .env
ports:
- "8001:8000"
restart: "no"

22
docker-compose.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
proxmox-mcp:
image: gitea.ext.ben.io/b3nw/proxmox-mcp-custom:latest
container_name: proxmox-mcp
environment:
# --- Proxmox API Credentials ---
# Base URL of your Proxmox VE instance (e.g., pve.local.ben.io:8006)
- PROXMOX_URL=your_proxmox_host:8006
# Proxmox API User (e.g., root@pam or user@pve)
- PROXMOX_USER=your_proxmox_user
# Proxmox API Token Secret (usually starts with "pve_mcp!")
- PROXMOX_PASSWORD=your_proxmox_token_secret
# Proxmox API Token ID (e.g., "mcp-token")
- PROXMOX_TOKEN_ID=your_proxmox_token_id
# Set to 'true' to skip SSL verification (e.g., for self-signed certs in homelab)
- PROXMOX_VERIFY_SSL=false
# --- MCP Server Settings ---
- PORT=8000 # Internal container port
ports:
- "8000:8000" # Host Port : Container Port
restart: unless-stopped

20
pyproject.toml Normal file
View File

@@ -0,0 +1,20 @@
[project]
name = "proxmox-mcp-custom"
version = "0.1.0"
description = "Custom Proxmox MCP Server"
dependencies = [
"mcp",
"proxmoxer",
"requests", # Required for proxmoxer https backend
"uvicorn",
"fastapi",
"python-dotenv", # For local development
]
requires-python = ">=3.11"
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.uv]
python-version = "3.11" # Specify desired Python version for uv

104
server.py Normal file
View File

@@ -0,0 +1,104 @@
import os
import logging
from mcp.server.fastmcp import FastMCP
from proxmoxer import ProxmoxAPI
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Proxmox API Configuration ---
PROXMOX_URL = os.getenv("PROXMOX_URL")
PROXMOX_USER = os.getenv("PROXMOX_USER")
PROXMOX_PASSWORD = os.getenv("PROXMOX_PASSWORD")
PROXMOX_TOKEN_ID = os.getenv("PROXMOX_TOKEN_ID")
PROXMOX_VERIFY_SSL = os.getenv("PROXMOX_VERIFY_SSL", "false").lower() == "true"
# --- Initialize Proxmox Client ---
proxmox = None
if PROXMOX_URL and PROXMOX_USER:
try:
if PROXMOX_TOKEN_ID and PROXMOX_PASSWORD:
# Token-based authentication
# PROXMOX_USER should be like "user@pam" or "user@pve"
# PROXMOX_TOKEN_ID is the token name (e.g., "mcp-token")
# PROXMOX_PASSWORD is the token secret value
proxmox = ProxmoxAPI(
PROXMOX_URL,
user=PROXMOX_USER,
token_name=PROXMOX_TOKEN_ID,
token_value=PROXMOX_PASSWORD,
verify_ssl=PROXMOX_VERIFY_SSL
)
logger.info(f"Proxmox API client configured with token auth for {PROXMOX_USER}")
elif PROXMOX_PASSWORD:
# Password-based authentication
proxmox = ProxmoxAPI(
PROXMOX_URL,
user=PROXMOX_USER,
password=PROXMOX_PASSWORD,
verify_ssl=PROXMOX_VERIFY_SSL
)
logger.info(f"Proxmox API client configured with password auth for {PROXMOX_USER}")
else:
logger.warning("PROXMOX_PASSWORD (or token secret) not set. Tools may fail.")
except Exception as e:
logger.error(f"Failed to configure Proxmox API: {e}")
else:
logger.warning("PROXMOX_URL or PROXMOX_USER not set. Tools may fail.")
# --- FastMCP Server ---
# Note: Host header validation is handled by reverse proxy in production
mcp = FastMCP("Proxmox MCP")
@mcp.tool()
def list_nodes() -> dict:
"""Lists all Proxmox nodes."""
if not proxmox: return {"error": "Proxmox API not configured"}
try:
nodes = proxmox.nodes.get()
return {"nodes": nodes}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
def get_cluster_resources() -> dict:
"""Gets a summary of all cluster resources."""
if not proxmox: return {"error": "Proxmox API not configured"}
try:
resources = proxmox.cluster.resources.get()
return {"resources": resources}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
def proxmox_api_call(path: str, method: str = "GET", data: dict = {}, node: str = None, vmid: int = None, lxcid: int = None) -> dict:
"""Executes a raw Proxmox API call."""
if not proxmox: return {"error": "Proxmox API not configured"}
try:
# Build path
api_path = proxmox
for segment in path.strip('/').split('/'):
if segment == "nodes" and node: api_path = api_path.nodes(node)
elif segment == "qemu" and vmid: api_path = api_path.qemu(vmid)
elif segment == "lxc" and lxcid: api_path = api_path.lxc(lxcid)
elif segment: api_path = api_path(segment)
# Execute
method_func = getattr(api_path, method.lower())
if method.upper() in ["POST", "PUT"]:
return {"result": method_func(**data)}
else:
return {"result": method_func()}
except Exception as e:
return {"error": str(e)}
if __name__ == "__main__":
import uvicorn
# Use the exposed sse_app from FastMCP and run it with uvicorn
uvicorn.run(mcp.sse_app, host="0.0.0.0", port=8000)