Skip to main content
Glama
vms.py15.5 kB
"""VM-related MCP tools for Proxmox.""" import logging from typing import Any from mcp.server.fastmcp import FastMCP from ..proxmox_client import proxmox logger = logging.getLogger(__name__) def register_vm_tools(mcp: FastMCP) -> None: """Register all VM-related tools with the MCP server.""" @mcp.tool() async def list_vms() -> list[dict[str, Any]]: """List all VMs and containers across all Proxmox nodes. Returns a list of all virtual machines and LXC containers with their basic information including: - vmid: The VM/container ID - name: The VM/container name - status: Current status (running, stopped, etc.) - node: The Proxmox node hosting this VM - type: 'qemu' for VMs or 'lxc' for containers - cpu: Number of CPUs - maxmem: Maximum memory in bytes - maxdisk: Maximum disk size in bytes """ logger.info("Listing all VMs and containers") # Get both VMs and containers vms = await proxmox.get_all_vms() containers = await proxmox.get_all_containers() # Mark VMs with type for vm in vms: vm["type"] = "qemu" all_guests = vms + containers # Return simplified, consistent format result = [] for guest in all_guests: result.append( { "vmid": guest.get("vmid"), "name": guest.get("name", f"VM-{guest.get('vmid')}"), "status": guest.get("status"), "node": guest.get("node"), "type": guest.get("type"), "cpus": guest.get("cpus", guest.get("maxcpu", 0)), "memory_bytes": guest.get("maxmem", 0), "disk_bytes": guest.get("maxdisk", 0), "uptime_seconds": guest.get("uptime", 0), } ) logger.info(f"Found {len(result)} VMs/containers") return result @mcp.tool() async def get_vm_info(vmid: int, node: str | None = None) -> dict[str, Any]: """Get detailed information about a specific VM or container. Args: vmid: The VM or container ID (e.g., 100, 101) node: The Proxmox node name. If not provided, will search all nodes. Returns detailed configuration including: - Hardware: CPU, memory, disks, network interfaces - Settings: Boot order, OS type, description - Status: Current state, uptime, resource usage """ logger.info(f"Getting info for VM {vmid} on node {node or 'auto-detect'}") # If node not provided, find the VM if node is None: all_vms = await proxmox.get_all_vms() all_containers = await proxmox.get_all_containers() for vm in all_vms + all_containers: if vm.get("vmid") == vmid: node = vm.get("node") break if node is None: return {"error": f"VM {vmid} not found on any node"} # Try QEMU first, then LXC try: config = await proxmox.get_vm_config(node, vmid) status = await proxmox.get_vm_status(node, vmid) vm_type = "qemu" except Exception: try: config = await proxmox.get_container_config(node, vmid) status = await proxmox.get_container_status(node, vmid) vm_type = "lxc" except Exception as e: return {"error": f"Failed to get VM/container info: {e}"} # Parse network interfaces networks = [] for key, value in config.items(): if key.startswith("net") and value: networks.append({"interface": key, "config": value}) # Parse disks disks = [] disk_prefixes = ("scsi", "virtio", "ide", "sata", "rootfs", "mp") for key, value in config.items(): if any(key.startswith(p) for p in disk_prefixes) and value: if isinstance(value, str) and (":" in value or "volume" in value.lower()): disks.append({"device": key, "config": value}) return { "vmid": vmid, "node": node, "type": vm_type, "name": config.get("name", status.get("name", f"VM-{vmid}")), "description": config.get("description", ""), "status": status.get("status"), "uptime_seconds": status.get("uptime", 0), "hardware": { "cpus": config.get("cores", config.get("cpulimit", 1)), "sockets": config.get("sockets", 1), "memory_mb": config.get("memory", 0), "balloon": config.get("balloon", None), }, "cpu_usage_percent": round(status.get("cpu", 0) * 100, 2), "memory_usage_bytes": status.get("mem", 0), "memory_total_bytes": status.get("maxmem", 0), "disk_read_bytes": status.get("diskread", 0), "disk_write_bytes": status.get("diskwrite", 0), "network_in_bytes": status.get("netin", 0), "network_out_bytes": status.get("netout", 0), "networks": networks, "disks": disks, "boot_order": config.get("boot", ""), "os_type": config.get("ostype", ""), "machine_type": config.get("machine", ""), } @mcp.tool() async def get_vm_status(vmid: int, node: str | None = None) -> dict[str, Any]: """Get the current runtime status of a VM or container. Args: vmid: The VM or container ID node: The Proxmox node name (optional, will auto-detect) Returns: - status: running, stopped, paused, etc. - uptime: How long the VM has been running - cpu: Current CPU usage percentage - memory: Current memory usage - network I/O stats - disk I/O stats """ logger.info(f"Getting status for VM {vmid}") # Find node if not provided if node is None: all_vms = await proxmox.get_all_vms() all_containers = await proxmox.get_all_containers() for vm in all_vms + all_containers: if vm.get("vmid") == vmid: node = vm.get("node") break if node is None: return {"error": f"VM {vmid} not found"} # Try QEMU first, then LXC try: status = await proxmox.get_vm_status(node, vmid) vm_type = "qemu" except Exception: try: status = await proxmox.get_container_status(node, vmid) vm_type = "lxc" except Exception as e: return {"error": f"Failed to get status: {e}"} return { "vmid": vmid, "node": node, "type": vm_type, "name": status.get("name", f"VM-{vmid}"), "status": status.get("status"), "uptime_seconds": status.get("uptime", 0), "cpu_usage_percent": round(status.get("cpu", 0) * 100, 2), "memory_used_bytes": status.get("mem", 0), "memory_total_bytes": status.get("maxmem", 0), "memory_usage_percent": round( (status.get("mem", 0) / max(status.get("maxmem", 1), 1)) * 100, 2 ), "disk_read_bytes": status.get("diskread", 0), "disk_write_bytes": status.get("diskwrite", 0), "network_in_bytes": status.get("netin", 0), "network_out_bytes": status.get("netout", 0), "pid": status.get("pid"), "qmpstatus": status.get("qmpstatus"), # QEMU Machine Protocol status } @mcp.tool() async def get_vm_metrics( vmid: int, node: str | None = None, timeframe: str = "hour" ) -> dict[str, Any]: """Get historical metrics/performance data for a VM. Args: vmid: The VM or container ID node: The Proxmox node name (optional) timeframe: Time range for metrics - one of: - 'hour': Last hour (default) - 'day': Last 24 hours - 'week': Last 7 days - 'month': Last 30 days - 'year': Last year Returns time-series data including: - CPU usage over time - Memory usage over time - Network I/O over time - Disk I/O over time """ logger.info(f"Getting metrics for VM {vmid} over {timeframe}") if timeframe not in ("hour", "day", "week", "month", "year"): return {"error": f"Invalid timeframe: {timeframe}. Use hour/day/week/month/year"} # Find node if not provided if node is None: all_vms = await proxmox.get_all_vms() for vm in all_vms: if vm.get("vmid") == vmid: node = vm.get("node") break if node is None: return {"error": f"VM {vmid} not found"} try: rrd_data = await proxmox.get_vm_rrddata(node, vmid, timeframe) except Exception as e: return {"error": f"Failed to get metrics: {e}"} if not rrd_data: return { "vmid": vmid, "node": node, "timeframe": timeframe, "message": "No metrics data available", "data_points": [], } # Process and return the metrics data_points = [] for point in rrd_data: data_points.append( { "timestamp": point.get("time"), "cpu_percent": round((point.get("cpu", 0) or 0) * 100, 2), "memory_bytes": point.get("mem", 0), "memory_max_bytes": point.get("maxmem", 0), "disk_read_bytes": point.get("diskread", 0), "disk_write_bytes": point.get("diskwrite", 0), "network_in_bytes": point.get("netin", 0), "network_out_bytes": point.get("netout", 0), } ) return { "vmid": vmid, "node": node, "timeframe": timeframe, "data_points_count": len(data_points), "data_points": data_points, } @mcp.tool() async def list_nodes() -> list[dict[str, Any]]: """List all Proxmox nodes in the cluster. Returns information about each node including: - Node name and status - CPU and memory resources - Running VMs count - Uptime """ logger.info("Listing all Proxmox nodes") nodes = await proxmox.get_nodes() result = [] for node in nodes: result.append( { "node": node.get("node"), "status": node.get("status"), "cpu_usage_percent": round((node.get("cpu", 0) or 0) * 100, 2), "memory_used_bytes": node.get("mem", 0), "memory_total_bytes": node.get("maxmem", 0), "memory_usage_percent": round( (node.get("mem", 0) / max(node.get("maxmem", 1), 1)) * 100, 2 ), "disk_used_bytes": node.get("disk", 0), "disk_total_bytes": node.get("maxdisk", 0), "uptime_seconds": node.get("uptime", 0), } ) return result @mcp.tool() async def list_vm_snapshots(vmid: int, node: str | None = None) -> list[dict[str, Any]]: """List all snapshots for a VM. Args: vmid: The VM ID node: The Proxmox node name (optional, will auto-detect) Returns list of snapshots with: - Snapshot name and description - Creation time - Whether it includes RAM state """ logger.info(f"Listing snapshots for VM {vmid}") # Find node if not provided if node is None: all_vms = await proxmox.get_all_vms() for vm in all_vms: if vm.get("vmid") == vmid: node = vm.get("node") break if node is None: return [{"error": f"VM {vmid} not found"}] try: snapshots = await proxmox.get_vm_snapshots(node, vmid) except Exception as e: return [{"error": f"Failed to get snapshots: {e}"}] result = [] for snap in snapshots or []: if snap.get("name") == "current": continue # Skip the 'current' pseudo-snapshot result.append( { "name": snap.get("name"), "description": snap.get("description", ""), "snaptime": snap.get("snaptime"), "vmstate": snap.get("vmstate", False), # True if RAM included "parent": snap.get("parent"), } ) return result @mcp.tool() async def get_cluster_status() -> dict[str, Any]: """Get overall Proxmox cluster status and health. Returns: - Cluster name and quorum status - List of all nodes with their status - Total resources (CPU, memory, storage) """ logger.info("Getting cluster status") try: cluster_status = await proxmox.get_cluster_status() except Exception as e: return {"error": f"Failed to get cluster status: {e}"} nodes = [] cluster_info = {} for item in cluster_status or []: if item.get("type") == "cluster": cluster_info = { "name": item.get("name"), "nodes_count": item.get("nodes", 0), "quorate": item.get("quorate", 0) == 1, "version": item.get("version"), } elif item.get("type") == "node": nodes.append( { "name": item.get("name"), "id": item.get("id"), "online": item.get("online", 0) == 1, "local": item.get("local", 0) == 1, "ip": item.get("ip"), } ) # Get aggregate stats node_details = await proxmox.get_nodes() total_cpu = 0 total_mem = 0 total_disk = 0 used_mem = 0 used_disk = 0 for n in node_details: total_mem += n.get("maxmem", 0) total_disk += n.get("maxdisk", 0) used_mem += n.get("mem", 0) used_disk += n.get("disk", 0) total_cpu += n.get("maxcpu", 0) return { "cluster": cluster_info, "nodes": nodes, "totals": { "cpu_cores": total_cpu, "memory_total_bytes": total_mem, "memory_used_bytes": used_mem, "memory_usage_percent": round((used_mem / max(total_mem, 1)) * 100, 2), "disk_total_bytes": total_disk, "disk_used_bytes": used_disk, "disk_usage_percent": round((used_disk / max(total_disk, 1)) * 100, 2), }, }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/teomarcdhio/proxmox-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server