Skip to main content
Glama
yongxiaodong

ssh-remote-mcp

by yongxiaodong
README.md
# ssh-remote-mcp

一个用于 **通过 SSH 安全诊断远程服务器** 的 [MCP](https://modelcontextprotocol.io) 服务。
让 AI(Claude Code / Codex 等)能去线上机器排查问题——看日志、查 CPU/内存/磁盘、服务、端口、
Docker、Plesk……**默认不能做破坏性改动**,写操作需要你点确认或显式开启。

## 两条设计原则

1. **提供能力,不提供流程**:像 Filesystem MCP 那样,只给少量通用、可组合的原语
   (`run_command` / `read_file` / `tail_file` / `list_dir` / `list_hosts`),命令由 Agent 自己组合,
   没有 `restart_nginx` 这种硬编码流程。
2. **读自动、写收口**:每条命令在**任何 SSH 之前**先判定——
   - **只读且安全** → 自动执行;
   - **写 / 敏感 / 未知** → 走 `run_write_command`,需你在客户端**点确认**后才执行(否则只返回建议命令,绝不静默写)。

   SSH 账号本身的 OS 权限是最终防线,**推荐用非 root 的排查账号**。

---

## 🚀 快速开始(Docker,推荐)

### 1. 克隆

```bash
git clone https://github.com/yongxiaodong/ssh-remote-mcp.git
cd ssh-remote-mcp
```

### 2. 配置要诊断的主机

```bash
cp config/hosts.example.yaml config/hosts.docker.yaml
$EDITOR config/hosts.docker.yaml
```

容器会把你的 `~/.ssh` 整个挂到 `/keys`,所以 `key_path` 用容器内路径 `/keys/<私钥文件名>`:

```yaml
transport: { mode: stdio }            # 容器里实际用 http 启动(见 docker-compose),此处随意
security:
  mode: diagnostic                    # 读自动;写经 run_write_command 确认
  enable_write_tool: true             # 暴露写工具(默认 true);设 false 则纯只读
hosts:
  my-server:
    host: 1.2.3.4
    port: 22
    user: root                        # 建议用非 root 排查账号
    key_path: /keys/id_rsa            # = ~/.ssh/id_rsa(~/.ssh 已挂到 /keys)
    accept_unknown_hosts: true        # 测试用;生产请挂 known_hosts
```

### 3. 启动(监听 `127.0.0.1:15555`)

```bash
docker compose -f docker/docker-compose.yml up -d --build
```

> 换机器 / 非 501 用户时,带上你的 uid:gid(让容器能读 0600 权限的私钥):
> ```bash
> SRM_UID=$(id -u) SRM_GID=$(id -g) docker compose -f docker/docker-compose.yml up -d --build
> ```

常用管理:

```bash
docker compose -f docker/docker-compose.yml logs -f      # 看日志/审计
docker compose -f docker/docker-compose.yml restart      # 改了 config 后重载
docker compose -f docker/docker-compose.yml down         # 停
```

> 改了**代码/规则表**要 `up -d --build`;只改 **config/hosts.docker.yaml** 用 `restart` 即可。

### 4. 接入 Claude Code

```bash
claude mcp add --transport http -s user ssh-remote-mcp http://127.0.0.1:15555/mcp
claude mcp list        # 看到 ✔ Connected
```

`-s user` = 所有项目可用。**新开一个会话**即可看到工具。

### 5. 接入 Codex

```bash
codex mcp add ssh-remote-mcp --url http://127.0.0.1:15555/mcp
codex mcp list
```

> Claude Code 和 Codex 共用同一个 HTTP 端点,只要容器在跑就都能连。

### 6. 跑一条测试指令

在 Claude Code / Codex 里直接说:

> 用 ssh-remote-mcp 看一下 my-server 的运行时长和磁盘使用

它会调用 `run_command`,只读命令自动执行,例如 `uptime; df -h`:

```
03:25:34 up 12 days, 10:45,  0 users,  load average: 0.29, 0.09, 0.03
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda2        96G   16G   77G  17% /
```

试试写操作(如"创建 /tmp/t1"):它会调用 `run_write_command`,客户端**弹出允许/拒绝**,你点允许才执行。

---

## 工具

| 工具 | 作用 |
|------|------|
| `list_hosts` | 列出已配置的主机(绝不泄露密钥) |
| `run_command` | 执行命令/管道;只读自动跑,写则返回建议 |
| `read_file` | 读取文件(敏感路径需确认,带大小上限) |
| `tail_file` | 查看文件末尾 N 行,可选关键字过滤 |
| `list_dir` | 列目录 |
| `run_write_command` | 执行写/变更命令,**必须经你确认**才运行(默认开启) |

## 安全行为速查

- ✅ **自动执行**:只读命令、只读管道/串接(`|` `;` `&&` `||`)、只读分组子shell `( … )`、
  无害重定向(`2>/dev/null`、`2>&1`)、变量赋值 `f=/path; … $f`。
- 🔒 **需确认 / 拦截**:写文件(`> file`)、`rm/mv/chmod/systemctl restart/docker run` 等变更、
  读敏感文件(key/`/etc/shadow`/`.env`…)、命令替换 `$(…)`/反引号、进程替换 `<(…)`、后台 `&`、
  以及 `awk/sed/php/mysql` 等可写/可执行代码的双刃命令。
- 输出会做**密钥/密码脱敏**,每条命令都写**审计日志**(不记原始敏感输出)。

判定逻辑是纯函数、无 I/O,"危险命令在连服务器之前就被拦下"由测试保证
(`tests/unit/test_engine.py`)。

## 配置

详见 [config/hosts.example.yaml](config/hosts.example.yaml) 和 [docs/configuration.md](docs/configuration.md)。
要新增放行命令而不重建镜像,写进 `security.ruleset_extra` 后 `restart` 即可。

## 本地开发(不用 Docker)

需要 Python 3.11+:

```bash
python3.12 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

cp config/hosts.example.yaml config/hosts.yaml      # 编辑主机(key_path 用本机 ~/.ssh 路径)
ssh-remote-mcp --transport stdio                    # 或 --transport http --port 15555
```

测试:

```bash
pytest tests/unit                                   # 离线,始终安全
SSH_REMOTE_MCP_IT=1 IT_SSH_HOST=1.2.3.4 IT_SSH_KEY=~/.ssh/id_rsa \
  pytest tests/integration -m integration           # 打真机,仅只读
```

## License

MIT