Remote SSH MCP logo

Remote SSH MCP

为 AI Agent 提供持久、有状态的远程 Bash 会话
基于 Model Context Protocol

为什么 · 功能 · 原理 · 工具 · 快速开始 · 配置 · 安全 · English

MIT License Node.js 20+ TypeScript MCP stdio OpenSSH Version 0.2.2 GitHub release npm version GitHub stars

Remote SSH MCP — AI 终端通过 SSH 连接远程服务器

--- ## 为什么需要 Remote SSH MCP? 多数 Agent 访问远程机器的方式是: ```text bash → ssh host "cmd" → 断开 → 再来一次 ``` 每次调用都在交同样的税: | 痛点 | 表现 | |------|------| | 🔁 **Token 浪费** | banner、MOTD、登录噪声、`pwd` / `whoami` 探测反复灌进上下文 | | 🧊 **状态丢失** | `cwd`、`export`、虚拟环境、shell 副作用无法延续 | | 🔌 **不稳定** | 每次新建连接:超时、host key、ProxyJump、鉴权抖动 | | 🌀 **错误放大** | 模型用更长探测命令补偿不确定性 → 更费 token | **Remote SSH MCP** 把一条长生命周期的远程 Bash 做成一等 MCP 工具。相同 session id 会保留工作目录、环境变量和 shell 副作用;需要干净环境时关闭旧 session,再新开一个。 ### 为什么选我们,而不是在 bash 里拼 `ssh`? 面向 Agent 多步远程任务(部署、排障、构建、看日志)的收益对照: | 维度 | 一次性 `ssh host "…"` | **Remote SSH MCP** | |------|----------------------|---------------------| | 💰 **Token** | 每一步都重交连接噪声 + 状态探测;模型常反复 `cd` / `pwd` | **`ssh_open` 只付一次**;后续 `ssh_run` 主要返回**命令本身输出**。结构化工具 + 头尾截断压住结果体积。多步会话里,相对「每次重连」通常可 **少约 50–80% 的远程工具上下文噪声**(取决于 MOTD 大小与模型是否爱探测)。 | | ✅ **成功率** | *N* 步 ≈ *N* 次握手 → *N* 次失败机会(超时、跳板、agent、host key) | 每个 session **只握手一次**;后续命令走已存活 shell。长任务用 `running` + `ssh_peek`,不必因工具调用超时就整段重来。重连变少 → **任务中途「SSH 又挂了」的假失败环显著减少**。 | | 🧳 **便携性** | 远端本身也无需装东西——但**每台跑 Agent 的机器**都要重复同一套脆弱的 `ssh …` 拼装 | **只在跑 Claude / Cursor / Grok 等的本机装一次**。**远端机器零安装**(不要 Node、不要 MCP 守护进程、不要常驻 agent)。只要普通 shell 账号 + SSH 本就会用的工具(`bash`、`base64`、`stty` 等)。密钥与跳板仍在**本机** `~/.ssh/config`。 | | 🧠 **模型心智** | 模型自己编 `ssh` 字符串、转义与恢复逻辑 | 稳定工具面:`open → run → peek → close`,session id 是唯一句柄 | | 🔐 **信任边界** | 容易把密钥读进上下文,或在带内要密码 | 只走本机 OpenSSH;工具绝不接收密码 / 私钥内容 | **便携性一句话:MCP 装在你的开发机 / AI 宿主上;`ssh_config` 里已有的机器都能管——服务器机群不用装任何包。** ```text ┌─────────────────────────┐ SSH(OpenSSH) ┌──────────────────┐ │ 笔记本 / CI Agent 机 │ ───────────────────────────► │ prod / staging │ │ Claude · Cursor · Grok │ ~/.ssh/config · agent │ 无需安装 MCP │ │ + remote-ssh-mcp │ │ 普通 Bash 即可 │ └─────────────────────────┘ └──────────────────┘ ``` **Token 示意(多步远程排障,示意而非基准测试):** ```text 一次性路径(每步 × 8): ssh 包装 + banner/MOTD + pwd/whoami + 重新 cd + 命令输出 → 噪声占主导,上下文被重连垃圾填满 会话路径: ssh_open → 一次(握手 + READY) ssh_run × 8 → 主要是真实 stdout/stderr(头尾截断) → 上下文留给工作产物,而不是传输层 ``` 实现**不重写 SSH**,而是复用本机 OpenSSH client,因此 `~/.ssh/config`、known_hosts、SSH agent、ProxyJump 和硬件密钥策略仍然生效。 ```text ssh_hosts() → 发现允许的 Host 别名 ssh_open(host) → session id ssh_run(id, command) → 同一 cwd + 环境 ssh_peek / ssh_interrupt → 观察或恢复长任务 / 卡住的命令 ssh_close(id) → 释放 shell 与连接 ``` --- ## 功能特性 ### 🧠 持久远程会话 - 一个**稳定 session ID** 对应一条长生命周期远程 Bash - **`cwd` 与环境变量** 在多次 `ssh_run` 之间保留 - 需要干净环境时**新开 session** 即可 - 可对同一或不同 host 开多个 session(受 `maxSessions` 限制) ### 🔧 原生 OpenSSH 集成 - 调用真实的 **`ssh` 可执行文件**,不自造加密栈 - 完整尊重 **`~/.ssh/config`**、`Include`、agent socket 与 **ProxyJump** - 强制 **`BatchMode=yes`** 与 **`StrictHostKeyChecking=yes`** - 绝不从模型侧接收密码、私钥文本或任意 SSH 参数 ### 📡 友好支持长任务 - `ssh_run` 最多同步等待 `wait_sec`(默认 **10 秒**),到期返回 `status: "running"`,远端命令继续 - 用 **`ssh_peek(wait_sec=...)`** 做长轮询,而不是空转 - 可选硬超时 **`timeout_sec`** 才会发 Ctrl-C;默认不自动杀进程 - 适合 `docker pull`、构建、下载、部署等不应占死工具调用的任务 ### 🛡️ 安全与控制面 - 来自 `ssh_config` + 配置 / 环境变量的**精确 Host 别名 allowlist** - 含 `*`、`?`、`!` 的模式不会进入 allowlist - **Fail-closed 中断**:Ctrl-C 后若无法确认 shell 恢复,则关闭 session - 内置少量明显高风险命令的 **denylist**(不是完整策略引擎) - **空闲回收**、session 上限,以及权限 `0600` 的 **JSONL 审计日志**(命令哈希) ### 📦 面向模型的干净输出 - **`stdout` / `stderr` 分离** - 头尾 **字节截断**,并保证 UTF-8 边界完整 - **展示前剥离 ANSI / PTY 噪声**(颜色、CSI、bracketed-paste 标记、纯控制空行) - **安静 open-frame**:`TERM=dumb`、`NO_COLOR`、关闭 bracketed-paste,从源头少产生垃圾输出 - **精简 JSON 载荷**:省略空 `stderr`、`false` 截断标志与请求回显字段,双通道 `content` + `structuredContent` 更省 token - `ssh_hosts` 只返回安全元数据:`alias`、`hostname`、`user`、`port`、`proxy_jump` - 永不泄露 `IdentityFile`、证书、agent socket 或 `ProxyCommand` ### 🔌 原生 MCP - **stdio** 传输,适配 Claude Desktop、Cursor 等 MCP 宿主 - 兼容新旧 MCP 握手 - 宿主 / stdio 退出时回收全部已建立及正在建立的 SSH 连接 --- ## 工作原理 ```mermaid flowchart LR A[AI Agent] -->|MCP 工具| B[Remote SSH MCP] B -->|spawn| C[OpenSSH client] C -->|SSH + PTY| D[远程 Bash] D --> E[(cwd / env / 副作用)] subgraph 本机 B C F[~/.ssh/config
agent / keys] C -.-> F end subgraph 远程主机 D E end ``` **推荐 Agent 流程** ```text 1. ssh_hosts() # 从 allowlist 选别名 2. ssh_open(host="prod") # 得到 session id "s_…" 3. ssh_run(id, "cd app && …") # 状态绑定在此 id 4. ssh_run(id, "npm test") # 仍在 app/,环境保留 5. ssh_peek(id, wait_sec=20) # 长轮询慢任务 6. ssh_close(id) # 用完清理 ``` --- ## MCP 工具 | 工具 | 作用 | |------|------| | 🗂️ **`ssh_hosts`** | 列出允许的 Host 别名(仅安全元数据)。改完 `~/.ssh/config` 后传 `reload=true` | | 🔓 **`ssh_open`** | 为允许的 Host 别名新建持久 shell → 返回 session `id` | | ▶️ **`ssh_run`** | 在已有 session 中执行非交互命令 | | 👀 **`ssh_peek`** | 查看状态与最新 N 行输出;可选 `wait_sec` 在运行中长轮询 | | ⛔ **`ssh_interrupt`** | 发送 Ctrl-C,并等待确认 shell 已恢复 | | 📋 **`ssh_list`** | 列出 session、cwd、状态、idle 回收倒计时与容量 | | 🔒 **`ssh_close`** | 清理远程临时状态并关闭连接 | ### 主要参数 | 工具 | 关键参数 | |------|----------| | `ssh_open` | `host`(必填 Host 别名),可选 `name` 标签 | | `ssh_run` | `id`、`command`,可选 `wait_sec`、可选 `timeout_sec` | | `ssh_peek` | `id`,可选 `lines`(默认 50,最大 1000),可选 `wait_sec` | | `ssh_interrupt` / `ssh_close` | `id` | | `ssh_hosts` | 可选布尔 `reload` | --- ## 快速开始 ### 环境要求 | 要求 | 说明 | |------|------| | **Node.js** | 20 或更新 | | **OpenSSH client** | 系统 `ssh` 在 PATH 上(或配置 `sshPath`) | | **远程主机** | Bash 以及 `base64`、`stty`、`mkdir`、`cat`、`rm` | | **SSH 配置** | Host 别名已写入 `~/.ssh/config`,host key 已信任 | > ⚠️ 首次连接的 host key 确认与鉴权请在普通终端完成。MCP 服务不会弹出密码或信任提示。 ### 安装 ```bash git clone https://github.com/the-nine-nation/remote-ssh-mcp.git cd remote-ssh-mcp npm install npm run build npm test ``` 启动: ```bash node /absolute/path/to/remote-ssh-mcp/dist/index.js ``` 或从 npm 安装(发布后): ```bash npx @zyluo/remote-ssh-mcp # 或 npm install -g @zyluo/remote-ssh-mcp remote-ssh-mcp ``` 本地从本仓库安装为包之后,也可使用 `remote-ssh-mcp` 可执行文件。 ### MCP 宿主配置 使用 stdio 的宿主通常类似下面(外层键名因产品而异): ```json { "mcpServers": { "remote-ssh": { "command": "node", "args": [ "/absolute/path/to/remote-ssh-mcp/dist/index.js" ], "env": { "SSH_MCP_ALLOWED_HOSTS": "prod,staging" } } } } ``` **Cursor** · **Claude Desktop** · **Claude Code** 等:把 `command` / `args` 指到构建好的 `dist/index.js`,并设置 `SSH_MCP_ALLOWED_HOSTS`(或依赖从 `~/.ssh/config` 自动发现)。 `SSH_MCP_ALLOWED_HOSTS` 是**附加** allowlist。默认还会读取 `~/.ssh/config` 及其 `Include` 中的精确 `Host` 别名。工具参数只接受安全别名,不接受 `user@host`、端口或额外 SSH 选项。 修改 `~/.ssh/config` 后调用 `ssh_hosts(reload=true)` 即可,无需重启 MCP。 ### 凭证边界 认证只发生在本机 OpenSSH client 内部: - 工具不接受密码 / 私钥参数 - `ssh_hosts` 不返回密钥路径、证书、agent socket 或 `ProxyCommand` - Agent 用 Host 别名调用 `ssh_open` 即可,**不要**用本地文件工具去读 `~/.ssh` 私钥 --- ## 配置 可选配置文件默认路径: ```text ~/.config/remote-ssh-mcp/config.json ``` ```json { "allowedHosts": ["prod", "staging"], "sshConfigPath": "~/.ssh/config", "sshPath": "ssh", "maxTimeoutSec": 1800, "defaultWaitSec": 10, "maxWaitSec": 30, "openTimeoutSec": 20, "idleTimeoutSec": 1800, "interruptGraceSec": 5, "maxSessions": 8, "outputMaxBytes": 32768, "outputHeadBytes": 4096, "auditLogPath": "~/.local/state/remote-ssh-mcp/audit.jsonl" } ``` ### 环境变量 | 环境变量 | 作用 | |----------|------| | `SSH_MCP_CONFIG` | 配置文件路径 | | `SSH_MCP_ALLOWED_HOSTS` | 逗号分隔的附加 Host allowlist | | `SSH_MCP_SSH_CONFIG` | SSH config 路径 | | `SSH_MCP_SSH_PATH` | OpenSSH 可执行文件 | | `SSH_MCP_MAX_TIMEOUT_SEC` | 显式 `timeout_sec` 的允许上限 | | `SSH_MCP_DEFAULT_WAIT_SEC` | `ssh_run` 返回 `running` 前的默认等待 | | `SSH_MCP_MAX_WAIT_SEC` | `ssh_run` / `ssh_peek` 的 `wait_sec` 上限 | | `SSH_MCP_OPEN_TIMEOUT_SEC` | 建连 / 握手超时 | | `SSH_MCP_IDLE_TIMEOUT_SEC` | idle 自动回收时间 | | `SSH_MCP_INTERRUPT_GRACE_SEC` | Ctrl-C 后等待 marker 的宽限期 | | `SSH_MCP_MAX_SESSIONS` | 最大并发 session 数 | | `SSH_MCP_OUTPUT_MAX_BYTES` | stdout、stderr 各自保留上限 | | `SSH_MCP_OUTPUT_HEAD_BYTES` | 截断时保留的头部字节数 | | `SSH_MCP_AUDIT_LOG` | JSONL 审计日志路径 | 环境变量覆盖配置文件。审计日志权限固定为 `0600`,记录 session、host、状态、时长、命令长度、命令名和 SHA-256;**不**记录完整命令参数,降低凭证入日志的风险。 --- ## 执行语义 | 主题 | 行为 | |------|------| | **并发** | 同一 id 同时只跑一个前台命令;再次 `ssh_run` 返回 `busy` | | **`wait_sec`** | 只限制 **MCP 调用** 等待时长;到期返回 `running`,远端继续 | | **不要重试** | 收到 `running` 后不要重发同一长命令 — 用 `ssh_peek` 轮询 | | **硬超时** | 只有显式 `timeout_sec` 才会在到期后 Ctrl-C | | **`ssh_peek`** | 默认最新 50 行(最大 1000);字节上限仍生效;可长轮询 | | **stdin** | 用户命令 stdin 为 `/dev/null` — 不要跑 `vim`、`top`、交互安装器 | | **中断恢复** | Ctrl-C + 宽限期等协议 marker;恢复失败则关闭 session(fail-closed) | | **输出** | stdout / stderr 独立保留头尾,始终在合法 UTF-8 边界截断 | | **Denylist** | 只拦少量高风险模式,不是完整策略引擎 | | **信任模型** | 面向本机可信开发者 — **不是**多租户远程执行服务 | | **宿主退出** | MCP 宿主 / stdio 断开会回收 SSH;`nohup` / `setsid` 进程可能继续 | **示例:** 用 `wait_sec: 10` 启动 `docker pull`,不设 `timeout_sec`。返回 `running` 表示原 pull 仍在进行 — **不要**再启一次。用带正 `wait_sec` 的 `ssh_peek` 等到 `idle`,或 `ssh_interrupt`,或另开 session 做并行工作。 --- ## 开发 ```bash npm run typecheck npm test npm run build npm audit --omit=dev ``` 测试覆盖 MCP stdio 发现与调用、cwd / 环境持久化、流分离、任意分片边界上的帧解析、超时 fail-closed、shell 死亡、allowlist 发现、输出截断与 denylist。 协议与设计决策见 [远程SSH-MCP设计.md](./远程SSH-MCP设计.md)。 --- ## 安全 请**不要**通过公开 GitHub Issue 报告安全漏洞。在配置私有安全公告流程之前,请通过维护者 [GitHub 主页](https://github.com/the-nine-nation) 上的邮箱联系。 即使 MCP 传输正常,远程命令也可能产生不可逆副作用。请使用最小权限账号、收紧 allowlist,并仔细审查目标主机权限。 --- ## 项目状态 | 项 | 状态 | |----|------| | 版本 | **0.2.2** | | 许可证 | [MIT](./LICENSE) | | 语言 | TypeScript(Node ≥ 20) | | 协议 | MCP over stdio | | 到主机的传输 | 系统 OpenSSH | --- ## 更新日志 ### 未发布 — 连接与进度反馈优化 - 自定义 `sshConfigPath` / `SSH_MCP_SSH_CONFIG` 现在同时用于主机发现和实际连接,通过 OpenSSH `-F` 传入。默认 `~/.ssh/config` 保留原生行为;自定义文件遵循 `-F` 语义,不再读取系统级配置。 - 主机发现兼容 `Host=prod`、`HostName = example.com` 和 `Include=...` 写法。 - `ssh_open` 失败时在原始错误之外返回 `reason` 和 `hint`,提供主机信任、认证、DNS、网络、超时和本机配置的排查方向。 - 短进度输出及时显示,仅在末尾可能是未接收完整的协议标记时暂存相关字节。 - 已完成命令的 `ssh_peek` 增加 `command_status` 和 `duration_ms`,区分 shell 空闲与命令执行结果。正在等待的 peek 遇到会话关闭时,保留已收到的输出及超时/中断原因;会话移除后的新调用仍返回 `session_gone`。 ### 0.2.2 — 更安静的远端输出,更少 token PTY + 交互式 bash 常注入转义序列;经 JSON 转义后像「二进制」(`\u001b[?2004h`、颜色 CSI、光标码等),每次 `ssh_peek` / `ssh_run` 都在浪费上下文。 | 改动 | 作用 | |------|------| | **展示时净化** | 剥离 ANSI/OSC/CSI,按 CR 覆盖(进度条),丢掉纯控制空行,再应用 `lines` 窗口 | | **安静会话打开** | 导出 `TERM=dumb` / `NO_COLOR` / `CLICOLOR=0`,关闭 bracketed-paste,open 时发送一次 `\033[?2004l` | | **精简工具载荷** | 省略空 `stderr`、`false` 标志(`truncated`、`interrupted` 等)与回显的 `lines`;保留空 `stdout` 以明确「无输出」 | | **测试** | 覆盖 sanitize、open-frame 安静化、session 展示路径与 slim JSON | 升级:`npm i -g @zyluo/remote-ssh-mcp@0.2.2`(或在 MCP 配置中 bump 版本),然后**重启 MCP 进程**以加载新服务端。 ### 0.2.1 - 修复 open frame 被 PTY 回显时 READY 标记解析失败 ### 0.2.0 - 首次公开发布(npm / GitHub) --- ## Star History 如果这个项目帮你省了 token、少踩了断线坑,点个 ⭐ 能让更多人发现它。 Star history

给需要「真·远程 shell」的 Agent —— 而不是再拼一次 ssh host "…"