Skip to main content
Glama
SC-ML-cmd
by SC-ML-cmd
README.md
# SSH MCP

一个不依赖 Xshell 的 Python SSH MCP Server。它由 MCP Server 自己建立交互式 SSH PTY 会话,适合 CMSM/堡垒机菜单、进入 master、进入 pod 后复用当前 shell 状态的排障流程。

## 能力概览

- `open_session(profile, owner_label=None)`:按配置打开持久 SSH 会话,并返回对应的浏览器 `viewer_url`。
- `list_sessions()`:查看当前 MCP Server 实例内的活动会话和运行时信息。
- `send_text(session_id, text, enter=True, wait_for="", timeout=30)`:向菜单或 shell 发送文本。
- `execute_command(session_id, command, wait_for_prompt=True, timeout=30)`:在当前 shell 状态下执行命令。
- `get_screen(session_id, lines=100)`:查看内存中最近的终端输出。
- `get_transcript(session_id, tail=200)`:读取 JSONL 审计记录。
- `interrupt(session_id)`:发送 `Ctrl+C`。
- `search_logs(...)`:在当前远端 shell 中执行 `find + grep` 日志搜索。

## 架构

```text
Claude / Codex / MCP Client
  -> ssh-mcp FastMCP Server
  -> Paramiko SSH interactive shell channel
  -> remote CMSM / master / pod shell

ssh-mcp Server
  -> runtime/instances/<server_instance_id>/logs/ssh_mcp.log
  -> runtime/instances/<server_instance_id>/transcripts/<session_id>.jsonl
  -> local browser viewer
```

每个 MCP Server 进程都会生成独立的 `server_instance_id`。多个 LLM 窗口同时启动 MCP Server 时,默认写入不同的实例目录,避免日志和 transcript 混在一起。

## 安装

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -e .
```

开发测试可直接使用:

```powershell
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
```

## SSH 配置

复制配置模板:

```powershell
Copy-Item .\config\profiles.example.json .\config\profiles.json
```

示例:

```json
{
  "profiles": {
    "dev": {
      "host": "10.0.0.10",
      "port": 22,
      "username": "your-user",
      "key_filename": "C:/Users/you/.ssh/id_rsa",
      "passphrase_env": "SSH_MCP_KEY_PASSPHRASE",
      "allow_agent": true,
      "look_for_keys": true,
      "auto_add_host_key": true,
      "term": "xterm-256color",
      "width": 120,
      "height": 40,
      "keepalive_interval": 30
    }
  }
}
```

`keepalive_interval` 使用 Paramiko transport keepalive,只做 SSH 探活,不会向远端 shell 注入命令。

## 运行

直接启动:

```powershell
.\.venv\Scripts\python.exe -m ssh_mcp.server --viewer-host 127.0.0.1 --viewer-port auto
```

常用环境变量:

- `SSH_MCP_CONFIG`:profile 配置路径,默认 `config/profiles.json`。
- `SSH_MCP_RUNTIME_DIR`:运行产物根目录,默认 `runtime`。
- `SSH_MCP_CLIENT_LABEL`:当前 MCP Server 来源标签,例如 `claude-order-debug`、`codex-local`。
- `SSH_MCP_KEY_PASSPHRASE`:私钥口令。
- `SSH_MCP_KEEPALIVE_INTERVAL`:默认 SSH keepalive 秒数,可被 profile 覆盖。

兼容旧配置:

- 显式设置 `SSH_MCP_LOG_PATH` 时,运行日志写到该路径。
- 显式设置 `SSH_MCP_TRANSCRIPTS_DIR` 时,transcript 写到该目录。

## Claude / Codex 接入

仓库提供 `.mcp.json` 和 `.codex/config.toml` 示例。默认建议只设置:

- `SSH_MCP_CONFIG`
- `SSH_MCP_RUNTIME_DIR`
- `SSH_MCP_CLIENT_LABEL`
- `SSH_MCP_KEY_PASSPHRASE`

不要让多个 MCP Server 固定写同一个 `logs/ssh_mcp.log` 或 `transcripts/`,否则排查时很难区分不同 LLM 窗口。

启动 Claude Code 前可设置:

```powershell
$env:SSH_MCP_KEY_PASSPHRASE = "<your-private-key-passphrase>"
$env:SSH_MCP_CLIENT_LABEL = "claude-payment-debug"
claude
```

`open_session` 时建议传入 `owner_label`:

```json
{"profile": "lab", "owner_label": "payment-log-check"}
```

`SSH_MCP_CLIENT_LABEL` 标记 MCP Server 来源,`owner_label` 标记具体 SSH session 的用途。

## 浏览器 Viewer

MCP Server 启动时会同时启动本地 viewer。默认从 `8765` 附近自动选择可用端口。

- `/`:按 `server_instance_id` 分组展示 sessions。
- `/sessions/<session_id>`:单个 session 的终端式页面。
- `/api/sessions`:返回 session 列表和 server 元数据。
- `/api/sessions/<session_id>/events`:长轮询读取 transcript 增量。

`open_session` 会返回准确的 `viewer_url`,不要手动猜 session URL。

也可以只启动历史 viewer:

```powershell
.\.venv\Scripts\python.exe -m ssh_mcp.viewer --port 8765 --transcripts-dir transcripts
```

## Runtime 目录

默认结构:

```text
runtime/
  instances/
    <server_instance_id>/
      server_meta.json
      logs/
        ssh_mcp.log
      transcripts/
        <session_id>.jsonl
```

`server_meta.json` 记录:

- `server_instance_id`
- pid
- cwd
- started_at
- client_label
- viewer_base_url
- log_path
- transcripts_dir
- config_path

旧的 `transcripts/*.jsonl` 仍可被 viewer 读取,并标记为 `legacy`。

## Transcript 与安全

每个 SSH session 都有独立 JSONL 文件。首行是 `session_meta`,包含 profile、host、username、owner_label、server_instance_id、client_label、viewer_url 等信息。

示例事件:

```json
{"dir":"session_meta","session_id":"lab-...","owner_label":"payment-log-check","server_instance_id":"ssh-mcp-..."}
{"dir":"send","tool":"send_text","text":"2\n"}
{"dir":"recv","text":"Logged in to master...\n"}
{"dir":"session_health","health_status":"unhealthy","health_error":"SSH transport is inactive"}
```

重要:`send_text(..., sensitive=true)` 不会脱敏,`text` 仍按原文写入 transcript。请保护 `runtime/`、`logs/`、`transcripts/` 的文件权限。

## SSH 心跳与断线处理

当前策略是“只探活,不自动重连”:

- 使用 Paramiko transport keepalive 保持 SSH 连接活跃。
- 后台 health monitor 定期检查 transport/channel。
- 断开后 session 会标记为 `unhealthy` 或 `closed`。
- 后续 `send_text` / `execute_command` 返回可读错误。
- 不自动重放 CMSM -> master -> pod 路径,避免误以为恢复了原始 shell 状态。

## 本地 CMSM 测试环境

仓库内置 CMSM simulator:

```text
lab/cmsm_simulator/
```

它模拟:

```text
CMSM 菜单 -> master shell -> kubectl exec -> pod shell
```

部署和测试步骤见 [lab/cmsm_simulator/README.md](lab/cmsm_simulator/README.md)。

## 常见问题

### 为什么 viewer 端口不是 8765?

如果端口被占用,viewer 会自动选择后续可用端口。以 `open_session` 返回的 `viewer_url` 为准。

### 多个 Claude/Codex 窗口如何区分?

设置不同的 `SSH_MCP_CLIENT_LABEL`,并在 `open_session` 传入 `owner_label`。运行日志、server_meta 和 transcript 都会记录这些字段。

### MCP 重启后 SSH session 还在吗?

不在。当前实现使用 Paramiko interactive shell channel,MCP Server 进程退出后 SSH 连接也会关闭。历史 transcript 会保留在 runtime 目录。

### 什么时候考虑 tmux?

如果需要 MCP 重启后仍能 attach 原 SSH 会话,可以在后续版本评估 tmux 底座。但 Windows 原生部署会更复杂,本项目当前优先保持 Python/Paramiko 的轻量实现。