mcp-sysadmin
by kreodevs
README.md
# MCP Sysadmin
Servidor MCP **agnóstico de proveedor** para administrar infraestructura heterogénea: servidores físicos, VPS por SSH, clusters **Proxmox VE**, paneles **Virtualizor**, **Hetzner Cloud** y **Cloudflare**.
A diferencia de MCPs atados a un hosting (p. ej. Cloudways), este proyecto usa un **inventario JSON** donde registras cada host con su proveedor y credenciales. Un mismo cliente MCP puede operar Proxmox en tu homelab, Virtualizor en un datacenter y servidores bare-metal en otra ubicación.
## Arquitectura
```mermaid
flowchart LR
Client[Cliente MCP / Cursor] --> MCP[mcp-sysadmin]
MCP --> Inv[(inventory.json)]
MCP --> SSH[SSH]
MCP --> PVE[Proxmox API]
MCP --> VZ[Virtualizor API]
MCP --> HZ[Hetzner API]
MCP --> CF[Cloudflare API]
SSH --> Physical[Servidores físicos / VPS]
PVE --> VMs1[VMs KVM / LXC]
VZ --> VMs2[VPS OpenVZ/KVM/Xen]
HZ --> VMs3[Cloud Servers]
CF --> DNS[DNS / CDN / WAF]
```
## Proveedores soportados
| Provider | Uso | Autenticación |
|----------|-----|---------------|
| `ssh` | Servidores físicos, VPS sin API, cualquier Linux | Clave privada o password |
| `proxmox` | Clusters / nodos Proxmox VE | API Token (`PVEAPIToken`) |
| `virtualizor` | Panel Virtualizor (Admin API) | `apiKey` + `apiPass` |
| `hetzner` | Hetzner Cloud (servidores, firewalls, volúmenes) | API Token (`Bearer`) |
| `cloudflare` | DNS, CDN, WAF (zonas y registros) | API Token (`Bearer`) |
## Tools incluidos (38)
### Inventario
- `list-hosts`, `get-host`
### Nodos / métricas
- `list-nodes`, `get-node-status`, `health-check`
### Máquinas virtuales / cloud
- `list-vms`, `list-containers`, `get-vm`, `vm-power`
- `list-vm-snapshots`, `create-vm-snapshot`
- `list-proxmox-tasks`, `get-proxmox-task`
- `list-storage-usage`, `list-backups`, `create-backup`
- `list-hetzner-firewalls`, `list-hetzner-volumes`
### Cloudflare (DNS / CDN)
- `list-zones`, `list-dns-records`, `get-dns-record`
- `create-dns-record`, `update-dns-record`, `delete-dns-record` (confirmToken)
- `purge-cache` (confirmToken)
- `list-waf-rules`
### Red
- `list-network`
### SSH — operaciones controladas
- `ssh-exec`, `ssh-read-file` (destructivas / confirmToken)
### SSH — diagnóstico read-only
- `ssh-tail-log` — journalctl o tail en `/var/log/`
- `list-firewall-rules` — UFW / nftables / iptables
- `list-systemd-units` — failed / running / all
- `cert-status` — certbot / fechas SSL
- `dns-lookup`, `check-endpoint`
- `list-cron`, `list-timers`
- `docker-compose-ps`
## Instalación
> 📖 **Manuales operativos:** [`manuales/`](./manuales/) — [manual general](./manuales/manual-general.md) y guías por provider.
### Opción A — GitHub Packages + npx (recomendado)
Publicado en [GitHub Packages](https://github.com/kreodevs/mcp-sysadmin/pkgs/npm/mcp-sysadmin) como **`@kreodevs/mcp-sysadmin`**. No necesitas clonar el repo.
**1. Registry de GitHub** (una vez por máquina):
```bash
echo "@kreodevs:registry=https://npm.pkg.github.com" >> ~/.npmrc
```
O copia [`.npmrc.example`](.npmrc.example). Los paquetes **públicos** no requieren token para instalar.
**2. Inventario** — crea tu `inventory.json` en cualquier ruta (p. ej. `~/mcp/inventory.json`). Puedes basarte en [`config/inventory.example.json`](config/inventory.example.json).
**3. Probar en terminal:**
```bash
export SYSADMIN_INVENTORY_PATH=~/mcp/inventory.json
export SYSADMIN_PRODUCTION_MODE=true
export SYSADMIN_CONFIRM_TOKEN=$(openssl rand -hex 32)
npx -y @kreodevs/mcp-sysadmin
```
**4. Cliente MCP** — configura `npx` (ver [Instalación por cliente MCP](#instalación-por-cliente-mcp) abajo).
| Cliente | Botón 1 clic |
|---------|--------------|
| **Cursor** | [](https://cursor.com/en/install-mcp?name=mcp-sysadmin&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcmVnaXN0cnk9aHR0cHM6Ly9ucG0ucGtnLmdpdGh1Yi5jb20iLCJAa3Jlb2RldnMvbWNwLXN5c2FkbWluIl0sImVudiI6eyJTWVNBRE1JTl9JTlZFTlRPUllfUEFUSCI6Ii9wYXRoL3RvL2ludmVudG9yeS5qc29uIiwiU1lTQURNSU5fUFJPRFVDVElPTl9NT0RFIjoidHJ1ZSIsIlNZU0FETUlOX0NPTkZJUk1fVE9LRU4iOiJHRU5FUkFfQ09OX29wZW5zc2xfcmFuZF9oZXhfMzIiLCJTWVNBRE1JTl9SRVFVSVJFX0NPTkZJUk0iOiJ0cnVlIn19) |
| **VS Code** | [](https://vscode.dev/redirect/mcp/install?name=mcp-sysadmin&config=%7B%22name%22%3A%22mcp-sysadmin%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22--registry%3Dhttps%3A%2F%2Fnpm.pkg.github.com%22%2C%22%40kreodevs%2Fmcp-sysadmin%22%5D%2C%22env%22%3A%7B%22SYSADMIN_INVENTORY_PATH%22%3A%22%2Fpath%2Fto%2Finventory.json%22%2C%22SYSADMIN_PRODUCTION_MODE%22%3A%22true%22%2C%22SYSADMIN_CONFIRM_TOKEN%22%3A%22GENERA_CON_openssl_rand_hex_32%22%2C%22SYSADMIN_REQUIRE_CONFIRM%22%3A%22true%22%7D%7D) |
> ⚠️ Tras el 1 clic, edita en el diálogo: **`SYSADMIN_INVENTORY_PATH`** (ruta a tu inventario) y **`SYSADMIN_CONFIRM_TOKEN`**.
Generar enlaces personalizados:
```bash
SYSADMIN_INVENTORY_PATH=/ruta/a/inventory.json ./scripts/generate-install-links.sh
# Modo desarrollo local (clone): INSTALL_MODE=local ./scripts/generate-install-links.sh
```
### Opción B — Desarrollo desde fuente
```bash
git clone https://github.com/kreodevs/mcp-sysadmin.git
cd mcp-sysadmin
npm install
npm run build
cp config/inventory.example.json config/inventory.json
```
Usa [`scripts/run-mcp.sh`](scripts/run-mcp.sh) o `npm run dev`.
### Publicar nueva versión (maintainers)
1. Sube la versión en `package.json` y `src/index.ts`
2. Crea un **GitHub Release** (tag `vX.Y.Z`) → el workflow [`.github/workflows/publish.yml`](.github/workflows/publish.yml) publica en GitHub Packages
3. Verifica en **Packages** del repo: `@kreodevs/mcp-sysadmin`
## Configuración
1. Copia el inventario de ejemplo:
```bash
cp config/inventory.example.json config/inventory.json
```
2. Edita `config/inventory.json` con tus hosts reales.
3. Variables de entorno (opcional):
```bash
cp .env.example .env
```
```env
SYSADMIN_INVENTORY_PATH=./config/inventory.json
SYSADMIN_PRODUCTION_MODE=true
SYSADMIN_CONFIRM_TOKEN=un-secreto-largo-que-el-llm-no-conoce
SYSADMIN_READ_ONLY=false
SYSADMIN_REQUIRE_CONFIRM=true
SYSADMIN_RATE_LIMIT_MAX=30
SYSADMIN_HTTP_TIMEOUT_MS=30000
SYSADMIN_SSH_TIMEOUT_MS=30000
```
### ACL por host (inventario)
Cada host puede restringir qué tools puede usar el LLM:
```json
{
"defaults": { "readOnly": false, "requireConfirm": true },
"hosts": [
{
"id": "pve-prod",
"readOnly": false,
"allowedTools": ["list-vms", "get-vm", "vm-power"],
"provider": "proxmox",
"...": "..."
}
]
}
```
- `readOnly: true` — solo tools de lectura en ese host
- `allowedTools` — lista blanca; si se omite, todas las tools del provider están permitidas
### Referencias a secretos en el inventario
Puedes usar `${VAR}` para no guardar credenciales en texto plano:
```json
{
"tokenSecret": "${PROXMOX_HOMELAB_TOKEN}",
"apiKey": "${VIRTUALIZOR_API_KEY}",
"apiPass": "${VIRTUALIZOR_API_PASS}"
}
```
### Ejemplo: Proxmox
```json
{
"id": "pve-prod",
"name": "Proxmox Producción",
"provider": "proxmox",
"url": "https://10.0.0.2:8006",
"tokenId": "root@pam!cursor-mcp",
"tokenSecret": "${PROXMOX_TOKEN}",
"verifySsl": false,
"defaultNode": "pve1",
"tags": ["production"]
}
```
Crea el token en Proxmox: **Datacenter → Permissions → API Tokens**.
### Ejemplo: Virtualizor
```json
{
"id": "vz-panel",
"name": "Virtualizor DC1",
"provider": "virtualizor",
"url": "https://panel.example.com:4085",
"apiKey": "${VIRTUALIZOR_API_KEY}",
"apiPass": "${VIRTUALIZOR_API_PASS}",
"tags": ["vps"]
}
```
### Ejemplo: Servidor físico (SSH)
```json
{
"id": "metal-01",
"name": "Bare Metal Rack A",
"provider": "ssh",
"host": "203.0.113.50",
"port": 22,
"username": "root",
"privateKeyPath": "~/.ssh/id_ed25519",
"tags": ["physical", "production"]
}
```
### Ejemplo: Hetzner Cloud
```json
{
"id": "hz-cloud",
"name": "Hetzner Cloud",
"provider": "hetzner",
"apiToken": "${HETZNER_API_TOKEN}",
"defaultLocation": "fsn1",
"allowedTools": ["list-vms", "get-vm", "vm-power", "list-nodes", "health-check"],
"tags": ["cloud", "hetzner"]
}
```
Crea el token en [Hetzner Cloud Console](https://console.hetzner.cloud/) → Security → API Tokens (permisos Read & Write para power actions).
### Ejemplo: Cloudflare
```json
{
"id": "cf-main",
"name": "Cloudflare Production",
"provider": "cloudflare",
"apiToken": "${CLOUDFLARE_API_TOKEN}",
"defaultZoneId": "${CLOUDFLARE_ZONE_ID}",
"readOnly": true,
"allowedTools": ["list-zones", "list-dns-records", "get-dns-record", "list-waf-rules"],
"tags": ["dns", "cdn"]
}
```
Crea un API Token en Cloudflare con permisos mínimos: Zone → DNS (Read) y, si necesitas escritura, DNS Edit + Cache Purge.
## Instalación por cliente MCP
Transporte **stdio**: el cliente lanza `npx @kreodevs/mcp-sysadmin` (GitHub Packages) o un script local en desarrollo.
> Requisito previo: [`@kreodevs:registry=https://npm.pkg.github.com`](.npmrc.example) en `~/.npmrc` **o** `--registry=https://npm.pkg.github.com` en los `args` de npx (incluido en los ejemplos).
### Instalación rápida (1 clic)
Los botones de la sección [Instalación → Opción A](#opción-a--github-packages--npx-recomendado) usan **npx + GitHub Packages**. Solo debes ajustar `SYSADMIN_INVENTORY_PATH` y `SYSADMIN_CONFIRM_TOKEN` en el diálogo del IDE.
```bash
# Enlaces con tu inventario:
SYSADMIN_INVENTORY_PATH=/ruta/a/inventory.json ./scripts/generate-install-links.sh
```
---
### Cursor
**Archivo:** `~/.cursor/mcp.json` (global) o `.cursor/mcp.json` (por proyecto)
**UI:** *Settings → Tools & MCP → New MCP Server*
**Manual (GitHub Packages):**
```json
{
"mcpServers": {
"sysadmin": {
"command": "npx",
"args": ["-y", "--registry=https://npm.pkg.github.com", "@kreodevs/mcp-sysadmin"],
"env": {
"SYSADMIN_INVENTORY_PATH": "/ruta/a/inventory.json",
"SYSADMIN_PRODUCTION_MODE": "true",
"SYSADMIN_CONFIRM_TOKEN": "tu-secreto-humano",
"SYSADMIN_REQUIRE_CONFIRM": "true",
"PROXMOX_HOMELAB_TOKEN": "..."
}
}
}
}
```
<details>
<summary>Desarrollo local (clone del repo)</summary>
```json
{
"mcpServers": {
"sysadmin": {
"command": "/ruta/absoluta/mcp-sysadmin/scripts/run-mcp.sh",
"env": {
"SYSADMIN_INVENTORY_PATH": "/ruta/absoluta/mcp-sysadmin/config/inventory.json",
"SYSADMIN_PRODUCTION_MODE": "true",
"SYSADMIN_CONFIRM_TOKEN": "tu-secreto-humano"
}
}
}
}
```
</details>
---
### Claude Desktop
**Archivo:**
| SO | Ruta |
|----|------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
**UI:** *Settings → Developer → Edit Config*
```json
{
"mcpServers": {
"sysadmin": {
"command": "npx",
"args": ["-y", "--registry=https://npm.pkg.github.com", "@kreodevs/mcp-sysadmin"],
"env": {
"SYSADMIN_INVENTORY_PATH": "/ruta/a/inventory.json",
"SYSADMIN_PRODUCTION_MODE": "true",
"SYSADMIN_CONFIRM_TOKEN": "tu-secreto-humano"
}
}
}
}
```
Reinicia Claude Desktop tras guardar.
---
### Claude Code (CLI)
```bash
claude mcp add sysadmin -- npx -y --registry=https://npm.pkg.github.com @kreodevs/mcp-sysadmin
```
Exporta `SYSADMIN_INVENTORY_PATH` y `SYSADMIN_CONFIRM_TOKEN` en el entorno o en la config de Claude Code.
---
### OpenCode
**Archivo:** `opencode.json` / `opencode.jsonc` (proyecto) o `~/.config/opencode/opencode.json`
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sysadmin": {
"type": "local",
"command": [
"npx",
"-y",
"--registry=https://npm.pkg.github.com",
"@kreodevs/mcp-sysadmin"
],
"enabled": true,
"environment": {
"SYSADMIN_INVENTORY_PATH": "/ruta/a/inventory.json",
"SYSADMIN_PRODUCTION_MODE": "true",
"SYSADMIN_CONFIRM_TOKEN": "tu-secreto-humano",
"SYSADMIN_REQUIRE_CONFIRM": "true"
}
}
}
}
```
> OpenCode usa `environment`, no `env`. El `command` debe ser un **array**.
```bash
opencode mcp add
opencode mcp list
```
---
### VS Code
**Archivo:** `.vscode/mcp.json` (workspace)
**Manual:**
```json
{
"servers": {
"sysadmin": {
"type": "stdio",
"command": "npx",
"args": ["-y", "--registry=https://npm.pkg.github.com", "@kreodevs/mcp-sysadmin"],
"env": {
"SYSADMIN_INVENTORY_PATH": "/ruta/a/inventory.json",
"SYSADMIN_PRODUCTION_MODE": "true",
"SYSADMIN_CONFIRM_TOKEN": "tu-secreto-humano"
}
}
}
}
```
Requiere GitHub Copilot con MCP o extensión compatible.
---
### Windsurf (Cascade)
**Archivo:** `~/.codeium/windsurf/mcp_config.json`
```json
{
"mcpServers": {
"sysadmin": {
"command": "npx",
"args": ["-y", "--registry=https://npm.pkg.github.com", "@kreodevs/mcp-sysadmin"],
"env": {
"SYSADMIN_INVENTORY_PATH": "/ruta/a/inventory.json",
"SYSADMIN_PRODUCTION_MODE": "true",
"SYSADMIN_CONFIRM_TOKEN": "tu-secreto-humano"
}
}
}
}
```
Pulsa **Refresh** en MCPs. Límite ~100 tools entre servidores.
---
### Variables de entorno recomendadas (todos los clientes)
```env
SYSADMIN_INVENTORY_PATH=/ruta/a/inventory.json
SYSADMIN_PRODUCTION_MODE=true
SYSADMIN_CONFIRM_TOKEN=<openssl rand -hex 32>
SYSADMIN_READ_ONLY=false
SYSADMIN_REQUIRE_CONFIRM=true
PROXMOX_HOMELAB_TOKEN=...
HETZNER_API_TOKEN=...
CLOUDFLARE_API_TOKEN=...
```
Desarrollo local del servidor (sin cliente MCP):
```bash
npm run dev
```
## Seguridad
### Modo producción
Activa siempre en prod:
```env
SYSADMIN_PRODUCTION_MODE=true
SYSADMIN_CONFIRM_TOKEN=<secreto-largo-aleatorio>
```
Con esto:
- SSH exige `hostKeyFingerprint` (anti-MITM) — **falla al arrancar** si falta
- **`SYSADMIN_CONFIRM_TOKEN` obligatorio** — falla al arrancar si falta
- SSH usa **allowlist** estricta (sin `cat`/`grep`; lecturas solo vía `ssh-read-file`)
- Prohibido `password` en inventario SSH
- `vm-power` requiere confirmación incluso para `start`
- Regex custom validadas (sin `.*` ni patrones demasiado amplios)
### Gate humano: `confirmToken`
El LLM puede poner `confirm: true` por prompt injection, pero **no conoce** `SYSADMIN_CONFIRM_TOKEN` (solo está en env del MCP, no en el chat):
```json
{
"hostId": "bare-metal-01",
"command": "systemctl status nginx",
"confirm": true,
"confirmToken": "tu-secreto-humano-no-compartir-con-el-modelo"
}
```
Tú proporcionas el token cuando apruebas la operación.
### Token de un solo uso (recomendado)
```bash
./scripts/mcp-approve.sh
# Válido 5 minutos; úsalo como confirmToken en la tool call
```
Alternativa: el token fijo `SYSADMIN_CONFIRM_TOKEN` en env MCP.
### Obtener fingerprint SSH
```bash
ssh-keyscan -H 10.0.0.5 | ssh-keygen -lf -
# Copia la línea SHA256:... al inventario como hostKeyFingerprint
```
### Controles implementados
| Control | Descripción |
|---------|-------------|
| **confirmToken** | Secreto humano en env MCP; el modelo no lo tiene por defecto |
| **Modo producción** | Allowlist SSH, host key pinning, sin passwords SSH |
| **Modo read-only** | `SYSADMIN_READ_ONLY=true` bloquea tools de escritura |
| **ACL por host** | `readOnly`, `allowedTools`, `allowedCommandPatterns` |
| **Allowlist SSH** | Solo diagnóstico (`systemctl status`, `journalctl`, `docker ps`, etc.) — **sin lectura de archivos** |
| **Lectura de archivos** | Exclusivamente vía `ssh-read-file` (paths + symlinks + confirmToken) |
| **cwd restringido** | Solo `/tmp`, `/var/log`, `/var/www`, `/home/*`, `/opt/*` en `ssh-exec` |
| **Regex inventario** | Patrones custom validados; prohibido `.*` y regex demasiado amplias |
| **Blocklist SSH** | Capa extra: `rm -rf`, pipes a shell, multiline, etc. |
| **Paths remotos** | `readlink -f` antes de leer; bloqueo de shadow/symlink bypass |
| **Rate limit** | 30 req/tool/host/min (configurable) |
| **TLS Proxmox** | `verifySsl` default `true` |
| **Redacción** | Secretos, configs VM, errores API filtrados |
| **Auditoría** | JSON en stderr: `[mcp-sysadmin:audit]` |
### Allowlist SSH por defecto
Incluye solo **diagnóstico operativo**: `systemctl status`, `journalctl`, `docker ps/logs`, `kubectl get`, `ls`, `df`, `free`, `nginx -t`, etc.
**No incluye** `cat`, `grep`, `head`, `tail` — usa `ssh-read-file` para leer archivos.
Añade patrones **específicos** en inventario (sin `.*`):
```json
{
"allowedCommandPatterns": ["^systemctl restart nginx$"]
}
```
### Tools que requieren `confirm` + `confirmToken`
- `ssh-exec` — siempre
- `ssh-read-file` — siempre
- `vm-power` — todas las acciones en producción; stop/shutdown/reboot/reset siempre
- `create-vm-snapshot` — siempre
- `create-backup` — siempre
- `create-dns-record`, `update-dns-record`, `delete-dns-record`, `purge-cache` — siempre
### Checklist pre-producción
- [ ] `SYSADMIN_PRODUCTION_MODE=true`
- [ ] `SYSADMIN_CONFIRM_TOKEN` generado (`openssl rand -hex 32`)
- [ ] Fingerprint SSH en cada host
- [ ] Tokens Proxmox / Hetzner / Cloudflare con permisos mínimos
- [ ] `verifySsl: true` en Proxmox
- [ ] `allowedTools` por host según necesidad
- [ ] Inventario sin passwords en texto plano
- [ ] Probar una operación destructiva con token manual
## CI y publicación
- **CI:** GitHub Actions ejecuta `typecheck` + `build` en cada push/PR a `main` ([`.github/workflows/ci.yml`](../.github/workflows/ci.yml))
- **Paquete npm:** [`@kreodevs/mcp-sysadmin`](https://github.com/kreodevs/mcp-sysadmin/pkgs/npm/mcp-sysadmin) en GitHub Packages — publicado al crear un **GitHub Release**
## Extensión
Para añadir otro proveedor (oVirt, VMware, AWS, etc.):
1. Añade el provider en `src/config/schema.ts`
2. Implementa cliente en `src/providers/<nombre>/client.ts`
3. Regístralo en `src/providers/registry.ts`
4. Expone tools en `src/tools/`
La estructura sigue el patrón del [cloudways-mcp-server](https://github.com/cgmorah/cloudways-mcp-server), pero con inventario multi-proveedor en lugar de una API única.
## Desarrollo
```bash
npm run typecheck
npm run build
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues