Skip to main content
Glama
thekk1
by thekk1

ssh-mcp

Сервер MCP, который позволяет LLM выполнять shell-команды по SSH, аутентифицируясь личным SSH-ключом каждого пользователя, а не единым общим служебным аккаунтом. Создан для многопользовательских чат-платформ (например, LibreChat), где сервер общий, но SSH-идентичность для каждого запроса общей быть не должна.

Один инструмент: ssh_exec(host, port, username, command). Никакого разрешённого списка хостов, никакого белого списка команд — почему и что это значит для тех, кто разворачивает этот сервер, см. в разделе «Модель безопасности» ниже.

Зачем это существует

Перед написанием этого сервера были изучены несколько существующих open-source SSH MCP-серверов (vignitin/multi-ssh-mcp, giuliolibrando/ssh-mcp-server, tufantunc/ssh-mcp). Ни один из них не поддерживает учётные данные для каждого запроса: каждый зашивает единственные хост/пользователя/учётные данные в переменные окружения или конфигурационный файл при запуске, что работает только для развёртывания с одним пользователем или с общим служебным аккаунтом. Ни один из них не подходит для сценария, где много разных людей, у каждого свой SSH-ключ, используют один запущенный MCP-сервер.

Поэтому это небольшой специализированный сервер на основе asyncssh, а не обёртка над существующим инструментом — оборачивать было нечего.

Related MCP server: terminal-mcp-server

Как это работает

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

Учётные данные передаются в HTTP-заголовках каждого запроса, а не в конфигурации сервера:

  • x-ssh-private-key — закрытый ключ для аутентификации, в кодировке base64 (сырой многострочный PEM-блок не может существовать как значение HTTP-заголовка)

  • x-ssh-key-passphrase — необязательно, если ключ защищён парольной фразой

Оба заголовка читаются заново при каждом вызове инструмента, декодируются, передаются напрямую в asyncssh и затем отбрасываются — ничего не записывается на диск и не кэшируется между запросами. Задача вызывающего клиента — прикрепить правильные заголовки для правильного пользователя; один из способов сделать это описан ниже в разделе «Использование с LibreChat».

Для ключей хостов используется настоящий trust-on-first-use (TOFU), а не «принимать что угодно и всегда»: при первом подключении к заданному host:port отпечаток его ключа закрепляется в JSON-файле на диске (hostkeys.py); каждое последующее подключение должно точно совпадать с этим закреплённым значением, иначе будет отклонено с ошибкой host_key_mismatch. Это не может предотвратить атаку «человек посередине» при самом первом контакте с хостом, но превращает последующее незаявленное изменение ключа — ротацию или реальную атаку — в громкий явный сбой, а не в тихую дыру.

На самом MCP-подключении нет ни API-ключа, ни bearer-токена. Это осознанный выбор в пользу простоты для конкретной формы развёртывания: сервер доступен только из доверенной внутренней сети, клиент сам прикрепляет SSH-учётные данные пользователя (см. ниже), и фактической границей доступа является размещение в сети. Если вы открываете доступ к этому серверу из менее доверенного окружения, поставьте перед ним барьер — в этом проекте его нет.

Модель безопасности

ssh_exec не фильтрует, какие хосты, команды или пользователи разрешены. Что бы вызывающая сторона ни передала в host/port/username/command, это будет выполнено, и точка. Это осознанный компромисс, а не упущение: фильтрация по хосту или команде внутри MCP-сервера была бы имитацией безопасности, поскольку любой вызывающий с валидным ключом может просто подключиться по SSH напрямую, в обход этого инструмента. Две вещи, которые на самом деле стоят между запросом и настоящей оболочкой:

  1. Тот, кто вообще может достучаться до этого сервера и задать заголовки с учётными данными — это полностью вне контроля данного кода. Если вы разворачиваете это за мультитенантным клиентом, ограничение того, какие из ваших пользователей вообще могут видеть/использовать этот инструмент, — задача этого клиента (один из конкретных способов описан в разделе «Использование с LibreChat»).

  2. Реальные права Unix, связанные с тем ключом, который используется. ssh_exec выполняется ровно с теми полномочиями, которые есть у целевой учётной записи этого ключа, — не больше и не меньше.

Если ни то ни другое в конкретном развёртывании реально не обеспечивается, этот инструмент ровно настолько же опасен, как выдача каждому вызывающему голого терминала на каждом хосте, до которого дотягивается его ключ. Это и есть задуманная модель — собственная авторизация SSH, а не её повторная реализация, — поэтому перед развёртыванием убедитесь, что вам действительно нужна такая модель.

Отсутствующие host/username: запрашиваются через MCP-элиситацию, а не угадываются моделью

host и username намеренно не входят в required-поля схемы инструмента (command остаётся обязательным — решать, что выполнять, задача модели, а не человека). Если бы host/username были обязательными, модель, соответствующая спецификации, отказалась бы даже вызывать инструмент без них и вместо этого сама импровизировала бы уточняющий вопрос в виде обычного текста — а именно этого UX данный подход избегает. Когда любого из них не хватает, elicit_missing_ssh_args() в ssh_mcp/app.py обращается напрямую к человеку, в одной объединённой форме, через MCP-элиситацию (elicitation/create, режим формы) — это не то, что модель должна формулировать сама, и не отдельные циклы запросов на каждое поле. port передаётся в той же форме заодно, заранее заполненный обычным значением по умолчанию (22) через default в схеме; его можно редактировать, но сам по себе он не повод прерывать запрос, если не задано только это поле.

В клиенте, не поддерживающем элиситацию, это безопасно деградирует. elicit_missing_ssh_args() проверяет объявленную возможность клиента (session.check_client_capability(...)) перед отправкой любого запроса и перехватывает любой сбой самого вызова; в любом случае происходит откат к простым ошибкам missing_host/missing_username, которые модель всё ещё может передать как текстовые вопросы, вместо того чтобы вызов инструмента завершился ошибкой или завис. Поддержка элиситации зависит от клиента: на момент написания несколько популярных MCP-клиентов (включая LibreChat) её ещё не реализовали, так что сейчас это в основном задел на будущее с прямой совместимостью. В неподдерживающих клиентах это ничего не стоит, а в любом клиенте, который позже добавит настоящую поддержку элиситации, активируется автоматически, без каких-либо изменений здесь.

Проверено с реальным ClientSession через внутрипроцессный транспорт mcp.shared.memory: с зарегистрированным elicitation_callback и без него, ветви accept/decline/cancel, а также полная объединённая форма (отсутствуют host и username, переопределено значение по умолчанию для port) — подтверждено, что все три значения реально доходят до SSH-вызова ровно в том виде, в котором были получены через элиситацию, а не просто выведены из чтения спецификации.

Использование с LibreChat

LibreChat умеет прикреплять значения для каждого пользователя к MCP-заголовкам запросов через customUserVars — каждый пользователь один раз вводит свой ключ в настройках, и LibreChat подставляет его в настроенный заголовок в каждом запросе этого пользователя. 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"

Обе записи customUserVars должны содержать и title, и description — запись только с title приводит к ошибке проверки конфигурации LibreChat при запуске с ZodError, которая, как это сбивает с толку, сообщается о полях, выглядящих несвязанными (LibreChat проверяет весь блок mcpServers как одно объединение типов транспорта, поэтому одно отсутствующее поле проявляется сразу как несколько, казалось бы, не связанных ошибок). librechat.yaml читается только при запуске контейнера — после его редактирования перезапустите LibreChat.

Ограничение круга пользователей, которым виден этот сервер

В этом проекте ничто не ограничивает круг тех, кто может им пользоваться, — любой пользователь, способный задать заголовок SSH_PRIVATE_KEY, может вызвать ssh_exec. Если вам нужно ограничить это подмножеством ваших пользователей, это должно делаться в LibreChat (или в любом другом используемом вами клиенте), а не здесь. Начиная с LibreChat 0.8.5+ в его админ-панели есть система переопределения конфигурации (Configuration Management), которая может ограничить дополнительную запись mcpServers конкретной ролью или группой — у пользователя вне этой группы в итоговой конфигурации вообще нет записи ssh, а не просто скрытой. Две вещи, которые стоит проверить в вашей версии LibreChat, прежде чем полагаться на это, а не предполагать:

  • На момент написания это документировано как «in preview», а не GA.

  • Известна история, когда переопределения, ограниченные группой, молча не применялись, в то время как ограниченные ролью применялись (danny-avila/LibreChat#13172). Подтвердите, что исправление присутствует в вашей запущенной версии, проверив напрямую: добавьте пользователя в группу или удалите из неё и посмотрите, действительно ли сервер для него появляется/исчезает.

Запуск

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

Том /data — это то, что позволяет закреплённым ключам хостов TOFU пережить пересоздание контейнера; без него каждое повторное развёртывание забывает все ранее виденные ключи хостов и закрепляет их заново при следующем контакте (это не дыра в безопасности, а лишь временная утрата свойства «обнаруживать последующие изменения», пока с каждым хостом не будет снова установлен контакт один раз).

Пример сервиса в docker-compose.yml со сборкой из локального клона:

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

volumes:
  ssh-mcp-hostkeys:

Проверка

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

Проверено вручную по полному циклу (не только юнит-тестами): собран образ, запущен, подключён реальный MCP-клиент по streamable-http с заголовками учётных данных, tools/list показал ssh_exec, tools/call против живого одноразового SSH-сервера на базе asyncssh выполнил реальную команду через реальное SSH-рукопожатие и вернул её фактический stdout. Также проверено напрямую (в обход HTTP-слоя) на том же одноразовом сервере: закрепление TOFU при первом контакте, принятие при совпадающем втором контакте, жёсткий отказ при изменённом/несовпадающем ключе хоста, мусорный закрытый ключ отклонён как invalid_key, неавторизованный ключ отклонён как connection_failed, а ненулевой код возврата удалённой команды проброшен как ok: true с этим кодом (не считается сбоем инструмента).

Тесты

pip install -e '.[dev]'
pytest

Юнит-тесты (разбор заголовков с учётными данными, логика закрепления/принятия/отклонения TOFU, схема инструмента, ветви elicit_missing_ssh_args для проверки возможностей/выбора полей/принятия/отклонения/отмены/сбоя на фиктивной сессии) — без реальной сети, подпроцессов или MCP-транспорта. Сценарии с реальным рукопожатием и реальные циклы элиситации с ClientSession (см. выше) запускались вручную и не входят в автоматизированный набор тестов.

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