ssh-mcp-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ssh-mcp-serverrun 'df -h' on prod-1 and show me the disk usage"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ssh-mcp-server
MCP server over SSH: the agent executes commands on remote machines, while keys, passwords, and sudo stay on our side.
A fork of classfang/ssh-mcp-server under ISC.
English | Русский
What it is
ssh-mcp-server is a bridge between an MCP client (Claude Code, Cursor, Cline) and SSH. The agent calls tools, the server connects to the machine and returns the output. The model never sees the private key, password, or sudo password: all of that is read from the local config and environment variables of the process.
One server serves any number of hosts. The host is selected on the fly by alias from ~/.ssh/config, so you don't need to add every machine to the MCP client config.
Related MCP server: ssh-mcp-server
What's new in this fork
Feature | Why |
Hosts from | One MCP for the whole fleet. The alias is passed in |
ProxyJump | A host behind a bastion is reachable by alias, the |
sudo from environment variable | The agent requests |
Forbidden core | A list of operations that never run: not under sudo, not in any profile, not via SFTP |
Guard profiles | Ready-made guard sets |
Tunnels | SOCKS5 (like |
Host key checking |
|
Modern cryptography | Ed25519 first in the list, no SHA-1, CBC, or DSA |
Audit log | Every call is written to JSONL with rotation and gzip archives |
No file upload |
|
Tools
Tool | What it does |
| Runs a command, supports |
| Fetches a file from the server |
| Shows configured connections, their status, and the active guard profile |
| Shows aliases from the SSH config available as |
| Starts a SOCKS5 proxy or port forward through the connection |
| Closes a tunnel |
| Shows open tunnels and connection counters |
list-ssh-hosts only appears with the --ssh-config-hosts flag, tunnel tools are removed with --disable-tunnels. The upload tool is not in the list: it is only published with --enable-upload.
Host facts without extra commands
On connection, the server collects machine state once: name, addresses, OS, kernel, uptime, disk, memory, process count. All probes are joined with markers into a single command, so it's one SSH session, not six.
The server caches the result and returns it from 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}}}]That is, you don't need to ask for uname -a, df -h, free -h, and uptime separately — the answers are already there. The agent calls list-servers once and reads the status from there.
Probes go through guards one by one. With the whitelist, only allowed fields remain in the status. An incomplete status does not mean the host is down.
A successful command with no output returns [exit code] 0, not an empty string. The model would read an empty response as an unclear result and go re-check via echo $? — that's an extra SSH session and extra tokens.
Quick start: one server for the whole fleet
MCP client config:
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"-y",
"@perhamm/ssh-mcp-server",
"--ssh-config-hosts",
"--guards-profile", "safe"
],
"env": {
"SSH_MCP_SUDO_PASSWORD": "..."
}
}
}
}Then the agent works like this:
Calls
list-ssh-hostsand finds the right alias, e.g.r-ulybka-prod-master. For large configs, the list is filtered, so the agent passesfilter: a substring or a pattern liker-ulybka-*.Calls
execute-commandwithconnectionName: "r-ulybka-prod-master".The server reads the alias from
~/.ssh/config, takesHostName,User,Port,IdentityFile, andProxyJumpfrom there, establishes the connection, and runs the command.
The key never leaves the machine: the server reads the file itself, only the path from the SSH config appears in the dialog. If IdentityFile is not set, the ssh-agent from SSH_AUTH_SOCK is used. An alias without HostName connects by its own name, just like ssh does.
Only an alias declared as a separate Host block in the config is reachable. A Host * block provides defaults but does not turn an arbitrary name into a reachable host.
The alias list can be narrowed down:
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--ssh-config-hosts",
"--allowed-hosts", "r-ulybka-*,*-stage-*",
"--ssh-config-file", "/home/user/.ssh/config_work"
]Patterns support * and ?. If an alias doesn't match any pattern, the connection is not established and the agent gets an SSH_HOST_NOT_ALLOWED error.
Safe mode and guards
Guards are a versioned set of rules that checks every command before it is sent to the server. The rules live in guards/default-guards.json and are updated together with the repository.
Profiles
Profile | Behavior |
| Profile rules are disabled, only the forbidden core works. Default value |
| Plus a ban on destructive commands, everything else is allowed |
| Only reading and diagnostics allowed, inherits all |
"args": ["-y", "@perhamm/ssh-mcp-server", "--ssh-config-hosts", "--guards-profile", "safe"]What safe catches beyond the core: shutdown and reboot, firewall reset, stopping sshd and kubelet, kubectl delete, helm uninstall, docker system prune, package removal, DROP DATABASE, curl | sh, git push --force, log cleanup, kernel module unloading, interactive editors. The full list with reasons is in the JSON.
readonly additionally requires every part of the command to be in the whitelist: ls, cat, grep, find, ps, ss, df, journalctl, systemctl status, kubectl get/describe/logs, docker ps/logs, and similar. sudo is completely forbidden in this profile, along with su, doas, and pkexec.
Forbidden core
Some operations never run: not in the off profile, not under sudo, not through a custom guards file, not through SFTP bypassing commands. The list lives in the forbidden block.
Category | What is closed |
Accounts |
|
sudo | Writing to |
Schedules |
|
systemd | Writing units and timers to |
SSH | Editing |
Interpreters |
|
Mass deletion |
|
Disks and secrets |
|
Normal work still works: crontab -l, cat /etc/ssh/sshd_config, systemctl restart nginx, rm -rf /var/lib/myapp/cache/tmp all pass. A parseable bash -c "..." also works: its contents are checked by the same rules.
The core also closes file tools. download won't fetch /etc/shadow or the contents of ~/.ssh, and allowedRemotePaths doesn't re-enable anything here. The local side is also protected: download won't place a file into our own ~/.ssh.
File upload is completely disabled. The upload tool is not published until --enable-upload is passed, and the readonly profile rejects uploads even with that flag.
If the server is needed specifically for creating users or editing cron, the core will have to be modified in the fork deliberately: there is no flag that disables it.
Why a semicolon doesn't bypass this
The command is split into parts by ;, |, &&, ||, &, newline, and $(...) substitutions, and each part is checked separately. Quotes are taken into account during parsing. So ls; rm -rf / doesn't pass in any profile, even though the whole string starts with the allowed ls.
Wrappers are stripped before checking: sudo, env, timeout 5, nohup, and assignments like LC_ALL=C don't hide the command from the rules. A script inside bash -c "..." is parsed separately and checked by the same rules. Command length is limited to 5000 characters.
Guards close agent mistakes, not deliberate bypass. An interpreter with arbitrary code inside, like python -c, cannot be parsed by the rules. Where bypass is unacceptable, we restrict the SSH user's own permissions.
Updating rules
Three ways to keep rules fresh:
Merge upstream into your fork. The rules file is versioned with the
versionfield; the version is visible inlist-serversand in the denial message.Keep your own file and point to it with
--guards-file /etc/ssh-mcp/guards.json. Rules from it are added to the built-in ones, the version becomes2026.08.19+local-1.Update the file on a schedule:
node scripts/update-guards.js https://example.com/guards.json /etc/ssh-mcp/guards.jsonThe script validates the JSON and compiles every regular expression before replacing the file. A broken download doesn't break the working rule set.
Format of your own file:
{
"version": "local-1",
"profiles": {
"safe": {
"deny": [
{ "id": "no-ansible", "pattern": "^ansible-playbook\\b", "reason": "выкат идёт из CI" }
]
}
}
}The scope: "command" field makes the rule check the entire command rather than individual parts. This is how the curl | sh and SQL rules work. You can add your own bans to the forbidden block of your file, but you can't remove the built-in ones: the lists are merged.
The old --whitelist and --blacklist are still there and are checked before the guards.
sudo without a password in the dialog
The sudo password is stored in the server process's environment variable. The agent passes sudo: true, but never sees the password itself, neither in the call arguments nor in the output.
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": ["-y", "@perhamm/ssh-mcp-server", "--ssh-config-hosts", "--guards-profile", "safe"],
"env": {
"SSH_MCP_SUDO_PASSWORD": "..."
}
}
}
}Tool invocation:
{
"tool": "execute-command",
"params": {
"cmdString": "systemctl restart nginx",
"connectionName": "r-ulybka-prod-master",
"sudo": true
}
}The command goes to the server as sudo -S -k -p '' -u root -- /bin/sh -c '<command>', and the password is written to the channel's stdin. It's not in the command line, so it doesn't end up in ps or in the history. The pseudo-terminal is disabled for such commands, otherwise the tty would echo the input back into the output. Just in case, the password is also stripped from the output and from error text.
The variable name and target user are changed with the --sudo-password-env and --sudo-user flags. If the variable is empty, the call fails with SUDO_PASSWORD_MISSING before even connecting. The readonly profile forbids sudo entirely.
In shell mode, the password is appended as a separate line right after the command, since sudo reads the same stdin as the shell itself. The -k flag guarantees the password prompt will always appear and the line won't be executed as a command. For sudo, exec mode is preferable.
Tunnels
open-tunnel starts a local listener and routes traffic through the SSH connection. Useful when there's no direct access to cluster services, but there is access to the node.
SOCKS5 on port 8777:
{
"tool": "open-tunnel",
"params": {
"type": "socks5",
"localPort": 8777,
"connectionName": "r-ulybka-prod-master"
}
}From then on, any client goes through the proxy, and names are resolved on the remote side:
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:8777Forwarding a single port, the equivalent of ssh -L:
{
"tool": "open-tunnel",
"params": {
"type": "local",
"localPort": 15432,
"remoteHost": "pg-master.internal",
"remotePort": 5432
}
}If localPort is not specified, the system picks the port and returns it in the response. Tunnels live until close-tunnel, until the SSH connection drops, or until the server stops.
Limits are set with flags:
Flag | Default value | What it does |
|
| Address on which tunnels listen |
| no limits | List of ports allowed to be occupied |
| 8 | How many tunnels we keep at once |
| off | Removes tunnel tools from the list |
The listener is brought up on loopback by default. An address wider than loopback opens the proxy to your network, so change it deliberately.
Host key verification
The server key is checked against known_hosts on every connection, including intermediate hosts in a ProxyJump chain. The default mode is strict: a host missing from known_hosts means refusal.
Mode | Behavior |
| Default. Only connects to hosts in |
| An unknown host is recorded on first connection; a key mismatch is still a refusal |
| No verification, upstream behavior |
Checked are ~/.ssh/known_hosts, ~/.ssh/known_hosts2, and /etc/ssh/ssh_known_hosts, and for an alias with UserKnownHostsFile, the file specified in the SSH config. A custom list is set with the --known-hosts-file flag. Hashed entries, patterns, the [host]:port form, and the @revoked marker are all understood.
A refusal comes with code SSH_HOST_KEY_REJECTED and text containing the fingerprint:
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.A key mismatch is never accepted, in any mode: the server refuses to connect and reports that the host was either recreated or someone is in the middle.
For a first acquaintance with a fleet, it's convenient to run once with --host-key-checking accept-new, then switch back to strict.
Audit log
Every call is written as a JSON line: command, connection, sudo flag, guard verdict, duration, output size. The output content does not go into the log, and the sudo password is stripped.
{"time":"2026-08-19T08:12:44.101Z","pid":8123,"event":"command","result":"blocked","connection":"r-ulybka-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":"r-ulybka-prod-master","command":"systemctl status nginx","sudo":false,"durationMs":412,"bytes":1840}The events connect, command, download, upload, tunnel-open, tunnel-close, and host-key are written.
By default, the file lives at $XDG_STATE_HOME/ssh-mcp-server/audit.jsonl, i.e. usually ~/.local/state/ssh-mcp-server/audit.jsonl, with permissions 0600.
Flag | Default | What it does |
| XDG state directory | Path to the log; the value |
| 10485760 | Size after which the file is rotated. |
| 10 | How many gzip archives we keep |
Rotation is built-in: once the limit is reached, the current file moves to audit.jsonl.1.gz, older archives shift, and everything beyond --audit-keep is deleted. Ten archives of 10 MiB each is on the order of a hundred megabytes uncompressed and noticeably less after gzip.
If logs are already managed by logrotate, set --audit-max-size 0 and configure rotation with the copytruncate mode.
A write error does not bring down the command: the server logs it once to stderr and keeps working.
Connection methods
Below are scenarios from simple to complex. In args, each flag and its value are two separate array elements: "--host", "192.168.1.1", not "--host 192.168.1.1".
Login and password
{
"mcpServers": {
"ssh-mcp-server": {
"command": "npx",
"args": [
"-y",
"@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--port", "22",
"--username", "root",
"--password", "pwd123456"
]
}
}
}Private key
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--privateKey", "~/.ssh/id_rsa",
"--passphrase", "pwd123456"
]The key passphrase doesn't have to be written in the config; it can be placed in the SSH_MCP_PASSPHRASE variable.
A single alias from ~/.ssh/config
"args": ["-y", "@perhamm/ssh-mcp-server", "--host", "myserver"]The server reads HostName, Port, User, IdentityFile, and ProxyJump from the Host myserver block, including Include directives and patterns. Command-line flags take precedence: --port 2222 overrides the port from the config.
Bastion and ProxyJump
If an alias has ProxyJump, the chain is brought up on its own:
Host r-ulybka-prod-master
HostName 10.20.30.40
User ops
ProxyJump bastion
IdentityFile ~/.ssh/prod_keyEach next hop connects through the channel of the previous one, just like ssh -J does. The chain can also be set manually: --proxy-jump "bastion,gateway:2222". The chain depth is limited to five hops.
Proxy
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--password", "pwd123456",
"--proxy", "socks5://user:pwd@proxy-host:1080"
]Supported are socks://, socks5://, http://, and https://. HTTP and HTTPS use the CONNECT method with Basic authentication, with default ports 80 and 443. For SOCKS5, the port is required. The old --socksProxy flag still works but only accepts SOCKS. --proxy and --proxy-jump are not used together.
Jump host with an interactive shell
transportMode defaults to exec. Switch to shell if commands don't execute after a successful login or the device only provides an interactive session:
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "bastion.example.com",
"--username", "ops",
"--password", "pwd123456",
"--transport-mode", "shell",
"--shell-ready-timeout", "15000"
]In shell mode, commands go one at a time through a single persistent session, and upload and download don't work: SFTP is disabled there.
Two-factor authentication
The --try-keyboard flag enables keyboard-interactive. The password and key are supplied automatically, and the second-factor code is read from the SSH_MCP_2FA_CODE variable.
Multiple connections in one server
Besides aliases from the SSH config, the old method remains: a file describing connections.
[
{
"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"]The object format where the key is the connection name is also supported. The connection is selected with the connectionName parameter; without it, the first one is used.
Command and path restrictions
Whitelist and blacklist
"args": [
"-y", "@perhamm/ssh-mcp-server",
"--host", "192.168.1.1",
"--username", "root",
"--privateKey", "~/.ssh/id_rsa",
"--whitelist", "^ls( .*)?,^cat .*,^df.*",
"--blacklist", "^rm .*,^shutdown.*"
]Patterns are comma-separated regular expressions. A command is first checked against the whitelist, then the blacklist, then the guard profile, and must pass all three checks.
Command template
--command-template wraps every command. <quotedCommand> substitutes the command as an escaped argument, <command> inserts it as is. The template is applied after the working directory substitution.
su root -c <quotedCommand>
docker exec -i mycontainer sh -c <quotedCommand>Paths for file operations
--allowed-local-paths extends the list of local directories available for upload and download (by default, only the current directory). --allowed-remote-paths restricts remote paths; absolute POSIX paths are written there, comma-separated. Without this flag, SFTP sees the entire host filesystem, which the server warns about at startup.
Timeouts and output limit
Parameter | Default | What it limits |
| none | A single command, overrides connection settings |
| 30000 | A command in |
| 30000 | A command in |
| 30000 | Connection setup and handshake |
| 300000 | SFTP operations |
| 10485760 | Captured output of a single command |
| 10000 | Keepalive interval |
When the output limit is exceeded, the command is terminated and the tool returns OUTPUT_LIMIT_EXCEEDED along with the already collected chunk. Errors come as a structure of code, message, and retriable.
Command-line flags
--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 СправкаSecurity
For production, enable
--guards-profile safe; for on-call incident triage,readonlyis suitable. Withoff, only the forbidden core remains: everything else will execute, which the server logs as a warning.The key, its passphrase, and the sudo password are read from files and environment variables. In the MCP client config, store the path to the key, not the key itself.
Tunnels listen on loopback. SOCKS5 has no authentication, so a proxy on
0.0.0.0opens the internal network to anyone who can reach the port, and the server logs a warning about this at startup.Without
--allowed-remote-paths, any path on the host can be read and written via SFTP, including~/.ssh/authorized_keys.The host key is verified against
known_hostsinstrictmode. Disabling verification via--host-key-checking offis only worth doing in a lab.There are no call rate limits.
Development
npm install
npm run build
npm testTests run with the built-in Node.js runner and live in test/.
Upstream and license
The project grew out of classfang/ssh-mcp-server (author junki.cn), ISC license. The upstream copyright is preserved in LICENSE, which also contains a link to the source repository.
The guard set is partially assembled from ideas in tufantunc/ssh-mcp (MIT).
The package on NPM: @perhamm/ssh-mcp-server.
This server cannot be installed
Maintenance
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
- AlicenseBqualityFmaintenanceAn MCP server that allows AI models to execute system commands on local machines or remote hosts via SSH, supporting persistent sessions and environment variables.13228MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables remote SSH command execution and bidirectional file transfers through a standardized interface. It allows AI assistants to securely manage remote servers while keeping credentials isolated and applying command-level security controls.ISC
- AlicenseAqualityBmaintenanceMCP server for managing multiple SSH servers via AI assistants, offering tools for remote command execution, file operations, and system monitoring.11MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that gives AI agents SSH capabilities to execute commands, transfer files, and inspect remote systems through a preconfigured host list.43MIT
Related MCP Connectors
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
MCP server connecting AI agents to non-custodial staking data across 130+ networks.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/perhamm/ssh-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server