Skip to main content
Glama
thekk1
by thekk1

ssh-mcp

Un servidor MCP que permite a un LLM ejecutar comandos de shell a través de SSH, autenticado con la clave SSH personal de cada usuario en lugar de una única cuenta de servicio compartida. Diseñado para plataformas de chat multiusuario (p. ej. LibreChat) donde el servidor es compartido pero la identidad SSH por solicitud no debería serlo.

Una sola herramienta: ssh_exec(host, port, username, command). Sin lista de hosts permitidos, sin lista blanca de comandos: consulte «Modelo de seguridad» más abajo para saber por qué y qué implica para cualquiera que despliegue esto.

Por qué existe

Antes de escribir este, se revisaron algunos servidores MCP SSH de código abierto existentes (vignitin/multi-ssh-mcp, giuliolibrando/ssh-mcp-server, tufantunc/ssh-mcp). Ninguno admite credenciales por solicitud: todos incorporan un único host/usuario/credencial en variables de entorno o en un archivo de configuración al arrancar, lo que solo funciona para un despliegue de un solo usuario o una cuenta de servicio compartida. Nada de eso encaja en un entorno donde muchas personas distintas, cada una con su propia clave SSH, comparten un mismo servidor MCP en ejecución.

Por eso este es un servidor pequeño y específico construido sobre asyncssh en lugar de un envoltorio alrededor de una herramienta existente: no había nada adecuado que envolver.

Related MCP server: terminal-mcp-server

Cómo funciona

MCP client --(streamable-http, /mcp, per-request headers)--> ssh-mcp
                                                                  |
                                                                  | asyncssh,
                                                                  | one connection
                                                                  | per tool call
                                                                  v
                                                            arbitrary target host

Las credenciales viajan como cabeceras HTTP por solicitud, no como configuración del servidor:

  • x-ssh-private-key — la clave privada con la que autenticarse, codificada en base64 (un bloque PEM sin procesar de varias líneas no puede sobrevivir como valor de cabecera HTTP)

  • x-ssh-key-passphrase — opcional, si esa clave está protegida por frase de contraseña

Ambas se vuelven a leer en cada llamada a la herramienta, se decodifican, se entregan directamente a asyncssh y luego se descartan: no se escribe nada en disco y no se guarda nada en caché entre solicitudes. Es responsabilidad del cliente que llama adjuntar las cabeceras correctas para el usuario correcto; consulte «Uso con LibreChat» más abajo para ver una forma de hacerlo.

Las claves de host utilizan auténtica confianza en el primer uso (TOFU), no «aceptar cualquier cosa, siempre»: la primera conexión a un host:port determinado fija la huella de su clave en un archivo JSON en disco (hostkeys.py); cada conexión posterior debe coincidir exactamente con esa fijación o se rechaza con host_key_mismatch. Esto no puede detener un ataque de intermediario en el primer contacto con un host, pero convierte un cambio de clave no anunciado posterior —rotación o ataque real— en un fallo ruidoso y explícito en lugar de un agujero silencioso.

No hay ninguna puerta de clave de API ni de token de portador en la propia conexión MCP. Es una elección deliberada de simplicidad para una forma de despliegue concreta: un servidor accesible solo desde una red interna de confianza, donde el cliente adjunta él mismo las credenciales SSH por usuario (ver más abajo) y la ubicación en la red es la frontera de acceso real. Si va a exponer esto en un lugar menos confiable, póngale una puerta delante: este proyecto no incluye una.

Modelo de seguridad

ssh_exec no filtra qué hosts, comandos o usuarios están permitidos. Todo host/puerto/usuario/comando que pase un llamante se intenta, y punto. Es una compensación deliberada, no un descuido: filtrar por host o comando desde dentro del servidor MCP sería teatro de seguridad, ya que cualquier llamante con una clave válida puede simplemente hacer SSH allí directamente también fuera de esta herramienta. Las dos cosas que realmente se interponen entre una solicitud y un shell real son:

  1. Quienquiera que pueda alcanzar este servidor y establecer las cabeceras de credenciales — completamente fuera del control de este código. Si está desplegando esto detrás de un cliente multiinquilino, restringir qué usuarios pueden siquiera ver/usar esta herramienta es trabajo de ese cliente (consulte «Uso con LibreChat» para una forma concreta de hacerlo).

  2. Los permisos Unix reales asociados a la clave que se utilice. ssh_exec se ejecuta con exactamente la autoridad que tiene la cuenta objetivo de esa clave: ni más, ni menos.

Si ninguna de esas dos cosas se aplica realmente en un despliegue determinado, esta herramienta es exactamente tan peligrosa como entregar a cada llamante una terminal desnuda en cada host al que su clave pueda llegar. Ese es el modelo previsto: la propia autorización de SSH, no una reimplementación de la misma. Así que asegúrese de que es un modelo que realmente desea antes de desplegar esto.

Host/usuario faltantes: se solicitan mediante la elicitación de MCP, no los adivina el modelo

host y username están deliberadamente fuera de los campos required del esquema de la herramienta (command sigue siendo obligatorio: decidir qué ejecutar es trabajo del modelo, no de un humano). Hacer que host/username sean obligatorios haría que un modelo conforme a la especificación se negara siquiera a llamar a la herramienta sin ellos y, en su lugar, improvisara él mismo una pregunta de seguimiento en texto plano, que es exactamente la UX que esto evita. Cuando falta cualquiera de los dos, elicit_missing_ssh_args() en ssh_mcp/app.py pregunta directamente al humano, en un único formulario combinado, mediante la elicitación de MCP (elicitation/create, modo formulario): no es algo que el modelo tenga que redactar por sí mismo, ni idas y vueltas separadas por campo. port viaja en ese mismo formulario, pre-rellenado con su valor predeterminado habitual (22) mediante el default del esquema, editable pero no es en sí mismo un motivo para interrumpir cuando es lo único que no se ha establecido.

Esto degrada con seguridad en un cliente que no admite la elicitación. elicit_missing_ssh_args() comprueba la capacidad declarada del cliente (session.check_client_capability(...)) antes de enviar siquiera una solicitud, y captura cualquier fallo de la propia llamada; en cualquier caso, recurre a errores simples missing_host/missing_username que el modelo aún puede transmitir como preguntas de texto, en lugar de que la llamada a la herramienta falle o se cuelgue. El soporte de elicitación varía según el cliente: en el momento de escribir esto, varios clientes MCP populares (incluido LibreChat) aún no lo implementan, por lo que hoy actúa sobre todo como trabajo preparatorio compatible con el futuro. No cuesta nada cuando no se admite y se activa automáticamente en cualquier cliente que añada soporte real de elicitación más adelante, sin necesidad de cambios aquí.

Verificado con un ClientSession real a través del transporte en memoria de mcp.shared.memory: con y sin un elicitation_callback registrado, las ramas de aceptar/rechazar/cancelar, y un formulario combinado completo (host + username faltantes, con el valor predeterminado de port sobrescrito) — se confirmó que los tres valores llegan realmente a la llamada SSH exactamente como se elicitaron, no solo se afirmó a partir de la lectura de la especificación.

Uso con LibreChat

LibreChat puede adjuntar valores por usuario a las cabeceras de las solicitudes MCP mediante customUserVars: cada usuario introduce su propia clave una vez en Ajustes, y LibreChat la inyecta en la cabecera configurada en cada solicitud de ese usuario. librechat.yaml:

mcpServers:
  ssh:
    type: streamable-http
    url: http://ssh-mcp:8080/mcp
    serverInstructions: true
    headers:
      X-SSH-Private-Key: '{{SSH_PRIVATE_KEY}}'
      X-SSH-Key-Passphrase: '{{SSH_KEY_PASSPHRASE}}'
    customUserVars:
      SSH_PRIVATE_KEY:
        title: "SSH Private Key (Base64)"
        description: "Your personal SSH private key, base64-encoded: `base64 -w0 ~/.ssh/id_ed25519`"
      SSH_KEY_PASSPHRASE:
        title: "SSH Key Passphrase (optional)"
        description: "Only fill in if your private key is passphrase-protected"

Ambas entradas de customUserVars necesitan tanto title como description: una entrada solo con title hace fallar la validación de configuración de LibreChat al arrancar con un ZodError que, de forma confusa, se notifica contra campos que parecen no relacionados (LibreChat valida todo el bloque mcpServers como una única unión de tipos de transporte, por lo que un campo faltante se manifiesta como varios errores aparentemente no relacionados a la vez). librechat.yaml solo se lee al arrancar el contenedor: reinicie LibreChat después de editarlo.

Restringir qué usuarios pueden ver este servidor

Nada en este proyecto restringe quién puede usarlo: cualquier usuario que pueda establecer la cabecera SSH_PRIVATE_KEY puede llamar a ssh_exec. Si necesita limitar eso a un subconjunto de sus usuarios, eso tiene que ocurrir en LibreChat (o en el cliente que esté usando), no aquí. A partir de LibreChat 0.8.5+, su panel de administración tiene un sistema de anulación de configuración (Configuration Management) que puede limitar una entrada adicional de mcpServers a un rol o grupo específico: un usuario fuera de ese grupo no tiene ninguna entrada ssh en su configuración resuelta, no solo una oculta. Dos cosas que vale la pena comprobar contra su propia versión de LibreChat antes de confiar en esto, en lugar de asumir:

  • Está documentado como «en vista previa», no como GA, al momento de escribir esto.

  • Hay un historial conocido de anulaciones con ámbito de grupo que no se aplicaban silenciosamente mientras que las de ámbito de rol sí lo hacían (danny-avila/LibreChat#13172). Confirme que la corrección está en su versión en ejecución probando directamente: ponga a un usuario dentro/fuera del grupo y compruebe si el servidor realmente (des)aparece para él.

Ejecución

docker build -t ssh-mcp .
docker run --rm -p 8080:8080 -v ssh-mcp-hostkeys:/data ssh-mcp

El volumen /data es lo que hace que las fijaciones de claves de host TOFU sobrevivan a una recreación del contenedor: sin él, cada redespliegue olvida toda clave de host vista anteriormente y vuelve a fijarla en el siguiente contacto (no es un agujero de seguridad, solo una pérdida temporal de la propiedad de «detectar un cambio posterior» hasta que cada host haya sido contactado de nuevo una vez).

Ejemplo de servicio docker-compose.yml, construyendo desde un clon local:

services:
  ssh-mcp:
    build: .
    container_name: ssh-mcp
    volumes:
      - ssh-mcp-hostkeys:/data
    restart: always

volumes:
  ssh-mcp-hostkeys:

Verificación

curl -s http://127.0.0.1:8080/readyz   # "ok" once the session manager is up

Verificado de extremo a extremo manualmente (no solo con pruebas unitarias): se construyó la imagen, se ejecutó, se conectó un cliente MCP real a través de streamable-http con las cabeceras de credenciales, tools/list mostró ssh_exec, tools/call contra un servidor SSH desechable basado en asyncssh ejecutó un comando real mediante un handshake SSH real y devolvió su stdout real. También se probó directamente (omitiendo la capa HTTP) contra ese mismo servidor desechable: fijación TOFU en el primer contacto, aceptación en un segundo contacto coincidente, rechazo contundente ante una clave de host cambiada/no coincidente, una clave privada basura rechazada como invalid_key, una clave no autorizada rechazada como connection_failed, y un código de salida remoto distinto de cero transmitido como ok: true con ese código de salida (no tratado como un fallo de la herramienta).

Pruebas

pip install -e '.[dev]'
pytest

Pruebas unitarias (análisis de cabeceras de credenciales, lógica de fijación/aceptación/rechazo TOFU, esquema de la herramienta, ramas de comprobación de capacidad/selección de campos/aceptación/rechazo/cancelación/fallo de elicit_missing_ssh_args contra una sesión falsa) — sin red real, subprocesos ni transporte MCP. Los escenarios de handshake real y los viajes de ida y vuelta de elicitación con un ClientSession real (ver arriba) se ejecutaron manualmente, no como parte de la suite automatizada.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables secure SSH connections to remote servers for executing shell commands and managing active sessions. It supports authentication via passwords or private keys and provides optional host-based access control.
    4
    207
    MIT

View all related MCP servers

Related MCP Connectors

  • Let AI operate servers without SSH. Choose actions, approve risky changes, and audit every step.

  • Issue, rotate and revoke scoped API-key passes for 25+ providers — the agent never sees a real key

  • Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.

View all MCP Connectors

Latest Blog Posts

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/thekk1/ssh-mcp'

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