ssh-mcp-server
Runs shell commands and moves files on remote machines over SSH, with command allow and deny lists
Runs commands and moves files on remote Linux and Windows hosts over SSH, behind roles, approvals and audit
By Tufan Tunç (individual) TypeScript Local · stdio DevOps and monitoringSecurity
78 popularity score ★ 862 ↓ 15k/wk
Opens the app, which asks before it adds the server.
claude mcp add ssh -- npx -y ssh-mcp{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"-y",
"ssh-mcp"
]
}
}
}{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"-y",
"ssh-mcp"
]
}
}
}{
"servers": {
"ssh": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"ssh-mcp"
]
}
}
}[mcp_servers.ssh]
command = "npx"
args = ["-y", "ssh-mcp"]gemini mcp add --scope user ssh npx -- -y ssh-mcpSSH_MCP_PASSWORD secret |
Optional: Password for profiles with auth = "password". SSH_MCP_<NAME>_PASSWORD sets one per profile and wins over this. |
|---|---|
SSH_AUTH_SOCK |
Optional: SSH agent socket, used for profiles with auth = "agent". Usually already set when an agent runs. |
SSH_MCP_KEY |
Optional: Path to a private key file, used when the profile has no keyRef. SSH_MCP_<NAME>_KEY sets one per profile. |
SSH_MCP_PASSPHRASE secret |
Optional: Passphrase for an encrypted private key. SSH_MCP_<NAME>_PASSPHRASE sets one per profile. |
SSH_MCP_SUDO_PASSWORD secret |
Optional: Password sent to sudo on stdin. SSH_MCP_<NAME>_SUDO_PASSWORD sets one per profile. |
This is an MCP server for reaching remote machines over SSH. You describe each host as a profile in a TOML config file: the login user, how it authenticates (SSH agent, key file, password or OS keychain), an optional bastion and a policy tier. The assistant can then run allowlisted read-only commands or any command at all, with sudo if the role permits. It can also keep named sessions open, signal remote processes and move files over SFTP. A reload-config tool shows which config is live and can re-read the file without restarting the server.
Before anything runs, the command is sorted into a class (read-only, safe, destructive, privileged or forbidden) and checked against a matrix of viewer, operator and admin roles across dev, staging and prod. A profile with no group and an unfamiliar name counts as prod, where even admin gets no sudo unless you change the matrix. In the default ask-destructive mode, destructive and privileged commands wait for a person to approve them in the client's elicitation dialog. Every decision lands in an audit log with secrets redacted, and --auditTamperEvident chains the entries by hash. An external OPA server and a daily command quota can be added, as can OpenTelemetry traces.
Stdio is the default transport. With --transport=http it serves Streamable HTTP, requires --bearerToken, and it listens on 127.0.0.1 unless --httpHost says otherwise. Since 2.0 the exec and sudo-exec tools and the password flags are gone.
Two things the README leaves out show up in the code. It registers three MCP resources (ssh://connections, ssh://connections/{profile} and ssh://sessions/{profile}/{session}) that return profile and session details as JSON. And the credential resolver reads SSH_MCP_PASSPHRASE, or SSH_MCP_<NAME>_PASSPHRASE per profile, to decrypt an encrypted private key.
| Runtime | Node.js 18+ |
|---|---|
| Runs on | macOS, Windows, Linux |
| A remote host reachable over SSH | Your own instanceLog in as a normal account, not root |
| Also |
|
package.json asks for Node.js ^18.19.0 or >=20.6.0. The config lives at ~/.config/ssh-mcp/config.toml on Linux, ~/Library/Application Support/ssh-mcp/config.toml on macOS and %APPDATA%\ssh-mcp\config.toml on Windows, or --config points elsewhere. The --host and --user flags build a one-profile config for a quick start. Keychain auth uses @napi-rs/keyring, an optional dependency that npm installs where it can.
Without a config the server starts but refuses every tool call. If the client lacks MCP elicitation, every destructive and privileged command is refused (APPROVAL_UNAVAILABLE); an approval prompt expires after 10 minutes. Interactive sessions need a POSIX shell on the remote host and fail on Windows OpenSSH. sftp-upload-file and sftp-download-file stay off until defaults.transferRoot is set, are capped at 256 MB per transfer by default, and do not work when the server runs on Windows. By default commands are capped at 5000 characters, 1 MB of output and 60 s. Trust-on-first-use host keys last only for one process. The makers warn it narrows prompt-injection risk but does not remove it.
Suits someone who wants an assistant to inspect or operate servers they already reach over SSH, with control over which hosts allow which kinds of commands and a record of what ran, and whose client can show approval dialogs. It is a heavy setup for a single throwaway box: it needs a config file with strict permissions and policy tiers before it does anything. Since it can run arbitrary and sudo commands on remote hosts, it is a poor fit where no human can review approvals; there, a viewer role or readOnly profiles limit it to reads.
6 only read, 4 change things, 5 can delete or overwrite. A ? marks an optional parameter.
list-connectionslist-sessionsprofile?: stringopen-session changesname: stringtype?: enumcommand?: stringprofile?: stringclose-session changesname: stringprofile?: stringread-session-outputname: stringlines?: numberprofile?: stringread-commandcommand: stringprofile?: stringrun-command deletescommand: stringprofile?: stringsession?: stringtty?: booleanprivileged-command deletescommand: stringprofile?: stringsftp-upload deletesremotePath: stringcontent: stringprofile?: stringsftp-downloadremotePath: stringprofile?: stringsftp-listremotePath: stringmaxEntries?: integerprofile?: stringsftp-upload-file deleteslocalPath: stringremotePath: stringoverwrite?: booleanmode?: integerprofile?: stringsftp-download-file changesremotePath: stringlocalPath: stringoverwrite?: booleanprofile?: stringsignal-process deletespid: integersignal?: enumprofile?: stringreload-config changesaction?: enumssh://connectionsssh://connections/{profile}ssh://sessions/{profile}/{session}