feat: Add multi-cluster support with JSON config
All checks were successful
Build and Push Proxmox MCP Docker Image / build (push) Successful in 8s

This commit is contained in:
Ben
2025-12-15 01:37:31 +00:00
parent 2bb507cdac
commit 4b576d40ad
8 changed files with 317 additions and 229 deletions

View File

@@ -1,30 +0,0 @@
# 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) ---
# IMPORTANT: Token ID is JUST the token name, NOT the full identifier!
# If your full token is "proxmox-mcp@pam!mytoken", use only "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
# --- MCP Transport Security ---
# Comma-separated list of allowed Host header values (for reverse proxy access)
# Supports wildcard ports with :* suffix (e.g., localhost:*)
MCP_ALLOWED_HOSTS=hostname,localhost:*,127.0.0.1:*

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.env .env
.env.local .env.local
.env.*.local .env.*.local
clusters.json
# Python # Python
__pycache__/ __pycache__/

View File

@@ -1,6 +1,6 @@
# Implementation Details # Implementation Details
Technical documentation for the Proxmox MCP Server implementation. Technical documentation for the Proxmox MCP Server.
## Architecture ## Architecture
@@ -8,93 +8,79 @@ Technical documentation for the Proxmox MCP Server implementation.
┌─────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────┐
│ MCP Client (Gemini CLI) │ │ MCP Client (Gemini CLI) │
└─────────────────────────┬───────────────────────────────┘ └─────────────────────────┬───────────────────────────────┘
│ SSE (Server-Sent Events) │ SSE
┌─────────────────────────────────────────────────────────┐
│ Reverse Proxy (Traefik) │
│ proxmox-mcp.ext.ben.io:443 │
└─────────────────────────┬───────────────────────────────┘
│ HTTP
┌─────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────┐
│ Docker Container (proxmox-mcp) │ │ Docker Container (proxmox-mcp) │
│ ┌───────────────────────────────────────────────────┐ │ │ ┌───────────────────────────────────────────────────┐ │
│ │ FastMCP + uvicorn (:8000) │ │ │ │ FastMCP + uvicorn (:8000) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ TransportSecuritySettings │ │ │
│ │ │ (DNS rebinding protection) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │ │ ┌───────────────────────────────────────────────────┐ │
│ │ proxmoxer │ │ │ │ ClusterManager │ │
│ │ (Proxmox API client) │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
└───────────────────────┬───────────────────────────┘ │ │ prod │ │ homelab │ │ ... │ │
└──────────────────────────┼──────────────────────────────┘ │ │ └────────┘ └────────┘ └────┬────┘ │ │
│ HTTPS └─────────┼────────────┼────────────┼──────────────┘ │
└────────────┼────────────┼────────────┼──────────────────┘
┌─────────────────────────────────────────────────────────┐ │ │ │
Proxmox VE API
pve.local.ben.io:8006 │ ┌──────────┐ ┌──────────┐ ┌──────────┐
└─────────────────────────────────────────────────────────┘ │ Proxmox │ │ Proxmox │ │ Proxmox │
│ Cluster 1│ │ Cluster 2│ │ Cluster N│
└──────────┘ └──────────┘ └──────────┘
``` ```
## Components ## Components
### Transport Layer ### ClusterManager
- **Protocol:** SSE (Server-Sent Events) over HTTP - Loads cluster configs from `clusters.json`
- **Framework:** `mcp.server.fastmcp.FastMCP` - Maintains `ProxmoxAPI` connections for each cluster
- **Server:** `uvicorn` (ASGI) - Handles cluster selection logic (default if single cluster)
- **Binding:** `0.0.0.0:8000`
### Security ### Transport Security
- **DNS Rebinding Protection:** `TransportSecuritySettings` validates Host headers - `TransportSecuritySettings` validates Host headers
- **Allowed Hosts:** Configurable via `MCP_ALLOWED_HOSTS` environment variable - Configurable via `MCP_ALLOWED_HOSTS`
- **SSL:** Configurable verification for self-signed certificates
### Proxmox Integration ### Tool Strategy
- **Client:** `proxmoxer.ProxmoxAPI`
- **Authentication:** API Token (recommended) or Username/Password
- **Token Format:** User (`user@realm`) + Token Name (not full ID) + Token Secret
## The Hybrid Tool Strategy **Layer 1: Curated Tools**
- `list_clusters()` - Discovery
- `list_nodes(cluster)` - Node status
- `get_cluster_resources(cluster)` - Resource summary
Instead of wrapping every Proxmox API endpoint (hundreds exist), we expose two layers: **Layer 2: Raw Access**
- `proxmox_api_call(cluster, path, method, data)` - Any API endpoint
### Layer 1: Curated Tools ## Configuration Format
High-frequency operations with simplified interfaces:
- `list_nodes()` - Cluster node status
- `get_cluster_resources()` - All VMs, LXCs, and storage
### Layer 2: Raw API Access ```json
- `proxmox_api_call(path, method, data)` - Direct access to any Proxmox API endpoint {
- **Benefit:** Zero maintenance. New Proxmox features work immediately without code changes. "clusters": {
"<name>": {
## Build & CI/CD "url": "host:port",
"user": "user@realm",
- **Build Tool:** `uv` (fast Python package manager) "token_id": "token_name",
- **Container:** Multi-stage Docker build "token_secret": "secret",
- **Registry:** Gitea Container Registry "verify_ssl": false
- **CI/CD:** Gitea Actions (build & push on commit to main/master) }
- **Orchestration:** Portainer (Docker Compose stack) }
}
## Key Implementation Notes ```
### Token Authentication ### Token Authentication
The `proxmoxer` library constructs auth headers as:
The `proxmoxer` library uses:
``` ```
Authorization: PVEAPIToken={user}!{token_name}={token_value} Authorization: PVEAPIToken={user}!{token_id}={token_secret}
``` ```
Therefore: So for token `mcp@pam!mytoken`:
- `PROXMOX_USER` = Full user (`proxmox-mcp@pam`) - `user` = `mcp@pam`
- `PROXMOX_TOKEN_ID` = Token name only (`token`) - `token_id` = `mytoken`
- `PROXMOX_PASSWORD` = Token secret value
### Host Header Validation ## Build & Deploy
The MCP SDK enforces strict Host header validation. For reverse proxy access:
```python - **Build:** `uv` + multi-stage Docker
transport_security = TransportSecuritySettings( - **Registry:** Gitea Container Registry
enable_dns_rebinding_protection=True, - **CI/CD:** Gitea Actions
allowed_hosts=["proxmox-mcp.ext.ben.io", "localhost:*"], - **Deploy:** Docker Compose / Portainer
)
```

100
README.md
View File

@@ -1,90 +1,96 @@
# Custom Proxmox MCP Server # Custom Proxmox MCP Server
A robust, maintenance-free Model Context Protocol (MCP) server for Proxmox VE, built with Python and `proxmoxer`. A robust, maintenance-free Model Context Protocol (MCP) server for Proxmox VE with **multi-cluster support**.
## Philosophy: The Hybrid Approach ## Features
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: - **Multi-Cluster:** Manage multiple Proxmox clusters from a single container
- **Hybrid Approach:** Curated high-level tools + raw API access for 100% coverage
1. **Core Tools:** A small set of high-value tools for discovery and context. - **Zero Maintenance:** Raw API tool works with any Proxmox feature without code changes
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.
## Tools ## Tools
### `list_nodes` | Tool | Description |
Returns a list of all nodes in the cluster with their status. |------|-------------|
| `list_clusters` | Lists all configured Proxmox clusters |
| `list_nodes` | Lists nodes in a cluster |
| `get_cluster_resources` | Gets VMs, LXCs, and storage summary |
| `proxmox_api_call` | Execute any Proxmox API call directly |
### `get_cluster_resources` All tools accept a `cluster` parameter. If only one cluster is configured, it's optional.
Returns a summary of all resources (VMs, LXC containers, storage) across the cluster.
### `proxmox_api_call`
Executes a raw API call to Proxmox.
* `path`: API path (e.g., `nodes/pve1/qemu/100/status/start`).
* `method`: HTTP method (GET, POST, PUT, DELETE).
* `data`: Optional JSON payload for POST/PUT requests.
## Configuration ## Configuration
Environment Variables: ### 1. Create `clusters.json`
| Variable | Required | Description | Example | ```json
|----------|----------|-------------|---------| {
| `PROXMOX_URL` | Yes | Proxmox host and port (no `https://`) | `pve.local.example.io:8006` | "clusters": {
| `PROXMOX_USER` | Yes | Full `user@realm` format | `proxmox-mcp@pam` | "production": {
| `PROXMOX_TOKEN_ID` | Yes* | Token name only (not full ID) | `token` | "url": "pve-prod.example.io:8006",
| `PROXMOX_PASSWORD` | Yes | Token secret or password | `xxxxxxxx-xxxx-...` | "user": "mcp@pam",
| `PROXMOX_VERIFY_SSL` | No | SSL verification (default: `false`) | `false` | "token_id": "token",
| `MCP_ALLOWED_HOSTS` | No | Allowed Host headers for reverse proxy | `mcp.example.io,localhost:*` | "token_secret": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"verify_ssl": false
},
"homelab": {
"url": "pve-home.local:8006",
"user": "root@pam",
"token_id": "mcp",
"token_secret": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"verify_ssl": false
}
}
}
```
> **Note:** If your full token is `proxmox-mcp@pam!mytoken`, set `PROXMOX_USER=proxmox-mcp@pam` and `PROXMOX_TOKEN_ID=mytoken`. > **Token Format:** If your full token is `user@pam!mytoken`, set `user: "user@pam"` and `token_id: "mytoken"`
## Deployment ### 2. Deploy with Docker Compose
### Docker Compose
```yaml ```yaml
services: services:
proxmox-mcp: proxmox-mcp:
image: gitea.ext.ben.io/b3nw/proxmox-mcp-custom:latest image: gitea.ext.ben.io/b3nw/proxmox-mcp-custom:latest
environment: environment:
- PROXMOX_URL=pve.local.example.io:8006
- PROXMOX_USER=proxmox-mcp@pam
- PROXMOX_TOKEN_ID=token
- PROXMOX_PASSWORD=your-token-secret
- PROXMOX_VERIFY_SSL=false
- MCP_ALLOWED_HOSTS=proxmox-mcp.example.io,localhost:* - MCP_ALLOWED_HOSTS=proxmox-mcp.example.io,localhost:*
volumes:
- ./clusters.json:/app/clusters.json:ro
ports: ports:
- "8000:8000" - "8000:8000"
restart: unless-stopped restart: unless-stopped
``` ```
### MCP Client Configuration ### 3. Connect MCP Client
Connect via SSE endpoint: `https://your-host/sse` SSE endpoint: `https://your-host/sse`
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_ALLOWED_HOSTS` | `localhost:*,127.0.0.1:*` | Allowed Host headers |
| `CLUSTERS_CONFIG_PATH` | `/app/clusters.json` | Path to clusters config |
## Local Development ## Local Development
```bash ```bash
# Setup # Setup
cp .env.example .env cp clusters.json.example clusters.json
# Edit .env with your Proxmox credentials # Edit clusters.json with your credentials
# Build and run # Build and run
make build make build
make dev make dev
# Test # Test
make logs # View container logs make logs
make test-sse # Test SSE endpoint make test-sse
# Cleanup
make stop make stop
``` ```
## Tech Stack ## Tech Stack
* **Language:** Python 3.11+ - Python 3.11+ / FastMCP / proxmoxer
* **MCP SDK:** `mcp` with `FastMCP` - SSE transport / uvicorn
* **Proxmox Client:** `proxmoxer` - Docker with multi-stage build
* **Transport:** SSE (Server-Sent Events)
* **Server:** `uvicorn` (ASGI)

18
clusters.json.example Normal file
View File

@@ -0,0 +1,18 @@
{
"clusters": {
"production": {
"url": "pve-prod.example.io:8006",
"user": "mcp@pam",
"token_id": "token",
"token_secret": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"verify_ssl": false
},
"homelab": {
"url": "pve-home.local:8006",
"user": "root@pam",
"token_id": "mcp",
"token_secret": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"verify_ssl": false
}
}
}

View File

@@ -4,8 +4,10 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: proxmox-mcp-dev container_name: proxmox-mcp-dev
env_file: environment:
- .env - MCP_ALLOWED_HOSTS=localhost:*,127.0.0.1:*
volumes:
- ./clusters.json:/app/clusters.json:ro
ports: ports:
- "8001:8000" - "8001:8000"
restart: "no" restart: "no"

View File

@@ -3,27 +3,12 @@ services:
image: gitea.ext.ben.io/b3nw/proxmox-mcp-custom:latest image: gitea.ext.ben.io/b3nw/proxmox-mcp-custom:latest
container_name: proxmox-mcp container_name: proxmox-mcp
environment: environment:
# --- Proxmox API Credentials ---
# Base URL of your Proxmox VE instance (host:port, no https://)
- PROXMOX_URL=hostname:8006
# Proxmox API User - full "user@realm" format
# Example: root@pam, admin@pve, proxmox-mcp@pam
- PROXMOX_USER=proxmox-mcp@pam
# Proxmox API Token ID - JUST the token name, NOT the full ID
# If your full token is "proxmox-mcp@pam!mytoken", use only "mytoken"
- PROXMOX_TOKEN_ID=token
# Proxmox API Token Secret (the UUID-like value)
- PROXMOX_PASSWORD=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# SSL verification - 'false' for self-signed certs in homelab
- PROXMOX_VERIFY_SSL=false
# --- MCP Transport Security --- # --- MCP Transport Security ---
# Allowed Host headers (comma-separated, supports :* for wildcard ports) # Allowed Host headers (comma-separated, supports :* for wildcard ports)
- MCP_ALLOWED_HOSTS=hostname,localhost:*,127.0.0.1:* - MCP_ALLOWED_HOSTS=proxmox-mcp.example.io,localhost:*,127.0.0.1:*
volumes:
# Mount your clusters.json configuration file
- ./clusters.json:/app/clusters.json:ro
ports: ports:
- "8000:8000" - "8000:8000"
restart: unless-stopped restart: unless-stopped

244
server.py
View File

@@ -1,5 +1,7 @@
import os import os
import json
import logging import logging
from pathlib import Path
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings from mcp.server.transport_security import TransportSecuritySettings
from proxmoxer import ProxmoxAPI from proxmoxer import ProxmoxAPI
@@ -12,103 +14,221 @@ load_dotenv()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) 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"
# --- MCP Transport Security --- # --- MCP Transport Security ---
# Allow external access via reverse proxy and local development MCP_ALLOWED_HOSTS = os.getenv("MCP_ALLOWED_HOSTS", "localhost:*,127.0.0.1:*")
MCP_ALLOWED_HOSTS = os.getenv("MCP_ALLOWED_HOSTS", "proxmox-mcp.ext.ben.io,localhost:*,127.0.0.1:*")
# --- Cluster Configuration ---
CLUSTERS_CONFIG_PATH = os.getenv("CLUSTERS_CONFIG_PATH", "/app/clusters.json")
class ClusterManager:
"""Manages multiple Proxmox cluster connections."""
def __init__(self, config_path: str):
self.clusters: dict[str, ProxmoxAPI] = {}
self.config: dict = {}
self._load_config(config_path)
def _load_config(self, config_path: str) -> None:
"""Load cluster configuration from JSON file."""
path = Path(config_path)
if not path.exists():
logger.warning(f"Clusters config not found at {config_path}")
return
# --- Initialize Proxmox Client ---
proxmox = None
if PROXMOX_URL and PROXMOX_USER:
try: try:
if PROXMOX_TOKEN_ID and PROXMOX_PASSWORD: with open(path) as f:
# Token-based authentication self.config = json.load(f)
# PROXMOX_USER should be like "user@pam" or "user@pve"
# PROXMOX_TOKEN_ID is the token name (e.g., "mcp-token") clusters_config = self.config.get("clusters", {})
# PROXMOX_PASSWORD is the token secret value for name, cfg in clusters_config.items():
proxmox = ProxmoxAPI( self._connect_cluster(name, cfg)
PROXMOX_URL,
user=PROXMOX_USER, except json.JSONDecodeError as e:
token_name=PROXMOX_TOKEN_ID, logger.error(f"Invalid JSON in clusters config: {e}")
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: except Exception as e:
logger.error(f"Failed to configure Proxmox API: {e}") logger.error(f"Failed to load clusters config: {e}")
else:
logger.warning("PROXMOX_URL or PROXMOX_USER not set. Tools may fail.") def _connect_cluster(self, name: str, cfg: dict) -> None:
"""Connect to a single Proxmox cluster."""
try:
url = cfg.get("url")
user = cfg.get("user")
token_id = cfg.get("token_id")
token_secret = cfg.get("token_secret")
verify_ssl = cfg.get("verify_ssl", False)
if not all([url, user, token_id, token_secret]):
logger.warning(f"Cluster '{name}' missing required fields, skipping")
return
client = ProxmoxAPI(
url,
user=user,
token_name=token_id,
token_value=token_secret,
verify_ssl=verify_ssl
)
self.clusters[name] = client
logger.info(f"Connected to cluster '{name}' ({url})")
except Exception as e:
logger.error(f"Failed to connect to cluster '{name}': {e}")
def get(self, name: str = None) -> ProxmoxAPI | None:
"""Get a cluster client by name. If name is None and only one cluster, return it."""
if not self.clusters:
return None
if name:
return self.clusters.get(name)
# If only one cluster, return it as default
if len(self.clusters) == 1:
return next(iter(self.clusters.values()))
return None
def list_names(self) -> list[str]:
"""Return list of configured cluster names."""
return list(self.clusters.keys())
def require_cluster_param(self) -> bool:
"""Returns True if cluster parameter is required (multiple clusters configured)."""
return len(self.clusters) > 1
# Initialize cluster manager
cluster_manager = ClusterManager(CLUSTERS_CONFIG_PATH)
# --- FastMCP Server --- # --- FastMCP Server ---
# Configure transport security to allow external access via reverse proxy
transport_security = TransportSecuritySettings( transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=True, enable_dns_rebinding_protection=True,
allowed_hosts=[h.strip() for h in MCP_ALLOWED_HOSTS.split(",")], allowed_hosts=[h.strip() for h in MCP_ALLOWED_HOSTS.split(",")],
allowed_origins=[], # Empty = allow any origin (or none for same-origin) allowed_origins=[],
) )
mcp = FastMCP("Proxmox MCP", transport_security=transport_security) mcp = FastMCP("Proxmox MCP", transport_security=transport_security)
def _get_cluster_or_error(cluster: str = None) -> tuple[ProxmoxAPI | None, dict | None]:
"""Helper to get cluster client or return error dict."""
if not cluster_manager.clusters:
return None, {"error": "No clusters configured. Mount clusters.json to /app/clusters.json"}
if cluster_manager.require_cluster_param() and not cluster:
return None, {
"error": "Multiple clusters configured. Specify 'cluster' parameter.",
"available_clusters": cluster_manager.list_names()
}
client = cluster_manager.get(cluster)
if not client:
return None, {
"error": f"Cluster '{cluster}' not found",
"available_clusters": cluster_manager.list_names()
}
return client, None
@mcp.tool() @mcp.tool()
def list_nodes() -> dict: def list_clusters() -> dict:
"""Lists all Proxmox nodes.""" """Lists all configured Proxmox clusters."""
if not proxmox: return {"error": "Proxmox API not configured"} names = cluster_manager.list_names()
if not names:
return {"error": "No clusters configured", "clusters": []}
return {"clusters": names}
@mcp.tool()
def list_nodes(cluster: str = None) -> dict:
"""Lists all nodes in a Proxmox cluster.
Args:
cluster: Cluster name (optional if only one cluster configured)
"""
client, error = _get_cluster_or_error(cluster)
if error:
return error
try: try:
nodes = proxmox.nodes.get() nodes = client.nodes.get()
return {"nodes": nodes} return {"cluster": cluster or cluster_manager.list_names()[0], "nodes": nodes}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
@mcp.tool() @mcp.tool()
def get_cluster_resources() -> dict: def get_cluster_resources(cluster: str = None) -> dict:
"""Gets a summary of all cluster resources.""" """Gets a summary of all resources in a Proxmox cluster.
if not proxmox: return {"error": "Proxmox API not configured"}
Args:
cluster: Cluster name (optional if only one cluster configured)
"""
client, error = _get_cluster_or_error(cluster)
if error:
return error
try: try:
resources = proxmox.cluster.resources.get() resources = client.cluster.resources.get()
return {"resources": resources} return {"cluster": cluster or cluster_manager.list_names()[0], "resources": resources}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
@mcp.tool() @mcp.tool()
def proxmox_api_call(path: str, method: str = "GET", data: dict = {}, node: str = None, vmid: int = None, lxcid: int = None) -> dict: def proxmox_api_call(
"""Executes a raw Proxmox API call.""" path: str,
if not proxmox: return {"error": "Proxmox API not configured"} method: str = "GET",
data: dict = None,
cluster: str = None,
node: str = None,
vmid: int = None,
lxcid: int = None
) -> dict:
"""Executes a raw Proxmox API call.
Args:
path: API path (e.g., 'nodes/pve1/qemu/100/status/start')
method: HTTP method (GET, POST, PUT, DELETE)
data: Optional JSON payload for POST/PUT
cluster: Cluster name (optional if only one cluster configured)
node: Node name (for path substitution)
vmid: VM ID (for path substitution)
lxcid: LXC container ID (for path substitution)
"""
client, error = _get_cluster_or_error(cluster)
if error:
return error
if data is None:
data = {}
try: try:
# Build path # Build path
api_path = proxmox api_path = client
for segment in path.strip('/').split('/'): for segment in path.strip('/').split('/'):
if segment == "nodes" and node: api_path = api_path.nodes(node) if segment == "nodes" and node:
elif segment == "qemu" and vmid: api_path = api_path.qemu(vmid) api_path = api_path.nodes(node)
elif segment == "lxc" and lxcid: api_path = api_path.lxc(lxcid) elif segment == "qemu" and vmid:
elif segment: api_path = api_path(segment) 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 # Execute
method_func = getattr(api_path, method.lower()) method_func = getattr(api_path, method.lower())
if method.upper() in ["POST", "PUT"]: if method.upper() in ["POST", "PUT"]:
return {"result": method_func(**data)} result = method_func(**data)
else: else:
return {"result": method_func()} result = method_func()
return {"cluster": cluster or cluster_manager.list_names()[0], "result": result}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn 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) uvicorn.run(mcp.sse_app, host="0.0.0.0", port=8000)