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 目录执行:
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或通过环境变量覆盖。
方式二:Docker build/run(单容器,依赖外部 MySQL)
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
方式三:本地运行
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 实例(示例):
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 环境变量注入):
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. 启动服务并访问
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 等)。
- 进程内内存 Map:键为
- MCP 工具调用内联 Gateway 的
- 会话访问:
- 浏览器访问
/session/<sessionId>时:- 如果内存 Map 中存在对应条目,则渲染 WebShell 终端页面并建立 WebSocket
/ws?sessionId=...。 - 如果不存在,则返回 404 文本:
会话不存在或已过期。
- 如果内存 Map 中存在对应条目,则渲染 WebShell 终端页面并建立 WebSocket
- 浏览器访问
- 会话结束:
- WebSocket 正常关闭时:
- 内联 Gateway 调用
updateSessionEndTime,将sessions表中的endTime更新为当前时间,status置为disconnected。
- 内联 Gateway 调用
- 连接远程主机出错时:
- 会话记录的
status置为error,endTime同样更新为当前时间。
- 会话记录的
- WebSocket 正常关闭时:
- 失效策略(当前版本):
sessionId仅保存在内存 Map 中,不会被持久化用于恢复实时 SSH 通道。- 服务重启后内存 Map 清空,所有历史链接都会返回“会话不存在或已过期”。
- 当前未对
sessionId设置基于时间的自动 TTL 清理,在服务不重启的情况下同一链接可被多次访问。 - 建议上层调用方将会话链接视为短期凭证,不长期缓存或下发到不可信环境。
认证会话(auth_sessions)
- auth_sessions 表用于保存认证会话 Token(预留能力):
token:会话 Token 主键。username/provider:标识对应用户与认证来源。expires_at:过期时间。
- 失效判断:
- 在读取会话时,如果
expires_at <= 当前时间,会被视为已过期并立刻从表中删除。 - 提供批量清理能力,用于定期清理历史过期会话。
- 在读取会话时,如果
- 当前版本尚未对外暴露基于该表的登录接口,后续如启用认证体系,可基于此表实现登录态管理。
工具输出格式
list_ssh_targets
输出为纯 JSON:
{"targets":[{"index":1,"id":"prod-a","name":"prod","host":"8.140.200.0","port":22,"username":"root"}]}
失败时输出:
{"error":"获取目标列表失败: ...","targets":[]}
create_ssh_web_terminal
输出文本中包含一次性会话链接。
open_ssh_terminal
输出文本中包含一次性会话链接,并包含匹配到的目标信息。
扩展方式
在 src/index.ts 中新增 MCP 工具,建议遵循以下步骤:
- 通过 mcpServer.registerTool 定义工具名称、描述与 inputSchema
- 在工具实现中调用 WebShell 网关 API 或其它后端服务
- 返回结构化 JSON 或稳定格式的文本输出
- 执行 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 服务
- Docker Compose 默认在容器内使用
- 检查会话访问地址:
- 确认 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 - 常见原因:
- 数据库连接失败(账号密码不正确或网络不可达)
- 环境变量缺失或配置错误
- 宿主机资源不足导致进程被杀死