关于 MCP SSH Gateway
MCP SSH Gateway 为 AI Agent 提供可保持状态的 SSH shell,可在命令结束时返回退出码。它面向需要远程执行命令和操作文件的 Agent,也支持 Keenetic 路由器 CLI。
主要功能
- 01运行并跟踪远程命令
- 02跨调用保持 shell 状态
- 03读取分段输出与历史内容
- 04处理交互输入和中断
- 05通过 SFTP 或 shell 操作文件
适用场景
- 01检查远程主机磁盘用量
- 02在持续会话中运行构建命令
- 03管理 Keenetic 路由器
- 04读取或编辑远程文件
MCP 相关
它通过 stdio 提供 MCP 工具,用于列出服务器、运行和读取命令、管理 shell 会话及操作远程文件。可从 SSH 配置或服务器配置文件载入主机。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx mcp-ssh-gatewaySSH_SERVERS_CONFIGPath to the JSON file with SSH server targets, or an inline JSON string. Overridden by --servers-config.SSH_MCP_CACHE_DIRWhere logs and known_hosts are kept. Default: the user cache directory.SSH_READ_ONLYSet to true/1 to force the global read-only guardrail.SSH_COMMAND_BLACKLISTComma-separated global prohibited command patterns.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
MCP SSH Gateway
为能判断命令何时完成的 AI Agent 提供 SSH 终端。
六种工具、能保留状态的真实 Shell、命令结束时立即返回的退出代码,以及小型模型也能据此采取行动的答复。仅有一个直接依赖项(paramiko),无需守护进程,也无需云服务。
English · Русский
为什么选择它
- 它知道命令何时完成。 Shell 会直接报告命令结束及其退出代码,因此
echo hi几毫秒内就会返回,失败的命令会显示exit_code: 2,而耗时三分钟的构建会在 10 秒后返回running,之后可通过read获取结果。无需等待,也无需靠沉默猜测。 - 保留状态的终端。 一个会话就是一个真实的 Shell:
cd、变量和虚拟环境会在多次调用间保留。不带session_id的run会打开新的 Shell,因此不会意外共享任何内容。 - 专为小型模型打造。 六种工具,描述总计约 1.6k 个 token。每次停止都会说明下一步该做什么(
hint);分页器已关闭,进度条只占一行,参数错误时会给出正确调用方式,长输出则会分页显示(has_more、tail、offset)。 - 提问不会卡住。
Continue? [y/N]、密码提示或未闭合的引号都会返回waiting_input。Agent 可通过signal作答或按下 Ctrl+C,Shell 会保留状态。 - 也支持路由器。 可通过同一组工具操作 Keenetic 路由器 CLI(支持处理
show interface和--More--),或其 Linux Shell。 - 已通过真实
sshd测试。 CI 会在每次推送时运行测试,涵盖使用 bash、BusyBox ash、zsh、dash、fish 和 tcsh 登录的容器,以及没有 SFTP 的主机。
快速开始
你需要 uv(或 Python 3.11+ 和 pip),以及对你所控制主机的 SSH 访问权限。
已经有 ~/.ssh/config? 将以下内容添加到 MCP 客户端,该文件中的每台主机都会成为一台服务器(通过密钥或 SSH Agent 登录):
{
"mcpServers": {
"ssh": {
"command": "uvx",
"args": ["mcp-ssh-gateway", "--import-ssh-config"]
}
}
}
这是 Cursor(~/.cursor/mcp.json)和 Claude Desktop(claude_desktop_config.json)所用的格式;其他客户端见下文。重启客户端并询问:“列出我的 SSH 服务器,并显示 web 上的磁盘使用情况。”
想使用密码登录、路由器或只读主机? 编写一个 servers.json:
{
"servers": {
"web": {"host": "192.168.1.10", "user": "deploy", "key_path": "~/.ssh/id_ed25519"},
"router": {"host": "192.168.1.1", "user": "admin", "password": "${ROUTER_PASSWORD}"}
}
}
然后通过 "args": ["mcp-ssh-gateway", "--servers-config", "/absolute/path/to/servers.json"] 将该文件路径传给网关。请使用绝对路径(客户端不会从你的文件夹启动;在 Windows 上,要在 JSON 中转义反斜杠),并在客户端的 "env" 块中提供该文件引用的变量。servers.json.example 还展示了只读生产主机的配置。如果不使用 uv:运行 pip install mcp-ssh-gateway,并设置 "command": "mcp-ssh-gateway"。
其他客户端
Claude Code
claude mcp add ssh --scope user -- uvx mcp-ssh-gateway --import-ssh-config
VS Code(.vscode/mcp.json:顶层键为 servers)
{
"servers": {
"ssh": {"type": "stdio", "command": "uvx", "args": ["mcp-ssh-gateway", "--import-ssh-config"]}
}
}
Codex(~/.codex/config.toml,或运行 codex mcp add ssh -- uvx mcp-ssh-gateway --import-ssh-config)
[mcp_servers.ssh]
command = "uvx"
args = ["mcp-ssh-gateway", "--import-ssh-config"]
startup_timeout_sec = 30 # the first uvx run downloads the package
此网关也已列入官方 MCP Registry,名称为 io.github.d00mus/mcp-ssh-gateway,供读取该注册表的客户端和目录使用。
Agent 看到的内容
客户端收到的回答,来自测试套件的 Debian 容器(主机地址和磁盘数据已替换为示例值):
server_list()
→ {"servers":[{"server":"web","host":"192.168.1.10:22","user":"deploy"}]}
run(server="web", command="df -h /") # no session_id: a new shell is opened
→ {"session_id":"web/1","status":"completed","exit_code":0,
"output":"$ df -h /\nFilesystem Size Used Avail Use% Mounted on\n/dev/vda1 40G 31G 7.2G 82% /\n"}
run(session_id="web/1", command="ls /nonexistent") # the same shell; a non-zero code is a result
→ {"session_id":"web/1","status":"completed","exit_code":2,
"output":"$ ls /nonexistent\nls: cannot access '/nonexistent': No such file or directory\n"}
run(session_id="web/1", command="read -p 'Continue? [y/N] ' a; echo got:$a")
→ {"session_id":"web/1","status":"waiting_input",
"output":"$ read -p 'Continue? [y/N] ' a; echo got:$a\nContinue? [y/N] ",
"hint":"The program waits for input. Answer with signal action=stdin text=..., or stop it with signal ctrl_c."}
signal(session_id="web/1", action="stdin", text="y")
→ {"session_id":"web/1","status":"completed","output":"got:y\n","exit_code":0}
run(session_id="web/1", command="seq 1 500", lines=5) # long output comes in pages
→ {"session_id":"web/1","status":"completed","output":"$ seq 1 500\n1\n2\n3\n4\n","exit_code":0,"has_more":496}
read(session_id="web/1", tail=3) # just the end
→ {"session_id":"web/1","status":"completed","output":"498\n499\n500\n","exit_code":0,"skipped_lines":493}
read(session_id="web/1", offset=-20, lines=3) # scroll back; the unread position stays put
→ {"session_id":"web/1","status":"completed","output":"481\n482\n483\n","exit_code":0}
run(session_id="web/1", command="sleep 30", wait=1)
→ {"session_id":"web/1","status":"running","output":"$ sleep 30\n",
"hint":"Still running. Call read(session_id='web/1') again, or stop it with signal ctrl_c."}
signal(session_id="web/1", action="ctrl_c")
→ {"session_id":"web/1","status":"interrupted","output":"\n","process_stopped":true}
出错时,回答会指出正确的调用方式。下面是模型为 run 臆造了 cwd 参数时读到的内容:
{"error": "run has no argument 'cwd'. It takes: command, server, session_id, shell, wait, timeout, lines. To work in a folder, start the command with 'cd /path && '."}
| 字段 | 含义 |
|---|---|
status | completed、running、waiting_input、interrupted、timed_out、failed 或 idle。 |
exit_code | 仅在 completed 时出现。非零值是结果,不是工具错误。 |
has_more | 会话中尚未读取的行;read 会返回这些行。 |
hint | 在 Agent 否则需要猜测时,提示下一步该做什么。 |
skipped_lines | 较新的命令或 read(tail=…) 越过的较早未读行。内容不会丢失:read(offset=0) 可回滚查看。 |
dropped_data | 已永久丢失的未读输出:会话缓冲区溢出(保留最近 400 万个字符)。 |
工具
| 工具 | 功能 |
|---|---|
server_list | 列出服务器及其打开的会话。 |
run | 运行命令。命令结束时返回;或在经过 wait 秒(默认 10 秒)后返回 running。未提供 session_id 时,会打开一个新的 shell。 |
read | 读取会话中接下来的未读行(运行中的命令最多等待 wait 秒);tail 用于读取末尾内容,offset 用于回滚查看。 |
signal | stdin 用于回答问题,ctrl_c 用于停止命令,ctrl_d 用于结束输入。 |
session_close | 关闭一个 shell。每台服务器只允许打开少量 shell(max_sessions,默认值为 8)。 |
file | 通过 SFTP 或在主机不支持 SFTP 时通过 shell,对远程文件执行 list、read、write 和 edit。读取可进行筛选(contains、tail_lines);编辑是精确文本替换,可通过 expected_sha256 进行校验。 |
server_add | 仅在使用 --allow-add-server 时可用:将服务器添加到 servers.json。 |
会话的名称格式为 server/N。Shell 会保留状态(目录、变量),因此没有默认 shell:不带 session_id 的 run 总会打开一个新的 shell 并返回其 ID,之后该 ID 会继续指向同一个 shell。用完后请关闭 shell。
支持的环境
| 主机 | 状态 |
|---|---|
| 使用 bash、dash、BusyBox ash 或 zsh 作为登录 shell 的 Linux | 可用。已在真实的 sshd 容器(Debian、Alpine、zsh 和 dash 登录环境)中测试。 |
| 使用 fish 或 tcsh 作为登录 shell 的主机 | 在 servers.json 中设置 "shell": "bash" 即可使用(网关会在登录后运行 exec bash)。否则网关会立即拒绝并说明原因。 |
| Keenetic 路由器(NDM CLI 及其背后的 Linux shell) | 可用。测试中使用了脚本模拟环境,也已在真实路由器上日常使用。 |
| 其他路由器 CLI(Cisco IOS、Junos 等) | 尚不支持: #1。普通厂商 CLI 可能可用,但尚未测试。 |
位于堡垒机后面的主机(ProxyJump) | 尚不支持: #2。--import-ssh-config 会忽略 ProxyJump。 |
| SSH shell 为 cmd 或 PowerShell 的 Windows 主机 | 不支持:网关需要 POSIX shell。 |
网关本身可运行在支持 Python 3.11+ 的任何环境中(Linux、macOS、Windows)。
安全
Agent 拥有你提供的 SSH 账户所具备的权限。以下措施可以缩小错误操作造成的损害,但不能替代受限账户。
- 防护措施用于防止误操作。
read_only和command_blacklist(按字面匹配,不区分大小写)会检查命令文本。Shell 技巧、base64或python -c都能绕过它们。要建立真正的边界,请使用受限 SSH 账户:只读 Shell、ForceCommand,并为不同信任级别设置不同凭据。 - 无法访问本地文件。 只有在使用
--project-root <folder>启动网关后,local_path(上传、下载)才可用,而且只能访问该文件夹内的内容。 - 主机密钥: 首次连接时会信任新主机,并将其密钥保存在缓存目录中的
known_hosts;如果密钥发生变化,则会拒绝连接。verify_host: false会关闭检查。 - 密钥: 在
servers.json中使用${NAME}引用,而不是直接填写密钥值。日志会隐藏类似密码的值(--log-output off不会写入任何日志)。 - 不开放监听端口: 网关仅通过 stdio 使用 MCP 通信。除非使用
--allow-add-server启动,否则server_add不可用;且此功能只会追加内容。
完整政策及问题报告方式:SECURITY.md。
配置
servers.json 字段:host、user、port(22)、key_path(支持 ~ 和 ${NAME})、password 和 key_passphrase(仅替换 ${NAME} 引用,其余内容按原样使用)、verify_host(true)、description、extra_path(添加到 PATH)、read_only、command_blacklist、max_sessions(8)、shell(登录后要执行的 POSIX Shell,适用于 fish 或 tcsh 账户)。网关运行期间会重新读取该文件;未更改的服务器会保留其会话。
命令行(除了服务器来源外,均为可选):
| 选项 | 含义 |
|---|---|
--servers-config | servers.json 的路径,或 JSON 内容本身。也可使用 $SSH_SERVERS_CONFIG;默认回退到 ./servers.json。 |
--import-ssh-config | 同时提供 ~/.ssh/config 中的主机(使用密钥或 Agent 登录)。 |
--read-only、--command-blacklist a,b | 对所有服务器启用防护措施(或使用 $SSH_READ_ONLY、$SSH_COMMAND_BLACKLIST)。 |
--log-output meta|full|off | 写入日志文件的内容(默认值为 meta:命令和生命周期事件)。 |
--cache-dir | 日志和 known_hosts 的存放目录。默认:用户缓存目录($SSH_MCP_CACHE_DIR)。 |
--project-root | file 工具可读写的本地文件夹(local_path)。不设置时无法访问本地文件;该工具仍可用于服务器。 |
--allow-add-server、--allow-system-temp、--allow-gateway-dir | 可选启用的额外功能。 |
日志:每个会话和每条命令在缓存目录下分别生成一个 JSON Lines 文件;旧文件会按时间和大小清理。
工作原理
网关会通过 PTY 打开一个交互式 Shell,并用一行初始化命令将其切换到安静、机器可读的模式:关闭回显,并设置一个会打印包含退出代码标记的提示符。标记出现时,命令即执行完毕,Agent 也会获得其退出代码。这同样适用于语法错误、后台任务和 Ctrl+C。未闭合的引号或 heredoc 会通过第二个标记识别。
登录后使用供应商 CLI 的路由器(Keenetic NDM)没有这样的 Shell。对于这类路由器,网关会从登录横幅中学习提示符,移除输入命令的回显,在 --More-- 处按空格,并将稳定的提示符视为命令结束。run(shell=false) 使用 CLI,run(shell=true) 则进入其背后的 Linux Shell;一个会话只会保持一种模式。
输出会写入每个会话各自的逐行缓冲区,并使用单一的未读位置。run 和 read 会推进该位置;tail 会跳到末尾;offset 只查看内容。超过 1024 个字符的行会被拆分,以确保分页结果可预测。
限制
- 一个 shell 一次只运行一条命令。要同时处理两件事,请打开两个 shell(或让客户端并行调用
run)。 - 在会话内启动的 shell(
sudo -s、su、bash)会隐藏命令结束的位置,因此网关会告知 Agent 退出该 shell。需要 root 权限时,请使用sudo <command>。 - 暂不支持跳板机(#2),也暂不支持其他路由器 CLI(#1)。
- 网关没有审批、策略引擎或审计跟踪。如果需要这些功能,请参阅下文。
其他用于 MCP 的 SSH 服务
它们的取舍各不相同(截至 2026 年 9 月;选择前请阅读它们的 README):
- tufantunc/ssh-mcp:安全优先。提供命令分类、角色和主机策略、人工审批、审计日志、
ProxyJump、SSH CA 和流式 SFTP;共 14 个工具。如果你需要审批和审计跟踪,适合用于生产环境中的服务器群。 - classfang/ssh-mcp-server:基于 Node(
npx)提供 4 个工具,具有命令白名单、SOCKS 和 HTTP 代理、堡垒机模式及 MFA。如果你需要代理或 MFA,且想要最精简的 Node 配置,就选它。 - bvisible/mcp-ssh-manager:提供 37 个工具,是一套包含备份、数据库和监控功能的 DevOps 工具箱。如果你想直接使用这些现成的工作流,就选它。
这个网关选择了另一端:工具数量少、shell 有状态,并且能为 Agent 遇到的每种卡住情况提供解决方案。
Docker
docker build -t mcp-ssh-gateway .
docker run -i --rm \
-v /absolute/path/to/servers.json:/app/servers.json:ro \
-v /absolute/path/to/.ssh:/root/.ssh:ro \
-v /absolute/path/to/cache:/app/.ssh-cache \
mcp-ssh-gateway --servers-config /app/servers.json --cache-dir /app/.ssh-cache
缓存卷会在多次运行之间保留 known_hosts 和日志。
开发
pip install -r requirements.txt ruff mypy
python -m unittest discover -s tests -t . # unit tests; integration tests need Docker
ruff check . && mypy
集成测试会启动真实的 sshd 容器,并通过客户端使用的同一 JSON-RPC 入口驱动网关。缺少 Docker 时,测试会自动跳过(设置 MCP_SSH_IT=0 可主动跳过)。请参阅 CONTRIBUTING.md 和变更日志。采用 MIT 许可证。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。