SSH MCP Server

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

Connect your client

Local (npm)

Add in one click

Opens the app, which asks before it adds the server.

Or add it by command or config

Claude CodeTerminal
claude mcp add ssh -- npx -y ssh-mcp
Claude Desktopclaude_desktop_config.json
{
  "mcpServers": {
    "ssh": {
      "command": "npx",
      "args": [
        "-y",
        "ssh-mcp"
      ]
    }
  }
}
Cursor~/.cursor/mcp.json
{
  "mcpServers": {
    "ssh": {
      "command": "npx",
      "args": [
        "-y",
        "ssh-mcp"
      ]
    }
  }
}
VS Code.vscode/mcp.json
{
  "servers": {
    "ssh": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "-y",
        "ssh-mcp"
      ]
    }
  }
}
Codex~/.codex/config.toml
[mcp_servers.ssh]
command = "npx"
args = ["-y", "ssh-mcp"]
Gemini CLITerminal
gemini mcp add --scope user ssh npx -- -y ssh-mcp

Settings 5

SSH_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. For privileged-command.

What it does

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.

What it needs

RuntimeNode.js 18+
Runs onmacOS, Windows, Linux
A remote host reachable over SSHYour own instanceLog in as a normal account, not root
Also
  • config.toml at the platform path, mode 0600 in a 0700 folder on Linux and macOS
  • An MCP client with elicitation support, for approvals

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.

Limits

  • Limit With no config file, every tool call except reload-config is refused.
  • Limit A client without MCP elicitation gets every destructive and privileged command refused with APPROVAL_UNAVAILABLE. Approval prompts expire after 10 minutes.
  • Platform Interactive sessions need a POSIX shell on the remote host, so they fail against Windows OpenSSH.
  • Limit sftp-upload-file and sftp-download-file refuse until defaults.transferRoot is set to a private 0700 folder.
  • Platform The two streaming transfer tools refuse when the server itself runs on Windows.
  • Size cap Streaming transfers are capped at 256 MB each by default (transferMaxBytes).
  • Size cap Commands are held to 5000 characters, 1 MB of output and 60 s by default. sftp-list stops at 200 entries unless maxEntries raises it, to 1000 at most.
  • Limit Host keys accepted on first use live in memory only, so after a restart a host is trusted afresh unless trustedHostKey pins it.
  • Limit On Linux and macOS, SIGHUP reloads the config and no longer stops the server. Stop an HTTP server with SIGTERM or SIGINT.
  • Limit Its makers say the policy checks narrow the damage prompt injection can do but cannot remove that risk.

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.

Who it suits

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.

Tools 15

6 only read, 4 change things, 5 can delete or overwrite. A ? marks an optional parameter.

list-connections
Lists the configured hosts and whether each is connected.
list-sessions
Lists the open sessions on a host.profile?: string
open-session changes
Starts a named interactive shell session or a background command.name: stringtype?: enumcommand?: stringprofile?: string
close-session changes
Ends a session, signalling a background command before the channel is dropped.name: stringprofile?: string
read-session-output
Returns the latest lines a background session printed.name: stringlines?: numberprofile?: string
read-command
Runs a command that is on the read-only allowlist, such as ls or grep.command: stringprofile?: string
run-command deletes
Runs any shell command, sending destructive or privileged ones for approval unless the profile is auto.command: stringprofile?: stringsession?: stringtty?: boolean
privileged-command deletes
Runs a command under sudo, passing the password on stdin, once approved.command: stringprofile?: string
sftp-upload deletes
Writes text it is given to a remote file over SFTP, replacing any file already there.remotePath: stringcontent: stringprofile?: string
sftp-download
Returns the contents of a remote file over SFTP.remotePath: stringprofile?: string
sftp-list
Lists a remote directory, 200 entries by default and 1000 at most.remotePath: stringmaxEntries?: integerprofile?: string
sftp-upload-file deletes
Copies a local file from inside the transfer root to the remote host without passing it through the chat.localPath: stringremotePath: stringoverwrite?: booleanmode?: integerprofile?: string
sftp-download-file changes
Saves a remote file to local disk inside the transfer root without passing it through the chat.remotePath: stringlocalPath: stringoverwrite?: booleanprofile?: string
signal-process deletes
Sends INT, TERM or KILL to a process ID on the remote host.pid: integersignal?: enumprofile?: string
reload-config changes
Reports which config is live, or re-reads the config file and applies it without a restart.action?: enum

Resources 3

ssh://connections
Every profile with its connection state, as JSON.
ssh://connections/{profile}
One profile's settings and connection state.
ssh://sessions/{profile}/{session}
Details of one session on a connected profile.

Similar servers