mcp-ssh-server-tool
v2.2.2
Published
MCP Server for SSH connections and remote command execution
Maintainers
Readme
SSH MCP Server
一个用于 SSH 连接和远程命令执行的 MCP (Model Context Protocol) 服务器。
功能
- 🔐 密码认证 - 使用用户名和密码连接到 SSH 服务器
- 🔑 公钥认证 - 使用私钥文件连接到 SSH 服务器
- 💻 远程命令执行 - 在连接的服务器上执行命令并获取输出
- 📊 会话管理 - 管理多个 SSH 连接会话
- 🛡️ 按需留痕 - 只读命令走轻量通道,变更命令走可审计通道(见下)
安装
npx -y mcp-ssh-server-toolMCP 工具
ssh_connect
建立 SSH 连接
参数:
host: 服务器地址 (必需)port: 端口号 (默认: 22)username: 用户名 (必需)auth_type: 认证方式 - "password" | "public_key" (必需)password: 密码 (auth_type 为 password 时必需)private_key_path: 私钥路径 (auth_type 为 public_key 时必需)passphrase: 私钥密码 (可选)timeout: 连接超时 (默认: 10秒)
ssh_exec
在 SSH 会话中执行命令
参数:
host/username/password等:单次执行时传入,自动连接→执行→断开session_id: 会话 ID(连续执行多命令时使用,先调ssh_connect获取)command: 要执行的命令(单条)commands: 命令数组(按顺序执行)audit: 留痕控制,取值"force"(可选,见下节)
返回:
stdout: 标准输出stderr: 标准错误exitCode: 退出码channel: 实际使用的通道(exec|shell)channelReason: 选择该通道的原因
ssh_disconnect
断开 SSH 连接
参数:
session_id: SSH 会话 ID (必需)
ssh_list_sessions
列出所有活动的 SSH 会话
按需留痕(audit)
背景
在堡垒机(JumpServer / KoKo 等)场景下,SSH 通道类型决定了会话是否被录像:
| SSH 通道 | 堡垒机归类 | 是否录像 |
|---|---|---|
| exec(client.exec()) | 命令执行 | ❌ 不支持录像 |
| shell(client.shell()) | 正常会话 | ✅ 完整录像 |
注意:是否分配 PTY 不影响这个判定。
exec即使带pty: true,仍归为「命令执行」、不录像。
这意味着:如果所有命令都走 exec,变更操作在堡垒机侧没有操作级审计;如果都走 shell,日常只读巡检又会灌满审计记录。
行为
ssh_exec 会按命令性质自动选择通道:
ssh_exec(command)
├─ audit === 'force' → shell 通道(强制留痕)
├─ 命中只读白名单 → exec 通道(不留痕,快且输出干净)
└─ 其余一律 → shell 通道(留痕)默认留痕,且不提供「关闭留痕」的选项。
这样判错的失败方向是单向的:
- 只读命令被误判为需留痕 → 多一条审计记录(无害)
- 变更命令被误判为只读 → 静默执行且不留痕(最坏情况,已在设计上避免)
只读判定
判定逻辑在 readonly_policy.js,采用「默认拒绝 + 显式约束」:
- 不在白名单里的命令 → 一律留痕
- 能读也能写的命令(
ip/docker/kubectl/find/sed…)必须显式约束写形态 - 无法静态分析的形态(
>重定向、$(...)命令替换、管道进 shell)直接否决 - 语义级写操作由
denyPattern拦截(如awk的system()、sysctl的key=value、mysql的INTO OUTFILE)
可通过 loadPolicy(jsonPath) 传入 JSON 覆盖内置白名单:
{
"ls": {},
"tail": { "denyArgs": ["-f", "--follow"] },
"sed": { "denyArgs": ["-i", "--in-place"] },
"systemctl": { "sub": ["status", "is-active", "list-units"] },
"mysql": { "requirePattern": "\\b(SELECT|SHOW)\\b" }
}字段说明:
| 字段 | 含义 |
|---|---|
| sub | 允许出现的子命令/位置参数,命中任一即通过;省略 = 不限制 |
| denyArgs | 禁止出现的参数,命中任一即否决 |
| denyPattern | 正则,匹配到即否决(语义级写操作) |
| requirePattern | 正则,必须匹配才通过(如 SQL 必须是查询语句) |
键名规则:命令名中的 - 换成 _(如 redis-cli → redis_cli)。
环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
| SSH_SHELL_TIMEOUT_MS | 30000 | shell 通道单条命令的超时上限(毫秒) |
使用示例
1. 密码认证连接
{
"host": "192.168.1.100",
"port": 22,
"username": "admin",
"auth_type": "password",
"password": "your_password"
}2. 公钥认证连接
{
"host": "192.168.1.100",
"port": 22,
"username": "admin",
"auth_type": "public_key",
"private_key_path": "/home/user/.ssh/id_rsa"
}3. 执行命令
{
"session_id": "<从 ssh_connect 返回的 session_id>",
"command": "ls -la /home"
}4. 强制留痕执行(变更操作)
{
"session_id": "<session_id>",
"command": "systemctl restart nginx",
"audit": "force"
}5. 断开连接
{
"session_id": "<session_id>"
}命令复核自动应答(auto_confirm_review)
背景
堡垒机(JumpServer)若开了命令过滤 ACL 且动作是「审批」,命中其命令组正则的命令会被 koko 拦下, 在终端弹出交互提示:
需要复核,是否继续?[Y/N]exec 通道没有 stdin,应答不了这个提示 —— 命令会一直卡住直到超时。
本工具的非只读命令走的是 PTY shell 通道,所以有能力把 y 写回去。
⚠️ 应答字符必须是「单字节 y」(2026-09-23 实测定论)
这是踩过的坑,写错会刷屏死循环:
| 写什么 | 结果 |
|---|---|
| 'y'(单字节,不带回车换行) | ✅ koko 立刻提交工单 + 发邮件通知审核人 + 进入倒计时等审批 |
| 'y\n' | ❌ \n 被当成额外的输入行吞掉,koko 无限重放同一条提示 |
| 'y\r' | ❌ 同 'y\n',死循环 |
| 'Y'(大写) | ❌ 不被接受,同样重放提示 |
实测数据:写 'y\n' 时 45 秒内重放 3195 次、刷出 15983 行输出,且不提交任何申请。
原因:PTY 行规程下 koko 读的是单个按键,不是「一行输入」。
应答成功后 koko 会回一段(含 ANSI 色):
需要工单命令执行复核,已发邮件通知审核人
工单审核人:<某人>(<工号>)
可复制审核地址,通知审核人:https://.../tickets/<uuid>?type=command_confirm
请等待审核人复核命令 `...`,取消按 CTRL+C 或 CTRL+D。随后进入每秒刷新的倒计时(0s → 1s → …),直到审批人处理或超时。
语义澄清(重要)
写 y 只是「提交审批申请」,命令不会立刻执行 —— 仍需审批人在 JumpServer 侧批准。
所以本功能不绕过任何管控,只是把「人工在终端按 y」这一步自动化了。
⚠️ 代价:工单的「申请人」会显示为当前堡垒机用户,但实际是工具自动发起的。 审批人无法从工单上区分「本人按的」与「工具自动按的」。启用前请确认这符合你们的管控要求。
行为
| auto_confirm_review | 检测到复核提示时 |
|---|---|
| false(默认) | 立刻返回 errorType: "ReviewRequired",不再白等到超时;stdout 里带上提示原文 |
| true | 自动写单字节 y 提交申请,然后把看门狗放宽到 review_wait(默认 10 分钟)等审批 |
返回值里新增这些字段,便于调用方判断:
reviewPrompted—— 是否检测到复核提示autoConfirmed—— 是否已自动应答yreviewSubmitted—— 工单是否已确认提交(koko 回了「已发邮件通知审核人」)ticketUrl—— 从应答文本里抽出的审核链接,可直接转给审批人
审批通过后,shell 会继续执行原命令并回显结束哨兵 —— 也就是说
现有解析逻辑无需改动,命令正常执行、exitCode 正常返回。
超时未获批则返回 errorType: "ReviewPending"。
开关
- 每次调用传参:
auto_confirm_review: true - 全局默认开启:环境变量
SSH_AUTO_CONFIRM_REVIEW=1 - 等待时长:
review_wait(秒)或环境变量SSH_REVIEW_WAIT_MS(毫秒)
已知限制
- 命令以
exit/exec结尾会终止 shell:哨兵无法回显,命令会走到超时。此时仍会返回已捕获的输出,但exitCode为null。 exitCode取的是最后一条命令的退出码:a; b; c形式的复合命令,返回的是c的退出码。- shell 通道有固定开销:每次都要走一次 shell 初始化(MOTD 等),比
exec慢约 1~2 秒。 - 只读判定是安全网,不是证明:它做静态字符串分析,绕过方式是存在的。真正的强约束应放在服务端(如堡垒机的命令过滤 ACL)。
- 复核提示的识别靠关键词匹配(
需要复核/是否继续/是否提交申请/命令需要审批)。 若目标环境的提示文案不同,需同步调整ssh_manager.js里的REVIEW_PROMPT_RE; 文案不匹配时表现为「没识别到提示,走普通超时」。 - 自动应答只在 shell 通道生效:只读命令走
exec通道,本来也不会被拦(除非 ACL 覆盖了只读命令名)。 - 工单上显示的命令文本不完全可控:koko 记录复核工单时,提取的是「一次写入的整行」。
实测同样用单行拼装写法,有时工单上只显示
rm -f /tmp/x(干净), 有时显示N=xxx; echo "..."; rm -f /tmp/x; echo "...$?"全串(含哨兵)。 推测与 SSH 通道的数据分包时机有关,不做保证。 如果工单命令的可读性很重要,建议先确认目标环境的实际表现。
依赖
- Node.js >= 18.0.0
- ssh2 >= 1.15.0
License
MIT
