diff --git a/src/proxmox_mcp/server.py b/src/proxmox_mcp/server.py index 0477033..efcd9be 100644 --- a/src/proxmox_mcp/server.py +++ b/src/proxmox_mcp/server.py @@ -1,11 +1,15 @@ from __future__ import annotations import json +import os +from typing import Any +import httpx from mcp.server.fastmcp import FastMCP from proxmox_mcp import __version__ from proxmox_mcp.config import settings +from proxmox_mcp.targets import registry from proxmox_mcp.tools import register_all mcp = FastMCP( @@ -18,6 +22,131 @@ mcp = FastMCP( ) +def _get_active_client() -> httpx.Client | None: + """Return an authenticated httpx client for the active target, or None.""" + target = registry.active() + if not target: + return None + verify = os.environ.get("PROXMOX_VERIFY_SSL", "true").lower() in ("1", "true", "yes") + client = httpx.Client( + base_url=target.url, + verify=verify, + headers={"Authorization": f"PVEAPIToken={target.token_id}={target.token_secret}"}, + timeout=30.0, + ) + return client + + +@mcp.tool() +def proxmox_list_all_guests() -> str: + """List all QEMU VMs and LXC containers across all Proxmox cluster nodes. + + This tool uses the cluster resources endpoint, which does not require a node + parameter. It returns a combined list of all VMs and containers regardless + of which node they reside on. + """ + client = _get_active_client() + if not client: + return json.dumps({"error": "No active target. Call proxmox_connect or proxmox_use_target first."}) + + try: + # Query cluster resources for both VMs and containers + resp = client.get("/api2/json/cluster/resources", params={"type": "vm"}) + resp.raise_for_status() + data = resp.json().get("data", []) + + # Filter: vmid with status and type + guests = [] + for entry in data: + if entry.get("type") in ("qemu", "lxc"): + guests.append({ + "vmid": entry.get("vmid"), + "node": entry.get("node"), + "name": entry.get("name"), + "type": entry.get("type"), + "status": entry.get("status"), + "mem": entry.get("mem"), + "maxmem": entry.get("maxmem"), + "cpu": entry.get("cpu"), + "maxcpu": entry.get("maxcpu"), + "disk": entry.get("disk"), + "maxdisk": entry.get("maxdisk"), + "uptime": entry.get("uptime"), + }) + return json.dumps(guests, indent=2) + except Exception as exc: + return json.dumps({"error": str(exc)}) + finally: + client.close() + + +@mcp.tool() +def proxmox_debug_guest_inventory(node: str | None = None) -> str: + """Return the raw JSON from the Proxmox API for debugging guest listing. + + If a node is given, queries /nodes/{node}/qemu and /nodes/{node}/lxc separately. + Otherwise, queries the cluster resources endpoint. + """ + client = _get_active_client() + if not client: + return json.dumps({"error": "No active target. Call proxmox_connect or proxmox_use_target first."}) + + try: + result = {} + if node: + # Per-node endpoints + for gtype in ("qemu", "lxc"): + resp = client.get(f"/api2/json/nodes/{node}/{gtype}") + resp.raise_for_status() + result[gtype] = resp.json().get("data", []) + else: + # Cluster resources + resp = client.get("/api2/json/cluster/resources", params={"type": "vm"}) + resp.raise_for_status() + result = resp.json() + return json.dumps(result, indent=2, default=str) + except Exception as exc: + return json.dumps({"error": str(exc)}) + finally: + client.close() + + +@mcp.tool() +def proxmox_list_qemu(node: str) -> str: + """List all QEMU VMs on a specific Proxmox node.""" + client = _get_active_client() + if not client: + return json.dumps({"error": "No active target. Call proxmox_connect or proxmox_use_target first."}) + + try: + resp = client.get(f"/api2/json/nodes/{node}/qemu") + resp.raise_for_status() + vms = resp.json().get("data", []) + return json.dumps(vms, indent=2) + except Exception as exc: + return json.dumps({"error": str(exc)}) + finally: + client.close() + + +@mcp.tool() +def proxmox_list_lxc(node: str) -> str: + """List all LXC containers on a specific Proxmox node.""" + client = _get_active_client() + if not client: + return json.dumps({"error": "No active target. Call proxmox_connect or proxmox_use_target first."}) + + try: + resp = client.get(f"/api2/json/nodes/{node}/lxc") + resp.raise_for_status() + lxcs = resp.json().get("data", []) + return json.dumps(lxcs, indent=2) + except Exception as exc: + return json.dumps({"error": str(exc)}) + finally: + client.close() + + @mcp.tool() def proxmox_get_config() -> str: from proxmox_mcp.targets import registry