内容
mcp-ssh-terminal
[](https://www.npmjs.com/package/mcp-ssh-terminal)
[](LICENSE)
[](https://nodejs.org)
[](https://modelcontextprotocol.io)
MCP 服务器,提供给 AI 代理 **持久、完全交互的 SSH 会话** —— 一个真正的 shell 进入远程主机,表现得像您在本地终端输入一样。
- **会话在工具调用之间持久存在** —— 连接一次,运行命令,检查输出,运行更多;状态(工作目录、环境、正在运行的程序)得以保留。
- **真正的终端,而非命令** —— 任意(Ctrl-C-X、箭头Tab 键、)都能忠实,因此交互式 TUI、分页器、选项全和提示符工作。
- **非 Unix 命令行界面也支持**设备如 **Mikrotik RouterOS** 是 一等公民,因为会话是一个真正的 PTY。
- **您现有的 SSH 配置直接生效** —— `~/.ssh/config` 别名、`IdentityFile`、`ssh-agent`、`ProxyJump` 链和 `known_hosts` 都由真正的 OpenSSH 客户端处理,不做任何改动。
```text
ssh_connect { "host": "prod-web" } → s1 + 登录屏幕
ssh_send { "session": "s1", "text": "htop", "appendEnter": true }
ssh_send { "session": "s1", "keys": ["F5"] } → 树形视图,已渲染
ssh_interrupt { "session": "s1" } → Ctrl-C,返回提示符
```
## 目录
- [工作原理](#工作原理)
- [需求](#需求)
- [安装](#安装)
- [与 MCP 客户端注册](#与MCP客户端注册)
- [Docker](#docker-可选)
- [工具](#工具)
- [特殊按键](#ssh_send的特殊按键)
- [使用实例](#使用实例)
- [安全注意事项](#安全注意事项)
- [故障排除](#故障排除)
- [开发](#开发)
## 工作原理
### 决策 1:驱动真正的 OpenSSH 客户端(`ssh`)在 PTY 内
而不是在 JavaScript 中重新实现 SSH,每个会话都启动您实际的 `ssh` 二进制文件,并将其附加到伪终端上。您可能需要手动构建的每个连接需求都是 OpenSSH 已经正确处理的东西:
| 需求 | 如何满足 |
| --- | --- |
| 遵守 `~/.ssh/config` | `ssh` 读取它原生 — `Host` 别名、`IdentityFile`、`Match`、`Include`,所有这些。我们的连接路径中没有自定义配置解析。 |
| ProxyJump A → B → C | 配置中的 `ProxyJump`,或 `jump` 参数(`ssh -J`),完全由 `ssh` 处理。 |
| known_hosts 验证 | `ssh` 的默认 `StrictHostKeyChecking` 行为保持不变。新主机的指纹提示出现在屏幕上;您使用 `ssh_send` (`yes`)批准。 |
| 基于密钥的身份验证 | `ssh` 使用您的 `IdentityFile`、`ssh-agent`、加密密钥、FIDO 令牌 — 不变。密码/密码/2FA 提示在屏幕上显示,您使用 `ssh_send` 回答。 |
| 任意控制字符 | 子进程在 PTY 上运行,因此原始字节(0x00–0x10x7F, escape 准确到达远程禁用 ssh 转(`-e none`),以便不会拦截任何内容。 |
| 非 Unix shell(RouterOS 因为它是一个忠实的终端(PTY + `- 强制远程 PTY),交互式 CLI、分页卡补全表现正常。 |
| 无中断会话 | `ssh` 进程是长寿命的,并由会话 ID 跟踪;您连接一次并反复交互。默认情况下设置保持活动(`ServerAliveInterval=30`,`ServerAliveCountMax=3`),以便 NAT/conntrack 不会在不活动时默默地丢弃会话;可以通过 `extraArgs` 覆盖。 |
纯 JS 库(例如 `ssh2`)将迫使我们重新实现 ssh_config 解析、`Match` 逻辑、ProxyJump 链接、known_hosts 哈希/验证和密钥处理 — 一个庞大且安全性敏感的接口,而 OpenSSH 已经做得很好。委托给 `ssh` 意味着服务器的行为与您自己的 `ssh <host>` 命令 **完全一致**。
### 决策 2:用于读取输出的无头终端模拟器
原始 PTY 输出是充满移动、颜色和的字节流。将喂给代理读的。因此, [`@xterm`](https://www/package/@xterm/headless) 进行管道模拟器维护一个虚拟工具返回看到的渲染屏幕折叠最终状态, escape为纯文本。(当您需要底层字节`ssh_read`有 `raw模式。)模拟器还回答远程端的终端查询标位置报告、设备属性),以便不会挂起探测终端的### 如何判断是否完成?”
输入后,服务器输出**在tleMs` 后止**(默认为 500 毫秒)或达到 `timeoutMs`渲染。长时间运行的命令在超时时返回部分输出,并提示再次调用 `ssh_read` — 不会丢失任何输出,短命令感觉很快。
### 终端复用
两个级别:
- **多个并发会话** — 同时连接到多个主机;每个主机都有自己的会话 ID(最多 32 个并发会话)。
- **在一个会话中使用 tmux/screen** — 由于每个会话都是一个真正的终端,您可以在远程主机上ux`,控制键驱动它ssh_send { keys: ["C-b", " }` 等) 需求
-.js ≥ 22**fs.glob`,用于配置列表,不存在20)
- ` 上的 **客户端**(`)— 与 使用的相同。
/Linux。(`node-pty` 附带的二进制文件;在 上,的 `spawn-helper` 必须是可执行的 —[故障排除](#故障排除)。)
##**从 npm** —无需操作;npx` 直接下一节)。或者全局安装:
```bash
npm install -g mcp-ssh-terminal
```
**从源码:**
```bash
git clone https/zhdkirill/mcp-ssh-terminal.git mcp-
npm install
build # 将 编译为 dist test # 可选:运行测试(密钥构建、ssh实时 PTY 会```
## 与客户端注册
** Code(CLI):bash
claude add ssh -- ny mcp-
# 或,从出:
claude mcp add ssh -- node/to/mcp-ssh-terminal/dist/index.js
```
**基于项目的 `.mcp.json` 或 Claude:**
```json
"mcpServers": {
"ssh": {
"commandnpx",
": ["-y", "mcp-ssh-terminal"]
}
}
(对于源码检使用 `"commandnode", "args"/path/to/mssh-terminal/dist/index.js"]` 代替。)通过 stdio 使用只在 stdout协议流量(日志),因此可以在任何 stdio MCP 主机上安全运行。
## Docker(可选)
**如果可以,请在本地运行。** 整个设计都将身份验证委托给 **您的** OpenSSH — 配置、密钥、代理、known_hosts — 并且容器除非您挂载,否则看不到这些。正确的选择它的机器不应该需要 Node,或者您想要一个固定的、密封的如果您依赖 ssh-agent、FIDO 密钥或仅在主机上存在的 VPN 路由,这是不正确的。
```bash
docker build -t mcp-ssh-terminal .
```
注册(以只读方式挂载您的 ssh 配置/密钥```bash
claude mcp add ssh -- docker run -i --rm --init -v "$HOME/.ssh:/home/node:ro" mcp-ssh-terminal
```
容器的注意事项:
- **`-i` 是强制性的** (stdio 传输),并且建议使用` (正确的 PID-1 信号处理;处理 SIGTERM 本身)。
- **ssh-agent:** Linux:添加 `-v "$SSH_AUTH_SOCK.sock" -e SSH_AUTH_SOCK=/agent.sock`。Docker Desktop for Mac:`-v /-services/ssh-auth.sock:/agent.sock -_AUTH_SOCK=/agent.sock`。如果没有代理,挂载的密钥仍然有效(您将地输入密码短语 via `ssh_send`)。
- **known_hosts:** 使用只读挂载,新接受的主机密钥无法持久化 — ssh 会发出继续进行该会话想记住它们,请以读写方式挂载 `~/.ssh`专用 known_hosts 文件)。
- **文件所有权:** 镜像以 `node` 用户(uid 1000)运行。如果您的绑定挂载密钥 uid 传递且不可读,请添加 `--` (容器是临时的,并且无论如何都已挂载您的密钥)。
- **网络:容器有自己的网络命名空间。主机专(VPN 隧道、`localhost` 目标需要 `--network host` (仅限 Linux将无法正常工作。
## 工具
| 工具 | 用途 |
| --- | --- |
| `ssh_connect` | 打开到主机的会话。返回会话 ID 和初始屏幕(提示符,或身份验证/主机密钥提示符以供回答)。 |
| `ssh_send` | 发送 `text` 和/或特殊 `keys` (和可选的 Enter),等待输出稳定,返回更新的屏幕。 |
| `ssh_read` | 读取当前屏幕而不发送 — 轮询长时间运行的输出。支持 `mode: "raw"` 和通过 `waitMs` 阻塞新输出。 |
| `ssh_interrupt` | 向前台程序发送 Ctrl-C。 |
| `ssh_resize` | 更改终端尺寸(影响换行和全屏应用程序)。 |
| `ssh_list` | 列出具有状态、pid、年龄、目的地的活动会话。 |
| `ssh_disconnect` | 关闭会话并终止其 `ssh` 进程。 |
| `ssh_config_hosts` | 列出 `Host` 别名来自 `~/.ssh/config` (尽力而为,遵循 `Include`)用于发现。 |
### `ssh_connect` 参数
| 参数 | 类型 | 描述 |
| --- | --- |
| `host` | 字符串 | ssh_config` 别名名或 `user@hostname` |
| ` | 字符串 | 用户名(作为 `user` 前缀) |
| `port`数字 | 端口(`ssh -p如果在配置中设置,通常不必要 |
| `identityFile` | 字符串 | 私钥路径(`i`);`~` 被扩展 `jump` | ProxyJump 链(`ssh -J`),例如 `"bastion"` 或 `"userA@a,userB@b"` |
| `remoteCommand` | 字符串 | 运行此命令而不是交互式登录 shell(仍在 PTY 上) |
| `extraArgs` | 字符串[] | 额外的原始 ssh 参数原样追加 `forceTty` | 布尔值 | 强制远程 PTY 分配(`ssh -tt`)。默认为 `true` |
| `cols`、`rows` | 数字 | 终端大小(默认 120×40) |
| `settleMs`、`timeout 数字 | 输出稳定阈值 / 最大等待(默认 700 / 20000) |
| `maxLines` | 数字 | 返回的最大屏幕行数(默认 200) |
每个返回屏幕的工具都接受 `settleMs`、`timeoutMs` 和 `maxLines`,并追加状态页脚:
```text
[session=s1 | state=live | wait=idle | cursor=12,1=40x120 | shown=24/```
`state变成 `exited` 当 ssh 时;`wait告诉您输出(`idle仍在流式`timeout`没有新的内容到达quiet`) `ssh_send` 的特殊按键
传递有序的 `keys` 数组。识别的令牌(不区分大小写):
- **控制键组合:** `C-c` (Ctrl-C)、`C-x`、`Ctrl-d`、`^u`、`C-[` (`C-@` (NUL)、`C-\`、`C-?` (DEL)— 涵盖所有 0x00-0x1F 和 0x7F。
- **命名按键:** `Enter`、`Tab`、`Backtab`、`Space`、`Escape`、`Backspace`、`Delete`、`Insert`、`Up`/`Down`/`Left`/`Right`、`Home`、`End`、`PageUp`、`PageDown`、`F1`-`F12`。
- **Alt/Meta:** `M-b`、`Alt-f`、`meta-.` → 以 ESC 前缀。
- **原始字节:** `hex:1b5b41` → 任意字节序列。
- **单个字符:** `y`、`Q` → 按原样发送。
任何其他多字符令牌都是 **错误** — 输入错误的组合键会拒绝,而不是在远程 shell 中键入。文字文本属于 `text`,而不是 `keys`。箭头键/Home/End 键在远程应用程序启用应用程序光标键模式(DECCKM)时自动切换到 SS3 序列,就像真正的终端一样。
`text原样发送发送 `keys`,如果 `appendEnter: true`,则发送 Enter。
## 使用实例
**基本:**
1. `ssh_connect { "host": "prod-web" }` → 屏幕显示 shell。
2. `ssh_send { "session": "s1", " "uname", "appendEnter": true }` → 屏幕显示输出。
3. `ssh_disconnect { "session": "s1" }`.
**Mikrotik RouterOS:**
1_connect { "host": "admin@192.0.2.1" }` → RouterOS 横幅 + `[admin@MikroTik] >` 提示符。
2. `ssh_send { "session":1", "text": "/interface print", "appendEnter": true }`.
3. 选项卡补全:`ssh_send { "session": "s1", "/ip ad", "keys": ["Tab"] }`.
4. 终止正在运行的命令:`ssh_interrupt { "session": "s1" }` (Ctrl-C),分页器发送 `Q` 以退出。
RouterOS 控制台值得注意的陷阱:
- `?` 和 Tab 是**实时热键** (内联帮助/补全) — 它们在到达时立即生效,因此请将它们排除在 `text` 之外,除非有意为之,并更喜欢单行命令(多行粘贴会被自动缩进破坏;使用 `/import` 进行脚本编写)。
- 长 `print` 输出在分页器行处停止(`-- [|D dump|down]`):发送 `D` 以转储其余部分,或运行 `print without-paging`.
- `keys: ["C-x"]` 切换 **安全模式** — 在进行配置更改之前进入该模式,以便在会话断开时自动恢复。
**Proxy → B → C:** 使用设置了 `ProxyJump` 的配置别直接 `ssh_connect { "hostinternal-box" }`。或者内设置链:`ssh_connect { "host": "10.0.0.5", "jump": "bastionA,bastionB"**回答身份验证/主机密钥** 如果初始屏幕显示 `Are you want to continue connecting (yes`,请回答 `ssh_sendsession": "s1", "text": "yes", "appendEnter": true }`. 对于密码提示,请以发送密码。永远不会禁用 known_hosts 检查。
**长时间运行的输出:** `ssh_send` 在稳定/超时时返回;如果页脚说明仍在接收输出,请调用 `ssh_read { "session": "s1", "waitMs": 2000 }` 轮询更多信息。`ssh_read` 阻止直到**新**输出到达(页脚 `wait=idle`/`timeout`)或报告 `wait=quiet` 如果在 `waitMs` 内没有新内容。
## 安全注意事项
本服务器用真实的 SSH 身份将 shell 交给 AI 代理。请在将其连接到任何内容之前阅读本节。
- **代理可以执行您的密钥可以执行的任何操作。** 您的 `~/.ssh/config` 和代理可访问的每个主机都可由模型访问。使用您的 MCP 客户端的权限提示(不要 blanket-allow `ssh_send`),并考虑为使用专用的受限密钥或用户。
- **远程输出是模型的未经信任的输入。** 来自主机的屏幕内容流回代理的上下文;受恶意的宿主可能会尝试通过横幅、MOTD 或命令输出进行提示注入。相应地处理对不受信任宿主的会话。
- **已启用known_hosts验证。** 服务器不添加 `StrictHostKeyChecking=no` 或类似选项;您通过回答指每个主机做出决定。
-参数注入** 用于 `hostuser`/`jump`:以 开头的数值被拒绝,目标` 分隔符后传递意主机名无法偷偷引入选项,如 `ProxyCommand`extraArgs` 仍然是一个刻直通 — 像对待样对待它:像 `-oProxy…` 这样的选项服务器的用户本地执行 **命令**您的 MCP 客户端支持工具参数审查,请仔细检查 `extraArgs`。
- **`ssh_config_hosts` 读取它指向的文件。** `configPath` 接受服务器用户可读取的任何路径(它仅回显配置形状的行,但仍然如此)。将其指向仅 ssh- **凭证从未由本服务器处理**;`ssh`/ssh-agent 拥有它们。您通过 `ssh_send` 输入的密码直接进入 PTY(不会记录到 stdout) — 但它们 **确实会通过 MCP 传输,并可能最终出现在客户端的对话日志中**。首选基于密钥的身份验证,而不是输入密码。
- 以您的普通用户身份运行它,以便它继承您的 `~/.ssh` 和代理。
## 故障排除
- **`posix_spawnp failed` 来自 node-pty 在 macOS 上:** 预编译的 `spawn-helper` 丢失了可执行位。修复:`chmod +_modules/node-pty/prebuilds/darwin-*/spawn-helper`。 run build` 不受影响,但新的 `npm install` 可能需要此操作。
- **`ssh: connect to host … Connection refused`:** 真实 `ssh` 忠实地报告;目标未监听。会话以代码 255 退出。
- **`Connection timed out during banner exchange` 在 ProxyJump 链上:** OpenSSH 的 `ConnectTimeout`(此处默认为 30)还限制了目标的横幅交换,并且该时钟在您在 *跳转* 步骤中回答交互式提示(主机密钥、密码)时继续及时回答,或提高它:`extraArgs: ["-o", "ConnectTimeout=120"]`. 请注意,`-o` 选项不会传播到跳转主机本身 — 其主机密钥会针对您的默认 known_hosts 进行检查。
channel 0: open failed: administratively prohibited` 通过跳转主机:** 跳转主机的 sshd 具有 `AllowTcpForwarding no`(/最小化发行版上常见,例如 Alpine 的默认设置)。在跳转主机上启用它;ProxyJump 需要 TCP 转发。
- **输出看起来被截断:** 增加 `ssh_send`/`ssh_read` 上的 `maxLines`,或使用 `ssh_read` 轮询。模拟器保留每个会话 5000 行的滚动缓冲区。
- **达到会话限制:** 最多 32 个并发会话;退出的会话会自动被逐出,活动的会话必须首先通过 `ssh_disconnect` 退出。
## 开发
```bash
npm run build # tsc → dist/
npm test # vitest:单元测试 + 实时 PTY 集成测试
```
```
src/
index.ts 入口点;stdio 传输 + 正常关闭
server.ts MCP 服务器 + 工具定义
manager.ts 会话注册 + ssh argv 构建器
session.ts PTY + 无头 xterm;空闲等待 + 屏幕渲染
keys.ts 密钥令牌 → 字节序列转换
sshConfig.ts 尽力而为的 ~/.ssh/config 主机列表器(仅)
test/ vitest 测试(密钥、argv 构建器、ssh_config、实时 PTY 会话)
Dockerfile 可选的(参见 Docker 部分)
```
欢迎贡献 — 请在打开 PR