#
tokens: 45442/50000 43/45 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/rekklesna/proxmoxmcp-plus?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── LICENSE
├── proxmox-config
│   └── config.example.json
├── pyproject.toml
├── README.md
├── requirements-dev.in
├── requirements.in
├── setup.py
├── src
│   └── proxmox_mcp
│       ├── __init__.py
│       ├── config
│       │   ├── __init__.py
│       │   ├── loader.py
│       │   └── models.py
│       ├── core
│       │   ├── __init__.py
│       │   ├── logging.py
│       │   └── proxmox.py
│       ├── formatting
│       │   ├── __init__.py
│       │   ├── colors.py
│       │   ├── components.py
│       │   ├── formatters.py
│       │   ├── templates.py
│       │   └── theme.py
│       ├── server.py
│       ├── tools
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── cluster.py
│       │   ├── console
│       │   │   ├── __init__.py
│       │   │   └── manager.py
│       │   ├── containers.py
│       │   ├── definitions.py
│       │   ├── node.py
│       │   ├── storage.py
│       │   └── vm.py
│       └── utils
│           ├── __init__.py
│           ├── auth.py
│           └── logging.py
├── start_openapi.sh
├── start_server.sh
├── test_scripts
│   ├── README.md
│   ├── test_common.py
│   ├── test_create_vm.py
│   ├── test_openapi.py
│   ├── test_vm_power.py
│   └── test_vm_start.py
└── tests
    ├── __init__.py
    ├── test_server.py
    └── test_vm_console.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | *.so
 6 | .Python
 7 | build/
 8 | develop-eggs/
 9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 | 
23 | # Virtual Environment
24 | .env
25 | .venv
26 | env/
27 | venv/
28 | ENV/
29 | 
30 | # IDE
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | .project
36 | .pydevproject
37 | .settings/
38 | 
39 | # Logs
40 | *.log
41 | logs/
42 | 
43 | # Test coverage
44 | .coverage
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 | 
57 | # UV
58 | .uv/
59 | 
60 | 
61 | # Local configuration
62 | config/config.json
63 | proxmox-config/config.json
64 | **/config.json
65 | 
```

--------------------------------------------------------------------------------
/test_scripts/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # ProxmoxMCP Test Scripts
  2 | 
  3 | This folder contains various test scripts and demo programs for the ProxmoxMCP project.
  4 | 
  5 | ## 📁 File Description
  6 | 
  7 | ### 🔧 VM Management Tests
  8 | - **`test_vm_power.py`** - VM power management functionality test
  9 |   - Test VM start, stop, restart and other operations
 10 |   - Check VM status and available operations
 11 | 
 12 | - **`test_vm_start.py`** - VM startup functionality specific test
 13 |   - Dedicated test for VM startup functionality
 14 |   - Suitable for single VM startup testing
 15 | 
 16 | - **`test_create_vm.py`** - VM creation functionality test
 17 |   - Test complete workflow of creating new virtual machines
 18 |   - Verify 1 CPU core + 2GB RAM + 10GB storage configuration
 19 | 
 20 | ### 🌐 API Tests
 21 | - **`test_openapi.py`** - OpenAPI service comprehensive test
 22 |   - Test all API endpoints
 23 |   - Include VM creation, power management and other functionalities
 24 |   - Verify integration with Open WebUI
 25 | 
 26 | ## 🚀 Usage
 27 | 
 28 | ### Environment Setup
 29 | ```bash
 30 | # Activate virtual environment
 31 | source ../.venv/bin/activate
 32 | 
 33 | # Set configuration path (if needed)
 34 | export PROXMOX_MCP_CONFIG=../proxmox-config/config.json
 35 | ```
 36 | 
 37 | ### Running Tests
 38 | 
 39 | #### 1. Test VM Power Management
 40 | ```bash
 41 | python test_vm_power.py
 42 | ```
 43 | 
 44 | #### 2. Test VM Creation
 45 | ```bash
 46 | python test_create_vm.py
 47 | ```
 48 | 
 49 | #### 3. Test OpenAPI Service
 50 | ```bash
 51 | python test_openapi.py
 52 | ```
 53 | 
 54 | #### 4. Test VM Startup
 55 | ```bash
 56 | python test_vm_start.py
 57 | ```
 58 | 
 59 | ## 📋 Test Coverage
 60 | 
 61 | ### ✅ Tested Features
 62 | - [x] VM list retrieval
 63 | - [x] VM status query
 64 | - [x] VM power management (start/stop/restart/shutdown)
 65 | - [x] VM creation (support custom CPU/memory/storage)
 66 | - [x] Storage type auto-detection
 67 | - [x] Disk format intelligent selection
 68 | - [x] OpenAPI service integration
 69 | - [x] Error handling verification
 70 | 
 71 | ### 🎯 Test Scenarios
 72 | - **Basic functionality**: Connection, authentication, basic operations
 73 | - **VM lifecycle**: Create, start, stop, delete
 74 | - **Storage compatibility**: LVM, filesystem storage
 75 | - **API integration**: REST API calls and responses
 76 | - **Error recovery**: Exception handling
 77 | 
 78 | ## 🔗 Related Documentation
 79 | 
 80 | - **Main project documentation**: [../README.md](../README.md)
 81 | - **VM creation guide**: [../VM_CREATION_GUIDE.md](../VM_CREATION_GUIDE.md)
 82 | - **OpenAPI deployment**: [../OPENAPI_DEPLOYMENT.md](../OPENAPI_DEPLOYMENT.md)
 83 | - **Quick deployment**: [../QUICK_DEPLOY_8811.md](../QUICK_DEPLOY_8811.md)
 84 | 
 85 | ## 📊 Test Results Examples
 86 | 
 87 | ### Success Cases
 88 | ```
 89 | ✅ VM 995: Created successfully (local-lvm, raw)
 90 | ✅ VM 996: Created successfully (vm-storage, raw)  
 91 | ✅ VM 998: Created successfully (local-lvm, raw)
 92 | ✅ VM 999: Created successfully (local-lvm, raw)
 93 | ```
 94 | 
 95 | ### API Endpoint Verification
 96 | ```
 97 | ✅ get_nodes: 200 - 134 chars
 98 | ✅ get_vms: 200 - 1843 chars
 99 | ✅ create_vm: 200 - VM created successfully
100 | ✅ start_vm: 200 - VM started successfully
101 | ```
102 | 
103 | ## 🛠️ Troubleshooting
104 | 
105 | If tests fail, please check:
106 | 
107 | 1. **Configuration file**: Whether `../proxmox-config/config.json` is correct
108 | 2. **Network connection**: Whether Proxmox server is reachable
109 | 3. **Authentication info**: Whether API token is valid
110 | 4. **Service status**: Whether OpenAPI service is running on port 8811
111 | 
112 | ## 📝 Contributing Guidelines
113 | 
114 | When adding new tests, please:
115 | 
116 | 1. Use descriptive filenames (e.g., `test_function_name.py`)
117 | 2. Include detailed docstrings
118 | 3. Add appropriate error handling
119 | 4. Update this README file
120 | 
121 | ---
122 | 
123 | **Last Updated**: December 2024
124 | **Maintainer**: ProxmoxMCP Development Team 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # ProxmoxMCP-Plus - Enhanced Proxmox MCP Server
  2 | 
  3 | 
  4 | An enhanced Python-based Model Context Protocol (MCP) server for interacting with Proxmox virtualization platforms. This project is built upon **[canvrno/ProxmoxMCP](https://github.com/canvrno/ProxmoxMCP)** with numerous new features and improvements, providing complete OpenAPI integration and more powerful virtualization management capabilities.
  5 | 
  6 | ## Acknowledgments
  7 | 
  8 | This project is built upon the excellent open-source project [ProxmoxMCP](https://github.com/canvrno/ProxmoxMCP) by [@canvrno](https://github.com/canvrno). Thanks to the original author for providing the foundational framework and creative inspiration!
  9 | 
 10 | ## 🆕 New Features and Improvements
 11 | 
 12 | ### Major enhancements compared to the original version:
 13 | 
 14 | - ✨ **Complete VM Lifecycle Management**
 15 |   - Brand new `create_vm` tool - Support for creating virtual machines with custom configurations
 16 |   - New `delete_vm` tool - Safe VM deletion (with force deletion option)
 17 |   - Enhanced intelligent storage type detection (LVM/file-based)
 18 | 
 19 | - 🔧 **Extended Power Management Features**
 20 |   - `start_vm` - Start virtual machines
 21 |   - `stop_vm` - Force stop virtual machines
 22 |   - `shutdown_vm` - Graceful shutdown
 23 |   - `reset_vm` - Restart virtual machines
 24 | 
 25 | - 🐳 **New Container Support**
 26 |   - `get_containers` - List all LXC containers and their status
 27 |   - `start_container` - Start LXC container
 28 |   - `stop_container` - Stop LXC container
 29 |   - `restart_container` - Restart LXC container (forcefully/gracefully)
 30 |   - `update_container_resources` - Adjust container CPU, memory, swap, or extend disk
 31 | 
 32 | - 📊 **Enhanced Monitoring and Display**
 33 |   - Improved storage pool status monitoring
 34 |   - More detailed cluster health status checks
 35 |   - Rich output formatting and themes
 36 | 
 37 | - 🌐 **Complete OpenAPI Integration**
 38 |   - 11 complete REST API endpoints
 39 |   - Production-ready Docker deployment
 40 |   - Perfect Open WebUI integration
 41 |   - Natural language VM creation support
 42 | 
 43 | - 🛡️ **Production-grade Security and Stability**
 44 |   - Enhanced error handling mechanisms
 45 |   - Comprehensive parameter validation
 46 |   - Production-level logging
 47 |   - Complete unit test coverage
 48 | 
 49 | ## Built With
 50 | 
 51 | - [Cline](https://github.com/cline/cline) - Autonomous coding agent - Go faster with Cline
 52 | - [Proxmoxer](https://github.com/proxmoxer/proxmoxer) - Python wrapper for Proxmox API
 53 | - [MCP SDK](https://github.com/modelcontextprotocol/sdk) - Model Context Protocol SDK
 54 | - [Pydantic](https://docs.pydantic.dev/) - Data validation using Python type annotations
 55 | 
 56 | ## Features
 57 | 
 58 | - 🤖 Full integration with Cline and Open WebUI
 59 | - 🛠️ Built with the official MCP SDK
 60 | - 🔒 Secure token-based authentication with Proxmox
 61 | - 🖥️ Complete VM lifecycle management (create, start, stop, reset, shutdown, delete)
 62 | - 💻 VM console command execution
 63 | - 🐳 LXC container management support
 64 | - 🗃️ Intelligent storage type detection (LVM/file-based)
 65 | - 📝 Configurable logging system
 66 | - ✅ Type-safe implementation with Pydantic
 67 | - 🎨 Rich output formatting with customizable themes
 68 | - 🌐 OpenAPI REST endpoints for integration
 69 | - 📡 11 fully functional API endpoints
 70 | 
 71 | 
 72 | ## Installation
 73 | 
 74 | ### Prerequisites
 75 | - UV package manager (recommended)
 76 | - Python 3.9 or higher
 77 | - Git
 78 | - Access to a Proxmox server with API token credentials
 79 | 
 80 | Before starting, ensure you have:
 81 | - [ ] Proxmox server hostname or IP
 82 | - [ ] Proxmox API token (see [API Token Setup](#proxmox-api-token-setup))
 83 | - [ ] UV installed (`pip install uv`)
 84 | 
 85 | ### Option 1: Quick Install (Recommended)
 86 | 
 87 | 1. Clone and set up environment:
 88 |    ```bash
 89 |    # Clone repository
 90 |    git clone https://github.com/RekklesNA/ProxmoxMCP-Plus.git
 91 |    cd ProxmoxMCP-Plus
 92 | 
 93 |    # Create and activate virtual environment
 94 |    uv venv
 95 |    source .venv/bin/activate  # Linux/macOS
 96 |    # OR
 97 |    .\.venv\Scripts\Activate.ps1  # Windows
 98 |    ```
 99 | 
100 | 2. Install dependencies:
101 |    ```bash
102 |    # Install with development dependencies
103 |    uv pip install -e ".[dev]"
104 |    ```
105 | 
106 | 3. Create configuration:
107 |    ```bash
108 |    # Create config directory and copy template
109 |    mkdir -p proxmox-config
110 |    cp proxmox-config/config.example.json proxmox-config/config.json
111 |    ```
112 | 
113 | 4. Edit `proxmox-config/config.json`:
114 |    ```json
115 |    {
116 |        "proxmox": {
117 |            "host": "PROXMOX_HOST",        # Required: Your Proxmox server address
118 |            "port": 8006,                  # Optional: Default is 8006
119 |            "verify_ssl": false,           # Optional: Set false for self-signed certs
120 |            "service": "PVE"               # Optional: Default is PVE
121 |        },
122 |        "auth": {
123 |            "user": "USER@pve",            # Required: Your Proxmox username
124 |            "token_name": "TOKEN_NAME",    # Required: API token ID
125 |            "token_value": "TOKEN_VALUE"   # Required: API token value
126 |        },
127 |        "logging": {
128 |            "level": "INFO",               # Optional: DEBUG for more detail
129 |            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
130 |            "file": "proxmox_mcp.log"      # Optional: Log to file
131 |        }
132 |    }
133 |    ```
134 | 
135 | ### Verifying Installation
136 | 
137 | 1. Check Python environment:
138 |    ```bash
139 |    python -c "import proxmox_mcp; print('Installation OK')"
140 |    ```
141 | 
142 | 2. Run the tests:
143 |    ```bash
144 |    pytest
145 |    ```
146 | 
147 | 3. Verify configuration:
148 |    ```bash
149 |    # Linux/macOS
150 |    PROXMOX_MCP_CONFIG="proxmox-config/config.json" python -m proxmox_mcp.server
151 | 
152 |    # Windows (PowerShell)
153 |    $env:PROXMOX_MCP_CONFIG="proxmox-config\config.json"; python -m proxmox_mcp.server
154 |    ```
155 | 
156 | ## Configuration
157 | 
158 | ### Proxmox API Token Setup
159 | 1. Log into your Proxmox web interface
160 | 2. Navigate to Datacenter -> Permissions -> API Tokens
161 | 3. Create a new API token:
162 |    - Select a user (e.g., root@pam)
163 |    - Enter a token ID (e.g., "mcp-token")
164 |    - Uncheck "Privilege Separation" if you want full access
165 |    - Save and copy both the token ID and secret
166 | 
167 | ## Running the Server
168 | 
169 | ### Development Mode
170 | For testing and development:
171 | ```bash
172 | # Activate virtual environment first
173 | source .venv/bin/activate  # Linux/macOS
174 | # OR
175 | .\.venv\Scripts\Activate.ps1  # Windows
176 | 
177 | # Run the server
178 | python -m proxmox_mcp.server
179 | ```
180 | 
181 | ### OpenAPI Deployment (Production Ready)
182 | 
183 | Deploy ProxmoxMCP Plus as standard OpenAPI REST endpoints for integration with Open WebUI and other applications.
184 | 
185 | #### Quick OpenAPI Start
186 | ```bash
187 | # Install mcpo (MCP-to-OpenAPI proxy)
188 | pip install mcpo
189 | 
190 | # Start OpenAPI service on port 8811
191 | ./start_openapi.sh
192 | ```
193 | 
194 | #### Docker Deployment
195 | ```bash
196 | # Build and run with Docker
197 | docker build -t proxmox-mcp-api .
198 | docker run -d --name proxmox-mcp-api -p 8811:8811 \
199 |   -v $(pwd)/proxmox-config:/app/proxmox-config proxmox-mcp-api
200 | 
201 | # Or use Docker Compose
202 | docker-compose up -d
203 | ```
204 | 
205 | #### Access OpenAPI Service
206 | Once deployed, access your service at:
207 | - **📖 API Documentation**: http://your-server:8811/docs
208 | - **🔧 OpenAPI Specification**: http://your-server:8811/openapi.json
209 | - **❤️ Health Check**: http://your-server:8811/health
210 | 
211 | ### Cline Desktop Integration
212 | 
213 | For Cline users, add this configuration to your MCP settings file (typically at `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
214 | 
215 | ```json
216 | {
217 |     "mcpServers": {
218 |         "ProxmoxMCP-Plus": {
219 |             "command": "/absolute/path/to/ProxmoxMCP-Plus/.venv/bin/python",
220 |             "args": ["-m", "proxmox_mcp.server"],
221 |             "cwd": "/absolute/path/to/ProxmoxMCP-Plus",
222 |             "env": {
223 |                 "PYTHONPATH": "/absolute/path/to/ProxmoxMCP-Plus/src",
224 |                 "PROXMOX_MCP_CONFIG": "/absolute/path/to/ProxmoxMCP-Plus/proxmox-config/config.json",
225 |                 "PROXMOX_HOST": "your-proxmox-host",
226 |                 "PROXMOX_USER": "username@pve",
227 |                 "PROXMOX_TOKEN_NAME": "token-name",
228 |                 "PROXMOX_TOKEN_VALUE": "token-value",
229 |                 "PROXMOX_PORT": "8006",
230 |                 "PROXMOX_VERIFY_SSL": "false",
231 |                 "PROXMOX_SERVICE": "PVE",
232 |                 "LOG_LEVEL": "DEBUG"
233 |             },
234 |             "disabled": false,
235 |             "autoApprove": []
236 |         }
237 |     }
238 | }
239 | ```
240 | 
241 | ## Available Tools & API Endpoints
242 | 
243 | The server provides 11 comprehensive MCP tools and corresponding REST API endpoints:
244 | 
245 | ### VM Management Tools
246 | 
247 | #### create_vm 
248 | Create a new virtual machine with specified resources.
249 | 
250 | **Parameters:**
251 | - `node` (string, required): Name of the node
252 | - `vmid` (string, required): ID for the new VM
253 | - `name` (string, required): Name for the VM
254 | - `cpus` (integer, required): Number of CPU cores (1-32)
255 | - `memory` (integer, required): Memory in MB (512-131072)
256 | - `disk_size` (integer, required): Disk size in GB (5-1000)
257 | - `storage` (string, optional): Storage pool name
258 | - `ostype` (string, optional): OS type (default: l26)
259 | 
260 | **API Endpoint:**
261 | ```http
262 | POST /create_vm
263 | Content-Type: application/json
264 | 
265 | {
266 |     "node": "pve",
267 |     "vmid": "200",
268 |     "name": "my-vm",
269 |     "cpus": 1,
270 |     "memory": 2048,
271 |     "disk_size": 10
272 | }
273 | ```
274 | 
275 | **Example Response:**
276 | ```
277 | 🎉 VM 200 created successfully!
278 | 
279 | 📋 VM Configuration:
280 |   • Name: my-vm
281 |   • Node: pve
282 |   • VM ID: 200
283 |   • CPU Cores: 1
284 |   • Memory: 2048 MB (2.0 GB)
285 |   • Disk: 10 GB (local-lvm, raw format)
286 |   • Storage Type: lvmthin
287 |   • Network: virtio (bridge=vmbr0)
288 |   • QEMU Agent: Enabled
289 | 
290 | 🔧 Task ID: UPID:pve:001AB729:0442E853:682FF380:qmcreate:200:root@pam!mcp
291 | ```
292 | 
293 | #### VM Power Management 🆕
294 | 
295 | **start_vm**: Start a virtual machine
296 | ```http
297 | POST /start_vm
298 | {"node": "pve", "vmid": "200"}
299 | ```
300 | 
301 | **stop_vm**: Force stop a virtual machine
302 | ```http
303 | POST /stop_vm
304 | {"node": "pve", "vmid": "200"}
305 | ```
306 | 
307 | **shutdown_vm**: Gracefully shutdown a virtual machine
308 | ```http
309 | POST /shutdown_vm
310 | {"node": "pve", "vmid": "200"}
311 | ```
312 | 
313 | **reset_vm**: Reset (restart) a virtual machine
314 | ```http
315 | POST /reset_vm
316 | {"node": "pve", "vmid": "200"}
317 | ```
318 | 
319 | **delete_vm** 🆕: Completely delete a virtual machine
320 | ```http
321 | POST /delete_vm
322 | {"node": "pve", "vmid": "200", "force": false}
323 | ```
324 | 
325 | ### 🆕 Container Management Tools
326 | 
327 | #### get_containers 🆕
328 | List all LXC containers across the cluster.
329 | 
330 | **API Endpoint:** `POST /get_containers`
331 | 
332 | **Example Response:**
333 | ```
334 | 🐳 Containers
335 | 
336 | 🐳 nginx-server (ID: 200)
337 |   • Status: RUNNING
338 |   • Node: pve
339 |   • CPU Cores: 2
340 |   • Memory: 1.5 GB / 2.0 GB (75.0%)
341 | ```
342 | 
343 | ### Monitoring Tools
344 | 
345 | #### get_nodes
346 | Lists all nodes in the Proxmox cluster.
347 | 
348 | **API Endpoint:** `POST /get_nodes`
349 | 
350 | **Example Response:**
351 | ```
352 | 🖥️ Proxmox Nodes
353 | 
354 | 🖥️ pve-compute-01
355 |   • Status: ONLINE
356 |   • Uptime: ⏳ 156d 12h
357 |   • CPU Cores: 64
358 |   • Memory: 186.5 GB / 512.0 GB (36.4%)
359 | ```
360 | 
361 | #### get_node_status
362 | Get detailed status of a specific node.
363 | 
364 | **Parameters:**
365 | - `node` (string, required): Name of the node
366 | 
367 | **API Endpoint:** `POST /get_node_status`
368 | 
369 | #### get_vms
370 | List all VMs across the cluster.
371 | 
372 | **API Endpoint:** `POST /get_vms`
373 | 
374 | #### get_storage
375 | List available storage pools.
376 | 
377 | **API Endpoint:** `POST /get_storage`
378 | 
379 | #### get_cluster_status
380 | Get overall cluster status and health.
381 | 
382 | **API Endpoint:** `POST /get_cluster_status`
383 | 
384 | #### execute_vm_command
385 | Execute a command in a VM's console using QEMU Guest Agent.
386 | 
387 | **Parameters:**
388 | - `node` (string, required): Name of the node where VM is running
389 | - `vmid` (string, required): ID of the VM
390 | - `command` (string, required): Command to execute
391 | 
392 | **API Endpoint:** `POST /execute_vm_command`
393 | 
394 | **Requirements:**
395 | - VM must be running
396 | - QEMU Guest Agent must be installed and running in the VM
397 | 
398 | ## Open WebUI Integration
399 | 
400 | ### Configure Open WebUI
401 | 
402 | 1. Access your Open WebUI instance
403 | 2. Navigate to **Settings** → **Connections** → **OpenAPI**
404 | 3. Add new API configuration:
405 | 
406 | ```json
407 | {
408 |   "name": "Proxmox MCP API Plus",
409 |   "base_url": "http://your-server:8811",
410 |   "api_key": "",
411 |   "description": "Enhanced Proxmox Virtualization Management API"
412 | }
413 | ```
414 | 
415 | ### Natural Language VM Creation
416 | 
417 | Users can now request VMs using natural language:
418 | 
419 | - **"Can you create a VM with 1 cpu core and 2 GB ram with 10GB of storage disk"**
420 | - **"Create a new VM for testing with minimal resources"**
421 | - **"I need a development server with 4 cores and 8GB RAM"**
422 | 
423 | The AI assistant will automatically call the appropriate APIs and provide detailed feedback.
424 | 
425 | ## Storage Type Support
426 | 
427 | ### Intelligent Storage Detection
428 | 
429 | ProxmoxMCP Plus automatically detects storage types and selects appropriate disk formats:
430 | 
431 | #### LVM Storage (local-lvm, vm-storage)
432 | - ✅ Format: `raw`
433 | - ✅ High performance
434 | - ⚠️ No cloud-init image support
435 | 
436 | #### File-based Storage (local, NFS, CIFS)
437 | - ✅ Format: `qcow2`
438 | - ✅ Cloud-init support
439 | - ✅ Flexible snapshot capabilities
440 | 
441 | ## Project Structure
442 | 
443 | ```
444 | ProxmoxMCP-Plus/
445 | ├── 📁 src/                          # Source code
446 | │   └── proxmox_mcp/
447 | │       ├── server.py                # Main MCP server implementation
448 | │       ├── config/                  # Configuration handling
449 | │       ├── core/                    # Core functionality
450 | │       ├── formatting/              # Output formatting and themes
451 | │       ├── tools/                   # Tool implementations
452 | │       │   ├── vm.py               # VM management (create/power) 🆕
453 | │       │   ├── container.py        # Container management 🆕
454 | │       │   └── console/            # VM console operations
455 | │       └── utils/                   # Utilities (auth, logging)
456 | │
457 | ├── 📁 tests/                       # Unit test suite
458 | ├── 📁 test_scripts/                # Integration tests & demos
459 | │   ├── README.md                   # Test documentation
460 | │   ├── test_vm_power.py           # VM power management tests 🆕
461 | │   ├── test_vm_start.py           # VM startup tests
462 | │   ├── test_create_vm.py          # VM creation tests 🆕
463 | │   └── test_openapi.py            # OpenAPI service tests
464 | │
465 | ├── 📁 proxmox-config/              # Configuration files
466 | │   └── config.json                # Server configuration
467 | │
468 | ├── 📄 Configuration Files
469 | │   ├── pyproject.toml             # Project metadata
470 | │   ├── docker-compose.yml         # Docker orchestration
471 | │   ├── Dockerfile                 # Docker image definition
472 | │   └── requirements.in            # Dependencies
473 | │
474 | ├── 📄 Scripts
475 | │   ├── start_server.sh            # MCP server launcher
476 | │   └── start_openapi.sh           # OpenAPI service launcher
477 | │
478 | └── 📄 Documentation
479 |     ├── README.md                  # This file
480 |     ├── VM_CREATION_GUIDE.md       # VM creation guide
481 |     ├── OPENAPI_DEPLOYMENT.md      # OpenAPI deployment
482 |     └── LICENSE                    # MIT License
483 | ```
484 | 
485 | ## Testing
486 | 
487 | ### Run Unit Tests
488 | ```bash
489 | pytest
490 | ```
491 | 
492 | ### Run Integration Tests
493 | ```bash
494 | cd test_scripts
495 | 
496 | # Test VM power management
497 | python test_vm_power.py
498 | 
499 | # Test VM creation
500 | python test_create_vm.py
501 | 
502 | # Test OpenAPI service
503 | python test_openapi.py
504 | ```
505 | 
506 | ### API Testing with curl
507 | ```bash
508 | # Test node listing
509 | curl -X POST "http://your-server:8811/get_nodes" \
510 |   -H "Content-Type: application/json" \
511 |   -d "{}"
512 | 
513 | # Test VM creation
514 | curl -X POST "http://your-server:8811/create_vm" \
515 |   -H "Content-Type: application/json" \
516 |   -d '{
517 |     "node": "pve",
518 |     "vmid": "300",
519 |     "name": "test-vm",
520 |     "cpus": 1,
521 |     "memory": 2048,
522 |     "disk_size": 10
523 |   }'
524 | ```
525 | 
526 | ## Production Security
527 | 
528 | ### API Key Authentication
529 | Set up secure API access:
530 | 
531 | ```bash
532 | export PROXMOX_API_KEY="your-secure-api-key"
533 | export PROXMOX_MCP_CONFIG="/app/proxmox-config/config.json"
534 | ```
535 | 
536 | ### Nginx Reverse Proxy
537 | Example nginx configuration:
538 | 
539 | ```nginx
540 | server {
541 |     listen 80;
542 |     server_name your-domain.com;
543 |     
544 |     location / {
545 |         proxy_pass http://localhost:8811;
546 |         proxy_set_header Host $host;
547 |         proxy_set_header X-Real-IP $remote_addr;
548 |     }
549 | }
550 | ```
551 | 
552 | ## Troubleshooting
553 | 
554 | ### Common Issues
555 | 
556 | 1. **Port already in use**
557 |    ```bash
558 |    netstat -tlnp | grep 8811
559 |    # Change port if needed
560 |    mcpo --port 8812 -- ./start_server.sh
561 |    ```
562 | 
563 | 2. **Configuration errors**
564 |    ```bash
565 |    # Verify config file
566 |    cat proxmox-config/config.json
567 |    ```
568 | 
569 | 3. **Connection issues**
570 |    ```bash
571 |    # Test Proxmox connectivity
572 |    curl -k https://your-proxmox:8006/api2/json/version
573 |    ```
574 | 
575 | ### View Logs
576 | ```bash
577 | # View service logs
578 | tail -f proxmox_mcp.log
579 | 
580 | # Docker logs
581 | docker logs proxmox-mcp-api -f
582 | ```
583 | 
584 | ## Deployment Status
585 | 
586 | ### ✅ Feature Completion: 100%
587 | 
588 | - [x] VM Creation (user requirement: 1 CPU + 2GB RAM + 10GB storage) 🆕
589 | - [x] VM Power Management (start VPN-Server ID:101) 🆕
590 | - [x] VM Deletion Feature 🆕
591 | - [x] Container Management (LXC) 🆕
592 | - [x] Storage Compatibility (LVM/file-based)
593 | - [x] OpenAPI Integration (port 8811)
594 | - [x] Open WebUI Integration
595 | - [x] Error Handling & Validation
596 | - [x] Complete Documentation & Testing
597 | 
598 | ### Production Ready!
599 | 
600 | **ProxmoxMCP Plus is now fully ready for production use!**
601 | 
602 | When users say **"Can you create a VM with 1 cpu core and 2 GB ram with 10GB of storage disk"**, the AI assistant can:
603 | 
604 | 1. 📞 Call the `create_vm` API
605 | 2. 🔧 Automatically select appropriate storage and format
606 | 3. 🎯 Create VMs that match requirements
607 | 4. 📊 Return detailed configuration information
608 | 5. 💡 Provide next-step recommendations
609 | 
610 | ## Development
611 | 
612 | After activating your virtual environment:
613 | 
614 | - Run tests: `pytest`
615 | - Format code: `black .`
616 | - Type checking: `mypy .`
617 | - Lint: `ruff .`
618 | 
619 | ## License
620 | 
621 | MIT License
622 | 
623 | ## Special Thanks
624 | 
625 | - Thanks to [@canvrno](https://github.com/canvrno) for the excellent foundational project [ProxmoxMCP](https://github.com/canvrno/ProxmoxMCP)
626 | - Thanks to the Proxmox community for providing the powerful virtualization platform
627 | - Thanks to all contributors and users for their support
628 | 
629 | ---
630 | 
631 | **Ready to Deploy!** 🎉 Your enhanced Proxmox MCP service with OpenAPI integration is ready for production use.
632 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/config/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/core/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Test suite for the Proxmox MCP server.
3 | """
4 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | MCP tools for interacting with Proxmox hypervisors.
3 | """
4 | 
5 | __all__ = []
6 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/utils/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Utility functions and helpers for the Proxmox MCP server.
3 | """
4 | 
5 | __all__ = []
6 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/console/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Console management package for Proxmox MCP.
3 | """
4 | from .manager import VMConsoleManager
5 | 
6 | __all__ = ['VMConsoleManager']
7 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Proxmox MCP Server - A Model Context Protocol server for interacting with Proxmox hypervisors.
3 | """
4 | 
5 | from .server import ProxmoxMCPServer
6 | 
7 | __version__ = "0.1.0"
8 | __all__ = ["ProxmoxMCPServer"]
9 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/formatting/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Proxmox MCP formatting package for styled output.
 3 | """
 4 | 
 5 | from .theme import ProxmoxTheme
 6 | from .colors import ProxmoxColors
 7 | from .formatters import ProxmoxFormatters
 8 | from .templates import ProxmoxTemplates
 9 | from .components import ProxmoxComponents
10 | 
11 | __all__ = [
12 |     'ProxmoxTheme',
13 |     'ProxmoxColors',
14 |     'ProxmoxFormatters',
15 |     'ProxmoxTemplates',
16 |     'ProxmoxComponents'
17 | ]
18 | 
```

--------------------------------------------------------------------------------
/proxmox-config/config.example.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "proxmox": {
 3 |         "host": "your-proxmox-host-ip",
 4 |         "port": 8006,
 5 |         "verify_ssl": false,
 6 |         "service": "PVE"
 7 |     },
 8 |     "auth": {
 9 |         "user": "username@pve",
10 |         "token_name": "your-token-name",
11 |         "token_value": "your-token-value"
12 |     },
13 |     "logging": {
14 |         "level": "DEBUG",
15 |         "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
16 |         "file": "proxmox_mcp.log"
17 |     }
18 | }
19 | 
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: '3.8'
 2 | 
 3 | services:
 4 |   proxmox-mcp-api:
 5 |     build: .
 6 |     ports:
 7 |       - "8811:8811"
 8 |     volumes:
 9 |       - ./proxmox-config:/app/proxmox-config:ro
10 |     environment:
11 |       - PROXMOX_MCP_CONFIG=/app/proxmox-config/config.json
12 |       - API_HOST=0.0.0.0
13 |       - API_PORT=8811
14 |     restart: unless-stopped
15 |     healthcheck:
16 |       test: ["CMD", "curl", "-f", "http://localhost:8811/health"]
17 |       interval: 30s
18 |       timeout: 10s
19 |       retries: 3
20 |     networks:
21 |       - proxmox-network
22 | 
23 | networks:
24 |   proxmox-network:
25 |     driver: bridge 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Use Python 3.11 slim image as base
 2 | FROM python:3.11-slim
 3 | 
 4 | # Set working directory
 5 | WORKDIR /app
 6 | 
 7 | # Install system dependencies
 8 | RUN apt-get update && apt-get install -y \
 9 |     git \
10 |     curl \
11 |     && rm -rf /var/lib/apt/lists/*
12 | 
13 | # Install Python dependencies
14 | RUN pip install mcpo uv
15 | 
16 | # Copy project files
17 | COPY . .
18 | 
19 | # Create virtual environment and install dependencies
20 | RUN uv venv && \
21 |     . .venv/bin/activate && \
22 |     uv pip install -e ".[dev]"
23 | 
24 | # Expose port
25 | EXPOSE 8811
26 | 
27 | # Set environment variables
28 | ENV PROXMOX_MCP_CONFIG="/app/proxmox-config/config.json"
29 | ENV API_HOST="0.0.0.0"
30 | ENV API_PORT="8811"
31 | 
32 | # Startup command
33 | CMD ["mcpo", "--host", "0.0.0.0", "--port", "8811", "--", \
34 |      "/bin/bash", "-c", "cd /app && source .venv/bin/activate && python -m proxmox_mcp.server"]
```

--------------------------------------------------------------------------------
/start_server.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | """
 3 | Proxmox MCP server startup script
 4 | """
 5 | 
 6 | echo "🚀 Starting Proxmox MCP server..."
 7 | echo ""
 8 | 
 9 | # Check virtual environment
10 | if [ ! -d ".venv" ]; then
11 |     echo "❌ Virtual environment does not exist, please run installation steps first"
12 |     exit 1
13 | fi
14 | 
15 | # Activate virtual environment
16 | source .venv/bin/activate
17 | 
18 | # Set environment variables
19 | export PROXMOX_MCP_CONFIG="proxmox-config/config.json"
20 | 
21 | # Check configuration file
22 | if [ ! -f "$PROXMOX_MCP_CONFIG" ]; then
23 |     echo "❌ Configuration file does not exist: $PROXMOX_MCP_CONFIG"
24 |     echo "Please ensure the configuration file is properly set up"
25 |     exit 1
26 | fi
27 | 
28 | echo "✅ Configuration file: $PROXMOX_MCP_CONFIG"
29 | echo "✅ Virtual environment activated"
30 | echo ""
31 | echo "🔍 Starting server..."
32 | echo "Press Ctrl+C to stop the server"
33 | echo ""
34 | 
35 | # Start server
36 | python -m proxmox_mcp.server 
37 | 
```

--------------------------------------------------------------------------------
/test_scripts/test_vm_start.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Test VM startup functionality
 4 | """
 5 | import os
 6 | import sys
 7 | 
 8 | def test_start_vm_101():
 9 |     """Test starting VM 101 (VPN-Server)"""
10 |     
11 |     # Set configuration
12 |     os.environ['PROXMOX_MCP_CONFIG'] = 'proxmox-config/config.json'
13 |     
14 |     try:
15 |         from proxmox_mcp.config.loader import load_config
16 |         from proxmox_mcp.core.proxmox import ProxmoxManager
17 |         from proxmox_mcp.tools.vm import VMTools
18 |         
19 |         config = load_config('proxmox-config/config.json')
20 |         manager = ProxmoxManager(config.proxmox, config.auth)
21 |         api = manager.get_api()
22 |         
23 |         vm_tools = VMTools(api)
24 |         
25 |         print("🚀 Test starting VPN-Server (VM 101)")
26 |         print("=" * 50)
27 |         
28 |         # Start VM 101
29 |         result = vm_tools.start_vm(node="pve", vmid="101")
30 |         
31 |         for content in result:
32 |             print(content.text)
33 |             
34 |         return True
35 |         
36 |     except Exception as e:
37 |         print(f"❌ Start failed: {e}")
38 |         return False
39 | 
40 | if __name__ == "__main__":
41 |     print("🔍 Test VM startup functionality")
42 |     print("=" * 50)
43 |     
44 |     success = test_start_vm_101()
45 |     
46 |     if success:
47 |         print("\n✅ Test completed")
48 |     else:
49 |         print("\n❌ Test failed")
50 |         sys.exit(1) 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/utils/logging.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Logging configuration for the Proxmox MCP server.
 3 | """
 4 | 
 5 | import logging
 6 | import sys
 7 | from typing import Optional
 8 | 
 9 | def setup_logging(
10 |     level: str = "INFO",
11 |     format_str: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
12 |     log_file: Optional[str] = None,
13 | ) -> logging.Logger:
14 |     """
15 |     Configure logging for the Proxmox MCP server.
16 | 
17 |     Args:
18 |         level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
19 |         format_str: The format string for log messages
20 |         log_file: Optional file path to write logs to
21 | 
22 |     Returns:
23 |         logging.Logger: Configured logger instance
24 |     """
25 |     # Create logger
26 |     logger = logging.getLogger("proxmox-mcp")
27 |     logger.setLevel(getattr(logging, level.upper()))
28 | 
29 |     # Create handlers
30 |     handlers = []
31 | 
32 |     # Console handler
33 |     console_handler = logging.StreamHandler(sys.stderr)
34 |     console_handler.setLevel(getattr(logging, level.upper()))
35 |     handlers.append(console_handler)
36 | 
37 |     # File handler if log_file is specified
38 |     if log_file:
39 |         file_handler = logging.FileHandler(log_file)
40 |         file_handler.setLevel(getattr(logging, level.upper()))
41 |         handlers.append(file_handler)
42 | 
43 |     # Create formatter
44 |     formatter = logging.Formatter(format_str)
45 | 
46 |     # Add formatter to handlers and handlers to logger
47 |     for handler in handlers:
48 |         handler.setFormatter(formatter)
49 |         logger.addHandler(handler)
50 | 
51 |     return logger
52 | 
```

--------------------------------------------------------------------------------
/start_openapi.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | # Proxmox MCP OpenAPI startup script
 3 | # Expose MCP server as OpenAPI REST endpoints through mcpo proxy
 4 | # Configurable deployment address
 5 | 
 6 | # Get host and port from environment variables or use defaults
 7 | HOST=${OPENAPI_HOST:-"localhost"}
 8 | PORT=${OPENAPI_PORT:-"8811"}
 9 | 
10 | echo "🛰️ Starting Proxmox MCP OpenAPI server..."
11 | echo ""
12 | 
13 | # Check if mcpo is installed
14 | if ! command -v mcpo &> /dev/null; then
15 |     echo "❌ mcpo not installed, installing..."
16 |     pip install mcpo
17 | fi
18 | 
19 | # Check virtual environment
20 | if [ ! -d ".venv" ]; then
21 |     echo "❌ Virtual environment does not exist, please run installation steps first"
22 |     exit 1
23 | fi
24 | 
25 | # Check configuration file
26 | if [ ! -f "proxmox-config/config.json" ]; then
27 |     echo "❌ Configuration file does not exist: proxmox-config/config.json"
28 |     echo "Please ensure the configuration file is properly set up"
29 |     exit 1
30 | fi
31 | 
32 | echo "✅ Configuration file: proxmox-config/config.json"
33 | echo "✅ mcpo proxy ready"
34 | echo ""
35 | echo "🚀 Starting OpenAPI proxy server..."
36 | echo "🌐 Service address: http://${HOST}:${PORT}"
37 | echo "📖 API documentation: http://${HOST}:${PORT}/docs"
38 | echo "🔧 OpenAPI specification: http://${HOST}:${PORT}/openapi.json"
39 | echo "❤️ Health check: http://${HOST}:${PORT}/health"
40 | echo ""
41 | echo "Press Ctrl+C to stop the server"
42 | echo ""
43 | echo "💡 To use a different host/port, set environment variables:"
44 | echo "   export OPENAPI_HOST=your-host"
45 | echo "   export OPENAPI_PORT=your-port"
46 | echo ""
47 | 
48 | # Set environment variables
49 | export PROXMOX_MCP_CONFIG="$(pwd)/proxmox-config/config.json"
50 | 
51 | # Start mcpo proxy server, bind to all interfaces on specified port
52 | mcpo --host 0.0.0.0 --port ${PORT} -- bash -c "cd $(pwd) && source .venv/bin/activate && python -m proxmox_mcp.server" 
```

--------------------------------------------------------------------------------
/test_scripts/test_common.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Common configuration helper for test scripts
 4 | """
 5 | import os
 6 | import sys
 7 | from pathlib import Path
 8 | 
 9 | def setup_test_environment():
10 |     """Set up test environment configuration paths"""
11 |     
12 |     # Get current script directory
13 |     current_dir = Path(__file__).parent
14 |     
15 |     # Calculate project root directory
16 |     project_root = current_dir.parent
17 |     
18 |     # Set configuration file path
19 |     config_path = project_root / "proxmox-config" / "config.json"
20 |     
21 |     # Set source code path
22 |     src_path = project_root / "src"
23 |     
24 |     # Ensure paths exist
25 |     if not config_path.exists():
26 |         raise FileNotFoundError(f"Configuration file does not exist: {config_path}")
27 |     
28 |     if not src_path.exists():
29 |         raise FileNotFoundError(f"Source code directory does not exist: {src_path}")
30 |     
31 |     # Set environment variables
32 |     os.environ['PROXMOX_MCP_CONFIG'] = str(config_path)
33 |     
34 |     # Add source code path to Python path
35 |     if str(src_path) not in sys.path:
36 |         sys.path.insert(0, str(src_path))
37 |     
38 |     return str(config_path)
39 | 
40 | def get_test_tools():
41 |     """Get test tools classes"""
42 |     
43 |     # Ensure environment is set up
44 |     config_path = setup_test_environment()
45 |     
46 |     try:
47 |         from proxmox_mcp.config.loader import load_config
48 |         from proxmox_mcp.core.proxmox import ProxmoxManager
49 |         from proxmox_mcp.tools.vm import VMTools
50 |         
51 |         config = load_config(config_path)
52 |         manager = ProxmoxManager(config.proxmox, config.auth)
53 |         api = manager.get_api()
54 |         
55 |         vm_tools = VMTools(api)
56 |         
57 |         return {
58 |             'config': config,
59 |             'manager': manager,
60 |             'api': api,
61 |             'vm_tools': vm_tools
62 |         }
63 |         
64 |     except Exception as e:
65 |         print(f"❌ Failed to initialize test tools: {e}")
66 |         raise
67 | 
68 | def print_test_header(title):
69 |     """Print test title"""
70 |     print(f"🔍 {title}")
71 |     print("=" * len(f"🔍 {title}"))
72 | 
73 | def print_test_result(success, message=""):
74 |     """Print test result"""
75 |     if success:
76 |         print(f"\n✅ Test completed {message}")
77 |     else:
78 |         print(f"\n❌ Test failed {message}")
79 |         sys.exit(1) 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["setuptools>=61.0.0", "wheel"]
 3 | build-backend = "setuptools.build_meta"
 4 | 
 5 | [project]
 6 | name = "proxmox-mcp"
 7 | version = "0.1.0"
 8 | description = "A Model Context Protocol server for interacting with Proxmox hypervisors"
 9 | requires-python = ">=3.11"
10 | authors = [
11 |     {name = "Kevin", email = "[email protected]"}
12 | ]
13 | readme = "README.md"
14 | license = {text = "MIT"}
15 | keywords = ["proxmox", "mcp", "virtualization", "cline", "qemu", "lxc"]
16 | classifiers = [
17 |     "Development Status :: 3 - Alpha",
18 |     "Intended Audience :: Developers",
19 |     "License :: OSI Approved :: MIT License",
20 |     "Programming Language :: Python :: 3",
21 |     "Programming Language :: Python :: 3.9",
22 |     "Programming Language :: Python :: 3.10",
23 |     "Programming Language :: Python :: 3.11",
24 |     "Programming Language :: Python :: 3.12",
25 |     "Topic :: Software Development :: Libraries :: Python Modules",
26 |     "Topic :: System :: Systems Administration",
27 | ]
28 | 
29 | dependencies = [
30 |     "mcp @ git+https://github.com/modelcontextprotocol/python-sdk.git",
31 |     "proxmoxer>=2.0.1,<3.0.0",
32 |     "requests>=2.31.0,<3.0.0",
33 |     "pydantic>=2.0.0,<3.0.0",
34 |     "fastapi>=0.115.0",
35 |     "uvicorn[standard]>=0.30.0",
36 |     "mcpo>=0.0.17",
37 | ]
38 | 
39 | [project.optional-dependencies]
40 | dev = [
41 |     "pytest>=7.0.0,<8.0.0",
42 |     "black>=23.0.0,<24.0.0",
43 |     "mypy>=1.0.0,<2.0.0",
44 |     "pytest-asyncio>=0.21.0,<0.22.0",
45 |     "ruff>=0.1.0,<0.2.0",
46 |     "types-requests>=2.31.0,<3.0.0",
47 | ]
48 | 
49 | [project.urls]
50 | Homepage = "https://github.com/yourusername/proxmox-mcp"
51 | Documentation = "https://github.com/yourusername/proxmox-mcp#readme"
52 | Repository = "https://github.com/yourusername/proxmox-mcp.git"
53 | Issues = "https://github.com/yourusername/proxmox-mcp/issues"
54 | 
55 | [project.scripts]
56 | proxmox-mcp = "proxmox_mcp.server:main"
57 | 
58 | [tool.pytest.ini_options]
59 | asyncio_mode = "strict"
60 | testpaths = ["tests"]
61 | python_files = ["test_*.py"]
62 | addopts = "-v"
63 | 
64 | [tool.mypy]
65 | python_version = "3.9"
66 | warn_return_any = true
67 | warn_unused_configs = true
68 | disallow_untyped_defs = true
69 | disallow_incomplete_defs = true
70 | check_untyped_defs = true
71 | disallow_untyped_decorators = true
72 | no_implicit_optional = true
73 | warn_redundant_casts = true
74 | warn_unused_ignores = true
75 | warn_no_return = true
76 | warn_unreachable = true
77 | 
78 | [tool.ruff]
79 | select = ["E", "F", "B", "I"]
80 | ignore = []
81 | line-length = 100
82 | target-version = "py39"
83 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/utils/auth.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Authentication utilities for the Proxmox MCP server.
 3 | """
 4 | 
 5 | import os
 6 | from typing import Dict, Optional, Tuple
 7 | 
 8 | from pydantic import BaseModel
 9 | 
10 | class ProxmoxAuth(BaseModel):
11 |     """Proxmox authentication configuration."""
12 |     user: str
13 |     token_name: str
14 |     token_value: str
15 | 
16 | def load_auth_from_env() -> ProxmoxAuth:
17 |     """
18 |     Load Proxmox authentication details from environment variables.
19 | 
20 |     Environment Variables:
21 |         PROXMOX_USER: Username with realm (e.g., 'root@pam' or 'user@pve')
22 |         PROXMOX_TOKEN_NAME: API token name
23 |         PROXMOX_TOKEN_VALUE: API token value
24 | 
25 |     Returns:
26 |         ProxmoxAuth: Authentication configuration
27 | 
28 |     Raises:
29 |         ValueError: If required environment variables are missing
30 |     """
31 |     user = os.getenv("PROXMOX_USER")
32 |     token_name = os.getenv("PROXMOX_TOKEN_NAME")
33 |     token_value = os.getenv("PROXMOX_TOKEN_VALUE")
34 | 
35 |     if not all([user, token_name, token_value]):
36 |         missing = []
37 |         if not user:
38 |             missing.append("PROXMOX_USER")
39 |         if not token_name:
40 |             missing.append("PROXMOX_TOKEN_NAME")
41 |         if not token_value:
42 |             missing.append("PROXMOX_TOKEN_VALUE")
43 |         raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
44 | 
45 |     return ProxmoxAuth(
46 |         user=user,
47 |         token_name=token_name,
48 |         token_value=token_value,
49 |     )
50 | 
51 | def parse_user(user: str) -> Tuple[str, str]:
52 |     """
53 |     Parse a Proxmox user string into username and realm.
54 | 
55 |     Args:
56 |         user: User string in format 'username@realm'
57 | 
58 |     Returns:
59 |         Tuple[str, str]: (username, realm)
60 | 
61 |     Raises:
62 |         ValueError: If user string is not in correct format
63 |     """
64 |     try:
65 |         username, realm = user.split("@")
66 |         return username, realm
67 |     except ValueError:
68 |         raise ValueError(
69 |             "Invalid user format. Expected 'username@realm' (e.g., 'root@pam' or 'user@pve')"
70 |         )
71 | 
72 | def get_auth_dict(auth: ProxmoxAuth) -> Dict[str, str]:
73 |     """
74 |     Convert ProxmoxAuth model to dictionary for Proxmoxer API.
75 | 
76 |     Args:
77 |         auth: ProxmoxAuth configuration
78 | 
79 |     Returns:
80 |         Dict[str, str]: Authentication dictionary for Proxmoxer
81 |     """
82 |     return {
83 |         "user": auth.user,
84 |         "token_name": auth.token_name,
85 |         "token_value": auth.token_value,
86 |     }
87 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/config/loader.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Configuration loading utilities for the Proxmox MCP server.
 3 | 
 4 | This module handles loading and validation of server configuration:
 5 | - JSON configuration file loading
 6 | - Environment variable handling
 7 | - Configuration validation using Pydantic models
 8 | - Error handling for invalid configurations
 9 | 
10 | The module ensures that all required configuration is present
11 | and valid before the server starts operation.
12 | """
13 | import json
14 | import os
15 | from typing import Optional
16 | from .models import Config
17 | 
18 | def load_config(config_path: Optional[str] = None) -> Config:
19 |     """Load and validate configuration from JSON file.
20 | 
21 |     Performs the following steps:
22 |     1. Verifies config path is provided
23 |     2. Loads JSON configuration file
24 |     3. Validates required fields are present
25 |     4. Converts to typed Config object using Pydantic
26 |     
27 |     Configuration must include:
28 |     - Proxmox connection settings (host, port, etc.)
29 |     - Authentication credentials (user, token)
30 |     - Logging configuration
31 |     
32 |     Args:
33 |         config_path: Path to the JSON configuration file
34 |                     If not provided, raises ValueError
35 | 
36 |     Returns:
37 |         Config object containing validated configuration:
38 |         {
39 |             "proxmox": {
40 |                 "host": "proxmox-host",
41 |                 "port": 8006,
42 |                 ...
43 |             },
44 |             "auth": {
45 |                 "user": "username",
46 |                 "token_name": "token-name",
47 |                 ...
48 |             },
49 |             "logging": {
50 |                 "level": "INFO",
51 |                 ...
52 |             }
53 |         }
54 | 
55 |     Raises:
56 |         ValueError: If:
57 |                  - Config path is not provided
58 |                  - JSON is invalid
59 |                  - Required fields are missing
60 |                  - Field values are invalid
61 |     """
62 |     if not config_path:
63 |         raise ValueError("PROXMOX_MCP_CONFIG environment variable must be set")
64 | 
65 |     try:
66 |         with open(config_path) as f:
67 |             config_data = json.load(f)
68 |             if not config_data.get('proxmox', {}).get('host'):
69 |                 raise ValueError("Proxmox host cannot be empty")
70 |             return Config(**config_data)
71 |     except json.JSONDecodeError as e:
72 |         raise ValueError(f"Invalid JSON in config file: {e}")
73 |     except Exception as e:
74 |         raise ValueError(f"Failed to load config: {e}")
75 | 
```

--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Setup script for the Proxmox MCP server.
 3 | This file is maintained for compatibility with older tools.
 4 | For modern Python packaging, see pyproject.toml.
 5 | """
 6 | 
 7 | from setuptools import setup, find_packages
 8 | 
 9 | # Metadata and dependencies are primarily managed in pyproject.toml
10 | # This file exists for compatibility with tools that don't support pyproject.toml
11 | 
12 | setup(
13 |     name="proxmox-mcp",
14 |     version="0.1.0",
15 |     packages=find_packages(where="src"),
16 |     package_dir={"": "src"},
17 |     python_requires=">=3.9",
18 |     install_requires=[
19 |         "mcp @ git+https://github.com/modelcontextprotocol/python-sdk.git",
20 |         "proxmoxer>=2.0.1,<3.0.0",
21 |         "requests>=2.31.0,<3.0.0",
22 |         "pydantic>=2.0.0,<3.0.0",
23 |     ],
24 |     extras_require={
25 |         "dev": [
26 |             "pytest>=7.0.0,<8.0.0",
27 |             "black>=23.0.0,<24.0.0",
28 |             "mypy>=1.0.0,<2.0.0",
29 |             "pytest-asyncio>=0.21.0,<0.22.0",
30 |             "ruff>=0.1.0,<0.2.0",
31 |             "types-requests>=2.31.0,<3.0.0",
32 |         ],
33 |     },
34 |     entry_points={
35 |         "console_scripts": [
36 |             "proxmox-mcp=proxmox_mcp.server:main",
37 |         ],
38 |     },
39 |     author="Kevin",
40 |     author_email="[email protected]",
41 |     description="A Model Context Protocol server for interacting with Proxmox hypervisors",
42 |     long_description=open("README.md").read(),
43 |     long_description_content_type="text/markdown",
44 |     license="MIT",
45 |     keywords=["proxmox", "mcp", "virtualization", "cline", "qemu", "lxc"],
46 |     classifiers=[
47 |         "Development Status :: 3 - Alpha",
48 |         "Intended Audience :: Developers",
49 |         "License :: OSI Approved :: MIT License",
50 |         "Programming Language :: Python :: 3",
51 |         "Programming Language :: Python :: 3.9",
52 |         "Programming Language :: Python :: 3.10",
53 |         "Programming Language :: Python :: 3.11",
54 |         "Programming Language :: Python :: 3.12",
55 |         "Topic :: Software Development :: Libraries :: Python Modules",
56 |         "Topic :: System :: Systems Administration",
57 |         "Topic :: System :: Virtualization",
58 |     ],
59 |     project_urls={
60 |         "Homepage": "https://github.com/yourusername/proxmox-mcp",
61 |         "Documentation": "https://github.com/yourusername/proxmox-mcp#readme",
62 |         "Repository": "https://github.com/yourusername/proxmox-mcp.git",
63 |         "Issues": "https://github.com/yourusername/proxmox-mcp/issues",
64 |     },
65 | )
66 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/formatting/theme.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Theme configuration for Proxmox MCP output styling.
  3 | """
  4 | 
  5 | class ProxmoxTheme:
  6 |     """Theme configuration for Proxmox MCP output."""
  7 |     
  8 |     # Feature flags
  9 |     USE_EMOJI = True
 10 |     USE_COLORS = True
 11 |     
 12 |     # Status indicators with emojis
 13 |     STATUS = {
 14 |         'online': '🟢',
 15 |         'offline': '🔴',
 16 |         'running': '▶️',
 17 |         'stopped': '⏹️',
 18 |         'unknown': '❓',
 19 |         'pending': '⏳',
 20 |         'error': '❌',
 21 |         'warning': '⚠️',
 22 |     }
 23 |     
 24 |     # Resource type indicators
 25 |     RESOURCES = {
 26 |         'node': '🖥️',
 27 |         'vm': '🗃️',
 28 |         'container': '📦',
 29 |         'storage': '💾',
 30 |         'cpu': '⚡',
 31 |         'memory': '🧠',
 32 |         'network': '🌐',
 33 |         'disk': '💿',
 34 |         'backup': '📼',
 35 |         'snapshot': '📸',
 36 |         'template': '📋',
 37 |         'pool': '🏊',
 38 |     }
 39 |     
 40 |     # Action and operation indicators
 41 |     ACTIONS = {
 42 |         'success': '✅',
 43 |         'error': '❌',
 44 |         'warning': '⚠️',
 45 |         'info': 'ℹ️',
 46 |         'command': '🔧',
 47 |         'start': '▶️',
 48 |         'stop': '⏹️',
 49 |         'restart': '🔄',
 50 |         'delete': '🗑️',
 51 |         'edit': '✏️',
 52 |         'create': '➕',
 53 |         'migrate': '➡️',
 54 |         'clone': '📑',
 55 |         'lock': '🔒',
 56 |         'unlock': '🔓',
 57 |     }
 58 |     
 59 |     # Section and grouping indicators
 60 |     SECTIONS = {
 61 |         'header': '📌',
 62 |         'details': '📝',
 63 |         'statistics': '📊',
 64 |         'configuration': '⚙️',
 65 |         'logs': '📜',
 66 |         'tasks': '📋',
 67 |         'users': '👥',
 68 |         'permissions': '🔑',
 69 |     }
 70 |     
 71 |     # Measurement and metric indicators
 72 |     METRICS = {
 73 |         'percentage': '%',
 74 |         'temperature': '🌡️',
 75 |         'uptime': '⏳',
 76 |         'bandwidth': '📶',
 77 |         'latency': '⚡',
 78 |     }
 79 |     
 80 |     @classmethod
 81 |     def get_status_emoji(cls, status: str) -> str:
 82 |         """Get emoji for a status value with fallback."""
 83 |         status = status.lower()
 84 |         return cls.STATUS.get(status, cls.STATUS['unknown'])
 85 |     
 86 |     @classmethod
 87 |     def get_resource_emoji(cls, resource: str) -> str:
 88 |         """Get emoji for a resource type with fallback."""
 89 |         resource = resource.lower()
 90 |         return cls.RESOURCES.get(resource, '📦')
 91 |     
 92 |     @classmethod
 93 |     def get_action_emoji(cls, action: str) -> str:
 94 |         """Get emoji for an action with fallback."""
 95 |         action = action.lower()
 96 |         return cls.ACTIONS.get(action, cls.ACTIONS['info'])
 97 |     
 98 |     @classmethod
 99 |     def get_section_emoji(cls, section: str) -> str:
100 |         """Get emoji for a section type with fallback."""
101 |         section = section.lower()
102 |         return cls.SECTIONS.get(section, cls.SECTIONS['details'])
103 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/core/logging.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Logging configuration for the Proxmox MCP server.
 3 | 
 4 | This module handles logging setup and configuration:
 5 | - File and console logging handlers
 6 | - Log level management
 7 | - Format customization
 8 | - Handler lifecycle management
 9 | 
10 | The logging system supports:
11 | - Configurable log levels
12 | - File-based logging with path resolution
13 | - Console logging for errors
14 | - Custom format strings
15 | - Multiple handler management
16 | """
17 | import logging
18 | import os
19 | from typing import Optional
20 | from ..config.models import LoggingConfig
21 | 
22 | def setup_logging(config: LoggingConfig) -> logging.Logger:
23 |     """Configure and initialize logging system.
24 | 
25 |     Sets up a comprehensive logging system with:
26 |     - File logging (if configured):
27 |       * Handles relative/absolute paths
28 |       * Uses configured log level
29 |       * Applies custom format
30 |     
31 |     - Console logging:
32 |       * Always enabled for errors
33 |       * Ensures critical issues are visible
34 |     
35 |     - Handler Management:
36 |       * Removes existing handlers
37 |       * Configures new handlers
38 |       * Sets up formatters
39 |     
40 |     Args:
41 |         config: Logging configuration containing:
42 |                - Log level (e.g., "INFO", "DEBUG")
43 |                - Format string
44 |                - Optional log file path
45 | 
46 |     Returns:
47 |         Configured logger instance for "proxmox-mcp"
48 |         with appropriate handlers and formatting
49 | 
50 |     Example config:
51 |         {
52 |             "level": "INFO",
53 |             "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
54 |             "file": "/path/to/log/file.log"  # Optional
55 |         }
56 |     """
57 |     # Convert relative path to absolute
58 |     log_file = config.file
59 |     if log_file and not os.path.isabs(log_file):
60 |         log_file = os.path.join(os.getcwd(), log_file)
61 |         
62 |     # Create handlers
63 |     handlers = []
64 |     
65 |     if log_file:
66 |         file_handler = logging.FileHandler(log_file)
67 |         file_handler.setLevel(getattr(logging, config.level.upper()))
68 |         handlers.append(file_handler)
69 |     
70 |     # Console handler for errors only
71 |     console_handler = logging.StreamHandler()
72 |     console_handler.setLevel(logging.ERROR)
73 |     handlers.append(console_handler)
74 |     
75 |     # Configure formatters
76 |     formatter = logging.Formatter(config.format)
77 |     for handler in handlers:
78 |         handler.setFormatter(formatter)
79 |     
80 |     # Configure root logger
81 |     root_logger = logging.getLogger()
82 |     root_logger.setLevel(getattr(logging, config.level.upper()))
83 |     
84 |     # Remove any existing handlers
85 |     for handler in root_logger.handlers[:]:
86 |         root_logger.removeHandler(handler)
87 |     
88 |     # Add new handlers
89 |     for handler in handlers:
90 |         root_logger.addHandler(handler)
91 |     
92 |     # Create and return server logger
93 |     logger = logging.getLogger("proxmox-mcp")
94 |     return logger
95 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/cluster.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Cluster-related tools for Proxmox MCP.
 3 | 
 4 | This module provides tools for monitoring and managing Proxmox clusters:
 5 | - Retrieving overall cluster health status
 6 | - Monitoring quorum status and node count
 7 | - Tracking cluster resources and configuration
 8 | - Checking cluster-wide service availability
 9 | 
10 | The tools provide essential information for maintaining
11 | cluster health and ensuring proper operation.
12 | """
13 | from typing import List
14 | from mcp.types import TextContent as Content
15 | from .base import ProxmoxTool
16 | from .definitions import GET_CLUSTER_STATUS_DESC
17 | 
18 | class ClusterTools(ProxmoxTool):
19 |     """Tools for managing Proxmox cluster.
20 |     
21 |     Provides functionality for:
22 |     - Monitoring cluster health and status
23 |     - Tracking quorum and node membership
24 |     - Managing cluster-wide resources
25 |     - Verifying cluster configuration
26 |     
27 |     Essential for maintaining cluster health and ensuring
28 |     proper operation of the Proxmox environment.
29 |     """
30 | 
31 |     def get_cluster_status(self) -> List[Content]:
32 |         """Get overall Proxmox cluster health and configuration status.
33 | 
34 |         Retrieves comprehensive cluster information including:
35 |         - Cluster name and identity
36 |         - Quorum status (essential for cluster operations)
37 |         - Active node count and health
38 |         - Resource distribution and status
39 |         
40 |         This information is critical for:
41 |         - Ensuring cluster stability
42 |         - Monitoring node membership
43 |         - Verifying resource availability
44 |         - Detecting potential issues
45 | 
46 |         Returns:
47 |             List of Content objects containing formatted cluster status:
48 |             {
49 |                 "name": "cluster-name",
50 |                 "quorum": true/false,
51 |                 "nodes": count,
52 |                 "resources": [
53 |                     {
54 |                         "type": "resource-type",
55 |                         "status": "status"
56 |                     }
57 |                 ]
58 |             }
59 | 
60 |         Raises:
61 |             RuntimeError: If cluster status query fails due to:
62 |                         - Network connectivity issues
63 |                         - Authentication problems
64 |                         - API endpoint failures
65 |         """
66 |         try:
67 |             result = self.proxmox.cluster.status.get()
68 |         
69 |             first_item = result[0] if result and len(result) > 0 else {}
70 |             status = {
71 |                 "name": first_item.get("name") if first_item else None,
72 |                 "quorum": first_item.get("quorate") if first_item else None,
73 |                 "nodes": len([node for node in result if node.get("type") == "node"]) if result else 0,
74 |                 "resources": [res for res in result if res.get("type") == "resource"] if result else []
75 |             }
76 |             return self._format_response(status, "cluster")
77 |         except Exception as e:
78 |             self._handle_error("get cluster status", e)
79 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/config/models.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Configuration models for the Proxmox MCP server.
 3 | 
 4 | This module defines Pydantic models for configuration validation:
 5 | - Proxmox connection settings
 6 | - Authentication credentials
 7 | - Logging configuration
 8 | - Tool-specific parameter models
 9 | 
10 | The models provide:
11 | - Type validation
12 | - Default values
13 | - Field descriptions
14 | - Required vs optional field handling
15 | """
16 | from typing import Optional, Annotated
17 | from pydantic import BaseModel, Field
18 | 
19 | class NodeStatus(BaseModel):
20 |     """Model for node status query parameters.
21 |     
22 |     Validates and documents the required parameters for
23 |     querying a specific node's status in the cluster.
24 |     """
25 |     node: Annotated[str, Field(description="Name/ID of node to query (e.g. 'pve1', 'proxmox-node2')")]
26 | 
27 | class VMCommand(BaseModel):
28 |     """Model for VM command execution parameters.
29 |     
30 |     Validates and documents the required parameters for
31 |     executing commands within a VM via QEMU guest agent.
32 |     """
33 |     node: Annotated[str, Field(description="Host node name (e.g. 'pve1', 'proxmox-node2')")]
34 |     vmid: Annotated[str, Field(description="VM ID number (e.g. '100', '101')")]
35 |     command: Annotated[str, Field(description="Shell command to run (e.g. 'uname -a', 'systemctl status nginx')")]
36 | 
37 | class ProxmoxConfig(BaseModel):
38 |     """Model for Proxmox connection configuration.
39 |     
40 |     Defines the required and optional parameters for
41 |     establishing a connection to the Proxmox API server.
42 |     Provides sensible defaults for optional parameters.
43 |     """
44 |     host: str  # Required: Proxmox host address
45 |     port: int = 8006  # Optional: API port (default: 8006)
46 |     verify_ssl: bool = True  # Optional: SSL verification (default: True)
47 |     service: str = "PVE"  # Optional: Service type (default: PVE)
48 | 
49 | class AuthConfig(BaseModel):
50 |     """Model for Proxmox authentication configuration.
51 |     
52 |     Defines the required parameters for API authentication
53 |     using token-based authentication. All fields are required
54 |     to ensure secure API access.
55 |     """
56 |     user: str  # Required: Username (e.g., 'root@pam')
57 |     token_name: str  # Required: API token name
58 |     token_value: str  # Required: API token secret
59 | 
60 | class LoggingConfig(BaseModel):
61 |     """Model for logging configuration.
62 |     
63 |     Defines logging parameters with sensible defaults.
64 |     Supports both file and console logging with
65 |     customizable format and log levels.
66 |     """
67 |     level: str = "INFO"  # Optional: Log level (default: INFO)
68 |     format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"  # Optional: Log format
69 |     file: Optional[str] = None  # Optional: Log file path (default: None for console logging)
70 | 
71 | class Config(BaseModel):
72 |     """Root configuration model.
73 |     
74 |     Combines all configuration models into a single validated
75 |     configuration object. All sections are required to ensure
76 |     proper server operation.
77 |     """
78 |     proxmox: ProxmoxConfig  # Required: Proxmox connection settings
79 |     auth: AuthConfig  # Required: Authentication credentials
80 |     logging: LoggingConfig  # Required: Logging configuration
81 | 
```

--------------------------------------------------------------------------------
/tests/test_vm_console.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Tests for VM console operations.
 3 | """
 4 | 
 5 | import pytest
 6 | from unittest.mock import Mock, patch
 7 | 
 8 | from proxmox_mcp.tools.console import VMConsoleManager
 9 | 
10 | @pytest.fixture
11 | def mock_proxmox():
12 |     """Fixture to create a mock ProxmoxAPI instance."""
13 |     mock = Mock()
14 |     # Setup chained mock calls
15 |     mock.nodes.return_value.qemu.return_value.status.current.get.return_value = {
16 |         "status": "running"
17 |     }
18 |     mock.nodes.return_value.qemu.return_value.agent.exec.post.return_value = {
19 |         "out": "command output",
20 |         "err": "",
21 |         "exitcode": 0
22 |     }
23 |     return mock
24 | 
25 | @pytest.fixture
26 | def vm_console(mock_proxmox):
27 |     """Fixture to create a VMConsoleManager instance."""
28 |     return VMConsoleManager(mock_proxmox)
29 | 
30 | @pytest.mark.asyncio
31 | async def test_execute_command_success(vm_console, mock_proxmox):
32 |     """Test successful command execution."""
33 |     result = await vm_console.execute_command("node1", "100", "ls -l")
34 | 
35 |     assert result["success"] is True
36 |     assert result["output"] == "command output"
37 |     assert result["error"] == ""
38 |     assert result["exit_code"] == 0
39 | 
40 |     # Verify correct API calls
41 |     mock_proxmox.nodes.return_value.qemu.assert_called_with("100")
42 |     mock_proxmox.nodes.return_value.qemu.return_value.agent.exec.post.assert_called_with(
43 |         command="ls -l"
44 |     )
45 | 
46 | @pytest.mark.asyncio
47 | async def test_execute_command_vm_not_running(vm_console, mock_proxmox):
48 |     """Test command execution on stopped VM."""
49 |     mock_proxmox.nodes.return_value.qemu.return_value.status.current.get.return_value = {
50 |         "status": "stopped"
51 |     }
52 | 
53 |     with pytest.raises(ValueError, match="not running"):
54 |         await vm_console.execute_command("node1", "100", "ls -l")
55 | 
56 | @pytest.mark.asyncio
57 | async def test_execute_command_vm_not_found(vm_console, mock_proxmox):
58 |     """Test command execution on non-existent VM."""
59 |     mock_proxmox.nodes.return_value.qemu.return_value.status.current.get.side_effect = \
60 |         Exception("VM not found")
61 | 
62 |     with pytest.raises(ValueError, match="not found"):
63 |         await vm_console.execute_command("node1", "100", "ls -l")
64 | 
65 | @pytest.mark.asyncio
66 | async def test_execute_command_failure(vm_console, mock_proxmox):
67 |     """Test command execution failure."""
68 |     mock_proxmox.nodes.return_value.qemu.return_value.agent.exec.post.side_effect = \
69 |         Exception("Command failed")
70 | 
71 |     with pytest.raises(RuntimeError, match="Failed to execute command"):
72 |         await vm_console.execute_command("node1", "100", "ls -l")
73 | 
74 | @pytest.mark.asyncio
75 | async def test_execute_command_with_error_output(vm_console, mock_proxmox):
76 |     """Test command execution with error output."""
77 |     mock_proxmox.nodes.return_value.qemu.return_value.agent.exec.post.return_value = {
78 |         "out": "",
79 |         "err": "command error",
80 |         "exitcode": 1
81 |     }
82 | 
83 |     result = await vm_console.execute_command("node1", "100", "invalid-command")
84 | 
85 |     assert result["success"] is True  # Success refers to API call, not command
86 |     assert result["output"] == ""
87 |     assert result["error"] == "command error"
88 |     assert result["exit_code"] == 1
89 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/formatting/colors.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Color utilities for Proxmox MCP output styling.
  3 | """
  4 | from typing import Optional
  5 | from .theme import ProxmoxTheme
  6 | 
  7 | class ProxmoxColors:
  8 |     """ANSI color definitions and utilities for terminal output."""
  9 |     
 10 |     # Foreground colors
 11 |     BLACK = '\033[30m'
 12 |     RED = '\033[31m'
 13 |     GREEN = '\033[32m'
 14 |     YELLOW = '\033[33m'
 15 |     BLUE = '\033[34m'
 16 |     MAGENTA = '\033[35m'
 17 |     CYAN = '\033[36m'
 18 |     WHITE = '\033[37m'
 19 |     
 20 |     # Background colors
 21 |     BG_BLACK = '\033[40m'
 22 |     BG_RED = '\033[41m'
 23 |     BG_GREEN = '\033[42m'
 24 |     BG_YELLOW = '\033[43m'
 25 |     BG_BLUE = '\033[44m'
 26 |     BG_MAGENTA = '\033[45m'
 27 |     BG_CYAN = '\033[46m'
 28 |     BG_WHITE = '\033[47m'
 29 |     
 30 |     # Styles
 31 |     BOLD = '\033[1m'
 32 |     DIM = '\033[2m'
 33 |     ITALIC = '\033[3m'
 34 |     UNDERLINE = '\033[4m'
 35 |     BLINK = '\033[5m'
 36 |     REVERSE = '\033[7m'
 37 |     HIDDEN = '\033[8m'
 38 |     STRIKE = '\033[9m'
 39 |     
 40 |     # Reset
 41 |     RESET = '\033[0m'
 42 |     
 43 |     @classmethod
 44 |     def colorize(cls, text: str, color: str, style: Optional[str] = None) -> str:
 45 |         """Add color and optional style to text with theme awareness.
 46 |         
 47 |         Args:
 48 |             text: Text to colorize
 49 |             color: ANSI color code
 50 |             style: Optional ANSI style code
 51 |             
 52 |         Returns:
 53 |             Formatted text string
 54 |         """
 55 |         if not ProxmoxTheme.USE_COLORS:
 56 |             return text
 57 |             
 58 |         if style:
 59 |             return f"{style}{color}{text}{cls.RESET}"
 60 |         return f"{color}{text}{cls.RESET}"
 61 |     
 62 |     @classmethod
 63 |     def status_color(cls, status: str) -> str:
 64 |         """Get appropriate color for a status value.
 65 |         
 66 |         Args:
 67 |             status: Status string to get color for
 68 |             
 69 |         Returns:
 70 |             ANSI color code
 71 |         """
 72 |         status = status.lower()
 73 |         if status in ['online', 'running', 'success']:
 74 |             return cls.GREEN
 75 |         elif status in ['offline', 'stopped', 'error']:
 76 |             return cls.RED
 77 |         elif status in ['pending', 'warning']:
 78 |             return cls.YELLOW
 79 |         return cls.BLUE
 80 |     
 81 |     @classmethod
 82 |     def resource_color(cls, resource_type: str) -> str:
 83 |         """Get appropriate color for a resource type.
 84 |         
 85 |         Args:
 86 |             resource_type: Resource type to get color for
 87 |             
 88 |         Returns:
 89 |             ANSI color code
 90 |         """
 91 |         resource_type = resource_type.lower()
 92 |         if resource_type in ['node', 'vm', 'container']:
 93 |             return cls.CYAN
 94 |         elif resource_type in ['cpu', 'memory', 'network']:
 95 |             return cls.YELLOW
 96 |         elif resource_type in ['storage', 'disk']:
 97 |             return cls.MAGENTA
 98 |         return cls.BLUE
 99 |     
100 |     @classmethod
101 |     def metric_color(cls, value: float, warning: float = 80.0, critical: float = 90.0) -> str:
102 |         """Get appropriate color for a metric value based on thresholds.
103 |         
104 |         Args:
105 |             value: Metric value (typically percentage)
106 |             warning: Warning threshold
107 |             critical: Critical threshold
108 |             
109 |         Returns:
110 |             ANSI color code
111 |         """
112 |         if value >= critical:
113 |             return cls.RED
114 |         elif value >= warning:
115 |             return cls.YELLOW
116 |         return cls.GREEN
117 | 
```

--------------------------------------------------------------------------------
/test_scripts/test_create_vm.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Test VM creation functionality
  4 | """
  5 | import os
  6 | import sys
  7 | 
  8 | def test_create_vm():
  9 |     """Test creating VM - 1 CPU, 2GB RAM, 10GB storage"""
 10 |     
 11 |     # Set configuration
 12 |     os.environ['PROXMOX_MCP_CONFIG'] = 'proxmox-config/config.json'
 13 |     
 14 |     try:
 15 |         from proxmox_mcp.config.loader import load_config
 16 |         from proxmox_mcp.core.proxmox import ProxmoxManager
 17 |         from proxmox_mcp.tools.vm import VMTools
 18 |         
 19 |         config = load_config('proxmox-config/config.json')
 20 |         manager = ProxmoxManager(config.proxmox, config.auth)
 21 |         api = manager.get_api()
 22 |         
 23 |         vm_tools = VMTools(api)
 24 |         
 25 |         print("🎉 Test creating new VM - user requested configuration")
 26 |         print("=" * 60)
 27 |         print("Configuration:")
 28 |         print("  • CPU: 1 core")
 29 |         print("  • RAM: 2 GB (2048 MB)")
 30 |         print("  • Storage: 10 GB")
 31 |         print("  • VM ID: 999 (test purpose)")
 32 |         print("  • Name: test-vm-demo")
 33 |         print()
 34 |         
 35 |         # Find an available VM ID
 36 |         vmid = "999"
 37 |         
 38 |         # Check if VM ID already exists
 39 |         try:
 40 |             existing_vm = api.nodes("pve").qemu(vmid).config.get()
 41 |             print(f"⚠️ VM {vmid} already exists, will try VM ID 998")
 42 |             vmid = "998"
 43 |             existing_vm = api.nodes("pve").qemu(vmid).config.get()
 44 |             print(f"⚠️ VM {vmid} also exists, will try VM ID 997")
 45 |             vmid = "997"
 46 |         except:
 47 |             print(f"✅ VM ID {vmid} is available")
 48 |         
 49 |         # Create VM
 50 |         result = vm_tools.create_vm(
 51 |             node="pve",
 52 |             vmid=vmid,
 53 |             name="test-vm-demo",
 54 |             cpus=1,
 55 |             memory=2048,  # 2GB in MB
 56 |             disk_size=10  # 10GB
 57 |         )
 58 |         
 59 |         for content in result:
 60 |             print(content.text)
 61 |             
 62 |         return True
 63 |         
 64 |     except Exception as e:
 65 |         print(f"❌ Creation failed: {e}")
 66 |         return False
 67 | 
 68 | def test_list_vms():
 69 |     """Test listing VMs to confirm successful creation"""
 70 |     
 71 |     os.environ['PROXMOX_MCP_CONFIG'] = 'proxmox-config/config.json'
 72 |     
 73 |     try:
 74 |         from proxmox_mcp.config.loader import load_config
 75 |         from proxmox_mcp.core.proxmox import ProxmoxManager
 76 |         from proxmox_mcp.tools.vm import VMTools
 77 |         
 78 |         config = load_config('proxmox-config/config.json')
 79 |         manager = ProxmoxManager(config.proxmox, config.auth)
 80 |         api = manager.get_api()
 81 |         
 82 |         vm_tools = VMTools(api)
 83 |         
 84 |         print("\n🔍 List all VMs to confirm creation results:")
 85 |         print("=" * 40)
 86 |         
 87 |         result = vm_tools.get_vms()
 88 |         for content in result:
 89 |             # Only show newly created VM information
 90 |             lines = content.text.split('\n')
 91 |             for line in lines:
 92 |                 if 'test-vm-demo' in line or 'VM 99' in line:
 93 |                     print(line)
 94 |         
 95 |         return True
 96 |         
 97 |     except Exception as e:
 98 |         print(f"❌ List query failed: {e}")
 99 |         return False
100 | 
101 | if __name__ == "__main__":
102 |     print("🔍 Test VM creation functionality")
103 |     print("=" * 50)
104 |     
105 |     success = test_create_vm()
106 |     
107 |     if success:
108 |         print("\n✅ Creation test completed")
109 |         # Test listing VMs
110 |         test_list_vms()
111 |     else:
112 |         print("\n❌ Creation test failed")
113 |         sys.exit(1) 
```

--------------------------------------------------------------------------------
/test_scripts/test_vm_power.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Test Proxmox VM power management functionality
 4 | """
 5 | import sys
 6 | from test_common import setup_test_environment, get_test_tools, print_test_header, print_test_result
 7 | 
 8 | def test_vm_power_operations():
 9 |     """Test VM power management operations"""
10 |     
11 |     try:
12 |         # Set up test environment
13 |         setup_test_environment()
14 |         tools = get_test_tools()
15 |         
16 |         api = tools['api']
17 |         nodes = api.nodes.get()
18 |         
19 |         # Safely get the first node to avoid index out of range error
20 |         if not nodes or len(nodes) == 0:
21 |             print("❌ No Proxmox nodes found")
22 |             return False
23 |             
24 |         node_name = nodes[0]['node']
25 |         
26 |         print(f"Test node: {node_name}")
27 |         
28 |         # Get all VMs
29 |         vms = api.nodes(node_name).qemu.get()
30 |         print(f"Found {len(vms)} virtual machines:")
31 |         
32 |         vm_101_found = False
33 |         for vm in vms:
34 |             vmid = vm['vmid']
35 |             name = vm['name']
36 |             status = vm['status']
37 |             print(f"  - VM {vmid}: {name} ({status})")
38 |             
39 |             if vmid == 101:
40 |                 vm_101_found = True
41 |                 print(f"\nFound VPN-Server (ID: 101), current status: {status}")
42 |                 
43 |                 # Test available status operations
44 |                 vm_api = api.nodes(node_name).qemu(vmid)
45 |                 status_api = vm_api.status
46 |                 
47 |                 print("Test available status operations:")
48 |                 
49 |                 # Try accessing different status endpoints
50 |                 try:
51 |                     # Check if start endpoint exists
52 |                     if hasattr(status_api, 'start'):
53 |                         print("  ✅ Supports start operation")
54 |                     else:
55 |                         print("  ❌ Does not support start operation")
56 |                         
57 |                     if hasattr(status_api, 'stop'):
58 |                         print("  ✅ Supports stop operation")
59 |                     else:
60 |                         print("  ❌ Does not support stop operation")
61 |                         
62 |                     if hasattr(status_api, 'reset'):
63 |                         print("  ✅ Supports reset operation")
64 |                     else:
65 |                         print("  ❌ Does not support reset operation")
66 |                         
67 |                     if hasattr(status_api, 'shutdown'):
68 |                         print("  ✅ Supports shutdown operation")
69 |                     else:
70 |                         print("  ❌ Does not support shutdown operation")
71 |                         
72 |                     # If VM is stopped, try to start
73 |                     if status == 'stopped':
74 |                         print(f"\nVM {vmid} is currently stopped, can try to start")
75 |                         print("Start command would be: api.nodes(node).qemu(101).status.start.post()")
76 |                         
77 |                     elif status == 'running':
78 |                         print(f"\nVM {vmid} is currently running")
79 |                         
80 |                 except Exception as e:
81 |                     print(f"  Error while testing operations: {e}")
82 |         
83 |         if not vm_101_found:
84 |             print("\n❌ VM 101 (VPN-Server) not found")
85 |             
86 |     except Exception as e:
87 |         print(f"Test failed: {e}")
88 |         return False
89 |         
90 |     return True
91 | 
92 | if __name__ == "__main__":
93 |     print_test_header("Test Proxmox VM power management functionality")
94 |     
95 |     success = test_vm_power_operations()
96 |     print_test_result(success) 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/storage.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Storage-related tools for Proxmox MCP.
 3 | 
 4 | This module provides tools for managing and monitoring Proxmox storage:
 5 | - Listing all storage pools across the cluster
 6 | - Retrieving detailed storage information including:
 7 |   * Storage type and content types
 8 |   * Usage statistics and capacity
 9 |   * Availability status
10 |   * Node assignments
11 | 
12 | The tools implement fallback mechanisms for scenarios where
13 | detailed storage information might be temporarily unavailable.
14 | """
15 | from typing import List
16 | from mcp.types import TextContent as Content
17 | from .base import ProxmoxTool
18 | from .definitions import GET_STORAGE_DESC
19 | 
20 | class StorageTools(ProxmoxTool):
21 |     """Tools for managing Proxmox storage.
22 |     
23 |     Provides functionality for:
24 |     - Retrieving cluster-wide storage information
25 |     - Monitoring storage pool status and health
26 |     - Tracking storage utilization and capacity
27 |     - Managing storage content types
28 |     
29 |     Implements fallback mechanisms for scenarios where detailed
30 |     storage information might be temporarily unavailable.
31 |     """
32 | 
33 |     def get_storage(self) -> List[Content]:
34 |         """List storage pools across the cluster with detailed status.
35 | 
36 |         Retrieves comprehensive information for each storage pool including:
37 |         - Basic identification (name, type)
38 |         - Content types supported (VM disks, backups, ISO images, etc.)
39 |         - Availability status (online/offline)
40 |         - Usage statistics:
41 |           * Used space
42 |           * Total capacity
43 |           * Available space
44 |         
45 |         Implements a fallback mechanism that returns basic information
46 |         if detailed status retrieval fails for any storage pool.
47 | 
48 |         Returns:
49 |             List of Content objects containing formatted storage information:
50 |             {
51 |                 "storage": "storage-name",
52 |                 "type": "storage-type",
53 |                 "content": ["content-types"],
54 |                 "status": "online/offline",
55 |                 "used": bytes,
56 |                 "total": bytes,
57 |                 "available": bytes
58 |             }
59 | 
60 |         Raises:
61 |             RuntimeError: If the cluster-wide storage query fails
62 |         """
63 |         try:
64 |             result = self.proxmox.storage.get()
65 |             storage = []
66 |             
67 |             for store in result:
68 |                 # Get detailed storage info including usage
69 |                 try:
70 |                     status = self.proxmox.nodes(store.get("node", "localhost")).storage(store["storage"]).status.get()
71 |                     storage.append({
72 |                         "storage": store["storage"],
73 |                         "type": store["type"],
74 |                         "content": store.get("content", []),
75 |                         "status": "online" if store.get("enabled", True) else "offline",
76 |                         "used": status.get("used", 0),
77 |                         "total": status.get("total", 0),
78 |                         "available": status.get("avail", 0)
79 |                     })
80 |                 except Exception:
81 |                     # If detailed status fails, add basic info
82 |                     storage.append({
83 |                         "storage": store["storage"],
84 |                         "type": store["type"],
85 |                         "content": store.get("content", []),
86 |                         "status": "online" if store.get("enabled", True) else "offline",
87 |                         "used": 0,
88 |                         "total": 0,
89 |                         "available": 0
90 |                     })
91 |                     
92 |             return self._format_response(storage, "storage")
93 |         except Exception as e:
94 |             self._handle_error("get storage", e)
95 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/core/proxmox.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Proxmox API setup and management.
  3 | 
  4 | This module handles the core Proxmox API integration, providing:
  5 | - Secure API connection setup and management
  6 | - Token-based authentication
  7 | - Connection testing and validation
  8 | - Error handling for API operations
  9 | 
 10 | The ProxmoxManager class serves as the central point for all Proxmox API
 11 | interactions, ensuring consistent connection handling and authentication
 12 | across the MCP server.
 13 | """
 14 | import logging
 15 | from typing import Dict, Any
 16 | from proxmoxer import ProxmoxAPI
 17 | from ..config.models import ProxmoxConfig, AuthConfig
 18 | 
 19 | class ProxmoxManager:
 20 |     """Manager class for Proxmox API operations.
 21 |     
 22 |     This class handles:
 23 |     - API connection initialization and management
 24 |     - Configuration validation and merging
 25 |     - Connection testing and health checks
 26 |     - Token-based authentication setup
 27 |     
 28 |     The manager provides a single point of access to the Proxmox API,
 29 |     ensuring proper initialization and error handling for all API operations.
 30 |     """
 31 |     
 32 |     def __init__(self, proxmox_config: ProxmoxConfig, auth_config: AuthConfig):
 33 |         """Initialize the Proxmox API manager.
 34 | 
 35 |         Args:
 36 |             proxmox_config: Proxmox connection configuration
 37 |             auth_config: Authentication configuration
 38 |         """
 39 |         self.logger = logging.getLogger("proxmox-mcp.proxmox")
 40 |         self.config = self._create_config(proxmox_config, auth_config)
 41 |         self.api = self._setup_api()
 42 | 
 43 |     def _create_config(self, proxmox_config: ProxmoxConfig, auth_config: AuthConfig) -> Dict[str, Any]:
 44 |         """Create a configuration dictionary for ProxmoxAPI.
 45 | 
 46 |         Merges connection and authentication configurations into a single
 47 |         dictionary suitable for ProxmoxAPI initialization. Handles:
 48 |         - Host and port configuration
 49 |         - SSL verification settings
 50 |         - Token-based authentication details
 51 |         - Service type specification
 52 | 
 53 |         Args:
 54 |             proxmox_config: Proxmox connection configuration (host, port, SSL settings)
 55 |             auth_config: Authentication configuration (user, token details)
 56 | 
 57 |         Returns:
 58 |             Dictionary containing merged configuration ready for API initialization
 59 |         """
 60 |         return {
 61 |             'host': proxmox_config.host,
 62 |             'port': proxmox_config.port,
 63 |             'user': auth_config.user,
 64 |             'token_name': auth_config.token_name,
 65 |             'token_value': auth_config.token_value,
 66 |             'verify_ssl': proxmox_config.verify_ssl,
 67 |             'service': proxmox_config.service
 68 |         }
 69 | 
 70 |     def _setup_api(self) -> ProxmoxAPI:
 71 |         """Initialize and test Proxmox API connection.
 72 | 
 73 |         Performs the following steps:
 74 |         1. Creates ProxmoxAPI instance with configured settings
 75 |         2. Tests connection by making a version check request
 76 |         3. Validates authentication and permissions
 77 |         4. Logs connection status and any issues
 78 | 
 79 |         Returns:
 80 |             Initialized and tested ProxmoxAPI instance
 81 | 
 82 |         Raises:
 83 |             RuntimeError: If connection fails due to:
 84 |                         - Invalid host/port
 85 |                         - Authentication failure
 86 |                         - Network connectivity issues
 87 |                         - SSL certificate validation errors
 88 |         """
 89 |         try:
 90 |             self.logger.info(f"Connecting to Proxmox host: {self.config['host']}")
 91 |             api = ProxmoxAPI(**self.config)
 92 |             
 93 |             # Test connection
 94 |             api.version.get()
 95 |             self.logger.info("Successfully connected to Proxmox API")
 96 |             
 97 |             return api
 98 |         except Exception as e:
 99 |             self.logger.error(f"Failed to connect to Proxmox: {e}")
100 |             raise RuntimeError(f"Failed to connect to Proxmox: {e}")
101 | 
102 |     def get_api(self) -> ProxmoxAPI:
103 |         """Get the initialized Proxmox API instance.
104 |         
105 |         Provides access to the configured and tested ProxmoxAPI instance
106 |         for making API calls. The instance maintains connection state and
107 |         handles authentication automatically.
108 | 
109 |         Returns:
110 |             ProxmoxAPI instance ready for making API calls
111 |         """
112 |         return self.api
113 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/base.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Base classes and utilities for Proxmox MCP tools.
  3 | 
  4 | This module provides the foundation for all Proxmox MCP tools, including:
  5 | - Base tool class with common functionality
  6 | - Response formatting utilities
  7 | - Error handling mechanisms
  8 | - Logging setup
  9 | 
 10 | All tool implementations inherit from the ProxmoxTool base class to ensure
 11 | consistent behavior and error handling across the MCP server.
 12 | """
 13 | import logging
 14 | from typing import Any, Dict, List, Optional, Union
 15 | from mcp.types import TextContent as Content
 16 | from proxmoxer import ProxmoxAPI
 17 | from ..formatting import ProxmoxTemplates
 18 | 
 19 | class ProxmoxTool:
 20 |     """Base class for Proxmox MCP tools.
 21 |     
 22 |     This class provides common functionality used by all Proxmox tool implementations:
 23 |     - Proxmox API access
 24 |     - Standardized logging
 25 |     - Response formatting
 26 |     - Error handling
 27 |     
 28 |     All tool classes should inherit from this base class to ensure consistent
 29 |     behavior and error handling across the MCP server.
 30 |     """
 31 | 
 32 |     def __init__(self, proxmox_api: ProxmoxAPI):
 33 |         """Initialize the tool.
 34 | 
 35 |         Args:
 36 |             proxmox_api: Initialized ProxmoxAPI instance
 37 |         """
 38 |         self.proxmox = proxmox_api
 39 |         self.logger = logging.getLogger(f"proxmox-mcp.{self.__class__.__name__.lower()}")
 40 | 
 41 |     def _format_response(self, data: Any, resource_type: Optional[str] = None) -> List[Content]:
 42 |         """Format response data into MCP content using templates.
 43 | 
 44 |         This method handles formatting of various Proxmox resource types into
 45 |         consistent MCP content responses. It uses specialized templates for
 46 |         different resource types (nodes, VMs, storage, etc.) and falls back
 47 |         to JSON formatting for unknown types.
 48 | 
 49 |         Args:
 50 |             data: Raw data from Proxmox API to format
 51 |             resource_type: Type of resource for template selection. Valid types:
 52 |                          'nodes', 'node_status', 'vms', 'storage', 'containers', 'cluster'
 53 | 
 54 |         Returns:
 55 |             List of Content objects formatted according to resource type
 56 |         """
 57 |         if resource_type == "nodes":
 58 |             formatted = ProxmoxTemplates.node_list(data)
 59 |         elif resource_type == "node_status":
 60 |             # For node_status, data should be a tuple of (node_name, status_dict)
 61 |             if isinstance(data, tuple) and len(data) == 2:
 62 |                 formatted = ProxmoxTemplates.node_status(data[0], data[1])
 63 |             else:
 64 |                 formatted = ProxmoxTemplates.node_status("unknown", data)
 65 |         elif resource_type == "vms":
 66 |             formatted = ProxmoxTemplates.vm_list(data)
 67 |         elif resource_type == "storage":
 68 |             formatted = ProxmoxTemplates.storage_list(data)
 69 |         elif resource_type == "containers":
 70 |             formatted = ProxmoxTemplates.container_list(data)
 71 |         elif resource_type == "cluster":
 72 |             formatted = ProxmoxTemplates.cluster_status(data)
 73 |         else:
 74 |             # Fallback to JSON formatting for unknown types
 75 |             import json
 76 |             formatted = json.dumps(data, indent=2)
 77 | 
 78 |         return [Content(type="text", text=formatted)]
 79 | 
 80 |     def _handle_error(self, operation: str, error: Exception) -> None:
 81 |         """Handle and log errors from Proxmox operations.
 82 | 
 83 |         Provides standardized error handling across all tools by:
 84 |         - Logging errors with appropriate context
 85 |         - Categorizing errors into specific exception types
 86 |         - Converting Proxmox-specific errors into standard Python exceptions
 87 | 
 88 |         Args:
 89 |             operation: Description of the operation that failed (e.g., "get node status")
 90 |             error: The exception that occurred during the operation
 91 | 
 92 |         Raises:
 93 |             ValueError: For invalid input, missing resources, or permission issues
 94 |             RuntimeError: For unexpected errors or API failures
 95 |         """
 96 |         error_msg = str(error)
 97 |         self.logger.error(f"Failed to {operation}: {error_msg}")
 98 | 
 99 |         if "not found" in error_msg.lower():
100 |             raise ValueError(f"Resource not found: {error_msg}")
101 |         if "permission denied" in error_msg.lower():
102 |             raise ValueError(f"Permission denied: {error_msg}")
103 |         if "invalid" in error_msg.lower():
104 |             raise ValueError(f"Invalid input: {error_msg}")
105 |         
106 |         raise RuntimeError(f"Failed to {operation}: {error_msg}")
107 | 
```

--------------------------------------------------------------------------------
/test_scripts/test_openapi.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Test OpenAPI functionality
  4 | """
  5 | import requests
  6 | import json
  7 | import os
  8 | 
  9 | # Get base URL from environment variable or use default localhost
 10 | BASE_URL = os.getenv('OPENAPI_BASE_URL', 'http://localhost:8811')
 11 | 
 12 | def test_basic_endpoints():
 13 |     """Test basic API endpoints"""
 14 |     
 15 |     print("🔍 Test basic API endpoints")
 16 |     print(f"🌐 Using base URL: {BASE_URL}")
 17 |     print("=" * 50)
 18 |     
 19 |     # Test get nodes
 20 |     try:
 21 |         response = requests.post(f"{BASE_URL}/get_nodes")
 22 |         print(f"✅ get_nodes: {response.status_code} - {len(response.text)} chars")
 23 |     except Exception as e:
 24 |         print(f"❌ get_nodes error: {e}")
 25 |     
 26 |     # Test get VM list
 27 |     try:
 28 |         response = requests.post(f"{BASE_URL}/get_vms")
 29 |         print(f"✅ get_vms: {response.status_code} - {len(response.text)} chars")
 30 |         if response.status_code == 200:
 31 |             # Check if our test VMs are included
 32 |             if "test-vm" in response.text:
 33 |                 print("  📋 Test VM found")
 34 |     except Exception as e:
 35 |         print(f"❌ get_vms error: {e}")
 36 | 
 37 | def test_vm_creation_api():
 38 |     """Test VM creation API"""
 39 |     
 40 |     print("\n🎉 Test VM creation API - user requested configuration")
 41 |     print("=" * 50)
 42 |     print("Configuration: 1 CPU core, 2GB RAM, 10GB storage")
 43 |     
 44 |     # VM creation parameters
 45 |     create_data = {
 46 |         "node": "pve",
 47 |         "vmid": "996",  # Use new VM ID
 48 |         "name": "user-requested-vm",
 49 |         "cpus": 1,
 50 |         "memory": 2048,  # 2GB in MB
 51 |         "disk_size": 10  # 10GB
 52 |     }
 53 |     
 54 |     try:
 55 |         response = requests.post(
 56 |             f"{BASE_URL}/create_vm",
 57 |             json=create_data,
 58 |             headers={"Content-Type": "application/json"}
 59 |         )
 60 |         
 61 |         print(f"📡 API response status: {response.status_code}")
 62 |         
 63 |         if response.status_code == 200:
 64 |             result = response.json()
 65 |             print("✅ VM creation successful!")
 66 |             print(f"📄 Response content: {json.dumps(result, indent=2, ensure_ascii=False)}")
 67 |         else:
 68 |             print(f"❌ VM creation failed: {response.text}")
 69 |             
 70 |     except requests.exceptions.ConnectionError:
 71 |         print("❌ Cannot connect to API server - please ensure OpenAPI service is running")
 72 |     except Exception as e:
 73 |         print(f"❌ API call error: {e}")
 74 | 
 75 | def test_vm_power_api():
 76 |     """Test VM power management API"""
 77 |     
 78 |     print("\n🚀 Test VM power management API")
 79 |     print("=" * 50)
 80 |     
 81 |     # Test starting VM 101 (VPN-Server)
 82 |     start_data = {
 83 |         "node": "pve",
 84 |         "vmid": "101"
 85 |     }
 86 |     
 87 |     try:
 88 |         response = requests.post(
 89 |             f"{BASE_URL}/start_vm",
 90 |             json=start_data,
 91 |             headers={"Content-Type": "application/json"}
 92 |         )
 93 |         
 94 |         print(f"📡 Start VM 101 response: {response.status_code}")
 95 |         
 96 |         if response.status_code == 200:
 97 |             result = response.json()
 98 |             print("✅ VM start command successful!")
 99 |             print(f"📄 Response: {json.dumps(result, indent=2, ensure_ascii=False)}")
100 |         else:
101 |             print(f"❌ VM start failed: {response.text}")
102 |             
103 |     except requests.exceptions.ConnectionError:
104 |         print("❌ Cannot connect to API server")
105 |     except Exception as e:
106 |         print(f"❌ API call error: {e}")
107 | 
108 | def list_available_apis():
109 |     """List all available API endpoints"""
110 |     
111 |     print("\n📋 Available API endpoints")
112 |     print("=" * 50)
113 |     
114 |     try:
115 |         response = requests.get(f"{BASE_URL}/openapi.json")
116 |         if response.status_code == 200:
117 |             openapi_spec = response.json()
118 |             paths = openapi_spec.get("paths", {})
119 |             
120 |             print(f"Found {len(paths)} API endpoints:")
121 |             for path, methods in paths.items():
122 |                 for method, details in methods.items():
123 |                     summary = details.get("summary", "No summary")
124 |                     print(f"  • {method.upper()} {path} - {summary}")
125 |         else:
126 |             print(f"❌ Cannot get API specification: {response.status_code}")
127 |             
128 |     except Exception as e:
129 |         print(f"❌ Get API list error: {e}")
130 | 
131 | if __name__ == "__main__":
132 |     print("🔍 ProxmoxMCP OpenAPI functionality test")
133 |     print("=" * 60)
134 |     
135 |     # List available APIs
136 |     list_available_apis()
137 |     
138 |     # Test basic functionality
139 |     test_basic_endpoints()
140 |     
141 |     # Test VM creation functionality
142 |     test_vm_creation_api()
143 |     
144 |     # Test VM power management
145 |     test_vm_power_api()
146 |     
147 |     print("\n✅ All tests completed")
148 |     print("\n💡 Usage instructions:")
149 |     print("When user says 'Can you create a VM with 1 cpu core and 2 GB ram with 10GB of storage disk',")
150 |     print("the AI assistant can call create_vm API to complete the task!")
151 |     print(f"\n🔧 To test with different server, set environment variable:")
152 |     print("export OPENAPI_BASE_URL=http://your-server:8811") 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/formatting/formatters.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Core formatting functions for Proxmox MCP output.
  3 | """
  4 | from typing import List, Union, Dict, Any
  5 | from .theme import ProxmoxTheme
  6 | from .colors import ProxmoxColors
  7 | 
  8 | class ProxmoxFormatters:
  9 |     """Core formatting functions for Proxmox data."""
 10 |     
 11 |     @staticmethod
 12 |     def format_bytes(bytes_value: int) -> str:
 13 |         """Format bytes with proper units.
 14 |         
 15 |         Args:
 16 |             bytes_value: Number of bytes
 17 |             
 18 |         Returns:
 19 |             Formatted string with appropriate unit
 20 |         """
 21 |         for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
 22 |             if bytes_value < 1024:
 23 |                 return f"{bytes_value:.2f} {unit}"
 24 |             bytes_value /= 1024
 25 |         return f"{bytes_value:.2f} TB"
 26 |     
 27 |     @staticmethod
 28 |     def format_uptime(seconds: int) -> str:
 29 |         """Format uptime in seconds to human readable format.
 30 |         
 31 |         Args:
 32 |             seconds: Uptime in seconds
 33 |             
 34 |         Returns:
 35 |             Formatted uptime string
 36 |         """
 37 |         days = seconds // 86400
 38 |         hours = (seconds % 86400) // 3600
 39 |         minutes = (seconds % 3600) // 60
 40 |         
 41 |         parts = []
 42 |         if days > 0:
 43 |             parts.append(f"{days}d")
 44 |         if hours > 0:
 45 |             parts.append(f"{hours}h")
 46 |         if minutes > 0:
 47 |             parts.append(f"{minutes}m")
 48 |             
 49 |         return f"{ProxmoxTheme.METRICS['uptime']} " + " ".join(parts) if parts else "0m"
 50 |     
 51 |     @staticmethod
 52 |     def format_percentage(value: float, warning: float = 80.0, critical: float = 90.0) -> str:
 53 |         """Format percentage with color based on thresholds.
 54 |         
 55 |         Args:
 56 |             value: Percentage value
 57 |             warning: Warning threshold
 58 |             critical: Critical threshold
 59 |             
 60 |         Returns:
 61 |             Formatted percentage string
 62 |         """
 63 |         color = ProxmoxColors.metric_color(value, warning, critical)
 64 |         return ProxmoxColors.colorize(f"{value:.1f}%", color)
 65 |     
 66 |     @staticmethod
 67 |     def format_status(status: str) -> str:
 68 |         """Format status with emoji and color.
 69 |         
 70 |         Args:
 71 |             status: Status string
 72 |             
 73 |         Returns:
 74 |             Formatted status string
 75 |         """
 76 |         status = status.lower()
 77 |         emoji = ProxmoxTheme.get_status_emoji(status)
 78 |         color = ProxmoxColors.status_color(status)
 79 |         return f"{emoji} {ProxmoxColors.colorize(status.upper(), color)}"
 80 |     
 81 |     @staticmethod
 82 |     def format_resource_header(resource_type: str, name: str) -> str:
 83 |         """Format resource header with emoji and styling.
 84 |         
 85 |         Args:
 86 |             resource_type: Type of resource
 87 |             name: Resource name
 88 |             
 89 |         Returns:
 90 |             Formatted header string
 91 |         """
 92 |         emoji = ProxmoxTheme.get_resource_emoji(resource_type)
 93 |         color = ProxmoxColors.resource_color(resource_type)
 94 |         return f"\n{emoji} {ProxmoxColors.colorize(name, color, ProxmoxColors.BOLD)}"
 95 |     
 96 |     @staticmethod
 97 |     def format_section_header(title: str, section_type: str = 'header') -> str:
 98 |         """Format section header with emoji and border.
 99 |         
100 |         Args:
101 |             title: Section title
102 |             section_type: Type of section for emoji selection
103 |             
104 |         Returns:
105 |             Formatted section header
106 |         """
107 |         emoji = ProxmoxTheme.get_section_emoji(section_type)
108 |         header = f"{emoji} {title}"
109 |         border = "═" * len(header)
110 |         return f"\n{header}\n{border}\n"
111 |     
112 |     @staticmethod
113 |     def format_key_value(key: str, value: str, emoji: str = "") -> str:
114 |         """Format key-value pair with optional emoji.
115 |         
116 |         Args:
117 |             key: Label/key
118 |             value: Value to display
119 |             emoji: Optional emoji prefix
120 |             
121 |         Returns:
122 |             Formatted key-value string
123 |         """
124 |         key_str = ProxmoxColors.colorize(key, ProxmoxColors.CYAN)
125 |         prefix = f"{emoji} " if emoji else ""
126 |         return f"{prefix}{key_str}: {value}"
127 |     
128 |     @staticmethod
129 |     def format_command_output(success: bool, command: str, output: str, error: str = None) -> str:
130 |         """Format command execution output.
131 |         
132 |         Args:
133 |             success: Whether command succeeded
134 |             command: The command that was executed
135 |             output: Command output
136 |             error: Optional error message
137 |             
138 |         Returns:
139 |             Formatted command output string
140 |         """
141 |         result = [
142 |             f"{ProxmoxTheme.ACTIONS['command']} Console Command Result",
143 |             f"  • Status: {'SUCCESS' if success else 'FAILED'}",
144 |             f"  • Command: {command}",
145 |             "",
146 |             "Output:",
147 |             output.strip()
148 |         ]
149 |         
150 |         if error:
151 |             result.extend([
152 |                 "",
153 |                 "Error:",
154 |                 error.strip()
155 |             ])
156 |             
157 |         return "\n".join(result)
158 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/definitions.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tool descriptions for Proxmox MCP tools.
  3 | """
  4 | 
  5 | # Node tool descriptions
  6 | GET_NODES_DESC = """List all nodes in the Proxmox cluster with their status, CPU, memory, and role information.
  7 | 
  8 | Example:
  9 | {"node": "pve1", "status": "online", "cpu_usage": 0.15, "memory": {"used": "8GB", "total": "32GB"}}"""
 10 | 
 11 | GET_NODE_STATUS_DESC = """Get detailed status information for a specific Proxmox node.
 12 | 
 13 | Parameters:
 14 | node* - Name/ID of node to query (e.g. 'pve1')
 15 | 
 16 | Example:
 17 | {"cpu": {"usage": 0.15}, "memory": {"used": "8GB", "total": "32GB"}}"""
 18 | 
 19 | # VM tool descriptions
 20 | GET_VMS_DESC = """List all virtual machines across the cluster with their status and resource usage.
 21 | 
 22 | Example:
 23 | {"vmid": "100", "name": "ubuntu", "status": "running", "cpu": 2, "memory": 4096}"""
 24 | 
 25 | CREATE_VM_DESC = """Create a new virtual machine with specified configuration.
 26 | 
 27 | Parameters:
 28 | node* - Host node name (e.g. 'pve')
 29 | vmid* - New VM ID number (e.g. '200', '300')
 30 | name* - VM name (e.g. 'my-new-vm', 'web-server')
 31 | cpus* - Number of CPU cores (e.g. 1, 2, 4)
 32 | memory* - Memory size in MB (e.g. 2048 for 2GB, 4096 for 4GB)
 33 | disk_size* - Disk size in GB (e.g. 10, 20, 50)
 34 | storage - Storage name (optional, will auto-detect if not specified)
 35 | ostype - OS type (optional, default: 'l26' for Linux)
 36 | 
 37 | Examples:
 38 | - Create VM with 1 CPU, 2GB RAM, 10GB disk: node='pve', vmid='200', name='test-vm', cpus=1, memory=2048, disk_size=10
 39 | - Create VM with 2 CPUs, 4GB RAM, 20GB disk: node='pve', vmid='201', name='web-server', cpus=2, memory=4096, disk_size=20"""
 40 | 
 41 | EXECUTE_VM_COMMAND_DESC = """Execute commands in a VM via QEMU guest agent.
 42 | 
 43 | Parameters:
 44 | node* - Host node name (e.g. 'pve1')
 45 | vmid* - VM ID number (e.g. '100')
 46 | command* - Shell command to run (e.g. 'uname -a')
 47 | 
 48 | Example:
 49 | {"success": true, "output": "Linux vm1 5.4.0", "exit_code": 0}"""
 50 | 
 51 | # VM Power Management tool descriptions
 52 | START_VM_DESC = """Start a virtual machine.
 53 | 
 54 | Parameters:
 55 | node* - Host node name (e.g. 'pve')
 56 | vmid* - VM ID number (e.g. '101')
 57 | 
 58 | Example:
 59 | Power on VPN-Server with ID 101 on node pve"""
 60 | 
 61 | STOP_VM_DESC = """Stop a virtual machine (force stop).
 62 | 
 63 | Parameters:
 64 | node* - Host node name (e.g. 'pve')  
 65 | vmid* - VM ID number (e.g. '101')
 66 | 
 67 | Example:
 68 | Force stop VPN-Server with ID 101 on node pve"""
 69 | 
 70 | SHUTDOWN_VM_DESC = """Shutdown a virtual machine gracefully.
 71 | 
 72 | Parameters:
 73 | node* - Host node name (e.g. 'pve')
 74 | vmid* - VM ID number (e.g. '101')
 75 | 
 76 | Example:
 77 | Gracefully shutdown VPN-Server with ID 101 on node pve"""
 78 | 
 79 | RESET_VM_DESC = """Reset (restart) a virtual machine.
 80 | 
 81 | Parameters:
 82 | node* - Host node name (e.g. 'pve')
 83 | vmid* - VM ID number (e.g. '101')
 84 | 
 85 | Example:
 86 | Reset VPN-Server with ID 101 on node pve"""
 87 | 
 88 | DELETE_VM_DESC = """Delete/remove a virtual machine completely.
 89 | 
 90 | ⚠️ WARNING: This operation permanently deletes the VM and all its data!
 91 | 
 92 | Parameters:
 93 | node* - Host node name (e.g. 'pve')
 94 | vmid* - VM ID number (e.g. '998')
 95 | force - Force deletion even if VM is running (optional, default: false)
 96 | 
 97 | This will permanently remove:
 98 | - VM configuration
 99 | - All virtual disks
100 | - All snapshots
101 | - Cannot be undone!
102 | 
103 | Example:
104 | Delete test VM with ID 998 on node pve"""
105 | 
106 | # Container tool descriptions
107 | GET_CONTAINERS_DESC = """List LXC containers across the cluster (or filter by node).
108 | 
109 | Parameters:
110 | - node (optional): Node name to filter (e.g. 'pve1')
111 | - include_stats (bool, default true): Include live CPU/memory stats
112 | - include_raw (bool, default false): Include raw Proxmox API payloads for debugging
113 | - format_style ('pretty'|'json', default 'pretty'): Pretty text or raw JSON list
114 | 
115 | Notes:
116 | - Live stats from /nodes/{node}/lxc/{vmid}/status/current.
117 | - If maxmem is 0 (unlimited), memory limit falls back to /config.memory (MiB).
118 | - If live returns zeros, the most recent RRD sample is used as a fallback.
119 | - Fields provided: cores (CPU cores/cpulimit), memory (MiB limit), cpu_pct, mem_bytes, maxmem_bytes, mem_pct, unlimited_memory.
120 | """
121 | 
122 | START_CONTAINER_DESC = """Start one or more LXC containers.
123 | selector: '123' | 'pve1:123' | 'pve1/name' | 'name' | comma list
124 | Example: start_container selector='pve1:101,pve2/web'
125 | """
126 | 
127 | STOP_CONTAINER_DESC = """Stop LXC containers. graceful=True uses shutdown; otherwise force stop.
128 | selector: same grammar as start_container
129 | timeout_seconds: 10 (default)
130 | """
131 | 
132 | RESTART_CONTAINER_DESC = """Restart LXC containers (reboot).
133 | selector: same grammar as start_container
134 | """
135 | 
136 | UPDATE_CONTAINER_RESOURCES_DESC = """Update resources for one or more LXC containers.
137 | 
138 | selector: same grammar as start_container
139 | cores: New CPU core count (optional)
140 | memory: New memory limit in MiB (optional)
141 | swap: New swap limit in MiB (optional)
142 | disk_gb: Additional disk size in GiB to add (optional)
143 | disk: Disk identifier to resize (default 'rootfs')
144 | """
145 | 
146 | # Storage tool descriptions
147 | GET_STORAGE_DESC = """List storage pools across the cluster with their usage and configuration.
148 | 
149 | Example:
150 | {"storage": "local-lvm", "type": "lvm", "used": "500GB", "total": "1TB"}"""
151 | 
152 | # Cluster tool descriptions
153 | GET_CLUSTER_STATUS_DESC = """Get overall Proxmox cluster health and configuration status.
154 | 
155 | Example:
156 | {"name": "proxmox", "quorum": "ok", "nodes": 3, "ha_status": "active"}"""
157 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/node.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Node-related tools for Proxmox MCP.
  3 | 
  4 | This module provides tools for managing and monitoring Proxmox nodes:
  5 | - Listing all nodes in the cluster with their status
  6 | - Getting detailed node information including:
  7 |   * CPU usage and configuration
  8 |   * Memory utilization
  9 |   * Uptime statistics
 10 |   * Health status
 11 | 
 12 | The tools handle both basic and detailed node information retrieval,
 13 | with fallback mechanisms for partial data availability.
 14 | """
 15 | from typing import List
 16 | from mcp.types import TextContent as Content
 17 | from .base import ProxmoxTool
 18 | from .definitions import GET_NODES_DESC, GET_NODE_STATUS_DESC
 19 | 
 20 | class NodeTools(ProxmoxTool):
 21 |     """Tools for managing Proxmox nodes.
 22 |     
 23 |     Provides functionality for:
 24 |     - Retrieving cluster-wide node information
 25 |     - Getting detailed status for specific nodes
 26 |     - Monitoring node health and resources
 27 |     - Handling node-specific API operations
 28 |     
 29 |     Implements fallback mechanisms for scenarios where detailed
 30 |     node information might be temporarily unavailable.
 31 |     """
 32 | 
 33 |     def get_nodes(self) -> List[Content]:
 34 |         """List all nodes in the Proxmox cluster with detailed status.
 35 | 
 36 |         Retrieves comprehensive information for each node including:
 37 |         - Basic status (online/offline)
 38 |         - Uptime statistics
 39 |         - CPU configuration and count
 40 |         - Memory usage and capacity
 41 |         
 42 |         Implements a fallback mechanism that returns basic information
 43 |         if detailed status retrieval fails for any node.
 44 | 
 45 |         Returns:
 46 |             List of Content objects containing formatted node information:
 47 |             {
 48 |                 "node": "node_name",
 49 |                 "status": "online/offline",
 50 |                 "uptime": seconds,
 51 |                 "maxcpu": cpu_count,
 52 |                 "memory": {
 53 |                     "used": bytes,
 54 |                     "total": bytes
 55 |                 }
 56 |             }
 57 | 
 58 |         Raises:
 59 |             RuntimeError: If the cluster-wide node query fails
 60 |         """
 61 |         try:
 62 |             result = self.proxmox.nodes.get()
 63 |             nodes = []
 64 |             
 65 |             # Get detailed info for each node
 66 |             for node in result:
 67 |                 node_name = node["node"]
 68 |                 try:
 69 |                     # Get detailed status for each node
 70 |                     status = self.proxmox.nodes(node_name).status.get()
 71 |                     nodes.append({
 72 |                         "node": node_name,
 73 |                         "status": node["status"],
 74 |                         "uptime": status.get("uptime", 0),
 75 |                         "maxcpu": status.get("cpuinfo", {}).get("cpus", "N/A"),
 76 |                         "memory": {
 77 |                             "used": status.get("memory", {}).get("used", 0),
 78 |                             "total": status.get("memory", {}).get("total", 0)
 79 |                         }
 80 |                     })
 81 |                 except Exception:
 82 |                     # Fallback to basic info if detailed status fails
 83 |                     nodes.append({
 84 |                         "node": node_name,
 85 |                         "status": node["status"],
 86 |                         "uptime": 0,
 87 |                         "maxcpu": "N/A",
 88 |                         "memory": {
 89 |                             # The nodes.get() API already returns memory usage
 90 |                             # in the "mem" field, so use that directly. The
 91 |                             # previous implementation subtracted this value
 92 |                             # from "maxmem" which actually produced the amount
 93 |                             # of *free* memory instead of the used memory.
 94 |                             "used": node.get("mem", 0),
 95 |                             "total": node.get("maxmem", 0)
 96 |                         }
 97 |                     })
 98 |             return self._format_response(nodes, "nodes")
 99 |         except Exception as e:
100 |             self._handle_error("get nodes", e)
101 | 
102 |     def get_node_status(self, node: str) -> List[Content]:
103 |         """Get detailed status information for a specific node.
104 | 
105 |         Retrieves comprehensive status information including:
106 |         - CPU usage and configuration
107 |         - Memory utilization details
108 |         - Uptime and load statistics
109 |         - Network status
110 |         - Storage health
111 |         - Running tasks and services
112 | 
113 |         Args:
114 |             node: Name/ID of node to query (e.g., 'pve1', 'proxmox-node2')
115 | 
116 |         Returns:
117 |             List of Content objects containing detailed node status:
118 |             {
119 |                 "uptime": seconds,
120 |                 "cpu": {
121 |                     "usage": percentage,
122 |                     "cores": count
123 |                 },
124 |                 "memory": {
125 |                     "used": bytes,
126 |                     "total": bytes,
127 |                     "free": bytes
128 |                 },
129 |                 ...additional status fields
130 |             }
131 | 
132 |         Raises:
133 |             ValueError: If the specified node is not found
134 |             RuntimeError: If status retrieval fails (node offline, network issues)
135 |         """
136 |         try:
137 |             result = self.proxmox.nodes(node).status.get()
138 |             return self._format_response((node, result), "node_status")
139 |         except Exception as e:
140 |             self._handle_error(f"get status for node {node}", e)
141 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/formatting/components.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Reusable UI components for Proxmox MCP output.
  3 | """
  4 | from typing import List, Optional
  5 | from .colors import ProxmoxColors
  6 | from .theme import ProxmoxTheme
  7 | 
  8 | class ProxmoxComponents:
  9 |     """Reusable UI components for formatted output."""
 10 |     
 11 |     @staticmethod
 12 |     def create_table(headers: List[str], rows: List[List[str]], title: Optional[str] = None) -> str:
 13 |         """Create an ASCII table with optional title.
 14 |         
 15 |         Args:
 16 |             headers: List of column headers
 17 |             rows: List of row data
 18 |             title: Optional table title
 19 |             
 20 |         Returns:
 21 |             Formatted table string
 22 |         """
 23 |         # Calculate column widths considering multi-line content
 24 |         widths = [len(header) for header in headers]
 25 |         for row in rows:
 26 |             for i, cell in enumerate(row):
 27 |                 cell_lines = str(cell).split('\n')
 28 |                 max_line_length = max(len(line) for line in cell_lines)
 29 |                 widths[i] = max(widths[i], max_line_length)
 30 |         
 31 |         # Create separator line
 32 |         separator = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
 33 |         
 34 |         # Calculate total width for title
 35 |         total_width = sum(widths) + len(widths) + 1
 36 |         
 37 |         # Build table
 38 |         result = []
 39 |         
 40 |         # Add title if provided
 41 |         if title:
 42 |             # Center the title
 43 |             title_str = ProxmoxColors.colorize(title, ProxmoxColors.CYAN, ProxmoxColors.BOLD)
 44 |             padding = (total_width - len(title) - 2) // 2  # -2 for the border chars
 45 |             title_separator = "+" + "-" * (total_width - 2) + "+"
 46 |             result.extend([
 47 |                 title_separator,
 48 |                 "|" + " " * padding + title_str + " " * (total_width - padding - len(title) - 2) + "|",
 49 |                 title_separator
 50 |             ])
 51 |         
 52 |         # Add headers
 53 |         header = "|" + "|".join(f" {ProxmoxColors.colorize(h, ProxmoxColors.CYAN):<{w}} " for w, h in zip(widths, headers)) + "|"
 54 |         result.extend([separator, header, separator])
 55 |         
 56 |         # Add rows with multi-line cell support
 57 |         for row in rows:
 58 |             # Split each cell into lines
 59 |             cell_lines = [str(cell).split('\n') for cell in row]
 60 |             max_lines = max(len(lines) for lines in cell_lines)
 61 |             
 62 |             # Pad cells with fewer lines
 63 |             padded_cells = []
 64 |             for lines in cell_lines:
 65 |                 if len(lines) < max_lines:
 66 |                     lines.extend([''] * (max_lines - len(lines)))
 67 |                 padded_cells.append(lines)
 68 |             
 69 |             # Create row strings for each line
 70 |             for line_idx in range(max_lines):
 71 |                 line_parts = []
 72 |                 for col_idx, cell_lines in enumerate(padded_cells):
 73 |                     line = cell_lines[line_idx]
 74 |                     line_parts.append(f" {line:<{widths[col_idx]}} ")
 75 |                 result.append("|" + "|".join(line_parts) + "|")
 76 |             
 77 |             # Add separator after each row except the last
 78 |             if row != rows[-1]:
 79 |                 result.append(separator)
 80 |         
 81 |         result.append(separator)
 82 |         return "\n".join(result)
 83 |     
 84 |     @staticmethod
 85 |     def create_progress_bar(value: float, total: float, width: int = 20) -> str:
 86 |         """Create a progress bar with percentage.
 87 |         
 88 |         Args:
 89 |             value: Current value
 90 |             total: Maximum value
 91 |             width: Width of progress bar in characters
 92 |             
 93 |         Returns:
 94 |             Formatted progress bar string
 95 |         """
 96 |         percentage = min(100, (value / total * 100) if total > 0 else 0)
 97 |         filled = int(width * percentage / 100)
 98 |         color = ProxmoxColors.metric_color(percentage)
 99 |         
100 |         bar = "█" * filled + "░" * (width - filled)
101 |         return f"{ProxmoxColors.colorize(bar, color)} {percentage:.1f}%"
102 |     
103 |     @staticmethod
104 |     def create_resource_usage(used: float, total: float, label: str, emoji: str) -> str:
105 |         """Create a resource usage display with progress bar.
106 |         
107 |         Args:
108 |             used: Used amount
109 |             total: Total amount
110 |             label: Resource label
111 |             emoji: Resource emoji
112 |             
113 |         Returns:
114 |             Formatted resource usage string
115 |         """
116 |         from .formatters import ProxmoxFormatters
117 |         percentage = (used / total * 100) if total > 0 else 0
118 |         progress = ProxmoxComponents.create_progress_bar(used, total)
119 |         
120 |         return (
121 |             f"{emoji} {label}:\n"
122 |             f"  {progress}\n"
123 |             f"  {ProxmoxFormatters.format_bytes(used)} / {ProxmoxFormatters.format_bytes(total)}"
124 |         )
125 |     
126 |     @staticmethod
127 |     def create_key_value_grid(data: dict, columns: int = 2) -> str:
128 |         """Create a grid of key-value pairs.
129 |         
130 |         Args:
131 |             data: Dictionary of key-value pairs
132 |             columns: Number of columns in grid
133 |             
134 |         Returns:
135 |             Formatted grid string
136 |         """
137 |         # Calculate max widths for each column
138 |         items = list(data.items())
139 |         rows = [items[i:i + columns] for i in range(0, len(items), columns)]
140 |         
141 |         key_widths = [0] * columns
142 |         val_widths = [0] * columns
143 |         
144 |         for row in rows:
145 |             for i, (key, val) in enumerate(row):
146 |                 key_widths[i] = max(key_widths[i], len(str(key)))
147 |                 val_widths[i] = max(val_widths[i], len(str(val)))
148 |         
149 |         # Format rows
150 |         result = []
151 |         for row in rows:
152 |             formatted_items = []
153 |             for i, (key, val) in enumerate(row):
154 |                 key_str = ProxmoxColors.colorize(f"{key}:", ProxmoxColors.CYAN)
155 |                 formatted_items.append(f"{key_str:<{key_widths[i] + 10}} {val:<{val_widths[i]}}")
156 |             result.append("  ".join(formatted_items))
157 |         
158 |         return "\n".join(result)
159 |     
160 |     @staticmethod
161 |     def create_status_badge(status: str) -> str:
162 |         """Create a status badge with emoji.
163 |         
164 |         Args:
165 |             status: Status string
166 |             
167 |         Returns:
168 |             Formatted status badge string
169 |         """
170 |         status = status.lower()
171 |         emoji = ProxmoxTheme.get_status_emoji(status)
172 |         return f"{emoji} {status.upper()}"
173 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/tools/console/manager.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Module for managing VM console operations.
  3 | 
  4 | This module provides functionality for interacting with VM consoles:
  5 | - Executing commands within VMs via QEMU guest agent
  6 | - Handling command execution lifecycle
  7 | - Managing command output and status
  8 | - Error handling and logging
  9 | 
 10 | The module implements a robust command execution system with:
 11 | - VM state verification
 12 | - Asynchronous command execution
 13 | - Detailed status tracking
 14 | - Comprehensive error handling
 15 | """
 16 | 
 17 | import logging
 18 | from typing import Dict, Any
 19 | 
 20 | class VMConsoleManager:
 21 |     """Manager class for VM console operations.
 22 |     
 23 |     Provides functionality for:
 24 |     - Executing commands in VM consoles
 25 |     - Managing command execution lifecycle
 26 |     - Handling command output and errors
 27 |     - Monitoring execution status
 28 |     
 29 |     Uses QEMU guest agent for reliable command execution with:
 30 |     - VM state verification before execution
 31 |     - Asynchronous command processing
 32 |     - Detailed output capture
 33 |     - Comprehensive error handling
 34 |     """
 35 | 
 36 |     def __init__(self, proxmox_api):
 37 |         """Initialize the VM console manager.
 38 | 
 39 |         Args:
 40 |             proxmox_api: Initialized ProxmoxAPI instance
 41 |         """
 42 |         self.proxmox = proxmox_api
 43 |         self.logger = logging.getLogger("proxmox-mcp.vm-console")
 44 | 
 45 |     async def execute_command(self, node: str, vmid: str, command: str) -> Dict[str, Any]:
 46 |         """Execute a command in a VM's console via QEMU guest agent.
 47 | 
 48 |         Implements a two-phase command execution process:
 49 |         1. Command Initiation:
 50 |            - Verifies VM exists and is running
 51 |            - Initiates command execution via guest agent
 52 |            - Captures command PID for tracking
 53 |         
 54 |         2. Result Collection:
 55 |            - Monitors command execution status
 56 |            - Captures command output and errors
 57 |            - Handles completion status
 58 |         
 59 |         Requirements:
 60 |         - VM must be running
 61 |         - QEMU guest agent must be installed and active
 62 |         - Command execution permissions must be enabled
 63 | 
 64 |         Args:
 65 |             node: Name of the node where VM is running (e.g., 'pve1')
 66 |             vmid: ID of the VM to execute command in (e.g., '100')
 67 |             command: Shell command to execute in the VM
 68 | 
 69 |         Returns:
 70 |             Dictionary containing command execution results:
 71 |             {
 72 |                 "success": true/false,
 73 |                 "output": "command output",
 74 |                 "error": "error output if any",
 75 |                 "exit_code": command_exit_code
 76 |             }
 77 | 
 78 |         Raises:
 79 |             ValueError: If:
 80 |                      - VM is not found
 81 |                      - VM is not running
 82 |                      - Guest agent is not available
 83 |             RuntimeError: If:
 84 |                        - Command execution fails
 85 |                        - Unable to get command status
 86 |                        - API communication errors occur
 87 |         """
 88 |         try:
 89 |             # Verify VM exists and is running
 90 |             vm_status = self.proxmox.nodes(node).qemu(vmid).status.current.get()
 91 |             if vm_status["status"] != "running":
 92 |                 self.logger.error(f"Failed to execute command on VM {vmid}: VM is not running")
 93 |                 raise ValueError(f"VM {vmid} on node {node} is not running")
 94 | 
 95 |             # Get VM's console
 96 |             self.logger.info(f"Executing command on VM {vmid} (node: {node}): {command}")
 97 |             
 98 |             # Get the API endpoint
 99 |             # Use the guest agent exec endpoint
100 |             endpoint = self.proxmox.nodes(node).qemu(vmid).agent
101 |             self.logger.debug(f"Using API endpoint: {endpoint}")
102 |             
103 |             # Execute the command using two-step process
104 |             try:
105 |                 # Start command execution
106 |                 self.logger.info("Starting command execution...")
107 |                 try:
108 |                     self.logger.debug(f"Executing command via agent: {command}")
109 |                     exec_result = endpoint("exec").post(command=command)
110 |                     self.logger.debug(f"Raw exec response: {exec_result}")
111 |                     self.logger.info(f"Command started with result: {exec_result}")
112 |                 except Exception as e:
113 |                     self.logger.error(f"Failed to start command: {str(e)}")
114 |                     raise RuntimeError(f"Failed to start command: {str(e)}")
115 | 
116 |                 if 'pid' not in exec_result:
117 |                     raise RuntimeError("No PID returned from command execution")
118 | 
119 |                 pid = exec_result['pid']
120 |                 self.logger.info(f"Waiting for command completion (PID: {pid})...")
121 | 
122 |                 # Add a small delay to allow command to complete
123 |                 import asyncio
124 |                 await asyncio.sleep(1)
125 | 
126 |                 # Get command output using exec-status
127 |                 try:
128 |                     self.logger.debug(f"Getting status for PID {pid}...")
129 |                     console = endpoint("exec-status").get(pid=pid)
130 |                     self.logger.debug(f"Raw exec-status response: {console}")
131 |                     if not console:
132 |                         raise RuntimeError("No response from exec-status")
133 |                 except Exception as e:
134 |                     self.logger.error(f"Failed to get command status: {str(e)}")
135 |                     raise RuntimeError(f"Failed to get command status: {str(e)}")
136 |                 self.logger.info(f"Command completed with status: {console}")
137 |             except Exception as e:
138 |                 self.logger.error(f"API call failed: {str(e)}")
139 |                 raise RuntimeError(f"API call failed: {str(e)}")
140 |             self.logger.debug(f"Raw API response type: {type(console)}")
141 |             self.logger.debug(f"Raw API response: {console}")
142 |             
143 |             # Handle different response structures
144 |             if isinstance(console, dict):
145 |                 # Handle exec-status response format
146 |                 output = console.get("out-data", "")
147 |                 error = console.get("err-data", "")
148 |                 exit_code = console.get("exitcode", 0)
149 |                 exited = console.get("exited", 0)
150 |                 
151 |                 if not exited:
152 |                     self.logger.warning("Command may not have completed")
153 |             else:
154 |                 # Some versions might return data differently
155 |                 self.logger.debug(f"Unexpected response type: {type(console)}")
156 |                 output = str(console)
157 |                 error = ""
158 |                 exit_code = 0
159 |             
160 |             self.logger.debug(f"Processed output: {output}")
161 |             self.logger.debug(f"Processed error: {error}")
162 |             self.logger.debug(f"Processed exit code: {exit_code}")
163 |             
164 |             self.logger.debug(f"Executed command '{command}' on VM {vmid} (node: {node})")
165 | 
166 |             return {
167 |                 "success": True,
168 |                 "output": output,
169 |                 "error": error,
170 |                 "exit_code": exit_code
171 |             }
172 | 
173 |         except ValueError:
174 |             # Re-raise ValueError for VM not running
175 |             raise
176 |         except Exception as e:
177 |             self.logger.error(f"Failed to execute command on VM {vmid}: {str(e)}")
178 |             if "not found" in str(e).lower():
179 |                 raise ValueError(f"VM {vmid} not found on node {node}")
180 |             raise RuntimeError(f"Failed to execute command: {str(e)}")
181 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/formatting/templates.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Output templates for Proxmox MCP resource types.
  3 | """
  4 | from typing import Dict, List, Any
  5 | from .formatters import ProxmoxFormatters
  6 | from .theme import ProxmoxTheme
  7 | from .colors import ProxmoxColors
  8 | from .components import ProxmoxComponents
  9 | 
 10 | class ProxmoxTemplates:
 11 |     """Output templates for different Proxmox resource types."""
 12 |     
 13 |     @staticmethod
 14 |     def node_list(nodes: List[Dict[str, Any]]) -> str:
 15 |         """Template for node list output.
 16 |         
 17 |         Args:
 18 |             nodes: List of node data dictionaries
 19 |             
 20 |         Returns:
 21 |             Formatted node list string
 22 |         """
 23 |         result = [f"{ProxmoxTheme.RESOURCES['node']} Proxmox Nodes"]
 24 |         
 25 |         for node in nodes:
 26 |             # Get node status
 27 |             status = node.get("status", "unknown")
 28 |             
 29 |             # Get memory info
 30 |             memory = node.get("memory", {})
 31 |             memory_used = memory.get("used", 0)
 32 |             memory_total = memory.get("total", 0)
 33 |             memory_percent = (memory_used / memory_total * 100) if memory_total > 0 else 0
 34 |             
 35 |             # Format node info
 36 |             result.extend([
 37 |                 "",  # Empty line between nodes
 38 |                 f"{ProxmoxTheme.RESOURCES['node']} {node['node']}",
 39 |                 f"  • Status: {status.upper()}",
 40 |                 f"  • Uptime: {ProxmoxFormatters.format_uptime(node.get('uptime', 0))}",
 41 |                 f"  • CPU Cores: {node.get('maxcpu', 'N/A')}",
 42 |                 f"  • Memory: {ProxmoxFormatters.format_bytes(memory_used)} / "
 43 |                 f"{ProxmoxFormatters.format_bytes(memory_total)} ({memory_percent:.1f}%)"
 44 |             ])
 45 |             
 46 |             # Add disk usage if available
 47 |             disk = node.get("disk", {})
 48 |             if disk:
 49 |                 disk_used = disk.get("used", 0)
 50 |                 disk_total = disk.get("total", 0)
 51 |                 disk_percent = (disk_used / disk_total * 100) if disk_total > 0 else 0
 52 |                 result.append(
 53 |                     f"  • Disk: {ProxmoxFormatters.format_bytes(disk_used)} / "
 54 |                     f"{ProxmoxFormatters.format_bytes(disk_total)} ({disk_percent:.1f}%)"
 55 |                 )
 56 |             
 57 |         return "\n".join(result)
 58 |     
 59 |     @staticmethod
 60 |     def node_status(node: str, status: Dict[str, Any]) -> str:
 61 |         """Template for detailed node status output.
 62 |         
 63 |         Args:
 64 |             node: Node name
 65 |             status: Node status data
 66 |             
 67 |         Returns:
 68 |             Formatted node status string
 69 |         """
 70 |         memory = status.get("memory", {})
 71 |         memory_used = memory.get("used", 0)
 72 |         memory_total = memory.get("total", 0)
 73 |         memory_percent = (memory_used / memory_total * 100) if memory_total > 0 else 0
 74 |         
 75 |         result = [
 76 |             f"{ProxmoxTheme.RESOURCES['node']} Node: {node}",
 77 |             f"  • Status: {status.get('status', 'unknown').upper()}",
 78 |             f"  • Uptime: {ProxmoxFormatters.format_uptime(status.get('uptime', 0))}",
 79 |             f"  • CPU Cores: {status.get('maxcpu', 'N/A')}",
 80 |             f"  • Memory: {ProxmoxFormatters.format_bytes(memory_used)} / "
 81 |             f"{ProxmoxFormatters.format_bytes(memory_total)} ({memory_percent:.1f}%)"
 82 |         ]
 83 |         
 84 |         # Add disk usage if available
 85 |         disk = status.get("disk", {})
 86 |         if disk:
 87 |             disk_used = disk.get("used", 0)
 88 |             disk_total = disk.get("total", 0)
 89 |             disk_percent = (disk_used / disk_total * 100) if disk_total > 0 else 0
 90 |             result.append(
 91 |                 f"  • Disk: {ProxmoxFormatters.format_bytes(disk_used)} / "
 92 |                 f"{ProxmoxFormatters.format_bytes(disk_total)} ({disk_percent:.1f}%)"
 93 |             )
 94 |         
 95 |         return "\n".join(result)
 96 |     
 97 |     @staticmethod
 98 |     def vm_list(vms: List[Dict[str, Any]]) -> str:
 99 |         """Template for VM list output.
100 |         
101 |         Args:
102 |             vms: List of VM data dictionaries
103 |             
104 |         Returns:
105 |             Formatted VM list string
106 |         """
107 |         result = [f"{ProxmoxTheme.RESOURCES['vm']} Virtual Machines"]
108 |         
109 |         for vm in vms:
110 |             memory = vm.get("memory", {})
111 |             memory_used = memory.get("used", 0)
112 |             memory_total = memory.get("total", 0)
113 |             memory_percent = (memory_used / memory_total * 100) if memory_total > 0 else 0
114 |             
115 |             result.extend([
116 |                 "",  # Empty line between VMs
117 |                 f"{ProxmoxTheme.RESOURCES['vm']} {vm['name']} (ID: {vm['vmid']})",
118 |                 f"  • Status: {vm['status'].upper()}",
119 |                 f"  • Node: {vm['node']}",
120 |                 f"  • CPU Cores: {vm.get('cpus', 'N/A')}",
121 |                 f"  • Memory: {ProxmoxFormatters.format_bytes(memory_used)} / "
122 |                 f"{ProxmoxFormatters.format_bytes(memory_total)} ({memory_percent:.1f}%)"
123 |             ])
124 |             
125 |         return "\n".join(result)
126 |     
127 |     @staticmethod
128 |     def storage_list(storage: List[Dict[str, Any]]) -> str:
129 |         """Template for storage list output.
130 |         
131 |         Args:
132 |             storage: List of storage data dictionaries
133 |             
134 |         Returns:
135 |             Formatted storage list string
136 |         """
137 |         result = [f"{ProxmoxTheme.RESOURCES['storage']} Storage Pools"]
138 |         
139 |         for store in storage:
140 |             used = store.get("used", 0)
141 |             total = store.get("total", 0)
142 |             percent = (used / total * 100) if total > 0 else 0
143 |             
144 |             result.extend([
145 |                 "",  # Empty line between storage pools
146 |                 f"{ProxmoxTheme.RESOURCES['storage']} {store['storage']}",
147 |                 f"  • Status: {store.get('status', 'unknown').upper()}",
148 |                 f"  • Type: {store['type']}",
149 |                 f"  • Usage: {ProxmoxFormatters.format_bytes(used)} / "
150 |                 f"{ProxmoxFormatters.format_bytes(total)} ({percent:.1f}%)"
151 |             ])
152 |             
153 |         return "\n".join(result)
154 |     
155 |     @staticmethod
156 |     def container_list(containers: List[Dict[str, Any]]) -> str:
157 |         """Template for container list output.
158 |         
159 |         Args:
160 |             containers: List of container data dictionaries
161 |             
162 |         Returns:
163 |             Formatted container list string
164 |         """
165 |         if not containers:
166 |             return f"{ProxmoxTheme.RESOURCES['container']} No containers found"
167 |             
168 |         result = [f"{ProxmoxTheme.RESOURCES['container']} Containers"]
169 |         
170 |         for container in containers:
171 |             memory = container.get("memory", {})
172 |             memory_used = memory.get("used", 0)
173 |             memory_total = memory.get("total", 0)
174 |             memory_percent = (memory_used / memory_total * 100) if memory_total > 0 else 0
175 |             
176 |             result.extend([
177 |                 "",  # Empty line between containers
178 |                 f"{ProxmoxTheme.RESOURCES['container']} {container['name']} (ID: {container['vmid']})",
179 |                 f"  • Status: {container['status'].upper()}",
180 |                 f"  • Node: {container['node']}",
181 |                 f"  • CPU Cores: {container.get('cpus', 'N/A')}",
182 |                 f"  • Memory: {ProxmoxFormatters.format_bytes(memory_used)} / "
183 |                 f"{ProxmoxFormatters.format_bytes(memory_total)} ({memory_percent:.1f}%)"
184 |             ])
185 |             
186 |         return "\n".join(result)
187 | 
188 |     @staticmethod
189 |     def cluster_status(status: Dict[str, Any]) -> str:
190 |         """Template for cluster status output.
191 |         
192 |         Args:
193 |             status: Cluster status data
194 |             
195 |         Returns:
196 |             Formatted cluster status string
197 |         """
198 |         result = [f"{ProxmoxTheme.SECTIONS['configuration']} Proxmox Cluster"]
199 |         
200 |         # Basic cluster info
201 |         result.extend([
202 |             "",
203 |             f"  • Name: {status.get('name', 'N/A')}",
204 |             f"  • Quorum: {'OK' if status.get('quorum') else 'NOT OK'}",
205 |             f"  • Nodes: {status.get('nodes', 0)}",
206 |         ])
207 |         
208 |         # Add resource count if available
209 |         resources = status.get('resources', [])
210 |         if resources:
211 |             result.append(f"  • Resources: {len(resources)}")
212 |         
213 |         return "\n".join(result)
214 | 
```

--------------------------------------------------------------------------------
/tests/test_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Proxmox MCP server.
  3 | """
  4 | 
  5 | import os
  6 | import json
  7 | import pytest
  8 | from unittest.mock import Mock, patch
  9 | 
 10 | from mcp.server.fastmcp import FastMCP
 11 | from mcp.server.fastmcp.exceptions import ToolError
 12 | from proxmox_mcp.server import ProxmoxMCPServer
 13 | 
 14 | @pytest.fixture
 15 | def mock_env_vars():
 16 |     """Fixture to set up test environment variables."""
 17 |     env_vars = {
 18 |         "PROXMOX_HOST": "test.proxmox.com",
 19 |         "PROXMOX_USER": "test@pve",
 20 |         "PROXMOX_TOKEN_NAME": "test_token",
 21 |         "PROXMOX_TOKEN_VALUE": "test_value",
 22 |         "LOG_LEVEL": "DEBUG"
 23 |     }
 24 |     with patch.dict(os.environ, env_vars):
 25 |         yield env_vars
 26 | 
 27 | @pytest.fixture
 28 | def mock_proxmox():
 29 |     """Fixture to mock ProxmoxAPI."""
 30 |     with patch("proxmox_mcp.core.proxmox.ProxmoxAPI") as mock:
 31 |         mock.return_value.nodes.get.return_value = [
 32 |             {"node": "node1", "status": "online"},
 33 |             {"node": "node2", "status": "online"}
 34 |         ]
 35 |         yield mock
 36 | 
 37 | @pytest.fixture
 38 | def server(mock_env_vars, mock_proxmox):
 39 |     """Fixture to create a ProxmoxMCPServer instance."""
 40 |     return ProxmoxMCPServer()
 41 | 
 42 | def test_server_initialization(server, mock_proxmox):
 43 |     """Test server initialization with environment variables."""
 44 |     assert server.config.proxmox.host == "test.proxmox.com"
 45 |     assert server.config.auth.user == "test@pve"
 46 |     assert server.config.auth.token_name == "test_token"
 47 |     assert server.config.auth.token_value == "test_value"
 48 |     assert server.config.logging.level == "DEBUG"
 49 | 
 50 |     mock_proxmox.assert_called_once()
 51 | 
 52 | @pytest.mark.asyncio
 53 | async def test_list_tools(server):
 54 |     """Test listing available tools."""
 55 |     tools = await server.mcp.list_tools()
 56 | 
 57 |     assert len(tools) > 0
 58 |     tool_names = [tool.name for tool in tools]
 59 |     assert "get_nodes" in tool_names
 60 |     assert "get_vms" in tool_names
 61 |     assert "get_containers" in tool_names
 62 |     assert "execute_vm_command" in tool_names
 63 |     assert "update_container_resources" in tool_names
 64 | 
 65 | @pytest.mark.asyncio
 66 | async def test_get_nodes(server, mock_proxmox):
 67 |     """Test get_nodes tool."""
 68 |     mock_proxmox.return_value.nodes.get.return_value = [
 69 |         {"node": "node1", "status": "online"},
 70 |         {"node": "node2", "status": "online"}
 71 |     ]
 72 |     response = await server.mcp.call_tool("get_nodes", {})
 73 |     result = json.loads(response[0].text)
 74 | 
 75 |     assert len(result) == 2
 76 |     assert result[0]["node"] == "node1"
 77 |     assert result[1]["node"] == "node2"
 78 | 
 79 | @pytest.mark.asyncio
 80 | async def test_get_node_status_missing_parameter(server):
 81 |     """Test get_node_status tool with missing parameter."""
 82 |     with pytest.raises(ToolError, match="Field required"):
 83 |         await server.mcp.call_tool("get_node_status", {})
 84 | 
 85 | @pytest.mark.asyncio
 86 | async def test_get_node_status(server, mock_proxmox):
 87 |     """Test get_node_status tool with valid parameter."""
 88 |     mock_proxmox.return_value.nodes.return_value.status.get.return_value = {
 89 |         "status": "running",
 90 |         "uptime": 123456
 91 |     }
 92 | 
 93 |     response = await server.mcp.call_tool("get_node_status", {"node": "node1"})
 94 |     result = json.loads(response[0].text)
 95 |     assert result["status"] == "running"
 96 |     assert result["uptime"] == 123456
 97 | 
 98 | @pytest.mark.asyncio
 99 | async def test_get_vms(server, mock_proxmox):
100 |     """Test get_vms tool."""
101 |     mock_proxmox.return_value.nodes.get.return_value = [{"node": "node1", "status": "online"}]
102 |     mock_proxmox.return_value.nodes.return_value.qemu.get.return_value = [
103 |         {"vmid": "100", "name": "vm1", "status": "running"},
104 |         {"vmid": "101", "name": "vm2", "status": "stopped"}
105 |     ]
106 | 
107 |     response = await server.mcp.call_tool("get_vms", {})
108 |     result = json.loads(response[0].text)
109 |     assert len(result) > 0
110 |     assert result[0]["name"] == "vm1"
111 |     assert result[1]["name"] == "vm2"
112 | 
113 | @pytest.mark.asyncio
114 | async def test_get_containers(server, mock_proxmox):
115 |     """Test get_containers tool."""
116 |     mock_proxmox.return_value.nodes.get.return_value = [{"node": "node1", "status": "online"}]
117 |     mock_proxmox.return_value.nodes.return_value.lxc.get.return_value = [
118 |         {"vmid": "200", "name": "container1", "status": "running"},
119 |         {"vmid": "201", "name": "container2", "status": "stopped"}
120 |     ]
121 | 
122 |     response = await server.mcp.call_tool("get_containers", {})
123 |     result = json.loads(response[0].text)
124 |     assert len(result) > 0
125 |     assert result[0]["name"] == "container1"
126 |     assert result[1]["name"] == "container2"
127 | 
128 | @pytest.mark.asyncio
129 | async def test_update_container_resources(server, mock_proxmox):
130 |     """Test update_container_resources tool."""
131 |     mock_proxmox.return_value.nodes.get.return_value = [{"node": "node1", "status": "online"}]
132 |     mock_proxmox.return_value.nodes.return_value.lxc.get.return_value = [
133 |         {"vmid": "200", "name": "container1", "status": "running"}
134 |     ]
135 | 
136 |     ct_api = mock_proxmox.return_value.nodes.return_value.lxc.return_value
137 |     ct_api.config.put.return_value = {}
138 |     ct_api.resize.put.return_value = {}
139 | 
140 |     response = await server.mcp.call_tool(
141 |         "update_container_resources",
142 |         {"selector": "node1:200", "cores": 2, "memory": 512, "swap": 256, "disk_gb": 1},
143 |     )
144 |     result = json.loads(response[0].text)
145 | 
146 |     assert result[0]["ok"] is True
147 |     ct_api.config.put.assert_called_with(cores=2, memory=512, swap=256)
148 |     ct_api.resize.put.assert_called_with(disk="rootfs", size="+1G")
149 | 
150 | @pytest.mark.asyncio
151 | async def test_get_storage(server, mock_proxmox):
152 |     """Test get_storage tool."""
153 |     mock_proxmox.return_value.storage.get.return_value = [
154 |         {"storage": "local", "type": "dir"},
155 |         {"storage": "ceph", "type": "rbd"}
156 |     ]
157 | 
158 |     response = await server.mcp.call_tool("get_storage", {})
159 |     result = json.loads(response[0].text)
160 |     assert len(result) == 2
161 |     assert result[0]["storage"] == "local"
162 |     assert result[1]["storage"] == "ceph"
163 | 
164 | @pytest.mark.asyncio
165 | async def test_get_cluster_status(server, mock_proxmox):
166 |     """Test get_cluster_status tool."""
167 |     mock_proxmox.return_value.cluster.status.get.return_value = {
168 |         "quorate": True,
169 |         "nodes": 2
170 |     }
171 | 
172 |     response = await server.mcp.call_tool("get_cluster_status", {})
173 |     result = json.loads(response[0].text)
174 |     assert result["quorate"] is True
175 |     assert result["nodes"] == 2
176 | 
177 | @pytest.mark.asyncio
178 | async def test_execute_vm_command_success(server, mock_proxmox):
179 |     """Test successful VM command execution."""
180 |     # Mock VM status check
181 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.status.current.get.return_value = {
182 |         "status": "running"
183 |     }
184 |     # Mock command execution
185 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.agent.exec.post.return_value = {
186 |         "out": "command output",
187 |         "err": "",
188 |         "exitcode": 0
189 |     }
190 | 
191 |     response = await server.mcp.call_tool("execute_vm_command", {
192 |         "node": "node1",
193 |         "vmid": "100",
194 |         "command": "ls -l"
195 |     })
196 |     result = json.loads(response[0].text)
197 | 
198 |     assert result["success"] is True
199 |     assert result["output"] == "command output"
200 |     assert result["error"] == ""
201 |     assert result["exit_code"] == 0
202 | 
203 | @pytest.mark.asyncio
204 | async def test_execute_vm_command_missing_parameters(server):
205 |     """Test VM command execution with missing parameters."""
206 |     with pytest.raises(ToolError):
207 |         await server.mcp.call_tool("execute_vm_command", {})
208 | 
209 | @pytest.mark.asyncio
210 | async def test_execute_vm_command_vm_not_running(server, mock_proxmox):
211 |     """Test VM command execution when VM is not running."""
212 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.status.current.get.return_value = {
213 |         "status": "stopped"
214 |     }
215 | 
216 |     with pytest.raises(ToolError, match="not running"):
217 |         await server.mcp.call_tool("execute_vm_command", {
218 |             "node": "node1",
219 |             "vmid": "100",
220 |             "command": "ls -l"
221 |         })
222 | 
223 | @pytest.mark.asyncio
224 | async def test_execute_vm_command_with_error(server, mock_proxmox):
225 |     """Test VM command execution with command error."""
226 |     # Mock VM status check
227 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.status.current.get.return_value = {
228 |         "status": "running"
229 |     }
230 |     # Mock command execution with error
231 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.agent.exec.post.return_value = {
232 |         "out": "",
233 |         "err": "command not found",
234 |         "exitcode": 1
235 |     }
236 | 
237 |     response = await server.mcp.call_tool("execute_vm_command", {
238 |         "node": "node1",
239 |         "vmid": "100",
240 |         "command": "invalid-command"
241 |     })
242 |     result = json.loads(response[0].text)
243 | 
244 |     assert result["success"] is True  # API call succeeded
245 |     assert result["output"] == ""
246 |     assert result["error"] == "command not found"
247 |     assert result["exit_code"] == 1
248 | 
249 | @pytest.mark.asyncio
250 | async def test_start_vm(server, mock_proxmox):
251 |     """Test start_vm tool."""
252 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.status.current.get.return_value = {
253 |         "status": "stopped"
254 |     }
255 |     mock_proxmox.return_value.nodes.return_value.qemu.return_value.status.start.post.return_value = "UPID:taskid"
256 | 
257 |     response = await server.mcp.call_tool("start_vm", {"node": "node1", "vmid": "100"})
258 |     assert "start initiated successfully" in response[0].text
259 | 
```

--------------------------------------------------------------------------------
/src/proxmox_mcp/server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Main server implementation for Proxmox MCP.
  3 | 
  4 | This module implements the core MCP server for Proxmox integration, providing:
  5 | - Configuration loading and validation
  6 | - Logging setup
  7 | - Proxmox API connection management
  8 | - MCP tool registration and routing
  9 | - Signal handling for graceful shutdown
 10 | 
 11 | The server exposes a set of tools for managing Proxmox resources including:
 12 | - Node management
 13 | - VM operations
 14 | - Storage management
 15 | - Cluster status monitoring
 16 | """
 17 | import logging
 18 | import os
 19 | import sys
 20 | import signal
 21 | from typing import Optional, List, Annotated, Literal
 22 | 
 23 | from mcp.server.fastmcp import FastMCP
 24 | from mcp.server.fastmcp.tools import Tool
 25 | from mcp.types import TextContent as Content
 26 | from pydantic import Field, BaseModel
 27 | from fastapi import Body
 28 | 
 29 | from .config.loader import load_config
 30 | from .core.logging import setup_logging
 31 | from .core.proxmox import ProxmoxManager
 32 | from .tools.node import NodeTools
 33 | from .tools.vm import VMTools
 34 | from .tools.storage import StorageTools
 35 | from .tools.cluster import ClusterTools
 36 | from .tools.containers import ContainerTools
 37 | from .tools.definitions import (
 38 |     GET_NODES_DESC,
 39 |     GET_NODE_STATUS_DESC,
 40 |     GET_VMS_DESC,
 41 |     CREATE_VM_DESC,
 42 |     EXECUTE_VM_COMMAND_DESC,
 43 |     START_VM_DESC,
 44 |     STOP_VM_DESC,
 45 |     SHUTDOWN_VM_DESC,
 46 |     RESET_VM_DESC,
 47 |     DELETE_VM_DESC,
 48 |     GET_CONTAINERS_DESC,
 49 |     START_CONTAINER_DESC,
 50 |     STOP_CONTAINER_DESC,
 51 |     RESTART_CONTAINER_DESC,
 52 |     UPDATE_CONTAINER_RESOURCES_DESC,
 53 |     GET_STORAGE_DESC,
 54 |     GET_CLUSTER_STATUS_DESC
 55 | )
 56 | 
 57 | class ProxmoxMCPServer:
 58 |     """Main server class for Proxmox MCP."""
 59 | 
 60 |     def __init__(self, config_path: Optional[str] = None):
 61 |         """Initialize the server.
 62 | 
 63 |         Args:
 64 |             config_path: Path to configuration file
 65 |         """
 66 |         self.config = load_config(config_path)
 67 |         self.logger = setup_logging(self.config.logging)
 68 |         
 69 |         # Initialize core components
 70 |         self.proxmox_manager = ProxmoxManager(self.config.proxmox, self.config.auth)
 71 |         self.proxmox = self.proxmox_manager.get_api()
 72 |         
 73 |         # Initialize tools
 74 |         self.node_tools = NodeTools(self.proxmox)
 75 |         self.vm_tools = VMTools(self.proxmox)
 76 |         self.storage_tools = StorageTools(self.proxmox)
 77 |         self.cluster_tools = ClusterTools(self.proxmox)
 78 |         self.container_tools = ContainerTools(self.proxmox)
 79 | 
 80 |         
 81 |         # Initialize MCP server
 82 |         self.mcp = FastMCP("ProxmoxMCP")
 83 |         self._setup_tools()
 84 | 
 85 |     def _setup_tools(self) -> None:
 86 |         """Register MCP tools with the server.
 87 |         
 88 |         Initializes and registers all available tools with the MCP server:
 89 |         - Node management tools (list nodes, get status)
 90 |         - VM operation tools (list VMs, execute commands, power management)
 91 |         - Storage management tools (list storage)
 92 |         - Cluster tools (get cluster status)
 93 |         
 94 |         Each tool is registered with appropriate descriptions and parameter
 95 |         validation using Pydantic models.
 96 |         """
 97 |         
 98 |         # Node tools
 99 |         @self.mcp.tool(description=GET_NODES_DESC)
100 |         def get_nodes():
101 |             return self.node_tools.get_nodes()
102 | 
103 |         @self.mcp.tool(description=GET_NODE_STATUS_DESC)
104 |         def get_node_status(
105 |             node: Annotated[str, Field(description="Name/ID of node to query (e.g. 'pve1', 'proxmox-node2')")]
106 |         ):
107 |             return self.node_tools.get_node_status(node)
108 | 
109 |         # VM tools
110 |         @self.mcp.tool(description=GET_VMS_DESC)
111 |         def get_vms():
112 |             return self.vm_tools.get_vms()
113 | 
114 |         @self.mcp.tool(description=CREATE_VM_DESC)
115 |         def create_vm(
116 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve')")],
117 |             vmid: Annotated[str, Field(description="New VM ID number (e.g. '200', '300')")],
118 |             name: Annotated[str, Field(description="VM name (e.g. 'my-new-vm', 'web-server')")],
119 |             cpus: Annotated[int, Field(description="Number of CPU cores (e.g. 1, 2, 4)", ge=1, le=32)],
120 |             memory: Annotated[int, Field(description="Memory size in MB (e.g. 2048 for 2GB)", ge=512, le=131072)],
121 |             disk_size: Annotated[int, Field(description="Disk size in GB (e.g. 10, 20, 50)", ge=5, le=1000)],
122 |             storage: Annotated[Optional[str], Field(description="Storage name (optional, will auto-detect)", default=None)] = None,
123 |             ostype: Annotated[Optional[str], Field(description="OS type (optional, default: 'l26' for Linux)", default=None)] = None
124 |         ):
125 |             return self.vm_tools.create_vm(node, vmid, name, cpus, memory, disk_size, storage, ostype)
126 | 
127 |         @self.mcp.tool(description=EXECUTE_VM_COMMAND_DESC)
128 |         async def execute_vm_command(
129 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve1', 'proxmox-node2')")],
130 |             vmid: Annotated[str, Field(description="VM ID number (e.g. '100', '101')")],
131 |             command: Annotated[str, Field(description="Shell command to run (e.g. 'uname -a', 'systemctl status nginx')")]
132 |         ):
133 |             return await self.vm_tools.execute_command(node, vmid, command)
134 | 
135 |         # VM Power Management tools
136 |         @self.mcp.tool(description=START_VM_DESC)
137 |         def start_vm(
138 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve')")],
139 |             vmid: Annotated[str, Field(description="VM ID number (e.g. '101')")]
140 |         ):
141 |             return self.vm_tools.start_vm(node, vmid)
142 | 
143 |         @self.mcp.tool(description=STOP_VM_DESC)
144 |         def stop_vm(
145 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve')")],
146 |             vmid: Annotated[str, Field(description="VM ID number (e.g. '101')")]
147 |         ):
148 |             return self.vm_tools.stop_vm(node, vmid)
149 | 
150 |         @self.mcp.tool(description=SHUTDOWN_VM_DESC)
151 |         def shutdown_vm(
152 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve')")],
153 |             vmid: Annotated[str, Field(description="VM ID number (e.g. '101')")]
154 |         ):
155 |             return self.vm_tools.shutdown_vm(node, vmid)
156 | 
157 |         @self.mcp.tool(description=RESET_VM_DESC)
158 |         def reset_vm(
159 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve')")],
160 |             vmid: Annotated[str, Field(description="VM ID number (e.g. '101')")]
161 |         ):
162 |             return self.vm_tools.reset_vm(node, vmid)
163 | 
164 |         @self.mcp.tool(description=DELETE_VM_DESC)
165 |         def delete_vm(
166 |             node: Annotated[str, Field(description="Host node name (e.g. 'pve')")],
167 |             vmid: Annotated[str, Field(description="VM ID number (e.g. '998')")],
168 |             force: Annotated[bool, Field(description="Force deletion even if VM is running", default=False)] = False
169 |         ):
170 |             return self.vm_tools.delete_vm(node, vmid, force)
171 | 
172 |         # Storage tools
173 |         @self.mcp.tool(description=GET_STORAGE_DESC)
174 |         def get_storage():
175 |             return self.storage_tools.get_storage()
176 | 
177 |         # Cluster tools
178 |         @self.mcp.tool(description=GET_CLUSTER_STATUS_DESC)
179 |         def get_cluster_status():
180 |             return self.cluster_tools.get_cluster_status()
181 | 
182 |         # Containers (LXC)
183 |         class GetContainersPayload(BaseModel):
184 |             node: Optional[str] = Field(None, description="Optional node name (e.g. 'pve1')")
185 |             include_stats: bool = Field(True, description="Include live stats and fallbacks")
186 |             include_raw: bool = Field(False, description="Include raw status/config")
187 |             format_style: Literal["pretty", "json"] = Field(
188 |                 "pretty", description="'pretty' or 'json'"
189 |             )
190 | 
191 |         @self.mcp.tool(description=GET_CONTAINERS_DESC)
192 |         def get_containers(
193 |             payload: GetContainersPayload = Body(..., embed=True, description="Container query options")
194 |         ):
195 |             return self.container_tools.get_containers(
196 |                 node=payload.node,
197 |                 include_stats=payload.include_stats,
198 |                 include_raw=payload.include_raw,
199 |                 format_style=payload.format_style,
200 |             )
201 | 
202 |         # Container controls
203 |         @self.mcp.tool(description=START_CONTAINER_DESC)
204 |         def start_container(
205 |             selector: Annotated[str, Field(description="CT selector: '123' | 'pve1:123' | 'pve1/name' | 'name' | comma list")],
206 |             format_style: Annotated[str, Field(description="'pretty' or 'json'", pattern="^(pretty|json)$")] = "pretty",
207 |         ):
208 |             return self.container_tools.start_container(selector=selector, format_style=format_style)
209 | 
210 |         @self.mcp.tool(description=STOP_CONTAINER_DESC)
211 |         def stop_container(
212 |             selector: Annotated[str, Field(description="CT selector (see start_container)")],
213 |             graceful: Annotated[bool, Field(description="Graceful shutdown (True) or forced stop (False)", default=True)] = True,
214 |             timeout_seconds: Annotated[int, Field(description="Timeout for stop/shutdown", ge=1, le=600)] = 10,
215 |             format_style: Annotated[Literal["pretty","json"], Field(description="Output format")] = "pretty",
216 |         ):
217 |             return self.container_tools.stop_container(
218 |                selector=selector, graceful=graceful, timeout_seconds=timeout_seconds, format_style=format_style
219 |             )
220 |         @self.mcp.tool(description=RESTART_CONTAINER_DESC)
221 |         def restart_container(
222 |             selector: Annotated[str, Field(description="CT selector (see start_container)")],
223 |             timeout_seconds: Annotated[int, Field(description="Timeout for reboot", ge=1, le=600)] = 10,
224 |             format_style: Annotated[str, Field(description="'pretty' or 'json'", pattern="^(pretty|json)$")] = "pretty",
225 |         ):
226 |             return self.container_tools.restart_container(
227 |                selector=selector, timeout_seconds=timeout_seconds, format_style=format_style
228 |             )
229 | 
230 |         @self.mcp.tool(description=UPDATE_CONTAINER_RESOURCES_DESC)
231 |         def update_container_resources(
232 |             selector: Annotated[str, Field(description="CT selector (see start_container)")],
233 |             cores: Annotated[Optional[int], Field(description="New CPU core count", ge=1)] = None,
234 |             memory: Annotated[Optional[int], Field(description="New memory limit in MiB", ge=16)] = None,
235 |             swap: Annotated[Optional[int], Field(description="New swap limit in MiB", ge=0)] = None,
236 |             disk_gb: Annotated[Optional[int], Field(description="Additional disk size in GiB", ge=1)] = None,
237 |             disk: Annotated[str, Field(description="Disk to resize", default="rootfs")] = "rootfs",
238 |             format_style: Annotated[Literal["pretty","json"], Field(description="Output format")] = "pretty",
239 |         ):
240 |             return self.container_tools.update_container_resources(
241 |                 selector=selector,
242 |                 cores=cores,
243 |                 memory=memory,
244 |                 swap=swap,
245 |                 disk_gb=disk_gb,
246 |                 disk=disk,
247 |                 format_style=format_style,
248 |             )
249 | 
250 | 
251 |     def start(self) -> None:
252 |         """Start the MCP server.
253 |         
254 |         Initializes the server with:
255 |         - Signal handlers for graceful shutdown (SIGINT, SIGTERM)
256 |         - Async runtime for handling concurrent requests
257 |         - Error handling and logging
258 |         
259 |         The server runs until terminated by a signal or fatal error.
260 |         """
261 |         import anyio
262 | 
263 |         def signal_handler(signum, frame):
264 |             self.logger.info("Received signal to shutdown...")
265 |             sys.exit(0)
266 | 
267 |         # Set up signal handlers
268 |         signal.signal(signal.SIGINT, signal_handler)
269 |         signal.signal(signal.SIGTERM, signal_handler)
270 | 
271 |         try:
272 |             self.logger.info("Starting MCP server...")
273 |             anyio.run(self.mcp.run_stdio_async)
274 |         except Exception as e:
275 |             self.logger.error(f"Server error: {e}")
276 |             sys.exit(1)
277 | 
278 | if __name__ == "__main__":
279 |     config_path = os.getenv("PROXMOX_MCP_CONFIG")
280 |     if not config_path:
281 |         print("PROXMOX_MCP_CONFIG environment variable must be set")
282 |         sys.exit(1)
283 |     
284 |     try:
285 |         server = ProxmoxMCPServer(config_path)
286 |         server.start()
287 |     except KeyboardInterrupt:
288 |         print("\nShutting down gracefully...")
289 |         sys.exit(0)
290 |     except Exception as e:
291 |         print(f"Error: {e}")
292 |         sys.exit(1)
293 | 
```
Page 1/2FirstPrevNextLast