@pyrokine/mcp-ssh
v2.4.0
Published
A comprehensive SSH MCP Server for Claude Code
Maintainers
Readme
MCP-SSH
English | 中文
SSH MCP Server for AI assistants (Claude, Cursor, Windsurf, etc.)
Features
- Multiple Authentication: Password, SSH key
- SSH Config Support: Read
~/.ssh/configwith Host aliases,Host *inheritance, ProxyJump (user@host:port) - Connection Management: Connection pooling, keepalive, auto-reconnect
- Session Persistence: Sessions info saved for reconnection
- Command Execution:
- Basic exec with timeout
- PTY mode (for interactive commands like
top,htop) sudoexecutionsu(switch user) execution - run commands as different user- Batch execution
- Parallel execution on multiple hosts
- Persistent PTY Sessions: For long-running interactive commands (top, htop, tmux, vim, etc.)
- Output buffering with polling read
- Send keystrokes and commands
- Window resize support
- File Operations: Upload, download, read, write, list directory (via SFTP)
- Smart Sync: Directory sync with rsync (auto-fallback to SFTP if rsync unavailable)
- Environment Configuration: LANG, LC_ALL, custom env vars
- Jump Host Support: Connect through bastion hosts
Compatible Clients
| Client | Status | |---------------------------|--------| | Claude Code | ✅ | | Claude Desktop | ✅ | | Cursor | ✅ | | Windsurf | ✅ | | Continue.dev | ✅ | | Cline | ✅ | | Any MCP-compatible client | ✅ |
Installation
Requires Node.js 20.19 or newer.
npm (Recommended)
npm install -g @pyrokine/mcp-ssh
claude mcp add ssh -- mcp-sshFrom Source
git clone https://github.com/Pyrokine/claude-tools.git
cd claude-tools/mcp-ssh
npm install
npm run build
claude mcp add ssh -- node "$PWD/dist/index.js"Configuration
Claude Code
# npm installation
claude mcp add ssh -- mcp-ssh
# source build
claude mcp add ssh -- node /path/to/mcp-ssh/dist/index.jsClaude Desktop / Other Clients
Add to your MCP settings (e.g., ~/.claude/settings.json or client-specific config):
{
"mcpServers": {
"ssh": {
"command": "node",
"args": [
"/path/to/mcp-ssh/dist/index.js"
]
}
}
}Available Tools (35 tools)
Connection Management
| Tool | Description |
|---------------------|---------------------------------------------------|
| ssh_connect | Establish SSH connection (supports ~/.ssh/config) |
| ssh_disconnect | Close connection |
| ssh_list_sessions | List brief sessions; optional detail/field selection |
| ssh_reconnect | Reconnect a disconnected session |
| ssh_config_list | List hosts from ~/.ssh/config |
ssh_list_sessions() returns only alias, canonical identity, runAs, connected, and lastUsedAt. Use detail=true for connection details or fields=[...] to select fields. Neither mode returns keyPath.
Command Execution
| Tool | Description |
|---------------------|-----------------------------------------------|
| ssh_exec | Execute command (supports PTY mode) |
| ssh_exec_as_user | Execute as different user (via su) |
| ssh_exec_sudo | Execute with sudo |
| ssh_exec_batch | Execute multiple commands sequentially |
| ssh_exec_parallel | Execute command on multiple hosts in parallel |
| ssh_quick_exec | One-shot: connect, execute, disconnect |
| ssh_exec_script | Upload and run a temporary remote script |
Tracked operations
| Tool | Description |
|------------------------|--------------------------------------------------|
| ssh_operation_start | Start a tracked long-running remote command |
| ssh_operation_status | Read status, PID, exit code, and output counters |
| ssh_operation_read | Read bounded stdout and stderr by byte offset |
| ssh_operation_cancel | Verify the operation marker, then send TERM |
| ssh_operation_list | List unexpired operations, optionally by alias |
File Operations
| Tool | Description |
|------------------|------------------------------------------|
| ssh_upload | Upload local file to remote server |
| ssh_download | Download remote file to local |
| ssh_read_file | Read remote file content |
| ssh_write_file | Write content to remote file |
| ssh_list_dir | List remote directory contents |
| ssh_file_info | Get file/directory metadata |
| ssh_mkdir | Create remote directory |
| ssh_sync | Smart sync with rsync (fallback to SFTP) |
PTY Sessions (Persistent Interactive Terminal)
| Tool | Description |
|------------------|----------------------------------------------------------------------|
| ssh_pty_start | Start persistent PTY session (for top, htop, tmux, etc.) |
| ssh_pty_write | Send data to PTY (keystrokes, commands) |
| ssh_pty_read | Read PTY output (screen mode: current screen, raw mode: ANSI stream) |
| ssh_pty_resize | Resize PTY window |
| ssh_pty_close | Close PTY session |
| ssh_pty_list | List all PTY sessions |
Port Forwarding
| Tool | Description |
|----------------------|--------------------------------------------------------|
| ssh_forward_local | Local port forwarding (ssh -L): access remote services |
| ssh_forward_remote | Remote port forwarding (ssh -R): expose local services |
| ssh_forward_close | Close port forwarding |
| ssh_forward_list | List all port forwards |
Usage Examples
Using SSH Config (Recommended)
If you have hosts configured in ~/.ssh/config:
# List available hosts
ssh_config_list()
# Connect using config host name
ssh_connect(configHost="myserver")
ssh_exec(alias="myserver", command="uptime")
# Use custom config file path
ssh_connect(configHost="myserver", configPath="/custom/path/config")Supported SSH config features:
Hostwith multiple aliases (e.g.,Host a b c)Host *global defaults inheritance (firstHost *block only)HostName,User,Port,IdentityFileProxyJumpwithuser@host:portformat (first hop only)- Explicit parameters override config values (e.g.,
ssh_connect(configHost="x", user="override"))
Not supported (skipped):
IncludedirectiveMatchblocks (entire block skipped until nextHost)- Wildcard patterns (e.g.,
Host *.example.com)
Behavior notes:
- Multiple
Host *blocks: only first is used - Duplicate Host definitions:
ssh_config_listshows all,ssh_connectuses first - IPv6 in ProxyJump: use bracket notation
[2001:db8::1]:22
Parallel Execution on Multiple Hosts
Execute the same command on multiple connected hosts:
1. ssh_connect(configHost="server1")
2. ssh_connect(configHost="server2")
3. ssh_connect(configHost="server3")
4. ssh_exec_parallel(aliases=["server1", "server2", "server3"], command="uptime")Each host result keeps the same execution metadata as ssh_exec, including failureKind, effectiveUser, cwd,
truncation fields, and diagnostic suggestions. ssh_exec_parallel limits one call to 32 aliases, runs up to 4 hosts at
a time by default (configurable with maxConcurrency, max 8), and accepts maxOutputSize to cap each host result.
Basic: Connect and Execute
ssh_connect(host="<server-host>", user="root", keyPath="/home/.ssh/id_rsa", alias="myserver")
ssh_exec(alias="myserver", command="ls -la /home")
ssh_disconnect(alias="myserver")Connection Templates and runAs
Templates can be provided through SSH_MCP_TEMPLATES or ~/.mcp-ssh/templates.json. Explicit tool arguments override
template values.
{
"app-dev": {
"host": "<server-host>",
"port": 22,
"user": "root",
"runAs": "appuser",
"defaultEnv": {
"APP_ENV": "dev"
}
}
}ssh_connect(template="app-dev", alias="app-dev")
ssh_exec(alias="app-dev", command="whoami && echo $APP_ENV")
ssh_exec(alias="app-dev", command="whoami", useLoginUser=true)ssh_connect returns identity, loginUser, runAs, reused, defaultEnvKeys, envKeys, and reusableSessions
for already connected aliases with the same user@host:port identity. Reusing an alias for a different identity is
rejected with the existing and requested identities. Missing templates include available template names when they can be
loaded. Missing configHost returns SSH config candidates and suggests ssh_config_list.
Jump Host (Bastion)
Connect to internal server via jump host:
ssh_connect(
host="<internal-host>",
user="root",
keyPath="/home/.ssh/id_rsa",
alias="internal",
jumpHost={
host: "bastion.example.com",
user: "admin",
keyPath: "/home/.ssh/bastion_key"
}
)Switch User Execution (su)
Perfect for scenarios where you SSH as root but need to run commands as another user:
1. ssh_connect(host="<server-host>", user="root", keyPath="/home/.ssh/id_rsa", alias="server")
2. ssh_exec_as_user(alias="server", command="whoami", targetUser="appuser")
// Output: appuserBy default, shell profile is loaded to ensure environment variables are available (since su -c creates a
non-interactive shell which doesn't execute rc files). Supports bash (.bashrc), zsh (.zshrc), and other shells (
.profile). Disable with loadProfile=false if not needed.
ssh_exec and ssh_exec_as_user return execution metadata: loginUser, effectiveUser, identity, cwd,
resolvedCwd, shell, profileLoaded, envInjectedKeys, failureKind, stdoutBytes, stderrBytes, and truncation
hints. Large output returns stdoutHead, stdoutTail, stderrHead, stderrTail, and recommendedReadCommand.
Timeout errors include the same head/tail fields from output produced before the timeout. Commands with destructive,
process-control, service-control, credential-bearing, unbounded find, recursive grep, long pipelines, background
tasks, direct su - user -c, or long-running patterns return a commandRisk block with categories and signals.
Tracked long-running commands
Use tracked operations when a command must continue beyond one synchronous tool call:
ssh_operation_start(alias="server", command="long-running-job", maxOutputBytes=1048576)
ssh_operation_status(operationId="op_xxx")
ssh_operation_read(operationId="op_xxx", stdoutOffset=0, stderrOffset=0, maxBytes=65536)
ssh_operation_cancel(operationId="op_xxx")The start call returns an unpredictable operationId. The server keeps bounded stdout and stderr buffers, records the
remote PID after verifying a per-operation marker, and expires completed records after retentionMs. maxOutputBytes
defaults to 1 MiB and accepts up to 8 MiB. retentionMs defaults to 1 hour and accepts up to 24 hours.
ssh_operation_read.maxBytes defaults to 64 KiB and accepts up to 1 MiB. Cancellation is refused until the marker and
PID are verified. If the SSH session disconnects while the command is active, its status becomes unknown; the server
does not claim that the remote process stopped. ssh_exec keeps its existing synchronous timeout behavior.
Interactive Commands (PTY mode)
For commands that need a terminal:
ssh_exec(alias="server", command="top -b -n 1", pty=true)Temporary Script Execution and Log Query Recipe
ssh_exec_script(alias="server", script="set -e\nwhoami\npwd", cwd="/tmp", runAs="appuser")Use ssh_exec_script for log discovery and filtering, then use ssh_read_file to read large result files in chunks:
ssh_exec_script(
alias="server",
script="find /var/log/app -maxdepth 1 -type f -printf '%T@ %p\\n' | sort -nr | head -20 > /tmp/mcp-log-files.txt\ngrep -n -I -F 'ERROR' /var/log/app/*.log | head -100 > /tmp/mcp-log-errors.txt",
timeout=30000
)
ssh_read_file(alias="server", remotePath="/tmp/mcp-log-errors.txt", maxBytes=65536)This keeps log querying in the general command and file tools instead of adding a separate log-specific MCP surface.
With Environment Variables
ssh_connect(
host="<server-host>",
user="root",
keyPath="/home/.ssh/id_rsa",
env={"LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8"}
)Quick One-shot Execution
No need to manage connections for single commands:
ssh_quick_exec(
host="<server-host>",
user="root",
keyPath="/home/.ssh/id_rsa",
command="uptime"
)File Operations
// Upload
ssh_upload(alias="server", localPath="/tmp/config.json", remotePath="/etc/app/config.json")
ssh_upload(alias="server", localPath="/tmp/config.json", remotePath="/etc/app/config.json", atomic=true, verifySize=true, verifyMd5=true, verifyMode="0644")
// Directories return UPLOAD_PATH_IS_DIRECTORY. Large files complete with a suggestion and recommended ssh_sync call.
// Download
ssh_download(alias="server", remotePath="/var/log/app.log", localPath="/tmp/app.log")
// Read file content
ssh_read_file(alias="server", remotePath="/etc/hosts")
ssh_read_file(alias="server", remotePath="/var/log/app.log", tail=true, maxBytes=65536)
ssh_read_file(alias="server", remotePath="/var/log/app.log", offset=1048576, maxBytes=65536)
ssh_read_file(alias="server", remotePath="/var/log/app.log", lineRange="120-180")ssh_upload returns local path policy diagnostics, remote parent probing, remote target metadata, and optional
verification checks. atomic=true uploads to a same-directory temporary file, returns diagnostics.tempRemotePath,
then renames it to the target path. verifySize, verifyMd5, verifyMode, verifyOwner, and verifyMtime add explicit
post-transfer checks.
Command execution results include emptyOutputFailure=true when the remote command exits non-zero without
stdout/stderr,
with the effective user, cwd, and a suggested follow-up read command when available.
ssh_read_file defaults to 1 MiB and rejects maxBytes above 16 MiB before remote transfer. It returns total_size,
read_offset, read_bytes, remaining_bytes, sample_kind, and truncated so callers can distinguish a full read
from a head, tail, byte range, or line range sample.
Directory sync with rsync or SFTP
Smart sync selects rsync only for a direct connection with a validated key path or usable SSH agent. Password, inline-key, jump-host, and other routes that cannot be passed safely to OpenSSH use SFTP directly:
// Sync local directory to remote (upload)
ssh_sync(
alias="server",
localPath="/local/project",
remotePath="/remote/project",
direction="upload"
)
// Returns selectedTransport, decisionReason, stage durations, and transfer counters
// Sync with exclude patterns
ssh_sync(
alias="server",
localPath="/local/project",
remotePath="/remote/project",
direction="upload",
exclude=["*.log", "node_modules", ".git"]
)
// Download from remote
ssh_sync(
alias="server",
localPath="/local/backup",
remotePath="/remote/data",
direction="download"
)
// Dry run (preview without actual transfer)
ssh_sync(..., dryRun=true)
// Upload one file and verify remote owner/mode after transfer
ssh_sync(..., direction="upload", verifyOwner="appuser", verifyMode="0644")
// Verify a directory with bounded manifests
ssh_sync(
...,
verify={count: true, sha256: true, owner: true, mode: true, staleFiles: true}
)
// Verify that entries present before --delete were removed
ssh_sync(..., delete=true, verify={deletions: true})A directory source always means “copy the source directory contents into the destination root” in both rsync and SFTP
mode. Directory sources reject recursive=false. Relative exclude patterns are evaluated against the path below the
source root; patterns without / match each basename.
Directory verification builds bounded local and remote manifests. It can compare entry count, a SHA-256 root manifest,
owner, mode, stale entries, and deletion results. verify.deletions=true requires delete=true; it records the destination
before transfer and only checks whether those deletion candidates disappeared. staleFiles independently checks every
extra destination entry. The response contains summaries and at most 20 mismatch samples, not the full manifest. The
default limits are 10,000 entries, 256 MiB per hashed file, and 1 GiB total hashed bytes. Set verify.maxEntries,
verify.maxFileBytes, and verify.maxTotalBytes to raise them, up to 50,000 entries, 4 GiB per file, and 16 GiB total.
Symlinks or unsupported filesystem entries that were skipped make verification explicitly skipped instead of
reporting a partial match.
If verification is requested, a mismatch, skipped check, or verification error sets top-level success=false while
transferSuccess still reports whether transfer completed. verificationStatus, verificationSuccess, and
failedChecks describe the verification outcome. SFTP single-file transfers also include local and remote size,
mode or permissions, mtime, owner/group, and SHA-256 comparison when the remote has sha256sum. verifyOwner and
verifyMode apply to upload single-file sync; use verify.owner and verify.mode for directory manifests.
Rsync is selected only for a direct connection with a validated key path or usable SSH agent. Password, inline-key, and
jump-host sessions use SFTP because their route or credentials cannot be passed safely to a separate OpenSSH process.
preflightTimeout, connectTimeout, and operationTimeout independently bound capability probing, rsync SSH setup, and
the overall transfer; their defaults are 10 seconds, 30 seconds, and 10 minutes. When the session is not eligible for
rsync, the response returns rsyncProbe.status="skipped" with the route decision reason instead of omitting the probe.
Rsync mode sets
StrictHostKeyChecking=accept-new. SFTP does not support delete=true; such a request fails explicitly instead of
claiming deletion. If you require strict host key verification and management, use SFTP mode.
Persistent PTY Sessions (top, tmux, etc.)
For interactive commands that continuously refresh or require ongoing interaction:
// 1. Start a PTY session with top
ssh_pty_start(alias="server", command="top", rows=24, cols=80)
// Returns: { "ptyId": "pty_1_1234567890" }
// 2. Read current output (polling)
ssh_pty_read(ptyId="pty_1_1234567890")
// Returns: { "data": "top - 10:30:15 up 5 days...", "active": true, "unreadRawBytes": 123, "foregroundProcess": "top" }
// 3. Send commands (e.g., quit top)
ssh_pty_write(ptyId="pty_1_1234567890", data="q")
// 4. Close when done
ssh_pty_close(ptyId="pty_1_1234567890")tmux session example:
// Start tmux
ssh_pty_start(alias="server", command="tmux new -s work")
// Send commands in tmux
ssh_pty_write(ptyId="pty_1_xxx", data="ls -la\r")
// Read output
ssh_pty_read(ptyId="pty_1_xxx")
// Detach tmux (Ctrl+B, D)
ssh_pty_write(ptyId="pty_1_xxx", data="\x02d")ssh_pty_read and ssh_pty_list expose lastInputAt, lastOutputAt, lastReadAt, unreadRawBytes,
rawBufferLimit, and foregroundProcess so long-running sessions can be monitored without reading the full raw stream.
When a finite command exits naturally, its final screen remains readable with active=false until explicit close or the
closed-session retention expires (5 minutes by default, configurable with SSH_MCP_PTY_CLOSED_RETENTION_MS).
Common control sequences:
- Enter:
\ror\n - Ctrl+C:
\x03 - Ctrl+D:
\x04 - Ctrl+Z:
\x1a - Arrow Up:
\x1b[A - Arrow Down:
\x1b[B
Port Forwarding
Access remote internal services or expose local services:
// Local forward: access remote MySQL (<service-host>:3306) via localhost:13306
ssh_forward_local(alias="server", localPort=13306, remoteHost="<service-host>", remotePort=3306)
// Remote forward: expose local dev server (3000) to remote port 8080
ssh_forward_remote(alias="server", remotePort=8080, localHost="127.0.0.1", localPort=3000)
// List all forwards
ssh_forward_list()
// Close after the listener is released
ssh_forward_close(forwardId="fwd_1_xxx", mode="graceful", timeoutMs=5000)
// Destroy active connections, then release the listener
ssh_forward_close(forwardId="fwd_1_xxx", mode="force", timeoutMs=5000)A successful close means the local server.close or remote unforwardIn callback completed. The result includes
listenerReleased, remoteUnforwarded, activeConnections, closeMode, and retryable. Timeout or callback failure
keeps the forward in ssh_forward_list, so the same forwardId can be retried. After a successful local close, the port
can be rebound immediately.
Configuration Options
Connection Options
| Option | Type | Default | Description |
|---------------------|--------|----------------|--------------------------------|
| host | string | required | Server address |
| user | string | required | Username |
| password | string | - | Password authentication |
| keyPath | string | - | Path to SSH private key |
| port | number | 22 | SSH port |
| alias | string | auto-generated | Connection alias for reference |
| template | string | - | Template name |
| env | object | - | Environment variables |
| defaultEnv | object | - | Default env for this session |
| runAs | string | - | Default execution user |
| keepaliveInterval | number | 30000 | Keepalive interval in ms |
| readyTimeout | number | 30000 | Wait for SSH ready in ms, max 600000 |
| jumpHost.readyTimeout | number | top-level value | Jump-host ready timeout, max 600000 |
Connection failures return failureStage, retryable, and a bounded suggestion. failureStage is one of preflight, authentication, ready_timeout, transport_or_handshake, or unknown; multi-hop failures may also include connectionStep as key_read, jump_connect, jump_forward, or target_connect.
Exec Options
| Option | Type | Default | Description |
|-----------------|---------|---------------|--------------------------------------------|
| timeout | number | 30000 | Command timeout in ms |
| cwd | string | - | Working directory |
| env | object | - | Additional environment variables |
| pty | boolean | false | Enable PTY mode for interactive commands |
| maxOutputSize | number | 10 MB chars | Output truncation limit |
| runAs | string | session runAs | Per-command execution user |
| useLoginUser | boolean | false | Skip session-level runAs |
| loadProfile | boolean | true | Load target user's shell profile for runAs |
Security
Path Whitelist for Key/Config Files
keyPath (private key) and configPath (SSH config) must reside under one of the following directories:
~/.ssh/— user SSH directory/etc/ssh/— system-wide SSH directory
To allow additional directories, set the SSH_MCP_ALLOWED_KEY_DIRS environment variable, separated by : on
Linux/macOS or ; on Windows:
export SSH_MCP_ALLOWED_KEY_DIRS=/opt/secrets:/var/lib/keysFiles outside the whitelist are rejected with Invalid private key path: ... or Invalid config path: ....
Optional File-Ops Path Whitelist
ssh_upload, ssh_download, ssh_sync accept arbitrary local paths by default. To restrict them to specific
directories (recommended for shared environments), set SSH_MCP_FILE_OPS_ALLOW_DIRS (paths separated by Node's
path.delimiter — : on POSIX, ; on Windows):
export SSH_MCP_FILE_OPS_ALLOW_DIRS=/tmp:/home/me/workWhen set, any local path outside the whitelist is rejected. Symlinks are resolved via realpath to prevent escape.
When unset, no restriction is applied (preserves flexibility).
File Size Limits
- Private key files: max 64 KB
- SSH config files: max 1 MB
Files exceeding these limits are rejected before being read.
Symlink Handling in ssh_sync
ssh_sync(direction="upload", ...) handles local symlinks differently depending on the underlying transport:
| followSymlinks | SFTP path | rsync path |
|-------------------|------------------------------------------------------------|-----------------------------------------------------------------------------|
| false (default) | Skipped, returned in skippedSymlinks, printed as warning | Skipped via --no-links |
| true | Followed: link target contents are uploaded | Followed via rsync -L / --copy-links: link target contents are uploaded |
Both transports skip device and special filesystem entries. SFTP returns skippedUnsupported and up to 10 sample paths;
rsync uses --no-devices --no-specials. Directory verification also reports skipped symlinks and unsupported entries and
does not treat an incomplete manifest as a match.
Project Structure
mcp-ssh/
├── src/
│ ├── index.ts # MCP Server entry, tool definitions
│ ├── session-manager.ts # Connection pool, exec, keepalive
│ ├── file-ops.ts # SFTP file operations
│ └── types.ts # TypeScript type definitions
├── dist/ # Compiled JavaScript
├── package.json
├── tsconfig.json
└── README.mdContributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE for details.
Related Projects
- Model Context Protocol - The MCP specification
- MCP Servers - Official MCP server implementations
