ssh-mcp-toolkit
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-toolkitcheck disk space on the staging host"
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-toolkit
A Model Context Protocol (MCP) server that gives LLM clients safe, persistent SSH access to remote machines — with multi-hop ProxyJump tunneling, SFTP file transfer, local port forwarding, internet egress for internal servers, and server-to-server transfers.
Table of Contents
Related MCP server: mcp-ssh
Overview
ssh-mcp-toolkit lets MCP-compatible clients (such as Claude Code, Cursor, or custom MCP inspectors) control remote machines through SSH. Once hosts are registered, the server maintains persistent shell sessions that retain environment state between commands—ideal for multi-step workflows, long-running processes, or interactive diagnostics. Hosts that are not directly reachable can be tunneled through any number of jump hosts, and files can be moved in either direction over SFTP across the same tunnel.
The server is implemented in TypeScript on top of the official @modelcontextprotocol/sdk.
Hosts that are not directly reachable can be tunneled through any number of jump hosts. The same chain also powers local port forwarding: an internal service's port can be exposed to the local machine over a dedicated SSH tunnel, so ordinary tools (browsers, database clients, admin consoles) can reach it at http://localhost:PORT.
New Features(新增功能一览)
本项目是 ssh-mcp-sessions@1.0.17 的二次开发。以下能力均为本项目在既有 SSH 会话 / SFTP 基础之上新增:
能力 | 相关工具 / 字段 | 说明 | 详见 |
多跳跳板 ProxyJump |
| 任意跳数的链式 SSH 穿透;纯 JS 实现(ssh2 | |
SFTP 文件传输 |
| 复用同一条跳板链上传/下载文件,自动创建远端缺失的父目录 | |
本地端口转发(隧道) |
| 把内网应用端口暴露到本机 | |
内网出网(Internet Egress) |
| 让内网服务器 B/C 经本地机器访问外网( | |
服务器间直传 |
| 两台已保存主机之间的文件传输: | |
异步命令执行 |
| 长命令(跑批/构建/迁移)后台执行、立即返回 | |
交互式输入 |
| 向运行中命令的 stdin 发送输入并返回增量输出( | |
会话级交互式输入 |
| 直接读写会话 shell 的增量输出与 stdin,无需后台 run;典型场景:堡垒机(AIS iFORT、奇治 Umap)登录后立即弹出的 TUI 资产菜单 —— | |
异步大文件下载/上传(断点续传) |
| 单文件后台异步传输,立即返回 transfer id;写入 |
所有新增均向后兼容:原有 hosts.json 格式与既有工具调用方式完全不变。
Key Features
MCP-compliant: exposes functionality through standard MCP tool definitions.
Persistent sessions: keep shell sessions alive, preserving working directory, environment variables, and process state across multiple commands.
Stored host profiles: manage SSH targets through durable JSON configuration (
~/.ssh-mcp/hosts.json).Flexible authentication: supports passwords, private keys, and SSH agent forwarding (fallback).
SFTP file transfer: upload and download files directly, including targets reached through a configured jump host.
Local port forwarding: expose an internal service's port to the local machine over the same SSH chain — including multi-hop setups — with no local
sshbinary required.Internet egress: let internal servers without direct internet access download packages through the local machine via an HTTP proxy on the jump host (
open-egress).Server-to-server transfer: copy files directly between two stored hosts — rsync on the source for large files (zero local bandwidth) or an SFTP pipe through the local machine when hosts can't reach each other (
start-transfer).Async command execution: run long-running commands (batch jobs, builds, migrations) in the background with
start-exec, then poll incremental output withexec-logsand the final result withexec-status— no MCP request timeouts on the synchronousexectool.Interactive command input: drive interactive programs (jump-host asset menus, setup wizards) by sending stdin to a running background command with
exec-inputand reading the incremental output — the run staysrunninguntil the program exits, so interrupt it withexec-cancelwhen done.Session-level interactive input: drive a bastion host's login-time TUI menu directly on the session shell — read the incremental output with
session-outputand write menu selections withsession-input, without starting a background run.Async large-file transfer: download or upload single files in the background —
start-download/start-uploadreturn a transfer id immediately and progress is polled viatransfer-status, avoiding MCP client request timeouts on multi-GB files. Transfers are resumable: interrupted transfers keep a.partfile and resume from the.partoffset on re-run, verified by size + sha256 before the final rename.Timeout & cleanup safeguards: sessions and tunnels auto-close after prolonged inactivity; commands are marked and monitored for completion.
Structured listings: query active sessions and saved hosts directly from the MCP client.
Architecture
MCP Client ─┬─> add-host / edit-host / remove-host
│
├─> list-hosts
│
├─> start-session ─┬─> PersistentSession (ssh2 shell)
│ ├─> exec (reuses shell, captures stdout/stderr)
│ ├─> start-exec ──> ExecRun (background, streaming logs)
│ │ exec-status / exec-logs / exec-cancel
│ └─> close-session / auto-timeout
│
├─> upload-file / download-file ─> temporary SFTP connection
│ └─> optional ProxyJump tunnel
│
├─> open-tunnel ──> PortForward (net.Server + conn.forwardOut)
│ close-tunnel / list-tunnels └─> dedicated SSH connection
│ └─> optional ProxyJump tunnel
│
├─> open-egress ──> InternetEgress (forwardIn on A + inline HTTP proxy)
│ close-egress / list-egress └─> local machine is the egress
│
├─> start-transfer ──> ServerTransfer (rsync on A | double SFTP pipe)
│ transfer-status / transfer-cancel └─> A --direct--> B | A -> local -> B
│
├─> start-download / start-upload ──> FileTransfer (async SFTP pipe)
│ transfer-status / transfer-cancel └─> background; transfer id returned immediately
│
└─> list-sessions
Persistent configuration → ~/.ssh-mcp/hosts.jsonEach active session maintains:
SSH connection via
ssh2Interactive shell (
conn.shell) to support multi-command pipelinesBuffered output with unique UUID markers to detect command completion
Inactivity timer (defaults to 2 hours)
Installation
This fork is not published to npm — the
ssh-mcp-sessionspackage name belongs to the upstream project. Install it directly from GitHub, which gives you thessh-mcp-toolkitexecutable.
Global install from GitHub (preferred)
npm install -g github:xunuo2345/ssh-mcp-toolkitThe prepare script compiles TypeScript during install. Launch the server anywhere by running ssh-mcp-toolkit.
From source
git clone https://github.com/xunuo2345/ssh-mcp-toolkit.git
cd ssh-mcp-toolkit
npm install # `prepare` runs the build for youThe entry point is then build/index.js; run it with node /absolute/path/to/build/index.js.
Claude Desktop
Add an entry to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the appropriate config path on Windows/Linux:
{
"mcpServers": {
"ssh-mcp": {
"command": "ssh-mcp-toolkit"
}
}
}Restart Claude Desktop after saving the file. You can now use the MCP Inspector or the command palette (Cmd/Ctrl+Shift+O) to call tools like add-host and start-session.
Claude Code (VS Code extension)
Update the Claude Code workspace settings (.vscode/settings.json or global settings) with:
{
"claude.mcpServers": {
"ssh-mcp": {
"command": "ssh-mcp-toolkit"
}
}
}Reload the window. The MCP panel will list ssh-mcp, and commands are available via the command palette (Ctrl/Cmd+Shift+P → “Claude: Run MCP Tool”).
Codex (OpenAI GPT-4o/5 with MCP)
Create or edit ~/.config/openai-codex/mcp.toml (the path may differ per platform—use the location documented by the client). Add:
[mcpServers."ssh-mcp"]
command = "ssh-mcp-toolkit"Restart Codex or re-open the MCP inspector. The ssh-mcp tools will appear under the configured servers list.
Cursor IDE
Open Cursor settings → “Model Context Protocol” (or edit ~/Library/Application Support/Cursor/mcp.json directly) and include:
{
"mcpServers": {
"ssh-mcp": {
"command": "ssh-mcp-toolkit"
}
}
}After saving, reload Cursor. The MCP sidebar exposes the server; you can invoke tools via chat or the command palette (Cmd/Ctrl+Shift+L).
Tip: If the executable is not on your
PATH(e.g. you installed from source rather than globally), point the client at the built file instead:{ "command": "node", "args": ["/absolute/path/to/ssh-mcp-toolkit/build/index.js"] }
Running the Server
ssh-mcp-toolkitThe server is purely stdio-based. Once running it prints:
SSH MCP Server running on stdioYou can register it with Claude Code or any other MCP client by pointing to the installed executable:
{
"mcpServers": {
"ssh-mcp": {
"command": "ssh-mcp-toolkit"
}
}
}If it is not on your PATH, use { "command": "node", "args": ["/absolute/path/to/build/index.js"] } instead.
Note: The server no longer accepts CLI arguments for host/user/password. Everything is configured dynamically via MCP tools.
Host Configuration
Host Storage
Hosts are persisted in
~/.ssh-mcp/hosts.json.The directory is created automatically if it does not exist.
File format:
{
"hosts": [
{
"id": "host",
"host": "host.local",
"port": 22,
"username": "user",
"password": "...", // optional
"keyPath": "~/.ssh/id_rsa" // optional
}
]
}Fields:
id(string) — unique identifier used by all session commands.host(string) — hostname or IP.port(number, default 22) — SSH port.username(string) — SSH user.password(optional string) — password auth.keyPath(optional string) — private key path; tilde expansion supported.If neither
passwordnorkeyPathis provided, the server attempts to use the local SSH agent viaSSH_AUTH_SOCK(with agent forwarding enabled).
The MCP tools ensure this file remains well-formed; never edit it manually unless you know what you’re doing.
Adding Hosts
Tool: add-host
{
"host_id": "user@host.local",
"host": "host.local",
"port": 22,
"username": "user",
"password": "optional",
"keyPath": "optional"
}host_id: new identifier. Must be unique.host: hostname or IP.port: optional (defaults to 22); provide integer > 0.username: SSH user.passwordorkeyPath: optional; configure one or rely on agent.
Example (Claude Code command palette or inspector):
/mcp mcp-remote-ssh add-host {"host_id":"host","host":"host.local","username":"user"}Listing Hosts
Tool: list-hosts
Returns text with one host per line:
id=host host=host.local:22 user=user auth=agentauth values:
password— password field presentkey— keyPath presentagent— neither password nor keyPath; agent fallback active
Editing Hosts
Tool: edit-host
{
"host_id": "user@host.local",
"port": 2222,
"password": "new-pass"
}Only supply the properties you want to change. Omitted fields remain unchanged; providing null to a field is not supported—set an empty string or remove the host instead.
Removing Hosts
Tool: remove-host
{
"host_id": "user@host.local"
}Deletes the entry from hosts.json. Active sessions using that host must be closed manually.
Session Management
Starting a Session
Tool: start-session
{
"host_id": "user@host.local"
}Optionally you can supply sessionId; otherwise, a UUID is returned.
Example response:
Listing Sessions
Tool: list-sessions
Shows all active sessions with metadata:
session=fff4… host=host.local:22 user=user uptime=3m12s lastCommand=ls -laExecuting Commands
Tool: exec
{
"session_id": "fff4b34b-56dd-4711-9555-c04e8b64249b",
"command": "pwd"
}Commands are sanitized (trimmed, length-limited).
Output is captured from the persistent shell and returned as plain text.
Non-zero exit codes raise
McpErrorwith stderr in the message.
Example output:
/home/userAsync Command Execution
start-exec runs a command in the background of an existing session's shell and returns a run_id immediately, so long-running commands (batch jobs, builds, migrations) no longer block the MCP client — the synchronous exec tool waits for completion, which can hit request timeouts. The old exec tool is retained for short, fast commands.
Starting a run
Tool: start-exec
{
"session_id": "fff4b34b-56dd-4711-9555-c04e8b64249b",
"command": "npm run build"
}The command is sanitized exactly like exec. The response returns the run_id immediately; the command keeps running afterwards:
Command '7f1c…' started on session 'fff4…'Status and full output
Tool: exec-status with run_id returns JSON with the full accumulated output, the state (running / completed / failed / cancelled), exitCode, and timestamps:
{
"run_id": "7f1c…",
"session_id": "fff4…",
"command": "npm run build",
"state": "running",
"output": "…",
"exitCode": null,
"startedAt": 1750000000000,
"finishedAt": null,
"cancelRequested": false,
"expiresAt": null
}Streaming logs
Tool: exec-logs with run_id and an offset (character index, default 0) returns only the output produced after that point. Use the returned nextOffset as the next offset to poll incrementally while the command is still running:
{ "run_id": "7f1c…", "state": "running", "output": "…new output…", "nextOffset": 412, "exitCode": null }Cancelling
Tool: exec-cancel with run_id sends Ctrl-C to the session's shell, aborting the foreground command and marking the run cancelled. Only a running run can be cancelled.
Interactive input
Tool: exec-input with run_id, text, offset (character index, default 0), and wait_ms (default 400) writes text to the run's stdin, waits wait_ms for the output it triggers, and returns the incremental output produced after offset — the same JSON shape as exec-logs. Pass the returned nextOffset as the next offset to step through an interactive session:
{ "run_id": "7f1c…", "state": "running", "output": "…new output…", "nextOffset": 821, "exitCode": null }Typical scenario: a jump-host asset-selection menu. start-exec launches the menu command, then exec-input sends the menu digits one at a time, stepping through the prompts into the target server's shell:
/mcp mcp-remote-ssh start-exec {"session_id":"<id>","command":"<资产菜单命令>","interactive":true}
/mcp mcp-remote-ssh exec-input {"run_id":"<run_id>","text":"1\n","offset":0}
/mcp mcp-remote-ssh exec-input {"run_id":"<run_id>","text":"2\n","offset":"<nextOffset>"}Interactive TUI commands never finish, so the run stays
runningwhile you interact with it — useexec-cancelto interrupt it when done.
If an interactive program exits on its own, the run stays
runningand blocks further commands on that session untilexec-cancel(marking itcancelled). 交互式程序自行退出后,run 仍显示 running,且会阻塞该会话后续命令,需用 exec-cancel 标为 cancelled 才能继续。
After an interactive program exits,
exec-inputtext is executed by the shell as a command, not delivered to the program. 程序退出后,exec-input 发送的文本会被 shell 当作命令执行,而不是传给程序。
Note: programs that trap or ignore SIGINT (Ctrl-C) may not be interruptible via
exec-cancel— the\n+ completion marker that interrupt injects will be consumed as ordinary stdin data, not as keys, so it will not reach a running command. Closing the session is the fallback. 捕获或忽略 SIGINT 的程序可能无法通过 exec-cancel 中断:中断时注入的换行与完成标记只会被当作普通 stdin 数据(而非按键)读走,不会送达正在运行的程序,可关闭会话作为兜底。
Retention
A finished run (completed, failed, or cancelled) stays queryable via exec-status / exec-logs for 10 minutes after it finishes, then is pruned automatically the next time a run is started. Running runs are never pruned. If the session is closed while a run is still running, the run resolves to failed.
Session-level interactive input
session-output and session-input work directly on a session's shell — unlike exec-input, which targets a background run created by start-exec, these tools drive the login shell itself. They exist for the case where the remote login banner is interactive: bastion hosts (AIS iFORT, 奇治 Umap) often drop you straight into a TUI asset menu as soon as start-session completes, before you can run any command.
Reading session output
Tool: session-output with session_id and offset (character index, default 0) returns the output the session's shell has produced from that offset onward, plus nextOffset for continuing:
{ "output": "…menu text…", "nextOffset": 412 }Writing input
Tool: session-input with session_id, text, offset (default 0), and wait_ms (default 400) writes text to the session shell's stdin, waits wait_ms for the output it triggers, then returns the incremental output produced after offset — the same JSON shape as session-output. Pass the returned nextOffset as the next offset to step through the menu.
Typical bastion login-menu flow — read the menu start-session already produced, then send the menu digits one at a time until you land in the target server's shell:
/mcp mcp-remote-ssh session-output {"session_id":"<id>","offset":0}
/mcp mcp-remote-ssh session-input {"session_id":"<id>","text":"1\n","offset":0}
/mcp mcp-remote-ssh session-input {"session_id":"<id>","text":"2\n","offset":"<nextOffset>"}The session output buffer is capped at 1 MB — when a session produces more, the oldest output is dropped. A long-lived, chatty session can therefore lose its earliest output; keep up by advancing
offsetas you go.The very first output of a fresh session includes shell bootstrap traces (the
export PS1=""/stty -echosetup echoed back) before any login menu appears — skip past them by advancingoffsetafter the firstsession-outputcall.
Closing Sessions
Tool: close-session
{
"sessionId": "fff4b34b-56dd-4711-9555-c04e8b64249b"
}Note: session IDs for
close-sessionusesessionId(camelCase) to remain backwards compatible with the underlying tool definition.
Legacy Helper
Function execSshCommand(hostId, command, sessionId?) remains exported for programmatic use and simply delegates through the session machinery described above.
File Transfer
File transfers use SFTP and do not need a persistent shell session. When the selected target host has proxyJump configured, the SFTP connection is tunnelled through that jump host. This supports the topology where the MCP machine can reach A, and A can reach internal hosts. Intermediate hosts only forward encrypted SSH traffic: no file or directory is created on them.
Multiple jump hosts
proxyJump can form a chain by referencing another configured host. For example, configure the locally reachable gateway first, then make each deeper host point to the immediately preceding hop:
MCP machine -> gateway -> A -> B
gateway: no proxyJump
A: proxyJump = "gateway"
B: proxyJump = "A"Use host_id: "B" with upload-file or download-file. The service creates a nested SSH forwarding channel for every hop, and the SFTP operation still runs only on B. Circular jump configurations are rejected.
Upload a file
Tool: upload-file
{
"host_id": "internal-host",
"local_path": "C:/artifacts/app.tar.gz",
"remote_path": "/opt/releases/app.tar.gz"
}The source must be a local regular file. Missing remote parent directories are created recursively. An existing remote file at remote_path is replaced.
Download a file
Tool: download-file
{
"host_id": "internal-host",
"remote_path": "/var/log/app.log",
"local_path": "C:/downloads/app.log"
}The local destination directory must already exist. An existing local file at local_path is replaced.
Asynchronous upload/download (large files)
Tool: start-upload and start-download
The synchronous tools above (upload-file / download-file) keep the connection open until the file is fully transferred, so a very large file (e.g. an 8 GB dump) can exceed the MCP client's request timeout. For those, use the asynchronous tools — they return a transfer id immediately and keep transferring in the background:
start-upload—host_id,local_path,remote_path(same semantics asupload-file; the local file must exist, missing remote parent directories are created).start-download—host_id,remote_path,local_path(same semantics asdownload-file; the local destination directory must already exist).
{
"host_id": "internal-host",
"local_path": "C:/artifacts/large.tar.gz",
"remote_path": "/opt/releases/large.tar.gz"
}Response:
Upload '0f7c...' started: 'C:/artifacts/large.tar.gz' -> internal-host:/opt/releases/large.tar.gzProgress is polled with transfer-status (pass the returned transfer id) and the transfer can be stopped with transfer-cancel — the same async trio used by server-to-server transfers. The transfer-status JSON now includes a kind field identifying the transfer type:
| Transfer |
| server-to-server ( |
| async local ← remote ( |
| async local → remote ( |
The synchronous download-file / upload-file tools are retained for small files and backward compatibility. Like server-to-server transfers, async transfers run inside the MCP server's own SSH session — closing the laptop or the MCP server interrupts them.
Async transfers are resumable. The file is first written to a <目标>.part intermediate file (the destination path plus a .part suffix). If the transfer is cancelled, fails, or is otherwise interrupted, the .part file is kept — re-running the same start-download / start-upload with the same destination automatically resumes from the .part offset instead of starting over. Only after the whole file has been transferred is the .part file renamed to the final destination path (replacing any existing file there at that point). Before the rename, the transferred data is verified against the source by size and sha256 hash; a mismatch marks the transfer failed and leaves the .part file in place for the next resume attempt. 传输先写入 <目标>.part 中间文件;中断或失败后 .part 会保留,重跑同一 start-download / start-upload 即自动从断点续传,成功后校验(大小 + sha256)并重命名为目标路径;校验不符则标记 failed 且保留 .part 供下次续传。
Note: seeing a
<目标>.partfile during or after an interrupted transfer is normal — it is the resume point, not garbage. Leave it in place to resume later, or delete it if you want to start from scratch. 传输中或中断后看到目标路径旁的<目标>.part文件属正常现象,它是续传断点;可保留以便后续续传,也可删除以重新开始。
Port Forwarding (Tunnels)
The server can expose a port of an internal service to the local machine over a dedicated SSH tunnel — the same style as ssh -L local port forwarding, implemented entirely in JavaScript on top of ssh2. The tunnel reuses the host's proxyJump chain, so multi-hop topologies work with no extra configuration, no local ssh binary, and no inbound firewall changes on the target network.
Typical scenario: the local machine can only reach an internal gateway over SSH, and an application (web service, database, admin console, …) runs on a machine further inside. After opening a tunnel, the service is reachable at http://localhost:PORT on the local machine.
Opening a tunnel
Tool: open-tunnel
Parameter | Type | Required | Default | Description |
| string | ✔ | — | Registered host whose SSH chain (including its |
| number | ✔ | — | Service port, 1–65535 |
| string |
| Service address as reachable from the chain's final hop | |
| number | auto-assigned | Local port to listen on | |
| string |
| Local address to bind; non-loopback binds log a warning | |
| string | auto UUID | Identifier used by |
{
"host_id": "internal-gateway",
"remote_host": "127.0.0.1",
"remote_port": 8080,
"local_port": 8080,
"tunnel_id": "web"
}Response:
Tunnel 'web' listening on 127.0.0.1:8080 -> 127.0.0.1:8080After this, http://localhost:8080 on the MCP machine reaches the internal service. With a multi-hop chain the response shows the full path, e.g.:
Tunnel 'web' listening on 127.0.0.1:8080 -> gateway -> app-server -> 127.0.0.1:8080Listing tunnels
Tool: list-tunnels
tunnel=web local=127.0.0.1:8080 -> host=app-server remote=127.0.0.1:8080 jump=gateway -> app-server state=active conns=0/1 idle=0sjump=shows the full jump path, ordirectwhen the host has noproxyJump.conns=active/totalcounts live vs. cumulative connections through the tunnel.idle=is shown while no connection is active.state=dead(withlastError=) marks a tunnel whose SSH chain dropped; the record is kept until you close it.
Closing a tunnel
Tool: close-tunnel
{
"tunnel_id": "web"
}Closes the local listener, destroys all active connections, and tears down the SSH chain (including every jump hop).
Lifecycle
Tunnels are independent of shell sessions — opening one does not require
start-session.A tunnel auto-closes after 2 hours of inactivity (the same timeout as sessions); any active connection keeps it alive.
Closing the MCP server process closes all tunnels.
If the SSH chain drops (network interruption, host restart), the tunnel transitions to
dead; a failed individual connection does not close the tunnel.
Internet Egress
Let internal servers that cannot reach the internet (B, C, …) download packages through the local machine. The server asks host A to listen on a port (remote port forwarding, ssh -R style); every connection to that port is forwarded back over the SSH tunnel to the local machine, which acts as an HTTP forward proxy (CONNECT for HTTPS, plain forwarding for HTTP).
Only the outbound SSH connection from the local machine to A is required — no inbound firewall changes on the internal network.
A-side prerequisites
The SSH server on A must allow TCP forwarding, and a non-loopback bind requires GatewayPorts:
# /etc/ssh/sshd_config (on the linuxserver docker image: /config/sshd/sshd_config)
AllowTcpForwarding yes
GatewayPorts clientspecifiedGatewayPorts clientspecified lets the client supply the bind address (the behavior open-egress needs); plain yes also works but forces binding to all interfaces.
Opening an egress
Tool: open-egress
Parameter | Type | Required | Default | Description |
| string | ✔ | — | Host (A) on which to open the proxy port |
| number | ✔ | — | Port to listen on A, 1–65535 |
| string | ✔ | — | Specific interface IP on A to bind (reachable by the machines that will use the proxy); wildcards like |
| string | auto UUID | Identifier used by |
{
"host_id": "A",
"proxy_bind": "192.168.1.10",
"proxy_port": 8080,
"egress_id": "apt-mirror"
}Response:
Egress 'apt-mirror' on A:192.168.1.10:8080 -> local internet egressUsing it from B/C
On any machine that can reach A, point your package manager at the proxy:
export http_proxy=http://192.168.1.10:8080
export https_proxy=http://192.168.1.10:8080
apt update # or: yum/dnf/apk/pip/npm/go …Listing / closing
Tool: list-egress shows one line per egress (bind address, state, connection counts, idle time, or lastError when dead). Tool: close-egress with egress_id removes the listener on A and tears down the connection.
Lifecycle
An egress auto-closes after 2 hours of inactivity (same timeout as sessions/tunnels), counted only while no connection is active.
If the SSH chain drops, the egress becomes
deadand stays visible inlist-egressuntil closed.There is no proxy authentication — the port is open to the host's network; bind
proxy_bindto a specific IP to limit exposure.
Server-to-Server Transfer
Copy a file (or directory) directly between two stored hosts without touching the local disk. Useful for moving 200GB database dumps between servers when you don't want to download and re-upload them.
Modes
Mode | Data path | When to use |
| source host → target host (rsync runs on the source) | Large files; source and target reach each other |
| source → local machine → target (two SFTP pipes) | Small files; hosts can't reach each other |
| tries | Unknown environment |
| files under | General use |
Starting a transfer
Tool: start-transfer
Parameter | Type | Required | Default | Description |
| string | ✔ | — | Source host id in |
| string | ✔ | — | Absolute path on the source host (file or directory) |
| string | ✔ | — | Target host id in |
| string | ✔ | — | Absolute destination path on the target host |
| enum |
|
| |
| number |
| Stream/direct threshold for | |
| string | auto UUID | Identifier used by |
{
"source_host": "db-primary",
"source_path": "/var/backup/full.dump",
"target_host": "db-dr",
"target_path": "/data/backup/full.dump",
"mode": "auto"
}Response:
Transfer '0f7c...' started: db-primary:/var/backup/full.dump -> db-dr:/data/backup/full.dump (mode=auto->direct)Direct mode prerequisites
direct runs rsync on the source host, so that host must:
have
rsyncand ansshclient installed, andbe able to reach the target host non-interactively (an ssh key for the target's user).
The destination's parent directory is created automatically. The source is copied with --partial --inplace --size-only so an interrupted transfer leaves a resumable partial file.
Because rsync runs with --size-only, a destination file already the same size as the source is treated as up to date and skipped without re-verifying its contents — delete the target or use hybrid/stream if you need content re-validation.
Checking status / cancelling
Tool: transfer-status with transfer_id returns JSON with state, kind, mode, sourceHost, sourcePath, targetHost, targetPath, transferredBytes, totalBytes, percent, error, createdAt, and finishedAt. Server-to-server transfers report kind: 'server'; transfers started by start-download / start-upload report kind: 'download' / 'upload', and for those one side of sourceHost / targetHost is 'local' (start-download: targetHost is 'local'; start-upload: sourceHost is 'local'). Tool: transfer-cancel with transfer_id stops a running transfer.
Lifecycle & limitations
Transfers run inside the MCP server's own SSH sessions — closing the laptop or the MCP server interrupts them.
streammode supports single files only; directories requiredirect(orauto, which routes directories todirect).Terminal transfers stay queryable via
transfer-statusfor the lifetime of the MCP server process.source_hostandtarget_hostmust differ.
Authentication Modes
Password — stored in
hosts.json; transmitted tossh2during connection.Private key —
keyPathread at runtime; supports encrypted keys (prompt user to setSSH_MCP_KEY_PASSPHRASEbefore launch if needed).SSH agent (fallback) — if neither password nor key is set and
SSH_AUTH_SOCKis present, the agent is passed tossh2(agentForward: true).
Timeouts & Inactivity Handling
Each session has a global inactivity timeout (default 2 hours). Timer resets whenever a command executes successfully, on incoming command output, and on
exec-input/session-input. Inactivity means no command output and no input activity — a continuously-streaming (tail -f-style) or interactive session stays alive. 闲置指既无命令输出也无输入活动:持续流式输出或正在交互的会话不会被回收。If the timer elapses, the session cleans up the SSH connection, shell, and resolver buffer, and removes itself from
activeSessions.Command completion uses a UUID marker:
printf '__MCP_DONE__{uuid}%d\n' $?. Output before the marker is returned; numeric code after the marker becomes the exit status.Each tunnel shares the same 2-hour inactivity timeout, but only counts while no connection is flowing through it. The timer is cleared as soon as a connection opens and restarts after the last one closes. Internet egress tunnels behave the same way.
Directory Structure
ssh-mcp-toolkit/
├── build/ # Compiled JS output (npm run build)
├── src/index.ts # Primary MCP server implementation
├── test/ # Vitest tests (CLI-only; integration tests skipped)
├── package.json
├── README.md # This document
└── ~/.ssh-mcp/hosts.json # Created at runtime (per user)Using the MCP Tools
Below is a typical workflow using Claude Code (commands start with /mcp), but the same JSON payloads apply to any MCP inspector.
Add host
/mcp mcp-remote-ssh add-host {"host_id":"host","host":"host.local","username":"user"}Start session
/mcp mcp-remote-ssh start-session {"host_id":"host"}→ returns
session_idRun commands
/mcp mcp-remote-ssh exec {"session_id":"<id>","command":"pwd"} /mcp mcp-remote-ssh exec {"session_id":"<id>","command":"ls -la"}Run long-running commands in the background (optional)
/mcp mcp-remote-ssh start-exec {"session_id":"<id>","command":"npm run build"} /mcp mcp-remote-ssh exec-logs {"run_id":"<run_id>","offset":0} /mcp mcp-remote-ssh exec-status {"run_id":"<run_id>"}→
start-execreturns arun_idimmediately; pollexec-logs(nextOffset→ nextoffset) for incremental output andexec-statusfor the final result. Cancel withexec-cancel.Interactive programs (menus, REPLs) must be launched with
interactive:true;exec-inputis only valid against such runs, driving their stdin and reading the incremental output:/mcp mcp-remote-ssh start-exec {"session_id":"<id>","command":"<菜单命令>","interactive":true} /mcp mcp-remote-ssh exec-input {"run_id":"<run_id>","text":"1\n","offset":0}Transfer files (optional)
/mcp mcp-remote-ssh upload-file {"host_id":"host","local_path":"C:/build/app.tar.gz","remote_path":"/tmp/app.tar.gz"} /mcp mcp-remote-ssh download-file {"host_id":"host","remote_path":"/var/log/app.log","local_path":"C:/downloads/app.log"}Expose an internal service (optional)
/mcp mcp-remote-ssh open-tunnel {"host_id":"host","remote_port":8080,"local_port":8080}→ returns a tunnel id; the service is then reachable at
http://localhost:8080. Close it withclose-tunnel.Provide internet access to internal machines (optional)
/mcp mcp-remote-ssh open-egress {"host_id":"A","proxy_bind":"192.168.1.10","proxy_port":8080}→ returns an egress id; machines that can reach A can then use
http://192.168.1.10:8080as their HTTP proxy. Close it withclose-egress.Transfer files between servers (optional)
/mcp mcp-remote-ssh start-transfer {"source_host":"db-primary","source_path":"/var/backup/full.dump","target_host":"db-dr","target_path":"/data/backup/full.dump"}→ returns a transfer id;
transfer-statusshows progress,transfer-cancelstops it.Transfer large files asynchronously (optional)
/mcp mcp-remote-ssh start-download {"host_id":"host","remote_path":"/var/log/app.log","local_path":"C:/downloads/app.log"} /mcp mcp-remote-ssh start-upload {"host_id":"host","local_path":"C:/build/app.tar.gz","remote_path":"/tmp/app.tar.gz"}→ each returns a transfer id immediately;
transfer-statusshows progress,transfer-cancelstops it. Prefer these overdownload-file/upload-filefor files large enough to time out the synchronous tools.Inspect
/mcp mcp-remote-ssh list-sessions /mcp mcp-remote-ssh list-hosts /mcp mcp-remote-ssh list-tunnelsClose session / tunnel
/mcp mcp-remote-ssh close-session {"sessionId":"<id>"}
/mcp mcp-remote-ssh close-tunnel {"tunnel_id":"<id>"}
/mcp mcp-remote-ssh close-egress {"egress_id":"<id>"}
/mcp mcp-remote-ssh transfer-status {"transfer_id":"<id>"}
/mcp mcp-remote-ssh transfer-cancel {"transfer_id":"<id>"}Testing
Unit tests (Vitest):
npm run testIntegration smoke tests for SSH are not included by default because they require external infrastructure. You can manually validate with the workflow above.
Troubleshooting
Symptom | Possible Cause | Suggested Action |
| Duplicate | Use |
| Missing entry | Run |
| Remote command returned non-zero | Inspect the command output. The session remains open. |
Session disappears from | Inactivity timeout reached | Start a new session or reduce idle periods. |
Permission denied (publickey) | Missing credentials | Ensure |
|
| Provide an absolute/tilde path that exists. |
| The requested | Pick another port, or omit |
Tunnel shows | SSH chain dropped (network, restart) | Read |
| sshd on A denies remote forwarding | Set |
Security Considerations
Treat
~/.ssh-mcp/hosts.jsonas sensitive; it may contain passwords or key paths.Prefer key-based or agent authentication where possible.
Limit
hosts.jsonpermissions:chmod 600 ~/.ssh-mcp/hosts.json.Sessions inherit all privileges of the configured SSH user.
Long-running sessions can be closed manually or rely on the inactivity timeout.
Contributing
Fork the repo and create a branch.
Make your changes with tests and documentation updates.
Run
npm run buildandnpm run testbefore submitting a PR.Follow the Code of Conduct.
Issues and feature requests are welcome via GitHub.
Credits & Acknowledgements(致谢)
本项目是基于开源项目的二次开发,没有前人的工作就没有它。在此向原作者致以诚挚的谢意:
ssh-mcp-sessions— 作者 Justin Fry(@fryjustinc) 本仓库的直接上游,分叉自 npmssh-mcp-sessions@1.0.17(MIT)。~/.ssh-mcp/hosts.json主机档案存储、带__MCP_DONE__完成标记的常驻 shell 会话模型、密码 → 私钥 → SSH agent 的认证回退链,以及最初的 8 个 MCP 工具,全部是他的工作成果。衷心感谢。ssh-mcp— 作者 Tufan Tunç(@tufantunc) 更早的 SSH-over-MCP 项目,也是整条血脉的起点(exec工具即源于此)。上游ssh-mcp-sessions于 2025-09-29 从该项目分叉(两个仓库共有 28 个相同的提交历史),本项目代码中McpServer至今仍标着的版本号1.0.9,正是当年分叉时它的版本。感谢他打下的地基。
两个上游项目均为 MIT 许可;本 fork 继续沿用 MIT,并在 LICENSE 中保留原始版权声明。
本项目在上游 1.0.17 之上新增的能力
ProxyJump 跳板机支持 —— 主机配置新增可选的
proxyJump字段,支持任意跳数的链式穿透;完全用 JavaScript 通过 ssh2 的forwardOut实现,无需本地ssh命令,在 Windows 和 Linux 上行为一致。保存配置时会拒绝自引用、不存在的跳板机以及成环配置。SFTP 文件传输 —— 新增
upload-file与download-file两个工具,复用同一条跳板链,自动创建远端缺失的父目录,且不在中间跳板机上留下任何文件。跳板信息可见 ——
list-hosts输出附带jump=<id>,list-sessions展示完整跳板路径(jump=gateway -> a -> b)或direct。本地端口转发(隧道) —— 新增
open-tunnel/close-tunnel/list-tunnels三个工具,把内网服务的端口通过专用 SSH 隧道暴露到本机回环地址(ssh -L风格,纯 JavaScript 实现)。完全复用同一套多跳跳板链,无需本地ssh命令;每条隧道 2 小时无活跃连接自动回收,SSH 链路断开时标记为dead供list-tunnels查看。内网出网(Internet Egress) —— 新增
open-egress/close-egress/list-egress三个工具:在跳板机 A 上反向监听端口(ssh -R风格),把内网 B/C 的 HTTP 代理流量经 SSH 隧道送回本地,由本地作为出口代理访问外网。无需内网开放任何入站端口,纯 JavaScript 实现。服务器间直传(Server-to-Server Transfer) —— 新增
start-transfer/transfer-status/transfer-cancel三个工具:在两台已保存主机之间传输文件。direct在源服务器上用 rsync 直连目标(本地 0 带宽,适合 200GB 级别备份);stream经本地双 SFTP 流转发(适合小文件或服务器间不通);hybrid先直连失败降级流式;auto按大小阈值自动选择。异步三件套便于大文件后台跟踪与取消。异步大文件下载/上传(Async Large-File Transfer,支持断点续传) —— 新增
start-download/start-upload两个工具:单文件经 SFTP 后台异步传输,立即返回 transfer id,用transfer-status轮询进度、transfer-cancel取消,避免大文件(如 8GB)在同步的download-file/upload-file上触发 MCP 客户端请求超时;transfer-status的kind字段区分server/download/upload三类传输。传输写入<目标>.part中间文件,中断/失败后保留并可自动断点续传,成功后经大小 + sha256 校验再 rename 为目标路径。旧同步工具保留用于小文件与向后兼容。单元测试 —— 覆盖主机 schema、跳板链解析与跳板配置校验。
会话级交互式输入 —— 新增
session-output/session-input两个工具:直接读写会话 shell 的增量输出与 stdin,无需后台 run。典型场景是堡垒机(AIS iFORT、奇治 Umap)登录后立即弹出的 TUI 资产菜单:start-session后先session-output读菜单,再用session-input逐层输入数字进入目标服务器。与exec-input的区别在于它作用于 session 本身而非start-exec的 run;会话输出缓冲上限 1MB(最旧部分被丢弃),初始输出可能混有 shell 引导回显(export PS1=""/stty -echo)。
以上新增均向后兼容:原有的 hosts.json 与原有的工具调用方式行为完全不变。
License
Happy automating! If this project improves your workflow, please star the repository or share feedback. Your contributions help make remote development safer and simpler for everyone.
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
- AlicenseAqualityAmaintenanceA server that enables secure interaction with remote SSH hosts through standardized MCP interface, providing functions like listing hosts, executing commands, and transferring files using native SSH tools.737190MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for SSH remote execution, file transfer, and file editing with automatic backup/trash and ~/.ssh/config integration.1171MIT
- AlicenseAqualityBmaintenanceMCP server for managing remote servers via SSH, enabling command execution, file transfer, rsync, tunnels, health checks, backups, and database operations.176281MIT
- AlicenseBqualityAmaintenanceMCP server for managing SSH tunnels and remote hosts, offering tools for host inventory, file operations, security scanning, and SSH configuration.82MIT
Related MCP Connectors
A MCP server built for developers enabling Git based project management with project and personal…
2,000+ MCP servers read at source level. Know what one does before you connect. Free, no key.
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
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/xunuo2345/ssh-mcp-toolkit'
If you have feedback or need assistance with the MCP directory API, please join our Discord server