router-ssh-mcp
router-ssh-mcp
通过 SSH 管理路由器(OpenWrt / ImmortalWrt / 通用 Linux)上服务的 MCP(Model Context Protocol)服务器。
上层模型可以列出服务、查询状态、读取日志,并在二次确认后执行启动 / 停止 / 重启,全程受命令白名单约束;此外提供一个默认关闭的高危 run_shell 能力,在显式开启并二次确认后可于远端执行任意 shell 命令,用于白名单工具覆盖不到的排障场景。
1. 能力概览
能力 | 说明 |
多 init 系统 | 自动探测 |
连接复用 | 进程内复用单条 SSH 连接,带 keepalive 与断线重连 |
只读 / 写入分离 | 4 个只读工具 + 3 个写入工具 + 1 个高危 shell 工具,通过 MCP annotations 标注 |
写入二次确认 | 写入工具必须传 |
命令白名单 | 所有命令以 argv 序列下发,不经 shell;服务名、路径、行数均做严格校验 |
可选 shell 能力 |
|
结构化输出 | 统一返回 |
结构化错误 | 连接、认证、超时、服务不存在等均映射为稳定错误码 + 修复建议 |
2. 目录结构
router-ssh-mcp/
├── pyproject.toml # 依赖与 console script 入口
├── config.example.json # 配置文件示例(不含真实口令)
├── .env.example # 环境变量示例
├── src/router_mcp/
│ ├── config.py # 配置加载(默认值 < 配置文件 < 环境变量)
│ ├── errors.py # 错误码与结构化异常
│ ├── commands.py # 命令白名单与 argv 模板(安全核心)
│ ├── ssh_pool.py # SSH 连接复用 / 保活 / 超时重连 / 错误归类
│ ├── controller.py # 编排层:安全策略、二次确认、工具语义
│ ├── models.py # pydantic 返回模型
│ ├── server.py # MCP 服务器与工具注册
│ ├── _compat.py # mcp 1.x / 2.x 兼容层
│ └── backends/
│ ├── base.py # 后端抽象与公共能力(启动时间推算)
│ ├── initd.py # /etc/init.d 通用实现(sysvinit)
│ ├── procd.py # OpenWrt ubus + procd
│ └── systemd.py # systemctl + journalctl
├── scripts/
│ ├── selftest.py # 真实设备自测(只读 + 可选写入演练)
│ └── mcp_smoke.py # stdio 协议级冒烟测试
└── tests/ # 离线单元测试 + MCP 层端到端测试调用链:MCP 工具 → controller(安全策略/确认) → backends(init 适配) → commands(白名单 argv) → ssh_pool(连接复用) → asyncssh。
3. 环境要求与依赖
项目 | 要求 |
Python | >= 3.10(CI 验证 3.11 / 3.12 / 3.13) |
mcp | >= 2.0, < 3(SDK,代码内含 1.x 兼容路径) |
asyncssh | >= 2.14(纯 Python SSH,自带 crypto,不需要系统 ssh 客户端) |
pydantic | >= 2.7(结构化输出模型) |
Linux / macOS
python3 -m venv .venv
source .venv/bin/activate
pip install -e . # 开发模式
# 或:pip install .Windows(PowerShell 7)
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .开发额外依赖:pip install -e ".[dev]"(pytest、pytest-asyncio)。
4. 配置
优先级:默认值 < JSON 配置文件 < 环境变量 < 命令行参数。
连接参数完全可自定义:目标路由器 IP、登录账号、密码或私钥均由你按实际设备填写,三种方式任意组合。本仓库所有示例中的
192.168.1.1仅为占位默认地址,请替换为你自己的路由器地址。密码与令牌切勿提交进仓库(config.json/.env已在.gitignore中忽略)。
配置文件查找顺序:--config → $ROUTER_MCP_CONFIG → ./router-mcp.json → ./config.json → ~/.config/router-mcp/config.json。
4.1 环境变量
变量 | 默认值 | 说明 |
|
| SSH 主机 |
|
| SSH 端口 |
|
| SSH 用户名 |
| — | 密码(与 |
| — | 私钥路径 |
| — | 私钥口令;未设置时会尝试用 |
|
|
|
|
| 连接超时(秒) |
|
| 单条命令超时(秒) |
|
| 保活间隔(秒) |
|
| 连续保活失败次数阈值 |
|
| 命令失败重连重试次数 |
|
| 单次日志读取上限 |
|
| 写入操作是否要求二次确认 |
| 空 | 服务白名单,逗号分隔;为空表示不限制 |
|
| 禁止写入的高危服务 |
|
| 是否校验 known_hosts(路由器重装后建议临时关闭) |
| — | known_hosts 路径(仅在严格模式下生效) |
|
| 是否启用高危的 |
|
|
|
|
| 是否拦截高危命令(rm -rf / mkfs / dd / 写 /dev/* / fork 炸弹 / 管道进 shell) |
|
| asyncssh 日志级别,排障时设 |
4.2 配置文件
复制 config.example.json 为 config.json(已在 .gitignore 中忽略,避免口令入库):
{
"host": "192.168.1.1",
"username": "root",
"password": "",
"init_system": "auto",
"security": {
"require_confirmation": true,
"allowed_services": [],
"denied_services": ["network", "firewall"]
}
}5. 启动
5.1 直接运行
# stdio(默认,供 MCP 客户端拉起)
ROUTER_MCP_HOST=192.168.1.1 ROUTER_MCP_PASSWORD=xxx python -m router_mcp
# 等价的 console script
ROUTER_MCP_HOST=192.168.1.1 ROUTER_MCP_PASSWORD=xxx router-ssh-mcp
# 命令行覆盖参数
router-ssh-mcp --host 192.168.1.1 --user root --key-path ~/.ssh/id_ed25519
# SSE / Streamable HTTP(远程部署时使用)
router-ssh-mcp --transport streamable-http5.2 接入 MCP 客户端
WorkBuddy(~/.workbuddy/mcp.json):
{
"mcpServers": {
"router-ssh": {
"description": "通过 SSH 管理路由器(OpenWrt/ImmortalWrt/systemd)服务的 MCP 连接器;只读工具可直接调用,写入操作需二次确认,run_shell 高危能力默认关闭。",
"command": "python",
"args": ["-m", "router_mcp"],
"env": {
"ROUTER_MCP_HOST": "192.168.1.1",
"ROUTER_MCP_USER": "root",
"ROUTER_MCP_PASSWORD": "你的密码"
}
}
}
}新增服务器后需在 WorkBuddy 的连接器管理页点击「信任」才会启用。
Claude Desktop / 其他支持 MCP 的客户端:claude_desktop_config.json 使用同样的 mcpServers 结构。
6. 工具清单
工具 | 类型 | 参数 | 说明 |
| 只读 | — | 连接健康、init 系统、设备信息、生效的安全策略 |
| 只读 |
| 列出服务及运行状态 |
| 只读 |
| 单服务详情(PID、启动时间、自启状态) |
| 只读 |
| 日志尾部(logread / journalctl) |
| 写入 |
| 启动服务 |
| 写入 |
| 停止服务 |
| 写入 |
| 重启服务(非幂等) |
| 高危 |
| 在路由器上执行任意 shell 命令(默认关闭,需二次确认) |
6.1 成功返回示例
{
"ok": true,
"tool": "service_status",
"host": "192.168.1.1",
"duration_ms": 412,
"data": {
"service": {
"name": "dnsmasq",
"running": true,
"enabled": true,
"pid": 2814,
"started_at": "2026-09-10 09:00:12",
"description": "/usr/sbin/dnsmasq -C /var/etc/dnsmasq.conf",
"source": "procd"
},
"checked_at": "2026-09-10T09:21:03+08:00"
},
"error": null
}6.2 需要二次确认时
{
"ok": false,
"tool": "service_restart",
"host": "192.168.1.1",
"error": {
"code": "CONFIRMATION_REQUIRED",
"message": "写入操作 restart 需要二次确认",
"hint": "确认无误后再次调用,并传入 confirm=\"dnsmasq\"",
"details": {"service": "dnsmasq", "expected_confirm": "dnsmasq"}
}
}6.3 常见用法(场景示例)
识别连接器本质(capability)
router_info 的返回中包含机器可读的 capability 字段(固定为 ssh-router-management),上层软件可据此确认本连接器是「可通过 SSH 操作路由器」的 MCP 连接器,而非普通工具:
{
"ok": true,
"tool": "router_info",
"data": {
"capability": "ssh-router-management",
"init_system": "procd",
"host": "192.168.1.1",
"security": {
"require_confirmation": true,
"allow_shell": false,
"denied_services": ["network", "firewall", "system", "boot", "done"]
}
}
}通过 run_shell 安装插件(OpenWrt / ImmortalWrt)
服务管理类工具只覆盖已注册 init 脚本的服务,安装插件这类操作需走 run_shell 高危通道。先确保已设置 ROUTER_MCP_ALLOW_SHELL=true 启用该能力,再按「命令本身」做二次确认:
# 1) 刷新软件源并安装插件(OpenWrt 包管理器 opkg)
run_shell(
command="opkg update && opkg install luci-app-sqm",
confirm="opkg update && opkg install luci-app-sqm"
)
# 2) 安装后启用并随系统自启
run_shell(
command="/etc/init.d/sqm enable && /etc/init.d/sqm start",
confirm="/etc/init.d/sqm enable && /etc/init.d/sqm start"
)经验证,
opkg update/opkg install <包名>/ 带&&的连写均不会被高危拦截列表误拦;被拦的只有rm -rf/mkfs/dd if=/ 写/dev/*/ fork 炸弹 /curl ... | sh等模式。run_shell以 root 身份在远端登录 shell 中执行,绕过服务白名单,仅应在可信网络内启用;每次执行都会写入审计日志(仅含命令本身,不含任何凭据)。
run_shell 返回示例
{
"ok": true,
"tool": "run_shell",
"data": {
"raw_command": "opkg update && opkg install luci-app-sqm",
"exit_status": 0,
"stdout": "Installing luci-app-sqm (...)",
"stderr": "",
"host": "192.168.1.1",
"reused_connection": true,
"confirmed": true,
"warning": "命令以 root 身份在路由器上执行,已绕过服务白名单;请确认输出不含敏感信息"
}
}7. 安全模型
无 shell 拼接:所有命令以 argv 列表经 exec 通道下发,全程不出现
sh -c。服务名白名单:
^[A-Za-z0-9][A-Za-z0-9_.@+-]{0,63}$,拒绝/、空格、;、|、&、$、反引号、换行与..。路径白名单:可读路径限定为
/proc/1/comm、/proc/uptime、/proc/stat、/etc/os-release、/etc/openwrt_release、/etc/init.d、/etc/rc.d、/run/systemd/system。写入二次确认:
confirm必须与目标服务名完全一致;可用ROUTER_MCP_REQUIRE_CONFIRM=false关闭(不推荐)。高危服务拒绝写入:
network、firewall、system、boot、done默认只可读不可写(重启这些服务会断网或重启设备)。可选服务白名单:设置
ROUTER_MCP_ALLOWED_SERVICES后,白名单外的服务读写均被拒绝。只读 / 写入标注:工具 annotations 标注
readOnlyHint/destructiveHint/idempotentHint,客户端可据此做权限提示。可选 shell 能力(高危,默认关闭):
run_shell必须显式设置ROUTER_MCP_ALLOW_SHELL=true才会出现并可调用;启用后命令仍经validate_shell_command校验,默认拦截rm -rf / mkfs / dd if= / 写入 /dev/* / fork 炸弹 / 管道进 shell等高危模式,且必须把confirm设为命令本身的完整字符串才会真正执行。shell 命令以 root 身份在远端登录 shell 中执行,绕过服务白名单——这是刻意保留的逃生通道,而非默认能力。口令不外泄:
config.redacted()用于输出快照,日志与工具返回值中不含密码。
8. 错误码
错误码 | 触发场景 | 典型修复 |
| 缺少 host / 认证方式,配置文件非法 | 设置 |
| 服务名 / 行数 / 参数非法 | 使用 |
| 路径或参数绕过白名单 | 属于实现缺陷,请提交 issue |
| TCP 不可达、连接被重置 | 检查 host/port、设备 SSH 服务与防火墙 |
| 用户名 / 密码 / 私钥错误 | 核对凭据;OpenWrt 默认用户为 |
| known_hosts 不匹配 | 更新 known_hosts 或临时关闭严格校验 |
| 命令超过 | 调大超时,或检查设备负载 |
| 命令返回非零退出码 | 查看返回中的 |
| 服务不存在 | 用 |
| 命中黑名单 / 白名单外 / polkit 拒绝 | 调整安全策略或以 root 连接 |
| 写入操作缺少正确 confirm | 传入 |
| 未开启 shell 能力却调用 | 设置 |
| 日志通道不可用(无 logread / journald) | 启用 logd 或 journald 持久化 |
| 解析 ubus / systemctl 输出失败 | 检查系统版本兼容性 |
9. 本地自测步骤
共四层,从无依赖到真实设备逐级验证。
第 1 层:离线单元测试(无需设备、无需网络)
pip install -e ".[dev]"
python -m pytest -q覆盖:命令白名单、配置优先级、procd/systemd 解析、确认机制、黑名单、超时映射、错误结构。
第 2 层:MCP 协议冒烟(验证客户端视角)
# 仅握手 + 列工具,不需要设备
python scripts/mcp_smoke.py
# 真实调用一次只读工具
ROUTER_MCP_HOST=192.168.1.1 ROUTER_MCP_USER=root ROUTER_MCP_PASSWORD=xxx \
python scripts/mcp_smoke.py --call router_info
# 带参数调用
python scripts/mcp_smoke.py --call service_status --args '{"name": "dnsmasq"}'预期输出:握手成功、8 个工具、读写标注正确;调用失败时返回 ok=false 与错误码。
第 3 层:真实设备自测(只读)
ROUTER_MCP_HOST=192.168.1.1 ROUTER_MCP_USER=root ROUTER_MCP_PASSWORD=xxx \
python scripts/selftest.py依次验证:SSH 连接与 init 探测 → 服务列表 → 单服务状态 → 日志读取 → 错误路径(服务不存在)→ 写入二次确认拦截,并输出表格化结果。加 --json 可查看原始返回。
第 4 层:写入演练(谨慎)
python scripts/selftest.py --host 192.168.1.1 --password xxx \
--service dnsmasq --do-write会真正执行一次 restart。建议先挑选无状态影响的服务(如 dnsmasq、odhcpd),不要在远程办公时段对 network / firewall 演练。
排障小贴士
现象 | 处理 |
| OpenWrt 默认禁止空密码;确认 dropbear 允许该用户登录 |
| 设备 CPU 繁忙或网络丢包,调大 |
| 确认 |
systemd 主机 | 以 root 连接,或配置 polkit / sudo 规则 |
想看 SSH 握手细节 | 设置 |
10. 已知限制
单条 SSH 连接复用,写入操作串行执行;高并发场景建议为每个设备起独立进程。
OpenWrt 的启动时间依赖
/proc/<pid>/stat与/proc/stat的btime,容器或受限命名空间内可能拿不到。service_list默认不填充started_at(避免对每个服务额外发起命令),需要时传with_start_time=true或调用service_status。systemd 主机上
enabled判定包含static状态(视为自启)。默认
run_shell不可用(刻意的安全设计);仅当用户显式设置ROUTER_MCP_ALLOW_SHELL=true后才提供任意命令执行能力。该通道仍受高危拦截列表与强制二次确认约束,但本质上以 root 身份运行,请仅在可信网络内启用。
11. 许可证
MIT