From 4b576d40ad1f710c78ab2277f4674c916d2fe82e Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 15 Dec 2025 01:37:31 +0000 Subject: [PATCH] feat: Add multi-cluster support with JSON config --- .env.example | 30 ----- .gitignore | 1 + IMPLEMENTATION.md | 122 +++++++++----------- README.md | 100 +++++++++-------- clusters.json.example | 18 +++ docker-compose.dev.yml | 6 +- docker-compose.yml | 23 +--- server.py | 246 ++++++++++++++++++++++++++++++----------- 8 files changed, 317 insertions(+), 229 deletions(-) delete mode 100644 .env.example create mode 100644 clusters.json.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 030f44f..0000000 --- a/.env.example +++ /dev/null @@ -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:* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 021802f..d0fe49f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env .env.local .env.*.local +clusters.json # Python __pycache__/ diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index bf245e6..0ef75e6 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -1,6 +1,6 @@ # Implementation Details -Technical documentation for the Proxmox MCP Server implementation. +Technical documentation for the Proxmox MCP Server. ## Architecture @@ -8,93 +8,79 @@ Technical documentation for the Proxmox MCP Server implementation. ┌─────────────────────────────────────────────────────────┐ │ MCP Client (Gemini CLI) │ └─────────────────────────┬───────────────────────────────┘ - │ SSE (Server-Sent Events) - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Reverse Proxy (Traefik) │ -│ proxmox-mcp.ext.ben.io:443 │ -└─────────────────────────┬───────────────────────────────┘ - │ HTTP + │ SSE ▼ ┌─────────────────────────────────────────────────────────┐ │ Docker Container (proxmox-mcp) │ │ ┌───────────────────────────────────────────────────┐ │ │ │ FastMCP + uvicorn (:8000) │ │ -│ │ ┌─────────────────────────────────────────────┐ │ │ -│ │ │ TransportSecuritySettings │ │ │ -│ │ │ (DNS rebinding protection) │ │ │ -│ │ └─────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────┐ │ -│ │ proxmoxer │ │ -│ │ (Proxmox API client) │ │ -│ └───────────────────────┬───────────────────────────┘ │ -└──────────────────────────┼──────────────────────────────┘ - │ HTTPS - ▼ -┌─────────────────────────────────────────────────────────┐ -│ Proxmox VE API │ -│ pve.local.ben.io:8006 │ -└─────────────────────────────────────────────────────────┘ +│ │ ClusterManager │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ prod │ │ homelab │ │ ... │ │ │ +│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ +│ └─────────┼────────────┼────────────┼──────────────┘ │ +└────────────┼────────────┼────────────┼──────────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ Proxmox │ │ Proxmox │ │ Proxmox │ + │ Cluster 1│ │ Cluster 2│ │ Cluster N│ + └──────────┘ └──────────┘ └──────────┘ ``` ## Components -### Transport Layer -- **Protocol:** SSE (Server-Sent Events) over HTTP -- **Framework:** `mcp.server.fastmcp.FastMCP` -- **Server:** `uvicorn` (ASGI) -- **Binding:** `0.0.0.0:8000` +### ClusterManager +- Loads cluster configs from `clusters.json` +- Maintains `ProxmoxAPI` connections for each cluster +- Handles cluster selection logic (default if single cluster) -### Security -- **DNS Rebinding Protection:** `TransportSecuritySettings` validates Host headers -- **Allowed Hosts:** Configurable via `MCP_ALLOWED_HOSTS` environment variable -- **SSL:** Configurable verification for self-signed certificates +### Transport Security +- `TransportSecuritySettings` validates Host headers +- Configurable via `MCP_ALLOWED_HOSTS` -### Proxmox Integration -- **Client:** `proxmoxer.ProxmoxAPI` -- **Authentication:** API Token (recommended) or Username/Password -- **Token Format:** User (`user@realm`) + Token Name (not full ID) + Token Secret +### Tool Strategy -## 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 -High-frequency operations with simplified interfaces: -- `list_nodes()` - Cluster node status -- `get_cluster_resources()` - All VMs, LXCs, and storage +## Configuration Format -### Layer 2: Raw API Access -- `proxmox_api_call(path, method, data)` - Direct access to any Proxmox API endpoint -- **Benefit:** Zero maintenance. New Proxmox features work immediately without code changes. - -## Build & CI/CD - -- **Build Tool:** `uv` (fast Python package manager) -- **Container:** Multi-stage Docker build -- **Registry:** Gitea Container Registry -- **CI/CD:** Gitea Actions (build & push on commit to main/master) -- **Orchestration:** Portainer (Docker Compose stack) - -## Key Implementation Notes +```json +{ + "clusters": { + "": { + "url": "host:port", + "user": "user@realm", + "token_id": "token_name", + "token_secret": "secret", + "verify_ssl": false + } + } +} +``` ### 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: -- `PROXMOX_USER` = Full user (`proxmox-mcp@pam`) -- `PROXMOX_TOKEN_ID` = Token name only (`token`) -- `PROXMOX_PASSWORD` = Token secret value +So for token `mcp@pam!mytoken`: +- `user` = `mcp@pam` +- `token_id` = `mytoken` -### Host Header Validation -The MCP SDK enforces strict Host header validation. For reverse proxy access: -```python -transport_security = TransportSecuritySettings( - enable_dns_rebinding_protection=True, - allowed_hosts=["proxmox-mcp.ext.ben.io", "localhost:*"], -) -``` +## Build & Deploy + +- **Build:** `uv` + multi-stage Docker +- **Registry:** Gitea Container Registry +- **CI/CD:** Gitea Actions +- **Deploy:** Docker Compose / Portainer diff --git a/README.md b/README.md index 5702ae7..e046ec5 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,96 @@ # 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: - -1. **Core Tools:** A small set of high-value tools for discovery and context. -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. +- **Multi-Cluster:** Manage multiple Proxmox clusters from a single container +- **Hybrid Approach:** Curated high-level tools + raw API access for 100% coverage +- **Zero Maintenance:** Raw API tool works with any Proxmox feature without code changes ## Tools -### `list_nodes` -Returns a list of all nodes in the cluster with their status. +| Tool | Description | +|------|-------------| +| `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` -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. +All tools accept a `cluster` parameter. If only one cluster is configured, it's optional. ## Configuration -Environment Variables: +### 1. Create `clusters.json` -| Variable | Required | Description | Example | -|----------|----------|-------------|---------| -| `PROXMOX_URL` | Yes | Proxmox host and port (no `https://`) | `pve.local.example.io:8006` | -| `PROXMOX_USER` | Yes | Full `user@realm` format | `proxmox-mcp@pam` | -| `PROXMOX_TOKEN_ID` | Yes* | Token name only (not full ID) | `token` | -| `PROXMOX_PASSWORD` | Yes | Token secret or password | `xxxxxxxx-xxxx-...` | -| `PROXMOX_VERIFY_SSL` | No | SSL verification (default: `false`) | `false` | -| `MCP_ALLOWED_HOSTS` | No | Allowed Host headers for reverse proxy | `mcp.example.io,localhost:*` | +```json +{ + "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 + } + } +} +``` -> **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 - -### Docker Compose +### 2. Deploy with Docker Compose ```yaml services: proxmox-mcp: image: gitea.ext.ben.io/b3nw/proxmox-mcp-custom:latest 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:* + volumes: + - ./clusters.json:/app/clusters.json:ro ports: - "8000:8000" 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 ```bash # Setup -cp .env.example .env -# Edit .env with your Proxmox credentials +cp clusters.json.example clusters.json +# Edit clusters.json with your credentials # Build and run make build make dev # Test -make logs # View container logs -make test-sse # Test SSE endpoint - -# Cleanup +make logs +make test-sse make stop ``` ## Tech Stack -* **Language:** Python 3.11+ -* **MCP SDK:** `mcp` with `FastMCP` -* **Proxmox Client:** `proxmoxer` -* **Transport:** SSE (Server-Sent Events) -* **Server:** `uvicorn` (ASGI) +- Python 3.11+ / FastMCP / proxmoxer +- SSE transport / uvicorn +- Docker with multi-stage build diff --git a/clusters.json.example b/clusters.json.example new file mode 100644 index 0000000..f9fbb00 --- /dev/null +++ b/clusters.json.example @@ -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 + } + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 50fca25..77c50b7 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -4,8 +4,10 @@ services: context: . dockerfile: Dockerfile container_name: proxmox-mcp-dev - env_file: - - .env + environment: + - MCP_ALLOWED_HOSTS=localhost:*,127.0.0.1:* + volumes: + - ./clusters.json:/app/clusters.json:ro ports: - "8001:8000" restart: "no" diff --git a/docker-compose.yml b/docker-compose.yml index 83dd93a..d6b00b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,27 +3,12 @@ services: 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 (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 --- # 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: - "8000:8000" restart: unless-stopped diff --git a/server.py b/server.py index b88a58e..ed0f529 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,7 @@ import os +import json import logging +from pathlib import Path from mcp.server.fastmcp import FastMCP from mcp.server.transport_security import TransportSecuritySettings from proxmoxer import ProxmoxAPI @@ -12,103 +14,221 @@ load_dotenv() 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" - # --- MCP Transport Security --- -# Allow external access via reverse proxy and local development -MCP_ALLOWED_HOSTS = os.getenv("MCP_ALLOWED_HOSTS", "proxmox-mcp.ext.ben.io,localhost:*,127.0.0.1:*") +MCP_ALLOWED_HOSTS = os.getenv("MCP_ALLOWED_HOSTS", "localhost:*,127.0.0.1:*") -# --- 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 +# --- 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 + + try: + with open(path) as f: + self.config = json.load(f) + + clusters_config = self.config.get("clusters", {}) + for name, cfg in clusters_config.items(): + self._connect_cluster(name, cfg) + + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in clusters config: {e}") + except Exception as e: + logger.error(f"Failed to load clusters config: {e}") + + 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 ) - 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.") + 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 --- -# Configure transport security to allow external access via reverse proxy transport_security = TransportSecuritySettings( enable_dns_rebinding_protection=True, 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) + +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() -def list_nodes() -> dict: - """Lists all Proxmox nodes.""" - if not proxmox: return {"error": "Proxmox API not configured"} +def list_clusters() -> dict: + """Lists all configured Proxmox clusters.""" + 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: - nodes = proxmox.nodes.get() - return {"nodes": nodes} + nodes = client.nodes.get() + return {"cluster": cluster or cluster_manager.list_names()[0], "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"} +def get_cluster_resources(cluster: str = None) -> dict: + """Gets a summary of all resources 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: - resources = proxmox.cluster.resources.get() - return {"resources": resources} + resources = client.cluster.resources.get() + return {"cluster": cluster or cluster_manager.list_names()[0], "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"} +def proxmox_api_call( + path: str, + 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: # Build path - api_path = proxmox + api_path = client 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) + 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)} + result = method_func(**data) else: - return {"result": method_func()} + result = method_func() + + return {"cluster": cluster or cluster_manager.list_names()[0], "result": result} 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) \ No newline at end of file