# bolt — 极速单文件 MCP 命令执行服务器 让 AI(Claude Code / Claude Desktop / Cursor 等 MCP 客户端)通过 **MCP 协议**连接后,在服务器上执行 shell 命令、读写和传输文件。 - **单文件**:全部代码在 `src/main.rs`,仅依赖 `serde_json`(Unix 下加 `libc`);base64 / SHA-256 / HMAC 均为手写 - **零框架**:手写 JSON-RPC 2.0 + MCP 协议 + Streamable HTTP 服务器,无 async runtime、无重量级依赖 - **双传输**:`stdio`(本地 AI 客户端默认)与 `Streamable HTTP`(远程连接) ## 安装 ### 一键安装(Linux / macOS) 在目标设备上执行(需要 root;已经是 root 时去掉 `sudo`)。IPv4 网络用 `git.jocayn.qzz.io`,IPv6 网络用 `git.draws.qzz.io`: ```bash # IPv4 curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌 # IPv6 curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌 ``` 脚本会:识别平台 → 从 Release 下载最新版二进制并校验 SHA-256 → 装到 `/usr/local/bin/bolt` → 写入配置 → 注册系统服务(Linux 为 systemd 的 `bolt`,macOS 为 launchd 的 `io.jocay.bolt`)→ 设置开机自启并启动。 - 目前 Release 提供 `linux-amd64`(glibc ≥ 2.28,Debian 10 / Ubuntu 18.10 及以上)和 `darwin-arm64`(macOS 11+,Apple Silicon);其他平台请[从源码构建](#从源码构建)后用 `--binary` 安装。 - 只在本机使用、或放在反向代理后面时,监听 `127.0.0.1:端口` 即可;直接对外监听 `0.0.0.0` 时务必设置 `--token`。 - 已安装的设备再次执行会被拒绝,以免按新参数重写配置时丢掉令牌。升级请用[更新](#更新),确实要重装加 `--reinstall`。 更多参数写在 `sh -s --` 之后,例如限制命令和文件目录: ```bash curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- \ --http 0.0.0.0:8080 --token 你的令牌 --allow "git,docker,ls" --file-root /srv/data ``` | 参数 | 含义 | |---|---| | `--http ` | 监听地址,默认 `127.0.0.1:8080`;对外用 `0.0.0.0:8080` | | `--token ` | Bearer 认证令牌(对外监听时必填) | | `--allow ` / `--deny ` | 命令白/黑名单 | | `--timeout-ms ` | 命令默认超时,默认 30000 | | `--file-root ` | 文件功能允许访问的目录(配置 `--allow` 时必填,否则文件功能禁用) | | `--max-upload ` | `PUT /files` 单次上传上限,默认 2G | | `--version ` | 安装指定版本,默认最新 | | `--binary <路径或URL>` | 不从 Release 下载,改用本地二进制(如 `scp` 过去的)或指定 URL | | `--reinstall` | 已安装时按本次参数重写配置 | | `--no-start` / `--no-enable` | 只安装不启动 / 不设开机自启 | 环境变量:`BOLT_GIT_HOST` 指定下载站点(默认先试 `git.jocayn.qzz.io`,不通再试 `git.draws.qzz.io`),`BOLT_DL_URL` 直接指定二进制地址,`BOLT_DL_TOKEN` 用于私有仓库下载(与 `--token` 无关)。 装好后查看状态: ```bash systemctl status bolt # Linux sudo launchctl print system/io.jocay.bolt # macOS ``` ### 连接 AI 客户端 ```bash # Claude Code 连接远程 bolt(带令牌) claude mcp add --transport http bolt http://服务器:8080/mcp --header "Authorization: Bearer 你的令牌" # 放在 HTTPS 反向代理后面时,直接用代理地址 claude mcp add --transport http bolt https://bolt.example.com/mcp --header "Authorization: Bearer 你的令牌" ``` 其他 MCP 客户端按 Streamable HTTP 方式配置 `http://服务器:8080/mcp`,并加上 `Authorization: Bearer 你的令牌` 请求头。 ### 更新 只替换二进制并重启服务,**保留现有配置**: ```bash # IPv4 curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh # IPv6 curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh ``` - 已是最新版本时直接跳过,不会自动降级;新版本服务起不来时自动回滚,旧版本备份在 `/usr/local/bin/bolt.bak`。 - 重启由独立进程完成,所以让 AI 通过 bolt 自己执行更新也安全(只是这次调用的连接会断开,稍后重连即可)。 - 可选参数(写在 `sh -s --` 之后):`--version v0.2.0` 安装指定版本,`--binary ./bolt` 使用本地二进制,`--force` 同版本也重装。 ### 卸载 ```bash # IPv4 curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh # IPv6 curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh ``` 删除服务、二进制和备份,默认同时删除配置与日志;要保留配置加 `sh -s -- --keep-config`。 ### 从源码构建 ```bash cargo build --release # 产物: target/release/bolt (~1MB) ``` **本地模式(stdio)**:不需要安装服务,由 MCP 客户端直接拉起本进程 ```bash claude mcp add --transport stdio bolt -- /abs/path/to/bolt # 通用 MCP 客户端: {"mcpServers": {"bolt": {"command": "/abs/path/to/bolt"}}} ``` **远程模式(Streamable HTTP)**:直接前台运行 ```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" --file-root /srv/data ``` 要注册成系统服务,把构建产物交给安装脚本:`sudo sh deploy/install.sh --binary target/release/bolt --http 0.0.0.0:8080 --token 你的令牌`。 ## 工具 | 工具 | 说明 | |---|---| | `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`;空闲 30 分钟自动回收 | | `shell_write` | 向会话写入输入(自动补换行) | | `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` 命令 | 所有工具结果都同时提供 `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。 - 签名覆盖方法、路径、写入模式和过期时间,链接只能用于签名时指定的那一个操作;签名密钥在进程启动时随机生成,重启后旧链接失效。 ## 配置 CLI 参数(环境变量 `BOLT_*` 为回退): | 参数 | 默认 | 说明 | |---|---|---| | `--http ` | 无(stdio) | 启用 Streamable HTTP 监听地址,如 `127.0.0.1:8080`(`:8080` 等价于 `127.0.0.1:8080`) | | `--token ` | 无 | HTTP 要求 `Authorization: Bearer ` | | `--allow ` | 空 | 命令白名单(按程序名精确匹配;非空时 shell 模式禁止元字符) | | `--deny ` | 空 | 命令黑名单(按程序名匹配命令中每一段,尽力而为) | | `--timeout-ms ` | 30000 | 命令默认超时(工具参数可覆盖,0 = 无限) | | `--cwd ` | 当前 | 命令与相对文件路径的默认工作目录 | | `--max-output ` | 1M | 单流输出捕获上限(支持 K/M/G) | | `--allow-origin ` | localhost/127.0.0.1 | 额外允许的 HTTP Origin(防 DNS rebinding) | | `--file-root ` | 不限 | 文件工具与 `/files` 只能访问这些目录;配置了 `--allow` 时必须设置,否则文件功能禁用 | | `--max-upload ` | 2G | `PUT /files` 单次上传上限(支持 K/M/G) | ```bash 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`(仅 stdio 生效)、`notifications/progress`(进度上报) - 工具执行错误以 `isError: true` 返回;协议错误返回标准 JSON-RPC 错误码(-32601/-32602/-32700 等) - 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` ## 测试 ```bash cargo test --release # 单元测试(策略/编码与摘要/HTTP 解析/文件读写与目录限制/会话缓冲/执行引擎等) cargo test --release test_files # 只跑某一个 ``` 端到端测试脚本在 `docs/`(需要 Unix 主机;二进制路径默认 `target/release/bolt`,可用 `BOLT_BIN` 指定): ```bash 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`):按程序名精确匹配,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` 杀进程树、Ctrl+C 清理退出) ## 部署细节 ### 配置文件与修改 | | Linux(systemd) | macOS(launchd) | |---|---|---| | 二进制 | `/usr/local/bin/bolt` | `/usr/local/bin/bolt` | | 配置 | `/etc/bolt.env`(`BOLT_*` 环境变量);监听地址在 `/etc/systemd/system/bolt.service` 的 `ExecStart` | `/Library/Application Support/bolt/io.jocay.bolt.plist`(开机自启时复制到 `/Library/LaunchDaemons/`) | | 日志 | `journalctl -u bolt -f` | `/var/log/bolt/stdout.log`、`/var/log/bolt/stderr.log` | | 重启 | `sudo systemctl restart bolt` | `sudo launchctl kickstart -k system/io.jocay.bolt` | Linux 上改完 `/etc/bolt.env` 后重启服务即可生效,可用的变量见 `deploy/bolt.env.example`。macOS 的两份 plist 需要保持一致,改参数建议用 `--reinstall` 重新执行安装脚本(记得带上原来的 `--token` 等参数)。两份配置均仅 root 可读写。 ### macOS(launchd) `--no-enable` 只保留手动启动配置,取消本服务的开机配置,仍可立即启动;`--no-start` 本次不加载服务,已运行的本服务也会卸载,下次开机是否启动由 `--no-enable` 决定。两个参数可一起使用。 ```bash # 查看服务 / 重启已加载的服务 / 停止并卸载 sudo launchctl print system/io.jocay.bolt sudo launchctl kickstart -k system/io.jocay.bolt sudo launchctl bootout system/io.jocay.bolt # 停止后或使用 --no-start 安装后,手动加载并启动 sudo launchctl bootstrap system '/Library/Application Support/bolt/io.jocay.bolt.plist' ``` macOS 服务的工作目录为 `/var/root`,PATH 包含 `/opt/homebrew/bin` 和 `/opt/homebrew/sbin`,不会加载个人 shell 配置。 ### systemd 手动部署 不用安装脚本时,仓库内 `deploy/` 提供现成的部署文件: ```bash sudo install -m 0755 target/release/bolt /usr/local/bin/bolt sudo cp deploy/bolt.service /etc/systemd/system/ sudo cp deploy/bolt.env.example /etc/bolt.env # 按需修改(监听地址在 service 文件 ExecStart) sudo systemctl daemon-reload sudo systemctl enable --now bolt # 开机自启 + 立即启动 ``` 服务特性:崩溃自动重启(`Restart=on-failure`,2 秒间隔)、优雅停机(SIGTERM 时杀掉所有子进程树)、日志进 journald、`LimitNOFILE=65536`。 ### 发布新版本 1. 修改 `Cargo.toml` 的 `version`,提交并推送。 2. 编译附件(需要 [cargo-zigbuild](https://github.com/rust-cross/cargo-zigbuild) 与 zig): ```bash cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.28 # bolt-linux-amd64,只依赖 glibc 2.28 cargo zigbuild --release --target aarch64-apple-darwin # bolt-darwin-arm64,安装时缺少签名会自动补 ad-hoc 签名 sha256sum bolt-* > SHA256SUMS ``` 直接 `cargo build` 的产物依赖构建机的 glibc 版本,拿到较老的系统上可能无法运行。 3. 在 Gitea 上新建 Release(tag 如 `v0.3.0`),上传 `bolt-linux-amd64`、`bolt-darwin-arm64` 和 `SHA256SUMS`。脚本按 `/api/v1/repos/jocay/bolt/releases/latest` 识别最新版本,发布后各设备执行[更新](#更新)命令即可。