ssh-mcp-server
MCP server that enables AI agents to execute commands on remote machines via SSH with secure credential handling, multiple host support, and configurable security guards.
README
<div align="center">
ssh-mcp-server
MCP-сервер поверх SSH: агент выполняет команды на удалённых машинах, а ключи, пароли и sudo остаются на нашей стороне.
Форк classfang/ssh-mcp-server под ISC.
Русский | English
</div>
Что это
ssh-mcp-server это мост между MCP-клиентом (Claude Code, Cursor, Cline) и SSH. Агент вызывает инструменты, сервер подключается к машине и возвращает вывод. Модель не видит ни приватного ключа, ни пароля, ни пароля sudo: всё это читается из локального конфига и переменных окружения процесса.
Один сервер обслуживает любое число хостов. Хост выбирается на лету по алиасу из ~/.ssh/config, поэтому в конфиг MCP-клиента не нужно вписывать каждую машину.
Что добавлено в этом форке
| Возможность | Зачем |
|---|---|
Хосты из ~/.ssh/config на лету |
Один MCP на весь парк. Алиас передаётся в connectionName, соединение поднимается при первом обращении |
| ProxyJump | Хост за бастионом доступен по алиасу, цепочка ProxyJump разбирается из SSH-конфига |
| sudo из переменной окружения | Агент просит sudo: true, пароль подставляет сервер и вырезает его из вывода |
| Запретное ядро | Список операций, которые не выполняются никогда: ни под sudo, ни в любом профиле, ни через SFTP |
| Профили гвардов | Готовый набор запретов safe и белый список readonly, версионированный и обновляемый |
| Туннели | SOCKS5 (аналог ssh -D) и проброс порта (аналог ssh -L) на произвольный локальный порт |
| Проверка хост-ключей | known_hosts сверяется по умолчанию, ключ не из списка означает отказ подключаться |
| Современная криптография | Ed25519 первым в списке, без SHA-1, CBC и DSA |
| Аудит-лог | Каждый вызов пишется в JSONL с ротацией и gzip-архивами |
| Без загрузки файлов | upload по умолчанию не публикуется: файл, который гварды не прочитают, это способ занести код на хост |
Инструменты
| Инструмент | Что делает |
|---|---|
execute-command |
Выполняет команду, умеет sudo и произвольный connectionName |
download |
Забирает файл с сервера |
list-servers |
Показывает настроенные соединения, их статус и активный профиль гвардов |
list-ssh-hosts |
Показывает алиасы из SSH-конфига, доступные как connectionName |
open-tunnel |
Поднимает SOCKS5-прокси или проброс порта через соединение |
close-tunnel |
Закрывает туннель |
list-tunnels |
Показывает открытые туннели и счётчики соединений |
list-ssh-hosts появляется только с флагом --ssh-config-hosts, туннельные инструменты убираются флагом --disable-tunnels. Инструмента upload в списке нет: он публикуется только с флагом --enable-upload.
Факты о хосте без лишних команд
При подключении сервер один раз снимает состояние машины: имя, адреса, ОС, ядро, аптайм, диск, память, число процессов. Все пробы склеены маркерами в одну команду, так что это один заход по ssh, а не шесть.
Сервер держит результат в кеше и отдаёт его в list-servers:
[connected] prod-1 | deploy@10.0.0.5:22 | hostname=prod-1 | os=Linux | updated=2026-08-19T18:14:23Z
Raw JSON:
[{"name":"prod-1","connected":true,"guards":"guards=safe ruleset=2026.08.19 ...",
"status":{"reachable":true,"osVersion":"Ubuntu 24.04.1 LTS","kernelVersion":"6.8.0-51-generic",
"uptime":"12 days","diskSpace":{"free":"9.8G","total":"229.6G"},
"memory":{"free":"5.6G","total":"15.5G"},"processes":{"running":214}}}]
То есть uname -a, df -h, free -h и uptime спрашивать не надо, ответы уже пришли. Агент вызывает list-servers один раз и читает статус оттуда.
Пробы проходят через гварды по одной. С белым списком в статусе останутся только разрешённые поля. Неполный статус не значит, что хост недоступен.
Успешная команда без вывода возвращает [exit code] 0, а не пустую строку. Пустой ответ модель читает как непонятный исход и идёт перепроверять через echo $?, а это лишний заход на сервер и лишние токены.
Быстрый старт: один сервер на весь парк
Конфиг MCP-клиента:
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"-y",
"@perhamm/ssh-mcp-server",
"--ssh-config-hosts",
"--guards-profile", "safe"
],
"env": {
"SSH_MCP_SUDO_PASSWORD": "..."
}
}
}
}
Дальше агент работает так:
- Вызывает
list-ssh-hostsи находит нужный алиас, напримерprod-master. У больших конфигов список режется, поэтому агент передаётfilter: подстроку или шаблон видаprod-*. - Вызывает
execute-commandсconnectionName: "prod-master". - Сервер читает алиас из
~/.ssh/config, берёт оттудаHostName,User,Port,IdentityFileиProxyJump, поднимает соединение и выполняет команду.
Ключ при этом не покидает машину: сервер читает файл сам, в диалог попадает только путь из SSH-конфига. Если IdentityFile не указан, берётся ssh-agent из SSH_AUTH_SOCK. Алиас без HostName подключается по собственному имени, как это делает ssh.
Достижим только тот алиас, который объявлен в конфиге отдельным блоком Host. Блок Host * даёт умолчания, но не превращает произвольное имя в достижимый хост.
Список алиасов можно сузить:
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--ssh-config-hosts",
"--allowed-hosts", "prod-*,*-stage-*",
"--ssh-config-file", "/home/user/.ssh/config_work"
]
Шаблоны поддерживают * и ?. Если алиас не подходит ни под один шаблон, соединение не поднимается, а агент получает ошибку SSH_HOST_NOT_ALLOWED.
Безопасный режим и гварды
Гварды это версионированный набор правил, который проверяет каждую команду до отправки на сервер. Правила лежат в guards/default-guards.json и обновляются вместе с репозиторием.
Профили
| Профиль | Поведение |
|---|---|
off |
Правила профиля выключены, работает только запретное ядро. Значение по умолчанию |
safe |
Плюс запрет разрушительных команд, остальное разрешает |
readonly |
Разрешает только чтение и диагностику, наследует все запреты safe |
"args": ["-y", "@perhamm/ssh-mcp-server", "--ssh-config-hosts", "--guards-profile", "safe"]
Что ловит safe сверх ядра: shutdown и reboot, сброс firewall, остановку sshd и kubelet, kubectl delete, helm uninstall, docker system prune, удаление пакетов, DROP DATABASE, curl | sh, git push --force, чистку логов, выгрузку модулей ядра, интерактивные редакторы. Полный список с причинами в JSON.
readonly дополнительно требует, чтобы каждая часть команды попадала в белый список: ls, cat, grep, find, ps, ss, df, journalctl, systemctl status, kubectl get/describe/logs, docker ps/logs и подобное. sudo в этом профиле запрещён целиком, вместе с su, doas и pkexec.
Запретное ядро
Часть операций не выполняется никогда: ни в профиле off, ни под sudo, ни через свой файл гвардов, ни через SFTP в обход команд. Список живёт в блоке forbidden.
| Категория | Что закрыто |
|---|---|
| Учётные записи | useradd, usermod, userdel, groupadd, passwd, chpasswd, chage, vipw и запись в /etc/passwd, /etc/shadow, /etc/group |
| sudo | Запись в /etc/sudoers и /etc/sudoers.d, visudo |
| Расписания | crontab кроме crontab -l, запись в /etc/cron*, /var/spool/cron, /etc/anacrontab, команды at и batch |
| systemd | Запись юнитов и таймеров в /etc/systemd, /lib/systemd, /usr/lib/systemd, systemctl edit, systemd-run |
| SSH | Правка /etc/ssh/*, ~/.ssh/*, authorized_keys, sshd_config, а также ssh-keygen, ssh-copy-id, ssh-add |
| Интерпретаторы | python, perl, ruby, node, php, lua, Rscript и запуск скрипта файлом: bash /tmp/x.sh, sh -s, source. Гварды не читают чужой код, поэтому такой запуск закрыт целиком |
| Массовое удаление | rm -r каталога первого уровня или системного подкаталога, rm -r по маске, find -delete, удаление через xargs rm, флаг --no-preserve-root |
| Диски и секреты | mkfs, wipefs, dd of=/dev/, запись в /dev/sd*, форк-бомба, чтение /etc/shadow и приватных ключей |
Обычная работа при этом остаётся: crontab -l, cat /etc/ssh/sshd_config, systemctl restart nginx, rm -rf /var/lib/myapp/cache/tmp проходят. Разбираемый bash -c "..." тоже работает: его содержимое проверяется теми же правилами.
Ядро закрывает и файловые инструменты. download не заберёт /etc/shadow и содержимое ~/.ssh, и allowedRemotePaths тут ничего не разрешает обратно. Локальная сторона тоже под защитой: download не положит файл в наш собственный ~/.ssh.
Загрузка файлов выключена совсем. Инструмент upload не публикуется, пока не передан --enable-upload, а профиль readonly отклоняет загрузку и с этим флагом.
Если сервер нужен именно для заведения пользователей или правки крона, ядро придётся править в форке осознанно: флага, который его снимает, нет.
Почему это не обходится точкой с запятой
Команда разбирается на части по ;, |, &&, ||, &, переводу строки и подстановкам $(...), и каждая часть проверяется отдельно. Кавычки при разборе учитываются. Так ls; rm -rf / не проходит ни в одном профиле, хотя целиком строка начинается с разрешённого ls.
Обёртки снимаются перед проверкой: sudo, env, timeout 5, nohup и присваивания вида LC_ALL=C не прячут команду от правил. Скрипт внутри bash -c "..." разбирается отдельно и проверяется теми же правилами. Длина команды ограничена 5000 символами.
Гварды закрывают ошибки агента, а не намеренный обход. Интерпретатор с произвольным кодом внутри, вроде python -c, правила не разберут. Там, где обход недопустим, ограничиваем права самого пользователя SSH.
Обновление правил
Три способа держать правила свежими:
- Мержим апстрим в свой форк. Файл правил версионирован полем
version, версия видна вlist-serversи в тексте отказа. - Держим свой файл и указываем его через
--guards-file /etc/ssh-mcp/guards.json. Правила из него добавляются к встроенным, версия становится2026.08.19+local-1. - Обновляем файл по расписанию:
node scripts/update-guards.js https://example.com/guards.json /etc/ssh-mcp/guards.json
Скрипт проверяет JSON и компилирует каждое регулярное выражение и только потом заменяет файл. Битая загрузка не ломает работающий набор правил.
Формат своего файла:
{
"version": "local-1",
"profiles": {
"safe": {
"deny": [
{ "id": "no-ansible", "pattern": "^ansible-playbook\\b", "reason": "выкат идёт из CI" }
]
}
}
}
Поле scope: "command" заставляет правило проверять команду целиком, а не по частям. Так работают правила про curl | sh и SQL. В блок forbidden своего файла можно дописать свои запреты, но встроенные из него не убрать: списки складываются.
Старые --whitelist и --blacklist никуда не делись и проверяются до гвардов.
sudo без пароля в диалоге
Пароль sudo лежит в переменной окружения процесса сервера. Агент передаёт sudo: true, но самого пароля не видит ни в аргументах вызова, ни в выводе.
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": ["-y", "@perhamm/ssh-mcp-server", "--ssh-config-hosts", "--guards-profile", "safe"],
"env": {
"SSH_MCP_SUDO_PASSWORD": "..."
}
}
}
}
Вызов инструмента:
{
"tool": "execute-command",
"params": {
"cmdString": "systemctl restart nginx",
"connectionName": "prod-master",
"sudo": true
}
}
Команда уходит на сервер как sudo -S -k -p '' -u root -- /bin/sh -c '<команда>', пароль пишется в stdin канала. В командной строке его нет, поэтому он не попадает в ps и в историю. Псевдотерминал для таких команд отключается, иначе tty отразил бы ввод обратно в вывод. На всякий случай пароль вырезается из вывода и из текста ошибок.
Имя переменной и целевого пользователя меняются флагами --sudo-password-env и --sudo-user. Если переменная пустая, вызов падает с SUDO_PASSWORD_MISSING ещё до подключения. Профиль readonly запрещает sudo вообще.
В режиме shell пароль дописывается отдельной строкой сразу за командой, так как sudo читает тот же stdin, что и сам shell. Флаг -k гарантирует, что запрос пароля будет всегда и строка не выполнится как команда. Для sudo лучше режим exec.
Туннели
open-tunnel поднимает локальный слушатель и гонит трафик через SSH-соединение. Полезно, когда до сервисов кластера нет прямого доступа, а до узла есть.
SOCKS5 на порту 8777:
{
"tool": "open-tunnel",
"params": {
"type": "socks5",
"localPort": 8777,
"connectionName": "prod-master"
}
}
Дальше любой клиент ходит через прокси, имена резолвятся на удалённой стороне:
curl --socks5-hostname 127.0.0.1:8777 http://prometheus.monitoring.svc:9090/api/v1/query?query=up
kubectl --request-timeout=30s ... # через HTTPS_PROXY=socks5h://127.0.0.1:8777
Проброс одного порта, аналог ssh -L:
{
"tool": "open-tunnel",
"params": {
"type": "local",
"localPort": 15432,
"remoteHost": "pg-master.internal",
"remotePort": 5432
}
}
Если localPort не указан, порт выбирает система и возвращает его в ответе. Туннели живут до close-tunnel, до обрыва SSH-соединения или до остановки сервера.
Ограничения задаются флагами:
| Флаг | Значение по умолчанию | Что делает |
|---|---|---|
--tunnel-bind-address |
127.0.0.1 |
Адрес, на котором слушают туннели |
--allowed-tunnel-ports |
нет ограничений | Список портов, которые разрешено занимать |
--max-tunnels |
8 | Сколько туннелей держим одновременно |
--disable-tunnels |
выключено | Убирает туннельные инструменты из списка |
Слушатель по умолчанию поднимается на loopback. Адрес шире loopback открывает прокси в вашу сеть, поэтому меняем его осознанно.
Проверка хост-ключей
Ключ сервера сверяется с known_hosts при каждом подключении, включая промежуточные хосты цепочки ProxyJump. По умолчанию режим strict: хоста нет в known_hosts означает отказ.
| Режим | Поведение |
|---|---|
strict |
По умолчанию. Подключаемся только к хостам из known_hosts |
accept-new |
Незнакомый хост записывается при первом подключении, расхождение ключа по-прежнему отказ |
off |
Проверки нет, поведение апстрима |
Проверяются ~/.ssh/known_hosts, ~/.ssh/known_hosts2 и /etc/ssh/ssh_known_hosts, а для алиаса с UserKnownHostsFile тот файл, который указан в SSH-конфиге. Свой список задаётся флагом --known-hosts-file. Понимаются хешированные записи, шаблоны, форма [host]:port и метка @revoked.
Отказ приходит кодом SSH_HOST_KEY_REJECTED и текстом с отпечатком:
Host key of prod.example.com is not in known_hosts (~/.ssh/known_hosts): ssh-ed25519 SHA256:xxxx.
Verify that fingerprint, add the host to known_hosts, or start the server with --host-key-checking accept-new.
Расхождение ключа не принимается никогда и ни в каком режиме: сервер отказывается подключаться и пишет, что хост либо пересоздан, либо кто-то встал посередине.
Для первого знакомства с парком удобно один раз пройти с --host-key-checking accept-new, а дальше вернуть strict.
Аудит-лог
Каждый вызов пишется строкой JSON: команда, соединение, флаг sudo, вердикт гварда, длительность, объём вывода. Содержимое вывода в лог не попадает, пароль sudo вырезается.
{"time":"2026-08-19T08:12:44.101Z","pid":8123,"event":"command","result":"blocked","connection":"prod-master","command":"useradd deploy","sudo":true,"code":"COMMAND_VALIDATION_FAILED","reason":"Blocked by the forbidden core ..."}
{"time":"2026-08-19T08:12:51.880Z","pid":8123,"event":"command","result":"ok","connection":"prod-master","command":"systemctl status nginx","sudo":false,"durationMs":412,"bytes":1840}
Пишутся события connect, command, download, upload, tunnel-open, tunnel-close, host-key.
По умолчанию файл лежит в $XDG_STATE_HOME/ssh-mcp-server/audit.jsonl, то есть обычно ~/.local/state/ssh-mcp-server/audit.jsonl, права 0600.
| Флаг | По умолчанию | Что делает |
|---|---|---|
--audit-log <path> |
каталог состояния XDG | Путь к логу, значение off выключает запись |
--audit-max-size <bytes> |
10485760 | Размер, после которого файл ротируется. 0 отключает встроенную ротацию |
--audit-keep <count> |
10 | Сколько gzip-архивов держим |
Ротация встроенная: по достижении лимита текущий файл переезжает в audit.jsonl.1.gz, старые архивы сдвигаются, всё за пределами --audit-keep удаляется. Десять архивов по 10 МиБ это порядка сотни мегабайт в несжатом виде и заметно меньше после gzip.
Если логами уже управляет logrotate, ставим --audit-max-size 0 и настраиваем ротацию режимом copytruncate.
Ошибка записи не роняет команду: сервер один раз пишет об этом в stderr и продолжает работать.
Способы подключения
Ниже сценарии от простого к сложному. В args каждый флаг и его значение это два отдельных элемента массива: "--host", "192.168.1.1", а не "--host 192.168.1.1".
Логин и пароль
{
"mcpServers": {
"ssh-mcp-server": {
"command": "npx",
"args": [
"-y",
"@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--port", "22",
"--username", "root",
"--password", "pwd123456"
]
}
}
}
Приватный ключ
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--privateKey", "~/.ssh/id_rsa",
"--passphrase", "pwd123456"
]
Пароль от ключа можно не писать в конфиг, а положить в переменную SSH_MCP_PASSPHRASE.
Один алиас из ~/.ssh/config
"args": ["-y", "@perhamm/ssh-mcp-server", "--host", "myserver"]
Сервер читает HostName, Port, User, IdentityFile и ProxyJump из блока Host myserver, включая директивы Include и шаблоны. Флаги командной строки приоритетнее: --port 2222 перебивает порт из конфига.
Бастион и ProxyJump
Если у алиаса есть ProxyJump, цепочка поднимается сама:
Host prod-master
HostName 10.20.30.40
User ops
ProxyJump bastion
IdentityFile ~/.ssh/prod_key
Каждый следующий хоп подключается через канал предыдущего, как это делает ssh -J. Цепочку можно задать и вручную: --proxy-jump "bastion,gateway:2222". Глубина цепочки ограничена пятью хопами.
Форма ProxyCommand ssh bastion -W %h:%p понимается как эквивалент ProxyJump bastion: именно в неё ssh разворачивает короткую запись, а в сгенерированных конфигах она встречается чаще. Флаги -l и -p у хопа учитываются, %r подставляется пользователем целевого хоста. Остальные варианты ProxyCommand игнорируются: своего процесса сервер не запускает.
Если хост достижим только через хоп, которого в SSH-конфиге нет, цепочку задаёт сам вызов. Параметр proxyJump есть у execute-command, download и open-tunnel, он перекрывает цепочку из конфига. Это нужно там, где конфиг генерится автоматикой и править его бессмысленно, правку затрёт следующая выкатка.
{
"tool": "execute-command",
"params": {
"cmdString": "hostname",
"connectionName": "legacy-master",
"proxyJump": "prod-master-0"
}
}
Такое соединение живёт под собственным именем вида алиас via цепочка, поэтому один алиас, поднятый двумя маршрутами, не схлопывается в одно соединение и виден в list-servers двумя строками. Каждый хоп обязан быть алиасом из конфига, иначе вызывающая сторона могла бы направить сервер на произвольный адрес. На чужой хоп приходит SSH_JUMP_NOT_ALLOWED.
Прокси
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--password", "pwd123456",
"--proxy", "socks5://user:pwd@proxy-host:1080"
]
Поддерживаются socks://, socks5://, http:// и https://. HTTP и HTTPS ходят методом CONNECT с Basic-аутентификацией, порт по умолчанию 80 и 443. Для SOCKS5 порт обязателен. Старый флаг --socksProxy работает, но принимает только SOCKS. Вместе --proxy и --proxy-jump не используются.
Джамп-хост с интерактивным shell
transportMode по умолчанию exec. Переключаемся на shell, если после успешного логина команды не выполняются или железка отдаёт только интерактивную сессию:
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "bastion.example.com",
"--username", "ops",
"--password", "pwd123456",
"--transport-mode", "shell",
"--shell-ready-timeout", "15000"
]
В режиме shell команды идут по очереди через одну постоянную сессию, а upload и download не работают: SFTP там отключён.
Двухфакторная аутентификация
Флаг --try-keyboard включает keyboard-interactive. Пароль и ключ подставляются сами, код из второго фактора читается из переменной SSH_MCP_2FA_CODE.
Несколько соединений в одном сервере
Кроме алиасов из SSH-конфига остаётся старый способ: файл с описанием соединений.
[
{
"name": "dev",
"host": "1.2.3.4",
"port": 22,
"username": "alice",
"privateKey": "~/.ssh/dev_key",
"guardProfile": "safe",
"commandTimeoutMs": 120000
},
{
"name": "prod",
"host": "5.6.7.8",
"port": 22,
"username": "bob",
"privateKey": "~/.ssh/prod_key",
"guardProfile": "readonly",
"allowedRemotePaths": ["/var/log", "/tmp"]
}
]
"args": ["-y", "@perhamm/ssh-mcp-server", "--config-file", "/abs/path/ssh-config.json"]
Формат объекта, где ключ это имя соединения, тоже поддерживается. Соединение выбирается параметром connectionName, без него берётся первое.
Ограничения команд и путей
Белый и чёрный списки
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--privateKey", "~/.ssh/id_rsa",
"--whitelist", "^ls( .*)?,^cat .*,^df.*",
"--blacklist", "^rm .*,^shutdown.*"
]
Шаблоны это регулярные выражения через запятую. Команда сначала проверяется по белому списку, потом по чёрному, потом по профилю гвардов, и должна пройти все три проверки.
Шаблон команды
--command-template оборачивает каждую команду. <quotedCommand> подставляет команду как экранированный аргумент, <command> вставляет её как есть. Шаблон применяется после подстановки рабочего каталога.
su root -c <quotedCommand>
docker exec -i mycontainer sh -c <quotedCommand>
Пути для файловых операций
--allowed-local-paths расширяет список локальных каталогов, доступных для upload и download (по умолчанию только текущий каталог). --allowed-remote-paths ограничивает удалённые пути, туда пишем абсолютные POSIX-пути через запятую. Без этого флага SFTP видит всю файловую систему хоста, о чём сервер предупреждает при старте.
Таймауты и лимит вывода
| Параметр | По умолчанию | Что ограничивает |
|---|---|---|
timeout в вызове инструмента |
нет | Одну команду, перебивает настройки соединения |
commandTimeoutMs |
30000 | Команду в режиме exec |
shellCommandTimeoutMs |
30000 | Команду в режиме shell |
connectionTimeoutMs |
30000 | Установку соединения и хендшейк |
sftpTimeoutMs |
300000 | Операции SFTP |
maxOutputBytes |
10485760 | Захваченный вывод одной команды |
keepaliveIntervalMs |
10000 | Интервал keepalive |
При превышении лимита вывода команда обрывается, а инструмент возвращает OUTPUT_LIMIT_EXCEEDED вместе с уже собранным куском. Ошибки приходят структурой из code, message и retriable.
Флаги командной строки
--config-file <path> Файл с описанием соединений
--ssh-config-file <path> Путь к SSH-конфигу (по умолчанию ~/.ssh/config)
--ssh <config> Соединение как JSON или пары key=value
-h, --host <host> Хост или алиас из SSH-конфига
-p, --port <port> Порт
-u, --username <name> Пользователь
-w, --password <password> Пароль
-k, --privateKey <path> Путь к приватному ключу
-P, --passphrase <passphrase> Пароль от ключа
-a, --agent <path> Сокет ssh-agent
-W, --whitelist <patterns> Белый список команд, через запятую
-B, --blacklist <patterns> Чёрный список команд, через запятую
--proxy <url> Прокси SOCKS5, HTTP или HTTPS
-s, --socksProxy <url> Старый флаг только для SOCKS5
--allowed-local-paths <paths> Локальные каталоги для upload и download
--allowed-remote-paths <paths> Удалённые каталоги для SFTP
--transport-mode <mode> exec или shell (по умолчанию exec)
--shell-ready-timeout <ms> Таймаут готовности shell (по умолчанию 10000)
--command-template <template> Шаблон с <command> или <quotedCommand>
--pty Псевдотерминал для exec (по умолчанию включён)
--try-keyboard Keyboard-interactive для 2FA
--pre-connect Подключиться ко всем хостам при старте
--ssh-config-hosts Разрешить хосты из SSH-конфига на лету
--allowed-hosts <patterns> Шаблоны разрешённых алиасов, через запятую
--proxy-jump <chain> Цепочка ProxyJump, через запятую
--guards-profile <name> off, safe или readonly (по умолчанию off)
--guards-file <path> Свой набор правил поверх встроенного
--sudo-password-env <var> Переменная с паролем sudo
--sudo-user <user> Пользователь для sudo (по умолчанию root)
--host-key-checking <mode> strict, accept-new или off (по умолчанию strict)
--known-hosts-file <paths> Свои файлы known_hosts, через запятую
--host-key-algorithms <list> Алгоритмы хост-ключа, через запятую
--enable-upload Опубликовать инструмент upload (по умолчанию выключен)
--audit-log <path|off> Путь к аудит-логу (по умолчанию каталог состояния XDG)
--audit-max-size <bytes> Порог ротации, 0 отключает (по умолчанию 10485760)
--audit-keep <count> Сколько архивов держим (по умолчанию 10)
--disable-tunnels Убрать туннельные инструменты
--tunnel-bind-address <addr> Адрес для туннелей (по умолчанию 127.0.0.1)
--allowed-tunnel-ports <ports> Разрешённые порты туннелей, через запятую
--max-tunnels <count> Лимит одновременных туннелей (по умолчанию 8)
--version, -v Версия пакета
--help Справка
Безопасность
- Для прода включаем
--guards-profile safe, для дежурного разбора инцидентов подходитreadonly. Сoffостаётся только запретное ядро: всё остальное выполнится, о чём сервер и пишет предупреждение в лог. - Ключ, его пароль и пароль sudo читаются из файлов и переменных окружения. В конфиге MCP-клиента храним путь к ключу, а не сам ключ.
- Туннели слушают loopback. У SOCKS5 нет аутентификации, поэтому прокси на
0.0.0.0открывает внутреннюю сеть всем, кто дотянется до порта, и сервер пишет об этом предупреждение при старте. - Без
--allowed-remote-pathsчерез SFTP читается и пишется любой путь на хосте, включая~/.ssh/authorized_keys. - Ключ хоста сверяется с
known_hostsв режимеstrict. Снимать проверку через--host-key-checking offстоит только в лаборатории. - Ограничений частоты вызовов нет.
Разработка
npm install
npm run build
npm test
Тесты запускаются встроенным раннером Node.js и лежат в test/.
Апстрим и лицензия
Проект вырос из classfang/ssh-mcp-server (автор junki.cn), лицензия ISC. Копирайт апстрима сохранён в LICENSE, там же ссылка на исходный репозиторий.
Набор гвардов частично собран по идеям tufantunc/ssh-mcp (MIT).
Пакет в NPM: @perhamm/ssh-mcp-server.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。