Skip to main content
Glama
README.md
# SSH MCP 服务

SSH MCP 是一个 MCP Server,用于把 WebShell 网关能力封装成 MCP 工具并通过 HTTP 提供给客户端(如 LangChain 原生 tools)。它会调用 WebShell 网关的 API 来获取目标列表与创建一次性 WebShell 会话链接。

## 功能概览

- 提供 MCP HTTP 端点:/mcp 与 /stream
- 工具能力:list_ssh_targets、create_ssh_web_terminal、open_ssh_terminal
- 通过 HTTP API 连接内联 WebShell 网关

## 架构与主要逻辑

- 进程模型:单 Node.js 进程同时承载 MCP Server 与内联 WebShell Gateway
- 数据存储:MySQL 保存 SSH 目标与会话历史记录
- 典型调用链:
  - list_ssh_targets:从 MySQL 读取目标表并返回结构化 JSON
  - create_ssh_web_terminal:在数据库中生成一次性会话记录,返回一次性 WebShell 会话链接
  - open_ssh_terminal:根据用户输入匹配 SSH 目标,复用 create_ssh_web_terminal 逻辑返回会话链接
- 网络与端口(Docker Compose 部署):
  - mysql 服务:提供 3306 端口,ssh-mcp 容器内通过主机名 mysql 访问
  - ssh-mcp 服务:对外暴露 3001(MCP 控制台 + HTTP 端点)和 9100(WebShell 会话页)
  - 容器内部 MCP 通过 `http://localhost:9100` 调用内联 WebShell Gateway

## 项目目录结构

```
ssh-mcp/
├── .env                      # 环境变量配置
├── Dockerfile                 # Docker 镜像构建文件
├── README.md                  # 项目说明文档
├── docker-compose.yml         # Docker Compose 配置
├── package.json               # Node.js 项目配置
├── package-lock.json          # 依赖版本锁定文件
├── tsconfig.json              # TypeScript 配置
├── dist/                      # 编译输出目录
├── node_modules/              # Node.js 依赖包
├── public/                    # 公共静态资源目录
│   └── index.html
└── src/                       # 源代码目录
    ├── index.ts               # 入口文件:MCP Server 主逻辑
    └── webshell-gateway/      # 内联 WebShell Gateway 模块
        ├── index.ts           # Gateway 主逻辑(HTTP/WS 服务)
        └── db.ts              # 数据库操作(SSH 目标与会话管理)
```

**目录说明:**

| 文件/目录 | 说明 |
|-----------|------|
| `package.json` | NPM 项目配置,包含依赖和脚本命令 |
| `tsconfig.json` | TypeScript 编译配置 |
| `Dockerfile` | 用于构建 Docker 镜像 |
| `docker-compose.yml` | 用于编排 Docker 容器 |
| `.env` | 环境变量配置文件 |
| `src/` | TypeScript 源代码目录 |
| `public/` | 静态资源目录(WebShell 终端页面) |
| `dist/` | 编译后的输出目录 |

## 快速开始

### 方式一:Docker Compose(推荐,用于生产与测试环境)

在 ssh-mcp 目录执行:

```bash
docker compose up -d --build
```

该命令会启动两个容器:

- mysql:WebShell 网关使用的 MySQL 数据库
- ssh-mcp:包含 MCP Server 与内联 WebShell Gateway

默认行为与端口:

- ssh-mcp 对外暴露 3001(MCP 控制台 + /mcp、/stream 等 HTTP 端点)
- ssh-mcp 对外暴露 9100(WebShell 会话页,对应一次性会话链接)
- 容器内部 MCP 通过 `SSH_WEBSHELL_API_URL=http://localhost:9100` 调用内联 Gateway
- 容器内部访问数据库使用主机名 `mysql` 与端口 `3306`

如需调整端口或数据库账号,请编辑
[docker-compose.yml](file:///c:/Users/Administrator/Desktop/jenkins-mcp/ssh-mcp/docker-compose.yml)
或通过环境变量覆盖。

### 方式二:Docker build/run(单容器,依赖外部 MySQL)

```bash
docker build -t ssh-mcp:latest .
docker run -d \
  -p 3001:3001 \
  -e PORT=3001 \
  -e SSH_WEBSHELL_API_URL=http://localhost:9100 \
  -e MOCK_WEBSHELL_PORT=9100 \
  -e WEBSHELL_DB_HOST=<你的 MySQL 主机地址> \
  -e WEBSHELL_DB_PORT=3306 \
  -e WEBSHELL_DB_USER=webshell \
  -e WEBSHELL_DB_PASSWORD=webshell_pass \
  -e WEBSHELL_DB_DATABASE=webshell \
  --name ssh-mcp \
  ssh-mcp:latest
```

### 方式三:本地运行

```bash
npm install
npm run build
npm start
```

## 配置说明

### 服务端口

- PORT:MCP HTTP 服务端口,默认 3001

### WebShell 网关连接

- SSH_WEBSHELL_API_URL:WebShell 网关地址,默认建议为 `http://localhost:9100`
- SSH_WEBSHELL_HTTP_TIMEOUT_MS:请求超时,默认 15000
- SSH_WEBSHELL_API_TOKEN:调用 WebShell 网关受保护接口时使用的固定 API Token(内联 Gateway 当前不会强制校验,仅用于兼容旧配置)

### 内联 WebShell Gateway(已合并进 ssh-mcp)

从当前版本起,WebShell Gateway 的核心能力已经内联到 ssh-mcp 中,由同一进程启动并监听独立端口。

- MOCK_WEBSHELL_PORT:内联 WebShell Gateway HTTP/WS 端口,默认 9100
- WEBSHELL_PUBLIC_BASE_URL:会话链接对外暴露的基础地址,默认 `http://localhost:9100`

### 数据库连接(用于存储 SSH 目标与会话历史)

内联 Gateway 使用 MySQL 存储 SSH 目标和会话历史记录,对应原 WebShell Gateway 的数据结构:

- WEBSHELL_DB_HOST:数据库主机,默认 `mysql`
- WEBSHELL_DB_PORT:数据库端口,默认 `3306`
- WEBSHELL_DB_USER:数据库用户名,默认 `webshell`
- WEBSHELL_DB_PASSWORD:数据库密码,默认 `webshell_pass`
- WEBSHELL_DB_DATABASE:数据库名称,默认 `webshell`

### 数据库表结构

#### targets 表(SSH 目标)

| 字段名     | 类型            | 说明                     | 备注                           |
| ---------- | --------------- | ------------------------ | ------------------------------ |
| id         | varchar(191)    | 目标唯一 ID              | MCP 工具与 UI 侧使用此 ID 标识 |
| host       | varchar(255)    | SSH 主机地址或 IP        |                                |
| port       | int             | SSH 端口                 | 默认为 22                      |
| username   | varchar(255)    | 登录用户名               |                                |
| password   | text (nullable) | 登录密码                 | 可为空,生产建议优先用密钥认证 |
| privateKey | text (nullable) | 私钥内容(PEM)          | 可为空                          |
| name       | varchar(255)    | 目标展示名称(别名)     | 可为空                          |

索引:

- 主键:`PRIMARY KEY (id)`

#### sessions 表(SSH 会话历史 / 审计日志)

| 字段名    | 类型                                           | 说明                      | 备注                                     |
| --------- | ---------------------------------------------- | ------------------------- | ---------------------------------------- |
| id        | varchar(191)                                   | 会话 ID                   | 同时用于 `/session/:id` 链接中的 ID      |
| targetId  | varchar(191)                                   | 关联的 SSH 目标 ID        | 对应 `targets.id`                        |
| targetName| varchar(255)                                   | 目标名称冗余              | 便于审计查询                             |
| host      | varchar(255)                                   | 连接时使用的主机名或 IP   | 冗余存储,避免后续目标变更影响审计      |
| port      | int                                            | 连接时使用的端口          | 冗余存储                                 |
| username  | varchar(255)                                   | 连接时使用的账户名        | 冗余存储                                 |
| startTime | datetime                                       | 会话开始时间              | 创建会话链接时写入                       |
| endTime   | datetime (nullable)                            | 会话结束时间              | WebSocket 关闭或异常时更新               |
| status    | enum('connected','disconnected','error')       | 会话最终状态              | `connected`/`disconnected`/`error`       |
| reason    | text (nullable)                                | 创建会话时的原因说明      | 由上游调用方传入,用于审计               |

索引:

- `idx_targetId (targetId)`
- `idx_startTime (startTime)`

#### users 表(预留用户信息)

| 字段名      | 类型         | 说明               | 备注                           |
| ----------- | ------------ | ------------------ | ------------------------------ |
| id          | varchar(191) | 用户唯一 ID        |                                |
| username    | varchar(191) | 用户名             | 唯一索引                       |
| password_hash | varchar(255)| 密码哈希          | 存储加密后的口令               |
| provider    | varchar(32)  | 认证提供方标识     | 当前默认值为 `local`           |
| created_at  | datetime     | 创建时间           |                                |

索引:

- 主键:`PRIMARY KEY (id)`
- `idx_username (username)`

> 说明:当前内联 Gateway 尚未暴露基于 users 表的对外登录接口,该表为未来用户体系预留。

#### auth_sessions 表(预留认证会话)

| 字段名    | 类型         | 说明                     | 备注                                   |
| --------- | ------------ | ------------------------ | -------------------------------------- |
| token     | varchar(191) | 会话 Token(主键)       | 用于标识登录态或访问会话               |
| username  | varchar(191) | 关联的用户名             | 对应 `users.username`                  |
| provider  | varchar(32)  | 认证提供方标识           | 当前默认值为 `local`                   |
| created_at| datetime     | 会话创建时间             |                                        |
| expires_at| datetime     | 会话过期时间             | 用于失效判断和定期清理                 |

索引:

- 主键:`PRIMARY KEY (token)`
- `idx_username (username)`
- `idx_expires_at (expires_at)`

> 说明:auth_sessions 表用于记录认证会话 Token,`expires_at` 字段用于决定会话是否过期。当前代码中已提供按过期时间自动清理的能力,实际登录流程可在未来接入。

### 生产环境配置建议

- 数据库账号与密码:
  - 强烈建议在生产环境中为数据库创建独立账号,并替换默认密码
  - 建议限制数据库账号仅具备所需最小权限(最小权限原则)
- WebShell 会话访问地址:
  - 将 WEBSHELL_PUBLIC_BASE_URL 设置为经过反向代理或网关暴露的 HTTPS 域名
  - 对公网暴露时建议配合 WAF、IP 白名单等防护手段
- API 调用安全:
  - 如需在 MCP 侧做额外鉴权,可结合上层调用方实现 RBAC 或 Token 校验
  - SSH_WEBSHELL_API_TOKEN 可预留用于与外部 Gateway 集成时的签名/鉴权
- 资源与容量规划:
  - 为 MySQL 分配独立存储卷(docker-compose.yml 已默认配置)
  - 根据实际会话量评估连接数与超时配置,合理设置 SSH_WEBSHELL_HTTP_TIMEOUT_MS

## MCP 端点

- POST /mcp
- POST /stream

## 内联 WebShell Gateway 使用方式

### 1. 准备 MySQL

可以使用 Docker 启动一个 MySQL 实例(示例):

```bash
docker run -d \
  --name webshell-mysql \
  -e MYSQL_ROOT_PASSWORD=webshell_root \
  -e MYSQL_DATABASE=webshell \
  -e MYSQL_USER=webshell \
  -e MYSQL_PASSWORD=webshell_pass \
  -p 3306:3306 \
  mysql:8.0
```

### 2. 配置 ssh-mcp 环境变量

在 ssh-mcp 目录下创建或编辑 `.env`(或通过 Docker 环境变量注入):

```env
PORT=3001

# MCP 通过 HTTP 调用内联 Gateway
SSH_WEBSHELL_API_URL=http://localhost:9100
SSH_WEBSHELL_HTTP_TIMEOUT_MS=15000

# 内联 Gateway 监听端口与对外访问地址
MOCK_WEBSHELL_PORT=9100
WEBSHELL_PUBLIC_BASE_URL=http://localhost:9100

# MySQL 连接
WEBSHELL_DB_HOST=127.0.0.1
WEBSHELL_DB_PORT=3306
WEBSHELL_DB_USER=webshell
WEBSHELL_DB_PASSWORD=webshell_pass
WEBSHELL_DB_DATABASE=webshell
```

### 3. 启动服务并访问

```bash
cd ssh-mcp
npm install
npm run build
npm start
```

- MCP 控制台 UI:访问 `http://localhost:3001`
- 内联 WebShell Gateway 会话页:通过 MCP 工具返回的一次性会话链接,例如 `http://localhost:9100/session/<uuid>`

在 Docker Compose 场景下,默认映射为宿主机的 `http://localhost:3001` 与 `http://localhost:9100`,如修改端口请同步更新 WEBSHELL_PUBLIC_BASE_URL。

## 会话与失效策略

### WebShell SSH 会话(/session/:id)

- 会话生成:
  - MCP 工具调用内联 Gateway 的 `/api/session` 接口创建会话。
  - 内联 Gateway 为每次请求生成随机 UUID 作为 `sessionId`。
  - 会话信息写入两处:
    - 进程内内存 Map:键为 `sessionId`,值包含 `targetId`、`reason`、`createdAt` 等。
    - 数据库 `sessions` 表:写入一条会话历史记录(startTime、status、reason 等)。
- 会话访问:
  - 浏览器访问 `/session/<sessionId>` 时:
    - 如果内存 Map 中存在对应条目,则渲染 WebShell 终端页面并建立 WebSocket `/ws?sessionId=...`。
    - 如果不存在,则返回 404 文本:`会话不存在或已过期`。
- 会话结束:
  - WebSocket 正常关闭时:
    - 内联 Gateway 调用 `updateSessionEndTime`,将 `sessions` 表中的 `endTime` 更新为当前时间,`status` 置为 `disconnected`。
  - 连接远程主机出错时:
    - 会话记录的 `status` 置为 `error`,`endTime` 同样更新为当前时间。
- 失效策略(当前版本):
  - `sessionId` 仅保存在内存 Map 中,不会被持久化用于恢复实时 SSH 通道。
  - 服务重启后内存 Map 清空,所有历史链接都会返回“会话不存在或已过期”。
  - 当前未对 `sessionId` 设置基于时间的自动 TTL 清理,在服务不重启的情况下同一链接可被多次访问。
  - 建议上层调用方将会话链接视为短期凭证,不长期缓存或下发到不可信环境。

### 认证会话(auth_sessions)

- auth_sessions 表用于保存认证会话 Token(预留能力):
  - `token`:会话 Token 主键。
  - `username` / `provider`:标识对应用户与认证来源。
  - `expires_at`:过期时间。
- 失效判断:
  - 在读取会话时,如果 `expires_at <= 当前时间`,会被视为已过期并立刻从表中删除。
  - 提供批量清理能力,用于定期清理历史过期会话。
- 当前版本尚未对外暴露基于该表的登录接口,后续如启用认证体系,可基于此表实现登录态管理。

## 工具输出格式

### list_ssh_targets

输出为纯 JSON:

```json
{"targets":[{"index":1,"id":"prod-a","name":"prod","host":"8.140.200.0","port":22,"username":"root"}]}
```

失败时输出:

```json
{"error":"获取目标列表失败: ...","targets":[]}
```

### create_ssh_web_terminal

输出文本中包含一次性会话链接。

### open_ssh_terminal

输出文本中包含一次性会话链接,并包含匹配到的目标信息。

## 扩展方式

在 src/index.ts 中新增 MCP 工具,建议遵循以下步骤:

1. 通过 mcpServer.registerTool 定义工具名称、描述与 inputSchema
2. 在工具实现中调用 WebShell 网关 API 或其它后端服务
3. 返回结构化 JSON 或稳定格式的文本输出
4. 执行 npm run build 生成 dist

## 开发注意事项

- Node.js 版本:
  - 建议使用 Node.js 18+,以获得更好的性能与长期支持
- 本地开发:
  - 使用 `npm run dev` 以 TS 源码方式启动,默认监听 `PORT` 与内联 Gateway 端口
  - 修改 TypeScript 源码后无需手动编译,`tsx` 会实时加载
- 类型与质量检查:
  - 提交代码前建议执行 `npm run type-check` 确认类型检查无误
  - 若项目新增脚本或配置,请同步更新本文档相关章节
- 与数据库联调:
  - 本地调试时推荐使用 Docker 启动 MySQL,并通过 WEBSHELL_DB_HOST 指向容器或本机
  - 避免在生产数据库直接进行开发调试操作

## 日志与审计字段

### 会话审计(sessions 表)

`sessions` 表既承担会话历史记录,也可以作为基础的审计日志,核心字段含义如下:

- `id`:会话唯一标识,等同于 `/session/:id` 中的路径参数。
- `targetId` / `targetName`:关联的 SSH 目标 ID 和冗余名称,便于按业务目标检索。
- `host` / `port` / `username`:会话建立时实际使用的连接参数,冗余存储,避免目标后续修改影响历史数据。
- `startTime` / `endTime`:会话起止时间,可用于统计使用时长和活跃度。
- `status`:`connected` / `disconnected` / `error`,代表会话最终状态。
- `reason`:上游调用方传入的业务原因描述,例如工单号、变更单号等,建议生产环境按规范填充便于审计。

在生产环境中,可以基于 sessions 表构建报表或对接审计系统,例如统计单用户、单目标或单时间区间内的会话次数与失败率。

### UI 最近会话列表(/ui/recent-sessions)

MCP UI 通过 `/ui/recent-sessions` 接口返回内存中最近会话列表,其数据结构为:

- `id`:最近会话记录的内部 ID,与数据库 sessions 表的 `id` 不同。
- `targetId`:目标 ID,对应 `targets.id`。
- `reason`:创建会话时填写的原因。
- `sessionUrl`:内联 Gateway 返回的 WebShell 会话链接。
- `createdAt`:记录创建时间(毫秒时间戳)。

recentSessions 列表只保存在内存中,默认只保留最近 20 条记录,便于在 UI 侧快速查看最新发起的会话。

### 服务启动与错误日志(标准输出)

当前服务的日志主要通过标准输出/错误输出产生,典型包括:

- MCP HTTP 服务启动日志:
  - `SSH MCP HTTP server listening on http://localhost:<port>`
- 内联 WebShell Gateway 启动日志:
  - `SSH WebShell gateway listening on http://localhost:<MOCK_WEBSHELL_PORT>`
- HTTP Server 错误:
  - 在监听端口出错时,会将错误信息写入标准错误输出并退出进程。

生产环境中通常通过 Docker 日志、systemd 日志或容器编排平台(如 Kubernetes)集中采集上述输出,如需要更细粒度的结构化日志,可以在现有基础上引入统一的日志框架并扩展记录字段。

## 常见问题

### 端口被占用

- 启动失败并提示端口占用时:
  - 修改 PORT,或停止占用 3001 端口的进程后再启动
  - 若 Docker 场景下 3001/9100 被占用,可在 docker-compose.yml 中调整宿主机端口映射

### WebShell 网关不可达或会话链接打不开

- 检查 SSH_WEBSHELL_API_URL:
  - Docker Compose 默认在容器内使用 `http://localhost:9100`,无需修改
  - 若自行拆分 Gateway,请确保 URL 指向可访问的 HTTP 服务
- 检查会话访问地址:
  - 确认 WEBSHELL_PUBLIC_BASE_URL 与对外访问地址一致
  - 宿主机上确认 9100 端口已映射且未被防火墙拦截

### 无法连接 MySQL 或 list_ssh_targets 无返回数据

- 检查数据库连接配置:
  - 确认 WEBSHELL_DB_HOST/PORT/USER/PASSWORD/DATABASE 与实际数据库一致
  - Docker Compose 场景下应保持 WEBSHELL_DB_HOST=mysql
- 检查数据库服务状态:
  - 查看容器日志:`docker compose logs mysql`
  - 确认数据库已初始化完成且能正常接受连接
- 检查数据是否存在:
  - 登录数据库确认目标表中已写入 SSH 目标数据

### 容器反复重启

- 查看 ssh-mcp 容器日志:`docker compose logs ssh-mcp`
- 常见原因:
  - 数据库连接失败(账号密码不正确或网络不可达)
  - 环境变量缺失或配置错误
  - 宿主机资源不足导致进程被杀死