ssh-deploy-mcp
v1.4.1
Published
Stateless MCP server to connect to a VPS over SSH and run commands.
Maintainers
Readme
vps-mcp
Servidor MCP stateless para conectarte a cualquier VPS por SSH usando tus propias credenciales y operar sobre él: ejecutar comandos, subir/bajar archivos y leer/escribir ficheros remotos.
Corre en local por stdio. No mantiene sesión ni guarda credenciales: cada llamada abre la conexión SSH, hace el trabajo y la cierra. El servidor no contiene datos de ningún VPS — las credenciales las pone cada usuario en la config de su propio cliente MCP.
Tools expuestas
| Tool | Qué hace |
|------|----------|
| ssh_test_connection | Verifica conectividad y autenticación (devuelve user + hostname). |
| ssh_exec | Ejecuta un comando shell y devuelve stdout, stderr y code. |
| ssh_read_file | Lee un archivo remoto (con límite de tamaño, por defecto 1 MiB). |
| ssh_upload_file | Sube un archivo local al VPS por SFTP (deploy de artefactos). |
| ssh_download_file | Descarga un archivo remoto a tu disco local (backups, binarios, sin límite). |
| ssh_write_file | Escribe texto directo a un archivo remoto (configs, .env, scripts). |
| ssh_task_start | Lanza un comando largo como task en background (sobrevive al cierre de la conexión). |
| ssh_task_status | Estado del task: running / finished(+exit) / stopped / crashed, con tail de salida. |
| ssh_task_logs | Devuelve la salida completa (o tail) de un task. |
| ssh_task_stop | Termina un task (SIGTERM→SIGKILL al grupo), con remove opcional. |
| ssh_task_list | Lista los tasks y su estado. |
Las credenciales se definen una vez por variables de entorno en la config del
cliente MCP. Cualquier campo se puede sobreescribir por llamada (host, port,
username, password, privateKeyPath, passphrase, timeoutMs).
Requisitos
- Node.js ≥ 18
- Un cliente MCP (Claude Code, Claude Desktop, etc.)
- Acceso SSH a un VPS (password o clave privada)
Instalación
git clone https://github.com/MauricioPerera/vps-mcp.git
cd vps-mcp
npm installAnotá la ruta absoluta a index.js (la necesitás para la config). En estos
ejemplos se usa /ruta/a/vps-mcp/index.js.
Configuración en Claude Code
Añadir el servidor con las credenciales como env vars:
claude mcp add vps --scope user \
--env VPS_HOST=<TU_IP> --env VPS_USER=root --env VPS_PASSWORD="<TU_PASSWORD>" \
-- node /ruta/a/vps-mcp/index.jsWindows: usá barras normales en la ruta (
C:/ruta/a/vps-mcp/index.js); el shell de PowerShell se come los\al pasarlos como argumento.
O manualmente en el JSON de config MCP:
{
"mcpServers": {
"vps": {
"command": "node",
"args": ["/ruta/a/vps-mcp/index.js"],
"env": {
"VPS_HOST": "<TU_IP>",
"VPS_PORT": "22",
"VPS_USER": "root",
"VPS_PASSWORD": "<TU_PASSWORD>"
}
}
}
}Autenticación por clave (recomendado sobre password)
"env": {
"VPS_HOST": "<TU_IP>",
"VPS_USER": "root",
"VPS_KEY_PATH": "C:\\Users\\<usuario>\\.ssh\\id_ed25519",
"VPS_KEY_PASSPHRASE": "opcional"
}Variables de entorno
| Variable | Default | Descripción |
|----------|---------|-------------|
| VPS_HOST | — | IP o host del VPS (obligatorio). |
| VPS_PORT | 22 | Puerto SSH. |
| VPS_USER | root | Usuario SSH. |
| VPS_PASSWORD | — | Password (si no usás clave). |
| VPS_KEY_PATH | — | Ruta a la clave privada (tiene prioridad sobre password). |
| VPS_KEY_PASSPHRASE | — | Passphrase de la clave, si tiene. |
| VPS_TIMEOUT_MS | 60000 | Timeout por comando (no aplica a tasks en background). |
| VPS_TASK_DIR | /tmp/vps-mcp-tasks | Dir remoto donde los tasks guardan estado/salida. |
Prueba rápida (sin cliente MCP)
El script smoke-client.js levanta el server por stdio y llama a
ssh_test_connection + ssh_exec. Con --write prueba además la escritura y
ejecución de un script remoto (y limpia al terminar).
PowerShell (Windows):
$env:VPS_HOST="<TU_IP>"; $env:VPS_USER="root"; $env:VPS_PASSWORD="<TU_PASSWORD>"
node smoke-client.js # solo lectura
node smoke-client.js --write # incluye write + exec en /tmpBash (Linux/macOS):
VPS_HOST=<TU_IP> VPS_USER=root VPS_PASSWORD="<TU_PASSWORD>" node smoke-client.jsTests
Tests unitarios de la lógica pura (sin red, sin VPS), con el runner nativo de Node:
npm test # node --check de index.js/lib.js + node --test (test/)
npm run smoke # smoke-client.js contra un VPS real (requiere VPS_* en el entorno)Arquitectura
MCP client (Claude Code/Desktop)
│ stdio (JSON-RPC)
▼
index.js ── por cada tool call ──► ssh2.Client.connect()
(stateless) exec / SFTP (get·put·read·write)
▲ conn.end() ◄── se cierra siempre
│
tasks: el comando corre detached EN EL VPS (setsid/nohup); el estado
vive en VPS_TASK_DIR, no en este server. Por eso sobrevive entre llamadas.
│
credenciales: env VPS_* (override por llamada)- Sin estado entre llamadas: no hay pool ni sesión persistente; cada tool abre y cierra su propia conexión. Robusto frente a sesiones colgadas.
- stdout reservado para el canal MCP; los logs van a stderr.
- Auth: si
VPS_KEY_PATHestá presente tiene prioridad sobreVPS_PASSWORD.
Estructura del proyecto
vps-mcp/
├── index.js # Entry del MCP: transporte stdio, sshExec/sshSftp y las 11 tools
├── lib.js # Helpers puros (sin red): buildConnConfig, shQuote,
│ # parseMetaBlock, assertTaskId, resolveTaskState
├── smoke-client.js # Smoke test manual end-to-end contra un VPS real (npm run smoke)
├── test/
│ └── lib.test.js # Tests unitarios de lib.js (node:test, sin red) — npm test
├── package.json
├── CHANGELOG.md
├── LICENSE # MIT
└── README.mdSeparación clave: la lógica pura vive en lib.js (testeable sin VPS); index.js
solo orquesta SSH/SFTP y registra tools, y únicamente arranca el server cuando se
ejecuta directamente (importarlo no abre stdio).
Detalle de cada tool
ssh_test_connection
Sin argumentos obligatorios. Ejecuta echo OK; id -un; hostname y devuelve
{ connected, code, stdout, stderr, host }.
ssh_exec
command(string, obligatorio): comando shell a ejecutar.- Devuelve
{ code, signal, stdout, stderr, host }.
ssh_read_file
path(string, obligatorio): ruta absoluta remota.maxBytes(int, opcional, default1048576): corta la lectura para no inundar el contexto; marcatruncated: truesi el archivo es mayor.
ssh_upload_file
localPath(string, obligatorio): ruta local del archivo a subir.remotePath(string, obligatorio): destino remoto. Si termina en/, se añade el nombre del archivo local.mode(string, opcional): permisos octales, ej.'0755'para un ejecutable.mkdirp(bool, opcional, defaulttrue): crea el directorio remoto si falta.
ssh_download_file
remotePath(string, obligatorio): ruta remota del archivo a descargar.localPath(string, obligatorio): destino local. Si termina en/o\, se añade el nombre del archivo remoto.mkdirp(bool, opcional, defaulttrue): crea el directorio local si falta.- Maneja binarios y archivos grandes (sin límite de tamaño), a diferencia de
ssh_read_file.
ssh_write_file
remotePath(string, obligatorio): ruta remota del archivo.content(string, obligatorio): contenido de texto a escribir.mode(string, opcional): permisos octales, ej.'0644'.append(bool, opcional, defaultfalse): añade en vez de sobrescribir.mkdirp(bool, opcional, defaulttrue): crea el directorio remoto si falta.
Tasks en background (comandos largos)
Para comandos que pueden superar el timeout de la llamada (builds, apt upgrade,
deploys, backups), usá tasks en vez de ssh_exec. El comando se lanza detached
en el VPS (sobrevive al cierre de la conexión SSH); su salida y exit code se
guardan en VPS_TASK_DIR. Después se consulta con llamadas independientes.
ssh_task_start—command(obligatorio). Devuelve{ taskId, pid }.ssh_task_status—taskId;lines(tail, default 40). Devuelvestate,exitCode,outputTail. Estados:running,finished(conexitCode),stopped(matado conssh_task_stop) ycrashed(murió solo: kill externo, OOM, reboot).ssh_task_logs—taskId;lines(default 200) ofull=true(+maxBytes).ssh_task_stop—taskId;remove(borra el dir del task, default false).ssh_task_list— lista todos los tasks y su estado.
Todas las task tools aceptan taskDir para sobreescribir el directorio remoto.
Todas las tools aceptan además los overrides opcionales: host, port,
username, password, privateKeyPath, passphrase, timeoutMs.
Ejemplo: comando largo en background
1) ssh_task_start command="apt-get update && apt-get -y upgrade" → { taskId: task-ab12cd34 }
2) ssh_task_status taskId=task-ab12cd34 → state=running, outputTail=...
(repetir hasta state=finished)
3) ssh_task_logs taskId=task-ab12cd34 full=true
4) ssh_task_stop taskId=task-ab12cd34 remove=true (opcional, para limpiar)Ejemplo: deploy en 2 pasos
1) ssh_upload_file localPath=./dist/app.tar.gz remotePath=/opt/app/ mode=0644
2) ssh_exec command="cd /opt/app && tar xzf app.tar.gz && systemctl restart app"Backup remoto → disco local:
1) ssh_exec command="pg_dump mydb | gzip > /tmp/db.sql.gz"
2) ssh_download_file remotePath=/tmp/db.sql.gz localPath=D:/backups/O config + script sin archivo local:
1) ssh_write_file remotePath=/opt/app/.env content="NODE_ENV=production\nPORT=3000"
2) ssh_write_file remotePath=/opt/app/deploy.sh content="#!/bin/sh\n..." mode=0755
3) ssh_exec command=/opt/app/deploy.shSeguridad
Esto es shell remoto completo. ssh_exec y ssh_task_start ejecutan
comandos arbitrarios en el VPS, por defecto como root (VPS_USER). Cualquier
cliente que hable con este MCP — es decir, el LLM al que se lo conectás —
obtiene control total del servidor: leer/borrar archivos, instalar software,
parar servicios. Tratá la conexión como darle una shell root a ese agente.
Mitigaciones recomendadas:
- Usuario no-root con
sudoacotado en vez deroot, si tu caso lo permite. - Clave SSH en lugar de password (
VPS_KEY_PATH); el password queda en texto plano en la config del MCP. - Restringí permisos del archivo de config del cliente MCP (contiene las credenciales).
- Conectá este MCP solo a agentes/sesiones en los que confiás.
Compatibilidad del remoto
El servidor asume un VPS GNU/Linux típico. El runner de tasks y algunas
tools usan utilidades GNU: setsid, nohup, base64 -d, mktemp -d TEMPLATE,
pkill -P, kill -- -PGID, stat. En remotos BusyBox (Alpine) o BSD
algunos de estos cambian de flags o no existen, y los tasks pueden no
comportarse igual. Probado contra Ubuntu/Debian.
