Skip to main content
Glama

PDM MCP Server

Ejecutar con Docker

Editá las credenciales PDM y ejecutá:

docker run -d \
  --name pdm-mcp-server \
  --restart unless-stopped \
  --pull always \
  -p 3000:3000 \
  -e PDM_URL="https://pdm.example.com:8443" \
  -e PDM_TOKEN_ID='readonly@pdm!mcp' \
  -e PDM_TOKEN_SECRET='replace-with-token-secret' \
  -e PDM_TLS_INSECURE="false" \
  -e MCP_AUTH_MODE="disabled" \
  jer3m/pdm-mcp-server:latest

El servidor queda disponible en http://localhost:3000/mcp. Verificá que arrancó con:

curl http://localhost:3000/healthz

Si PDM utiliza un certificado interno o autofirmado, cambiá PDM_TLS_INSECURE a true. Para evitar guardar el secreto en el historial del shell, también podés pasar estas mismas variables mediante --env-file.

Servidor MCP estrictamente read-only para Proxmox Datacenter Manager (PDM). Expone Streamable HTTP stateless en /mcp; cada request recibe un servidor y transporte nuevos, por lo que no depende de sesiones en memoria.

Related MCP server: Proxmox MCP Server

Autenticación y autorización por request

Elegí explícitamente MCP_AUTH_MODE=disabled para conservar el comportamiento sin autenticación, o MCP_AUTH_MODE=jwt para restringir cada request a los remotes permitidos. Si el modo falta, es desconocido o la configuración JWT es inválida, el servidor no arranca; nunca vuelve automáticamente al acceso sin restricciones. .env.example y Compose seleccionan disabled por compatibilidad.

Para activar JWT, además de las credenciales PDM, configurá:

MCP_AUTH_MODE=jwt
MCP_JWT_SECRET=<server-side-random-signing-secret>
MCP_JWT_AUDIENCE=pdm-mcp

Reemplazá el marcador del secreto por un valor aleatorio de al menos 32 bytes, generado y almacenado en el servidor. MCP_JWT_AUDIENCE es opcional y tiene como valor predeterminado pdm-mcp; el secreto es obligatorio en modo JWT. Compose recibe estas variables del entorno o de .env. Para ejecutar Node directamente, cargalas en el entorno o usá node --env-file=.env dist/index.js después del build.

El cliente debe enviar Authorization: Bearer <jwt> en cada request a /mcp, incluyendo initialize y tools/list. Se acepta únicamente HS256 y se verifican firma, audiencia (aud), vencimiento obligatorio (exp, segundos Unix) y nbf si existe. El claim pdm_remotes debe ser un array no vacío de strings no vacíos, por ejemplo {"pdm_remotes":["LAB-A"]} o {"pdm_remotes":["LAB-A","LAB-B"]}. Los nombres se comparan exactamente, distinguiendo mayúsculas y minúsculas; no se aceptan espacios al principio o al final. Emití tokens de corta duración con la audiencia configurada y un vencimiento futuro. Un JWT ausente, inválido o sin alcance válido recibe HTTP 401 con un error genérico. GET /healthz permanece público.

El modo JWT está pensado para aplicaciones que derivan la autorización del lado del servidor. El backend emisor debe obtener los remotes permitidos de su propia política de acceso: nunca debe dejar que el usuario final elija su claim pdm_remotes. El secreto de firma nunca debe exponerse a browsers ni LLMs. Usá HTTPS delante de /mcp para proteger los bearer tokens en tránsito. Esta integración verifica tokens emitidos por tu aplicación; no implementa un servidor OAuth ni un flujo de login.

Una misma instancia MCP puede atender múltiples alcances aislados. Cada request crea su propio cliente PDM con el alcance del JWT verificado. El MCP vuelve a aplicar la autorización aunque el caller o el LLM proporcionen un argumento remote: un remote no permitido produce Access denied. sin consultar PDM ni revelar otros remotes. Los argumentos de las tools no pueden ampliar el alcance.

En modo JWT, list_resources, list_vms, list_nodes, list_containers y list_storages sin remote consultan secuencialmente solo los remotes permitidos y agregan sus resultados; no utilizan el inventario global ni lo filtran después. En esas consultas max_age no aplica, porque se usan las rutas existentes por remote. list_remotes consulta la colección de configuración y devuelve únicamente las entradas con ID autorizado, sanitizando secretos. Los errores de autenticación y los errores de requests PDM en modo JWT son genéricos y no incluyen tokens, headers, secretos ni cuerpos de respuesta.

Con MCP_AUTH_MODE=disabled, se mantienen el inventario global y el acceso sin alcance previo. Restringí ese endpoint a clientes confiables mediante la red o un reverse proxy. Los permisos del token PDM siguen siendo el límite superior y todas las tools siguen siendo read-only.

Tools

Tool

Fuente PDM

Uso

list_remotes

GET /api2/json/remotes/remote

Remotes configurados

list_resources

GET /api2/json/resources/list o GET .../resources

Inventario global cacheado o de un remote

list_vms

Inventario global o GET .../qemu

VMs QEMU, globales o por remote

get_vm

GET .../qemu/{vmid}/status y /config

Runtime y configuración sanitizada

list_nodes

Inventario global o GET .../nodes

Nodos globales o por remote

get_node

GET .../nodes/{node}/status

Estado detallado del nodo

list_containers

Inventario global o GET .../lxc

Containers LXC, globales o por remote

get_container

GET .../lxc/{vmid}/status y /config

Runtime y configuración sanitizada

list_storages

Inventario global o GET .../nodes/{node}/storage

Estado general, sin listar contenido

get_storage

Inventario global y GET .../storage/{storage}/status

Estado de un storage, sin su contenido

get_remote_summary

GET .../nodes, /qemu y /lxc

Totales y capacidad física sin listar cada recurso

list_tasks

GET .../cluster/tasks o GET .../nodes/{node}/tasks

Tareas recientes y activas (cluster o por nodo/vmid)

get_task

GET .../nodes/{node}/tasks/{upid}/status

Estado detallado de una tarea por UPID

En la tabla, ... significa /api2/json/pve/remotes/{remote}. La colección de configuración de remotes es distinta y conserva /api2/json/remotes/remote. Los nombres de remote, node y storage se codifican como segmentos URL. Las respuestas incluyen texto JSON por compatibilidad y structuredContent para clientes MCP que lo soportan.

El inventario global envía max-age=300 por defecto para reutilizar el cache de PDM y evitar recolectar todos los remotes en cada consulta. list_resources acepta max_age; usá 0 cuando necesites forzar una actualización. Con remote se consulta directamente /pve/remotes/{remote}/resources, evitando el fan-out global. PDM_TIMEOUT_MS sigue siendo configurable y mantiene su default de 15 segundos. El inventario global envía max-age=300 por defecto para reutilizar el cache de PDM y evitar recolectar todos los remotes en cada consulta. list_resources acepta max_age; usá 0 cuando necesites forzar una actualización. Con remote se consulta directamente /pve/remotes/{remote}/resources, evitando el fan-out global. PDM_TIMEOUT_MS sigue siendo configurable y mantiene su default de 60 segundos.

get_vm, get_container y las consultas de tareas eliminan claves sensibles como passwords, tokens, secrets, claves SSH y valores equivalentes embebidos. El servidor nunca incluye el header de autorización ni el body de un error PDM en sus errores.

Respuestas compactas

Las tools de listado devuelven una frase corta en content y los datos una sola vez en structuredContent. La vista predeterminada es siempre compacta:

  • list_vms y list_containers: summary, hardware, runtime o full.

  • list_nodes: summary, capacity, runtime o full.

  • list_storages: summary, capacity o full.

  • list_resources y list_tasks: summary o full.

summary sirve para descubrir e identificar recursos. hardware/capacity normalizan bytes a GiB; runtime normaliza ratios a porcentajes. full debe pedirse explícitamente y continúa sanitizando secretos. Para el detalle de un único elemento usá su tool get_*.

Para preguntas agregadas como cantidad de VMs encendidas o RAM física total, preferí get_remote_summary: usa solamente los listados de nodes, QEMU y LXC, sin hacer una request por guest.

Requisitos y configuración

  • Node.js 24 o posterior.

  • PDM 1.x accesible por HTTPS.

  • Usuario y token dedicados con rol Auditor propagado sobre /resource. Los permisos del token nunca superan los del usuario.

Variables requeridas:

PDM_URL=https://pdm.example.com:8443
PDM_TOKEN_ID=readonly@pdm!mcp
PDM_TOKEN_SECRET=replace-me

Variables opcionales:

PDM_TLS_INSECURE=false
PDM_TIMEOUT_MS=15000
PDM_TIMEOUT_MS=60000
MCP_PORT=3000

PDM_TLS_INSECURE=true deshabilita la validación TLS solamente para esa conexión PDM; usalo únicamente con certificados internos/autofirmados.

Desarrollo y prueba manual

npm install
npm test
npm start

Comprobá primero la salud:

curl.exe http://localhost:3000/healthz

Para ejecutar tools sin poner credenciales PDM en comandos, iniciá MCP Inspector:

npx @modelcontextprotocol/inspector

En Inspector elegí Streamable HTTP, usá http://localhost:3000/mcp, conectá y abrí Tools. Pruebas sugeridas:

list_resources {}
list_vms {"view":"summary"}
list_vms {"remote":"LAB-A"}
list_vms {"remote":"LAB-A","view":"hardware"}
list_vms {"remote":"LAB-A","view":"runtime"}
get_vm {"remote":"LAB-A","vmid":105}
list_nodes {"view":"summary"}
list_nodes {"remote":"LAB-A"}
list_containers {}
list_storages {}
list_storages {"remote":"LAB-A","node":"pve-test-01"}
get_remote_summary {"remote":"LAB-A"}
list_tasks {"remote":"LAB-A"}
list_tasks {"remote":"LAB-A","errors_only":true}
list_tasks {"remote":"LAB-A","node":"pve-test-01","vmid":105}
get_task {"remote":"LAB-A","upid":"UPID:pve-test-01:00001234:00005678:65A4B3C2:vzdump:105:readonly@pdm!mcp:"}

Para Codex:

[mcp_servers.pdm]
url = "http://mcp.example.internal:3000/mcp"

En modo JWT, configurá también el bearer token emitido por tu backend en el cliente MCP. En modo disabled, publicá el puerto 3000 únicamente en una red confiable o restringilo mediante firewall/reverse proxy.

Docker y CI

La imagen pública es jer3m/pdm-mcp-server:latest y se construye desde GitHub Actions. Para publicar nuevas versiones configurá:

  • Variable DOCKERHUB_IMAGE, por ejemplo tuusuario/pdm-mcp-server.

  • Secret DOCKERHUB_USERNAME.

  • Secret DOCKERHUB_TOKEN, un access token de Docker Hub con permiso de escritura.

El workflow Publish Docker image se ejecuta manualmente con el tag indicado o con un tag Git v*; en este último caso también publica latest.

compose.yml publica 3000:3000 y muestra todas las variables necesarias. Después de editar sus valores:

docker pull jer3m/pdm-mcp-server:latest
docker compose up -d
curl http://localhost:3000/healthz

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables read-only interaction with Proxmox homelab VMs and containers, allowing LLM agents to list VMs, monitor status and performance metrics, view snapshots, and check cluster health through natural language queries.
    8
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    MCP server for Proxmox VE and Proxmox Datacenter Manager, covering every API endpoint via six consolidated tools for list, describe, and call operations with a read-only safety gate.
    6
    16 npm
    2
    AGPL 3.0
  • A
    license
    A
    quality
    C
    maintenance
    A read-only MCP server for Proxmox VE that provides AI assistants with structured visibility into cluster nodes, guests, storage, and Docker workloads. It is designed to prevent any mutating operations by construction.
    12
    MIT
  • F
    license
    B
    quality
    C
    maintenance
    Read-only Proxmox VE MCP server providing 25 tools for VM, LXC, node, storage, and cluster inspection via stdio transport.
    25
    -