feat: v0.2.0 文件传输(MCP 工具 + HTTP 直传)与安全/正确性修复

文件功能:
- 新增 file_read / file_write / file_stat 工具(文本/base64/图片,分块读取,原子替换写入,SHA-256)
- 新增 file_link 工具与 GET/PUT /files 端点:HMAC 签名限时链接或 Bearer 认证,
  支持 Range、HEAD、chunked、Expect: 100-continue,大文件不经过模型上下文
- 新增 --file-root / --max-upload;配置白名单但未限定目录时文件功能自动禁用

安全:
- 白名单改为程序名精确匹配,shell 模式禁止元字符,shell_write 输入逐行受检;黑名单检查每一段
- HTTP 签发 Mcp-Session-Id,按会话隔离 shell 会话与取消,支持 DELETE 终止会话
- HTTP 客户端不能再用 notifications/exit 关停服务;token 常量时间比较

正确性:
- 后台进程占用输出管道时 run_command 不再挂起(detached=true)
- shell_read 改用绝对偏移,修复缓冲裁剪后错位;被信号杀死的会话能报告退出;阻塞读可取消
- 输出与分块读取不再切断 UTF-8 多字节字符
- shell_* 结果改为标准 content + structuredContent
- POST + SSE 的 tools/call 在同一响应流中推送进度与结果
- /mcp 支持 Expect: 100-continue(curl 发送大请求时不再白等 1 秒)
- CLI 参数优先于环境变量;SIGTERM 在 stdio 模式下也能清理退出;Windows 支持 Ctrl+C 清理与优雅退出

资源:并发工具调用 / HTTP 连接 / shell 会话数量上限,空闲会话自动回收

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jocay
2026-10-18 10:23:15 +08:00
parent 63d48ec951
commit 84653bec61
9 changed files with 3083 additions and 752 deletions
+61 -24
View File
@@ -1,8 +1,8 @@
# bolt — 极速单文件 MCP 命令执行服务器
让 AI(Claude Code / Claude Desktop / Cursor 等 MCP 客户端)通过 **MCP 协议**连接后,在服务器上执行 shell 命令。
让 AI(Claude Code / Claude Desktop / Cursor 等 MCP 客户端)通过 **MCP 协议**连接后,在服务器上执行 shell 命令、读写和传输文件。
- **单文件**:全部代码在 `src/main.rs`(约 1900 行,其中 400 行为单元测试),仅依赖 `serde_json`(Unix 下加 `libc`)
- **单文件**:全部代码在 `src/main.rs`,仅依赖 `serde_json`(Unix 下加 `libc`);base64 / SHA-256 / HMAC 均为手写
- **零框架**:手写 JSON-RPC 2.0 + MCP 协议 + Streamable HTTP 服务器,无 async runtime、无重量级依赖
- **双传输**:`stdio`(本地 AI 客户端默认)与 `Streamable HTTP`(远程连接)
@@ -27,7 +27,7 @@ claude mcp add --transport stdio bolt -- /abs/path/to/bolt
```bash
bolt --http 127.0.0.1:8080 # 仅本机
bolt --http 0.0.0.0:8080 --token mysecret # 对外 + Bearer 认证
bolt --http 0.0.0.0:8080 --token mysecret --allow "git,docker,curl"
bolt --http 0.0.0.0:8080 --token mysecret --allow "git,docker,curl" --file-root /srv/data
# Claude Code 连接远程
claude mcp add --transport http bolt http://127.0.0.1:8080/mcp
@@ -38,12 +38,35 @@ claude mcp add --transport http bolt http://127.0.0.1:8080/mcp
| 工具 | 说明 |
|---|---|
| `run_command` | 执行 shell 命令:完整 shell 语法(管道/重定向/`&&`)、`cwd`、`env`、`timeout_ms`(超时自动杀进程树)、`max_output_bytes`(输出截断防膨胀)、`shell=false` 直接执行二进制;客户端可传 `_meta.progressToken` 接收进度通知,支持 `notifications/cancelled` 取消 |
| `shell_open` | 开启持久化交互式 shell 会话(`cd`、环境变量等状态跨次保留),可指定 `shell`(sh/bash/zsh/fish)与 `cwd` |
| `shell_open` | 开启持久化交互式 shell 会话(`cd`、环境变量等状态跨次保留),可指定 `shell`(sh/bash/zsh/fish)与 `cwd`;空闲 30 分钟自动回收 |
| `shell_write` | 向会话写入输入(自动补换行) |
| `shell_read` | 读取会话新输出,可阻塞等待(`timeout_ms`,0 = 等到会话退出) |
| `shell_read` | 读取会话新输出,可阻塞等待(`timeout_ms`,0 = 等到会话退出),可被取消 |
| `shell_close` | 关闭并杀掉会话进程树 |
| `file_read` | 读取文件:UTF-8 文本按文本返回,图片按 image 返回,其余按 base64;`offset` + `length` 分块读取,不会切断多字节字符 |
| `file_write` | 写入文件:`overwrite`(默认,临时文件 + rename 原子替换,保留原权限)/ `create`(已存在则失败)/ `append`;`encoding=base64` 写二进制;`mkdirs` 自动建目录;返回 SHA-256 |
| `file_stat` | 文件信息(是否存在、类型、大小、修改时间、权限),可选计算 SHA-256 |
| `file_link` | 仅 HTTP 模式:生成带 HMAC 签名、限时有效的 `/files` 直传链接,并给出可在客户端本机执行的 `curl` 命令 |
`run_command` 返回:`exit_code` / `signal` / `timed_out` / `cancelled` / `duration_ms` / `stdout` / `stderr` / 截断标志等(同时提供 `content` 与 `structuredContent`)。
所有工具结果都同时提供 `content` 与 `structuredContent`(`file_read` 为两块 `content`:元数据 JSON + 文件内容)。
`run_command` 返回:`exit_code` / `signal` / `timed_out` / `cancelled` / `detached` / `duration_ms` / `stdout` / `stderr` / 截断标志等。命令退出后若仍有后台进程占用输出管道(如 `python -m http.server &`),等待 300ms 后返回 `detached=true`,后台进程继续运行。
## 文件传输
MCP 工具的参数和结果都要经过模型上下文,所以分两层:
- **小文件**(配置、脚本、日志片段、截图):直接用 `file_read` / `file_write`,stdio 和 HTTP 都可用。
- **大文件**(HTTP 模式):数据走 `/files` 端点,不经过模型。
- AI 调用 `file_link` 拿到签名链接,在客户端本机执行返回的 `curl` 命令即可:
```bash
curl -fsSL -o ./backup.tar.gz 'http://host:8080/files?path=/srv/data/backup.tar.gz&exp=...&sig=...' # 下载
curl -fsS -T ./dataset.zip 'http://host:8080/files?path=/srv/data/dataset.zip&exp=...&sig=...&mode=overwrite&mkdirs=0' # 上传
```
- 也可以直接带 Bearer 令牌访问:`curl -H "Authorization: Bearer $T" -T f.bin "http://host:8080/files?path=/srv/data/f.bin&mkdirs=1"`。
- 支持 `Range` 断点续传(206)、`HEAD`、chunked 上传、`Expect: 100-continue`;上传同样是原子替换,失败不破坏原文件,响应里带 SHA-256。
- 签名覆盖方法、路径、写入模式和过期时间,链接只能用于签名时指定的那一个操作;签名密钥在进程启动时随机生成,重启后旧链接失效。
## 配置
@@ -51,59 +74,71 @@ CLI 参数(环境变量 `BOLT_*` 为回退):
| 参数 | 默认 | 说明 |
|---|---|---|
| `--http <addr>` | 无(stdio) | 启用 Streamable HTTP 监听地址,如 `127.0.0.1:8080` |
| `--http <addr>` | 无(stdio) | 启用 Streamable HTTP 监听地址,如 `127.0.0.1:8080`(`:8080` 等价于 `127.0.0.1:8080`) |
| `--token <t>` | 无 | HTTP 要求 `Authorization: Bearer <t>` |
| `--allow <a,b>` | 空 | 命令白名单(命令首词前缀匹配;非空时仅允许列表内命令) |
| `--deny <a,b>` | 空 | 命令黑名单(前缀匹配) |
| `--allow <a,b>` | 空 | 命令白名单(按程序名精确匹配;非空时 shell 模式禁止元字符) |
| `--deny <a,b>` | 空 | 命令黑名单(按程序名匹配命令中每一段,尽力而为) |
| `--timeout-ms <n>` | 30000 | 命令默认超时(工具参数可覆盖,0 = 无限) |
| `--cwd <dir>` | 当前 | 命令默认工作目录 |
| `--cwd <dir>` | 当前 | 命令与相对文件路径的默认工作目录 |
| `--max-output <n>` | 1M | 单流输出捕获上限(支持 K/M/G) |
| `--allow-origin <url>` | localhost/127.0.0.1 | 额外允许的 HTTP Origin(防 DNS rebinding) |
| `--file-root <d1,d2>` | 不限 | 文件工具与 `/files` 只能访问这些目录;配置了 `--allow` 时必须设置,否则文件功能禁用 |
| `--max-upload <n>` | 2G | `PUT /files` 单次上传上限(支持 K/M/G) |
```bash
export BOLT_TOKEN=xxx BOLT_ALLOW="git,docker" BOLT_TIMEOUT_MS=60000
export BOLT_TOKEN=xxx BOLT_ALLOW="git,docker" BOLT_TIMEOUT_MS=60000 BOLT_FILE_ROOT=/srv/data
bolt --http 127.0.0.1:8080
```
## 协议实现(2025-06-18 规范)
- `initialize`(协议版本协商,支持 2024-11-05 / 2025-03-26 / 2025-06-18)、`ping`、`tools/list`、`tools/call`、`shutdown`
- `notifications/initialized`、`notifications/cancelled`、`notifications/exit`、`notifications/progress`(进度上报)
- `notifications/initialized`、`notifications/cancelled`、`notifications/exit`(仅 stdio 生效)、`notifications/progress`(进度上报)
- 工具执行错误以 `isError: true` 返回;协议错误返回标准 JSON-RPC 错误码(-32601/-32602/-32700 等)
- Streamable HTTP:POST 请求(通知 → 202;请求 → JSON 或 SSE 响应)、GET 长连接 SSE 流(服务器通知广播 + 10s 心跳)、CORS、Origin 校验、Bearer 认证、无状态(不要求会话 ID)
- Streamable HTTP:
- `initialize` 时签发 `Mcp-Session-Id`;携带该头的请求按会话隔离 shell 会话与取消,未知/过期 ID 返回 404,`DELETE /mcp` 终止会话并清理其资源;不带该头的客户端共用一个匿名作用域(兼容无状态客户端)
- POST:通知 → 202;请求 → JSON 或 SSE。`Accept` 含 `text/event-stream` 的 `tools/call` 会在同一个 SSE 响应里先推进度通知、最后推结果
- GET:长连接 SSE 流(接收本会话的服务器通知 + 10s 心跳)
- 支持 chunked 请求体、CORS、Origin 校验、Bearer 认证
## 性能
## 性能与资源
- 单线程事件循环 + 复用缓冲区,无锁热路径;输出计数用原子操作,进度推送不占主循环
- 复用缓冲区,输出计数用原子操作,进度推送不占主循环;stdio 每个工具调用一个工作线程,HTTP 每个连接一个线程
- 子进程独占进程组(`setpgid`),超时/取消按进程组 `SIGKILL`,连带杀掉整棵进程树,无僵尸
- 上限:并发工具调用 128、HTTP 连接 512、shell 会话 64(空闲 30 分钟回收)、MCP 会话 1024(空闲 24 小时回收)
- 实测(本机,含 Python 客户端逐字节 I/O 开销):
- ping 往返吞吐 ≈ **14,700 msg/s**
- 并发 `echo` 命令平均 **4.7 ms/次**(进程 spawn + 捕获输出)
- 构建配置:`lto = "fat"`、`codegen-units = 1`、`strip = true`,二进制约 0.7MB
- 构建配置:`lto = "fat"`、`codegen-units = 1`、`strip = true`
## 测试
```bash
cargo test --release # 8 个单元测试(协议分发/策略/HTTP 解析/执行引擎/会话)
cargo test --release # 单元测试(策略/编码与摘要/HTTP 解析/文件读写与目录限制/会话缓冲/执行引擎等)
cargo test --release test_files # 只跑某一个
```
集成测试脚本在 `docs/`(stdio 12 项、HTTP 19 项,覆盖超时/取消/进程树/进度/并发/auth/Origin):
端到端测试脚本在 `docs/`(需要 Unix 主机;二进制路径默认 `target/release/bolt`,可用 `BOLT_BIN` 指定):
```bash
python3 /tmp/bolt_stdio_test.py # stdio 端到端
python3 /tmp/bolt_http_test.py # HTTP 端到端
python3 docs/bolt_stdio_test.py # stdio 端到端
python3 docs/bolt_http_test.py # HTTP 端到端(含 Mcp-Session-Id、/files、签名链接)
```
## 安全说明
- 默认只接受本机连接(`127.0.0.1`),对外公开需显式 `0.0.0.0` + `--token`
- 该服务器等于远程 root shell 入口,务必配置白名单(`--allow`)与认证后再暴露公网
- Unix 下命令在独立进程组运行,取消/超时会连带杀掉子进程
- 默认只接受本机连接(`127.0.0.1`),对外公开需显式 `0.0.0.0` + `--token`(未设置时启动日志会警告)
- 该服务器等于远程 root shell 入口,务必配置认证后再暴露公网
- 白名单(`--allow`):按程序名精确匹配,shell 模式下禁止 `; & | $ \` < > ( ) { }` 和换行,防止 `git status; rm -rf ~` 这类串联绕过;`shell_write` 写入的每一行同样受检。注意白名单内的程序如果自身能执行任意命令(`sh`、`env`、`docker`、`find -exec`、`git -c ...`),白名单就约束不了它
- 黑名单(`--deny`):检查命令每一段的程序名(去掉路径、引号、转义和前置的 `VAR=x`),但仍可能被包装命令绕过,只适合防误操作
- 文件功能等同于任意写文件(可写 `authorized_keys`、crontab),所以配置白名单时必须用 `--file-root` 限定目录,否则文件功能自动禁用;路径会解析符号链接并拒绝 `..` 逃逸
- 不同 MCP 会话之间的 shell 会话与取消相互隔离;HTTP 客户端无法通过 `notifications/exit` 关停服务
- Unix 下命令在独立进程组运行,取消/超时会连带杀掉子进程;收到 SIGINT/SIGTERM(Windows 为 Ctrl+C)时先清理全部子进程再退出
## 平台
- Linux / macOS(完整功能:进程组杀、信号处理)
- Windows(基础功能:`cmd /C`、`taskkill /T` 杀进程树;无信号优雅退出)
- Windows(基础功能:`cmd /C`、`taskkill /T` 杀进程树、Ctrl+C 清理退出)
## 在其他设备上部署(最快方式)
一键脚本支持 Linux(systemd)和 macOS(launchd),自动识别 amd64 / arm64;Apple Silicon 即使在 Rosetta 终端中也优先选择 arm64。安装需要 root 权限。
@@ -164,6 +199,8 @@ macOS 服务的工作目录为 `/var/root`,PATH 包含 `/opt/homebrew/bin` 和 `
| `--token <t>` | Bearer 认证令牌(公网必填) |
| `--allow <a,b>` / `--deny <a,b>` | 命令白/黑名单 |
| `--timeout-ms <n>` | 默认命令超时 |
| `--file-root <d1,d2>` | 文件功能允许访问的目录(配置 `--allow` 时必填,否则文件功能禁用) |
| `--max-upload <n>` | `PUT /files` 单次上传上限(默认 2G) |
| `--binary <路径或URL>` | 跳过下载,用本地已拷贝的二进制(如 `scp` 过去的) |
| `--no-start` / `--no-enable` | 只安装不启动 / 不设开机自启 |