#!/usr/bin/env python3 """ Proxmox MCP Server Exposes Proxmox operations as MCP tools for Claude Code """ import json import sys from typing import Any, Dict, List from proxmox_client import ProxmoxClient def create_mcp_server(): """Create MCP server configuration""" client = ProxmoxClient() # Define MCP tools tools = [ { "name": "proxmox_list_vms", "description": "List all virtual machines with their current status, resource usage (CPU, memory), and configuration", "inputSchema": { "type": "object", "properties": { "node": { "type": "string", "description": "Proxmox node name (optional, defaults to configured node)" } } } }, { "name": "proxmox_get_vm", "description": "Get detailed information about a specific VM including configuration, status, and resource usage", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID number" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_start_vm", "description": "Start a virtual machine", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID to start" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_stop_vm", "description": "Stop a virtual machine (graceful shutdown or force)", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID to stop" }, "force": { "type": "boolean", "description": "Force immediate shutdown (default: false)", "default": False }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_restart_vm", "description": "Restart a virtual machine", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID to restart" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_create_snapshot", "description": "Create a snapshot of a VM for backup or before making changes", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID" }, "snapname": { "type": "string", "description": "Snapshot name (e.g., 'before-upgrade')" }, "description": { "type": "string", "description": "Human-readable description", "default": "" }, "vmstate": { "type": "boolean", "description": "Include VM RAM state (for running VMs)", "default": False }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid", "snapname"] } }, { "name": "proxmox_list_snapshots", "description": "List all snapshots for a VM", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_rollback_snapshot", "description": "Rollback a VM to a previous snapshot", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID" }, "snapname": { "type": "string", "description": "Snapshot name to rollback to" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid", "snapname"] } }, { "name": "proxmox_delete_snapshot", "description": "Delete a VM snapshot", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "VM ID" }, "snapname": { "type": "string", "description": "Snapshot name to delete" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid", "snapname"] } }, { "name": "proxmox_clone_vm", "description": "Clone an existing VM to create a new one", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "Source VM ID to clone from" }, "newid": { "type": "integer", "description": "New VM ID for the clone" }, "name": { "type": "string", "description": "Name for the cloned VM (optional)" }, "full": { "type": "boolean", "description": "Create full clone (true) or linked clone (false)", "default": True }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid", "newid"] } }, { "name": "proxmox_get_node_status", "description": "Get Proxmox node status including CPU, memory, storage, and uptime", "inputSchema": { "type": "object", "properties": { "node": { "type": "string", "description": "Proxmox node name (optional)" } } } }, { "name": "proxmox_list_containers", "description": "List all LXC containers", "inputSchema": { "type": "object", "properties": { "node": { "type": "string", "description": "Proxmox node name (optional)" } } } }, { "name": "proxmox_start_container", "description": "Start an LXC container", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "Container ID to start" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_stop_container", "description": "Stop an LXC container", "inputSchema": { "type": "object", "properties": { "vmid": { "type": "integer", "description": "Container ID to stop" }, "node": { "type": "string", "description": "Proxmox node name (optional)" } }, "required": ["vmid"] } }, { "name": "proxmox_list_storage", "description": "List all storage pools and their capacity", "inputSchema": { "type": "object", "properties": { "node": { "type": "string", "description": "Proxmox node name (optional)" } } } } ] return { "tools": tools, "client": client } def handle_tool_call(tool_name: str, arguments: Dict[str, Any], client: ProxmoxClient) -> Dict[str, Any]: """Handle MCP tool calls""" try: if tool_name == "proxmox_list_vms": vms = client.get_all_vms_status(arguments.get("node")) return {"content": [{"type": "text", "text": json.dumps(vms, indent=2)}]} elif tool_name == "proxmox_get_vm": vm = client.get_vm(arguments["vmid"], arguments.get("node")) return {"content": [{"type": "text", "text": json.dumps(vm, indent=2)}]} elif tool_name == "proxmox_start_vm": upid = client.start_vm(arguments["vmid"], arguments.get("node")) return {"content": [{"type": "text", "text": f"VM {arguments['vmid']} starting. Task ID: {upid}"}]} elif tool_name == "proxmox_stop_vm": upid = client.stop_vm( arguments["vmid"], arguments.get("node"), arguments.get("force", False) ) action = "Force stopping" if arguments.get("force") else "Stopping" return {"content": [{"type": "text", "text": f"{action} VM {arguments['vmid']}. Task ID: {upid}"}]} elif tool_name == "proxmox_restart_vm": upid = client.restart_vm(arguments["vmid"], arguments.get("node")) return {"content": [{"type": "text", "text": f"Restarting VM {arguments['vmid']}. Task ID: {upid}"}]} elif tool_name == "proxmox_create_snapshot": upid = client.create_snapshot( arguments["vmid"], arguments["snapname"], arguments.get("description", ""), arguments.get("vmstate", False), arguments.get("node") ) return {"content": [{"type": "text", "text": f"Creating snapshot '{arguments['snapname']}' for VM {arguments['vmid']}. Task ID: {upid}"}]} elif tool_name == "proxmox_list_snapshots": snapshots = client.list_snapshots(arguments["vmid"], arguments.get("node")) return {"content": [{"type": "text", "text": json.dumps(snapshots, indent=2)}]} elif tool_name == "proxmox_rollback_snapshot": upid = client.rollback_snapshot( arguments["vmid"], arguments["snapname"], arguments.get("node") ) return {"content": [{"type": "text", "text": f"Rolling back VM {arguments['vmid']} to snapshot '{arguments['snapname']}'. Task ID: {upid}"}]} elif tool_name == "proxmox_delete_snapshot": upid = client.delete_snapshot( arguments["vmid"], arguments["snapname"], arguments.get("node") ) return {"content": [{"type": "text", "text": f"Deleting snapshot '{arguments['snapname']}' from VM {arguments['vmid']}. Task ID: {upid}"}]} elif tool_name == "proxmox_clone_vm": upid = client.clone_vm( arguments["vmid"], arguments["newid"], arguments.get("name"), arguments.get("node"), arguments.get("full", True) ) clone_type = "full" if arguments.get("full", True) else "linked" return {"content": [{"type": "text", "text": f"Cloning VM {arguments['vmid']} to {arguments['newid']} ({clone_type} clone). Task ID: {upid}"}]} elif tool_name == "proxmox_get_node_status": status = client.get_node_status(arguments.get("node")) return {"content": [{"type": "text", "text": json.dumps(status, indent=2)}]} elif tool_name == "proxmox_list_containers": containers = client.list_containers(arguments.get("node")) return {"content": [{"type": "text", "text": json.dumps(containers, indent=2)}]} elif tool_name == "proxmox_start_container": upid = client.start_container(arguments["vmid"], arguments.get("node")) return {"content": [{"type": "text", "text": f"Starting container {arguments['vmid']}. Task ID: {upid}"}]} elif tool_name == "proxmox_stop_container": upid = client.stop_container(arguments["vmid"], arguments.get("node")) return {"content": [{"type": "text", "text": f"Stopping container {arguments['vmid']}. Task ID: {upid}"}]} elif tool_name == "proxmox_list_storage": storage = client.list_storage(arguments.get("node")) return {"content": [{"type": "text", "text": json.dumps(storage, indent=2)}]} else: return {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True} except Exception as e: return {"content": [{"type": "text", "text": f"Error: {str(e)}"}], "isError": True} def main(): """MCP stdio server main loop""" server = create_mcp_server() client = server["client"] tools = server["tools"] # Read messages from stdin for line in sys.stdin: try: message = json.loads(line) if message.get("method") == "tools/list": response = { "jsonrpc": "2.0", "id": message.get("id"), "result": {"tools": tools} } print(json.dumps(response), flush=True) elif message.get("method") == "tools/call": params = message.get("params", {}) tool_name = params.get("name") arguments = params.get("arguments", {}) result = handle_tool_call(tool_name, arguments, client) response = { "jsonrpc": "2.0", "id": message.get("id"), "result": result } print(json.dumps(response), flush=True) elif message.get("method") == "initialize": response = { "jsonrpc": "2.0", "id": message.get("id"), "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, "serverInfo": { "name": "proxmox-mcp-server", "version": "1.0.0" } } } print(json.dumps(response), flush=True) except Exception as e: error_response = { "jsonrpc": "2.0", "id": message.get("id") if 'message' in locals() else None, "error": { "code": -32603, "message": str(e) } } print(json.dumps(error_response), flush=True) if __name__ == "__main__": main()