Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mcpServers": {
"proxmox": {
"command": "/home/bill/ProxmoxMCP/venv/bin/python",
"args": ["-m", "proxmox_mcp.server"],
"cwd": "/home/bill/ProxmoxMCP",
"env": {
"PYTHONPATH": "/home/bill/ProxmoxMCP/src",
"PROXMOX_MCP_CONFIG": "/home/bill/ProxmoxMCP/proxmox-config/config.json"
}
}
}
}
65 changes: 65 additions & 0 deletions proxmox-config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"clusters": [
{
"name": "Building 1-ABE",
"proxmox": {
"host": "10.8.0.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "1125dae8-0bd6-4342-a385-7da770aff655"
}
},
{
"name": "Building 2",
"proxmox": {
"host": "10.8.64.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "9f4912cb-dcc9-4e6a-968f-636f2a003454"
}
},
{
"name": "Building 3",
"proxmox": {
"host": "10.8.128.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "7a3e4cf0-ada2-459a-8037-b8bdf932a09b"
}
},
{
"name": "Building 4",
"proxmox": {
"host": "10.8.192.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "dcf00cab-3369-4084-8d42-3bc282a08d73"
}
}
],
"logging": {
"level": "DEBUG",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "proxmox_mcp.log"
}
}
65 changes: 65 additions & 0 deletions proxmox-config/config.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"clusters": [
{
"name": "Building 1-ABE",
"proxmox": {
"host": "10.8.0.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "1125dae8-0bd6-4342-a385-7da770aff655"
}
},
{
"name": "Building 2",
"proxmox": {
"host": "10.8.64.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "9f4912cb-dcc9-4e6a-968f-636f2a003454"
}
},
{
"name": "Building 3",
"proxmox": {
"host": "10.8.128.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "7a3e4cf0-ada2-459a-8037-b8bdf932a09b"
}
},
{
"name": "Building 4",
"proxmox": {
"host": "10.8.192.200",
"port": 8006,
"verify_ssl": false,
"service": "PVE"
},
"auth": {
"user": "root@pam",
"token_name": "mcp-token",
"token_value": "dcf00cab-3369-4084-8d42-3bc282a08d73"
}
}
],
"logging": {
"level": "DEBUG",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "proxmox_mcp.log"
}
}
109 changes: 72 additions & 37 deletions src/proxmox_mcp/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

This module handles loading and validation of server configuration:
- JSON configuration file loading
- Environment variable handling
- Multi-cluster configuration support
- Legacy single-cluster config conversion
- Configuration validation using Pydantic models
- Error handling for invalid configurations

Expand All @@ -15,58 +16,92 @@
from typing import Optional
from .models import Config

def _convert_legacy_config(config_data: dict) -> dict:
"""Convert legacy single-cluster config to new multi-cluster format.

Legacy format:
{
"proxmox": {...},
"auth": {...},
"logging": {...}
}

New format:
{
"clusters": [{"name": "default", "proxmox": {...}, "auth": {...}}],
"logging": {...}
}
"""
return {
"clusters": [{
"name": "default",
"proxmox": config_data["proxmox"],
"auth": config_data["auth"]
}],
"logging": config_data.get("logging", {"level": "INFO"})
}

def _validate_cluster_names(config_data: dict) -> None:
"""Validate that all cluster names are unique."""
names = [c["name"] for c in config_data.get("clusters", [])]
if len(names) != len(set(names)):
duplicates = [n for n in names if names.count(n) > 1]
raise ValueError(f"Duplicate cluster names found: {set(duplicates)}")

def load_config(config_path: Optional[str] = None) -> Config:
"""Load and validate configuration from JSON file.

Performs the following steps:
1. Verifies config path is provided
2. Loads JSON configuration file
3. Validates required fields are present
4. Converts to typed Config object using Pydantic

Configuration must include:
- Proxmox connection settings (host, port, etc.)
- Authentication credentials (user, token)
- Logging configuration

Supports both multi-cluster and legacy single-cluster formats.
Legacy configs are automatically converted to multi-cluster format.

Multi-cluster format:
{
"clusters": [
{"name": "Building 4", "proxmox": {...}, "auth": {...}},
{"name": "Building 3", "proxmox": {...}, "auth": {...}}
],
"logging": {...}
}

Legacy format (auto-converted):
{
"proxmox": {...},
"auth": {...},
"logging": {...}
}

Args:
config_path: Path to the JSON configuration file
If not provided, raises ValueError

Returns:
Config object containing validated configuration:
{
"proxmox": {
"host": "proxmox-host",
"port": 8006,
...
},
"auth": {
"user": "username",
"token_name": "token-name",
...
},
"logging": {
"level": "INFO",
...
}
}
Config object containing validated configuration

Raises:
ValueError: If:
- Config path is not provided
- JSON is invalid
- Required fields are missing
- Field values are invalid
ValueError: If config path not provided, JSON invalid,
required fields missing, or duplicate cluster names
"""
if not config_path:
raise ValueError("PROXMOX_MCP_CONFIG environment variable must be set")

try:
with open(config_path) as f:
config_data = json.load(f)
if not config_data.get('proxmox', {}).get('host'):
raise ValueError("Proxmox host cannot be empty")

# Check if this is a legacy config (has 'proxmox' at root level)
if "proxmox" in config_data and "clusters" not in config_data:
if not config_data.get('proxmox', {}).get('host'):
raise ValueError("Proxmox host cannot be empty")
config_data = _convert_legacy_config(config_data)

# Validate new format
if "clusters" in config_data:
if not config_data["clusters"]:
raise ValueError("At least one cluster must be configured")
_validate_cluster_names(config_data)
for cluster in config_data["clusters"]:
if not cluster.get("proxmox", {}).get("host"):
raise ValueError(f"Proxmox host cannot be empty for cluster '{cluster.get('name', 'unknown')}'")

return Config(**config_data)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in config file: {e}")
Expand Down
25 changes: 17 additions & 8 deletions src/proxmox_mcp/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- Field descriptions
- Required vs optional field handling
"""
from typing import Optional, Annotated
from typing import Optional, Annotated, List
from pydantic import BaseModel, Field

class NodeStatus(BaseModel):
Expand Down Expand Up @@ -59,7 +59,7 @@ class AuthConfig(BaseModel):

class LoggingConfig(BaseModel):
"""Model for logging configuration.

Defines logging parameters with sensible defaults.
Supports both file and console logging with
customizable format and log levels.
Expand All @@ -68,13 +68,22 @@ class LoggingConfig(BaseModel):
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # Optional: Log format
file: Optional[str] = None # Optional: Log file path (default: None for console logging)

class ClusterConfig(BaseModel):
"""Model for a single Proxmox cluster configuration.

Combines connection settings and authentication for one cluster.
Used in multi-cluster configurations where each cluster has a
unique name identifier.
"""
name: str # Required: Cluster identifier (e.g., 'Building 4', 'prod')
proxmox: ProxmoxConfig # Required: Connection settings for this cluster
auth: AuthConfig # Required: Authentication for this cluster

class Config(BaseModel):
"""Root configuration model.

Combines all configuration models into a single validated
configuration object. All sections are required to ensure
proper server operation.

Supports multi-cluster configurations where each cluster
has its own connection and authentication settings.
"""
proxmox: ProxmoxConfig # Required: Proxmox connection settings
auth: AuthConfig # Required: Authentication credentials
clusters: List[ClusterConfig] # Required: List of cluster configurations
logging: LoggingConfig # Required: Logging configuration
Loading