init: custom proxmox MCP server with SSE impementation
All checks were successful
Build and Push Proxmox MCP Docker Image / build (push) Successful in 7s
All checks were successful
Build and Push Proxmox MCP Docker Image / build (push) Successful in 7s
This commit is contained in:
30
.env.example
Normal file
30
.env.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 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:*
|
||||||
30
.gitea/workflows/build.yaml
Normal file
30
.gitea/workflows/build.yaml
Normal 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
19
.gitignore
vendored
Normal 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
37
Dockerfile
Normal 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"]
|
||||||
100
IMPLEMENTATION.md
Normal file
100
IMPLEMENTATION.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Implementation Details
|
||||||
|
|
||||||
|
Technical documentation for the Proxmox MCP Server implementation.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ MCP Client (Gemini CLI) │
|
||||||
|
└─────────────────────────┬───────────────────────────────┘
|
||||||
|
│ SSE (Server-Sent Events)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Reverse Proxy (Traefik) │
|
||||||
|
│ proxmox-mcp.ext.ben.io:443 │
|
||||||
|
└─────────────────────────┬───────────────────────────────┘
|
||||||
|
│ HTTP
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Docker Container (proxmox-mcp) │
|
||||||
|
│ ┌───────────────────────────────────────────────────┐ │
|
||||||
|
│ │ FastMCP + uvicorn (:8000) │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ TransportSecuritySettings │ │ │
|
||||||
|
│ │ │ (DNS rebinding protection) │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌───────────────────────────────────────────────────┐ │
|
||||||
|
│ │ proxmoxer │ │
|
||||||
|
│ │ (Proxmox API client) │ │
|
||||||
|
│ └───────────────────────┬───────────────────────────┘ │
|
||||||
|
└──────────────────────────┼──────────────────────────────┘
|
||||||
|
│ HTTPS
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Proxmox VE API │
|
||||||
|
│ pve.local.ben.io:8006 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Transport Layer
|
||||||
|
- **Protocol:** SSE (Server-Sent Events) over HTTP
|
||||||
|
- **Framework:** `mcp.server.fastmcp.FastMCP`
|
||||||
|
- **Server:** `uvicorn` (ASGI)
|
||||||
|
- **Binding:** `0.0.0.0:8000`
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **DNS Rebinding Protection:** `TransportSecuritySettings` validates Host headers
|
||||||
|
- **Allowed Hosts:** Configurable via `MCP_ALLOWED_HOSTS` environment variable
|
||||||
|
- **SSL:** Configurable verification for self-signed certificates
|
||||||
|
|
||||||
|
### Proxmox Integration
|
||||||
|
- **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
|
||||||
|
|
||||||
|
Instead of wrapping every Proxmox API endpoint (hundreds exist), we expose two layers:
|
||||||
|
|
||||||
|
### Layer 1: Curated Tools
|
||||||
|
High-frequency operations with simplified interfaces:
|
||||||
|
- `list_nodes()` - Cluster node status
|
||||||
|
- `get_cluster_resources()` - All VMs, LXCs, and storage
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Token Authentication
|
||||||
|
The `proxmoxer` library constructs auth headers as:
|
||||||
|
```
|
||||||
|
Authorization: PVEAPIToken={user}!{token_name}={token_value}
|
||||||
|
```
|
||||||
|
|
||||||
|
Therefore:
|
||||||
|
- `PROXMOX_USER` = Full user (`proxmox-mcp@pam`)
|
||||||
|
- `PROXMOX_TOKEN_ID` = Token name only (`token`)
|
||||||
|
- `PROXMOX_PASSWORD` = Token secret value
|
||||||
|
|
||||||
|
### 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:*"],
|
||||||
|
)
|
||||||
|
```
|
||||||
36
Makefile
Normal file
36
Makefile
Normal 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
|
||||||
90
README.md
Normal file
90
README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 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.
|
||||||
|
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
|
||||||
|
|
||||||
|
### `list_nodes`
|
||||||
|
Returns a list of all nodes in the cluster with their status.
|
||||||
|
|
||||||
|
### `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.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
|
||||||
|
| 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:*` |
|
||||||
|
|
||||||
|
> **Note:** If your full token is `proxmox-mcp@pam!mytoken`, set `PROXMOX_USER=proxmox-mcp@pam` and `PROXMOX_TOKEN_ID=mytoken`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### 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:*
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Client Configuration
|
||||||
|
|
||||||
|
Connect via SSE endpoint: `https://your-host/sse`
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your Proxmox credentials
|
||||||
|
|
||||||
|
# Build and run
|
||||||
|
make build
|
||||||
|
make dev
|
||||||
|
|
||||||
|
# Test
|
||||||
|
make logs # View container logs
|
||||||
|
make test-sse # Test SSE endpoint
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
* **Language:** Python 3.11+
|
||||||
|
* **MCP SDK:** `mcp` with `FastMCP`
|
||||||
|
* **Proxmox Client:** `proxmoxer`
|
||||||
|
* **Transport:** SSE (Server-Sent Events)
|
||||||
|
* **Server:** `uvicorn` (ASGI)
|
||||||
11
docker-compose.dev.yml
Normal file
11
docker-compose.dev.yml
Normal 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"
|
||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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 (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:*
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal 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
|
||||||
114
server.py
Normal file
114
server.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
from mcp.server.transport_security import TransportSecuritySettings
|
||||||
|
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"
|
||||||
|
|
||||||
|
# --- 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:*")
|
||||||
|
|
||||||
|
# --- 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 ---
|
||||||
|
# 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)
|
||||||
|
)
|
||||||
|
mcp = FastMCP("Proxmox MCP", transport_security=transport_security)
|
||||||
|
|
||||||
|
@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)
|
||||||
Reference in New Issue
Block a user