caveman-mcp:把 Caveman 压缩引擎以 stdio MCP 服务暴露给任意 Agent 主机的完整实现解析
本篇以仓库中的 mcp/README.md 为核心,完整讲解 Caveman 的 MCP 服务:如何在 Claude Code 等任意 MCP 主机上一行配置接入 caveman-mcp,五个压缩工具(caveman_compress、caveman_retrieve、caveman_stats、caveman_toon_encode、caveman_toon_decode)的输入输出契约与 fail-open/fail-closed 语义,并深入源码剖析其 stdio JSON-RPC 协议帧、恢复存储(CCR store)、防"检索风暴"账本等关键实现,读完后可独立部署、排错并理解每个设计决策的依据。
一、caveman-mcp 是什么:薄适配层 + 进程内引擎
caveman-mcp 是 Caveman 项目的 stdio MCP 服务器:它把 Caveman Engine 的压缩能力包装成五个 MCP 工具,通过 stdin/stdout 上的 JSON-RPC 与任何 MCP 主机(Claude Code、Cursor 等)通信。两个核心定位在 mcp/README.md 中一句话讲清:
- Local-only:不打开任何网络连接。源码层面这一点被严格约束——mcp/server.go 的包注释声明 "Servers built here open no network connection",且 mcp/AGENTS.md 记录的 "zero-egress" 不变量:该适配层不导入
net/net/http/os/exec,并有测试解析源码来强制执行这一约束。 - inferred-only:它永远只报告
basis:"inferred"的推断式节省数据,绝不出现在语义上更强的verified字样(这一点在工具描述字符串中也被固化,见下文 stats 工具)。
架构上是"薄适配层 + 进程内链接":MCP 帧处理(initialize、tools/list、tools/call、notifications)归 mcp/server.go 所有,而全部压缩逻辑属于引擎,以进程内链接方式引入(无子进程、无版本漂移),见 mcp/engine_tools.go 中引入的 github.com/JuliusBrussee/caveman/engine 与 engine/ccr 包。为了让帧处理逻辑可以脱离真实压缩器测试,EngineTools 依赖的是一个 Engine 接口而非具体类型:
// mcp/engine_tools.go
// Engine is the slice of the Caveman Engine the Caveman tools need.
// *engine.Engine satisfies it; tests inject a mock so the framing can be proven
// without the real compressors.
type Engine interface {
Compress(input []byte, opts engine.Options) (engine.Result, error)
Retrieve(handle string) ([]byte, error)
RetrieveQuery(handle, query string) ([]byte, error)
Stats() (ccr.Stats, error)
EncodeTOON(input []byte) ([]byte, error)
DecodeTOON(input []byte) ([]byte, error)
}
这种可注入设计使得 mcp/engine_tools_antistorm_test.go 能用内存 map 模拟引擎验证账本规则,而 mcp/retrieve_integration_test.go 则用真实引擎 + 内存 CCR store 做端到端验证。
二、安装:npm 启动器下载签名校验的二进制
Claude Code 配置
mcp/README.md 给出的标准接入方式是在 .mcp.json(或 claude mcp config)中注册:
// .mcp.json / claude mcp config
{
"mcpServers": {
"caveman": { "command": "npx", "args": ["-y", "caveman-mcp"] }
}
}
npm 包 caveman-mcp(元数据见 mcp/package.json)本身是一个 MIT 许可的启动器:bin 指向 bin/caveman-mcp.mjs,首次运行时下载与平台匹配的 BSL-1.1 许可二进制,验证密钥签名的 checksum 清单和产物 SHA-256,并缓存到 ~/.caveman/bin。整个过程不需要 Go 工具链,也不需要全局安装 Caveman。
如果想跳过下载、直接使用一个已审查的二进制,可以显式指定:
CAVEMAN_MCP_BIN=/path/to/caveman-mcp npx caveman-mcp
双许可结构在 mcp/BINARY_LICENSE.md 中明确划分:
MIT covers npm launcher files only.
caveman-mcpGo source and official binary are governed by BSL 1.1; see repositoryLICENSE.BSLandLICENSING.md.
即 npm 启动器文件为 MIT,Go 源码与官方二进制遵循 BSL 1.1(对应仓库根目录的 LICENSE.BSL)。
启动器行为有测试佐证:mcp/tests/launcher.test.mjs 验证了启动器会跳过 npm .bin shim、直接复用缓存且已验证的二进制(通过 CAVEMAN_HOME 指向的 bin/caveman-mcp);mcp/tests/package.test.mjs 则完整走一遍 npm pack → 安装到消费者目录 → 用显式二进制做 --version 冒烟测试,并断言打包产物里包含 bin/caveman-mcp.mjs、bin/binary-installer.generated.mjs、bin/release.generated.mjs 三个文件。
命令行与版本探测
二进制本身只支持一个显式子命令(见 mcp/cmd/caveman-mcp/main.go):
usage: caveman-mcp [version --json]
version --json 输出 caveman.mcp.version.v1 schema 的 JSON,其中 capabilities 声明了 mcp_recovery 与 build_stamped_version。注意 version 是构建时打戳的:服务器构造用的是 mcp.NewServerVersion(而非 NewServer),目的是让 initialize 响应里的版本号与 version --json 永远一致,避免"兼容性信号漂移"(见 mcp/server.go 注释)。
三、五个工具的完整契约
mcp/README.md 的 Tools 表格是本文的核心骨架,下面在完整继承该表的基础上,结合 mcp/engine_tools.go 中的精确 schema 与 payload 结构逐项展开。
| 工具 | 输入 | 返回 |
|---|---|---|
caveman_compress |
input (string) |
压缩后文本、推断的 ratio、recovery_handle(直通时为 null) |
caveman_retrieve |
recovery_handle (string) |
字节级精确的原文;未知 handle 返回错误 |
caveman_stats |
— | 会话级合计:前后 token 数、ratio、basis:"inferred"、scope:"session" |
caveman_toon_encode |
input (JSON 字符串) |
显式 JSON→TOON 结果,含两侧大小;不可编码时原样返回并附 note |
caveman_toon_decode |
input (TOON 字符串) |
解码后的 JSON;非法 TOON 返回错误 |
工具名是恰好这五个、大小写敏感的常量(mcp/engine_tools.go),EngineTools 是唯一注册入口。
3.1 caveman_compress:fail-open 的有损但可逆压缩
工具描述(tools/list 中 Agent 实际看到的文案)声明了关键语义:
Lossy (S4) but reversible: returns the compressed text, an inferred token ratio, and a recovery_handle for caveman_retrieve. Fails closed: unusable input or a result that is not smaller returns unchanged with ratio 0.
输入 schema 接受三个字段(mcp/engine_tools.go):
input(必填):要压缩的载荷;content_type(可选):引擎内容类型,如json、toon;type:content_type的别名,两者都传时以content_type优先(见compressTool的取值逻辑)。
返回是一个 JSON 文本项(MCP 的 ToolText 会把结果序列化为 Agent 可解析的 JSON),字段结构为 compressPayload(mcp/engine_tools.go):
type compressPayload struct {
Compressed string `json:"compressed"`
Ratio float64 `json:"ratio"`
TokensBefore int `json:"tokens_before"`
TokensAfter int `json:"tokens_after"`
Basis string `json:"basis"`
ContentType string `json:"content_type"`
RecoveryHandle *string `json:"recovery_handle"`
Method string `json:"method,omitempty"`
LosslessToModel *bool `json:"lossless_to_model,omitempty"`
}
注意 RecoveryHandle 是 *string:直通(未产生压缩收益)时为 JSON null,与 README 表格中 "null on pass-through" 的约定一一对应。
错误路径的边界处理很值得注意(mcp/engine_tools.go):当引擎返回错误时,如果引擎同时返回了一个"完全记账的字节级精确直通结果",工具会保留它;但如果第三方 Engine 实现返回了"不安全/空结果 + 错误"这种组合,工具会 fail-loud 返回 cave_compress_failed——源码注释解释了这个理由:"让非空内容看起来零 token 成本"是绝对不允许的记账错误。
3.2 caveman_retrieve:字节级精确恢复,且是唯一豁免结果上限的工具
caveman_retrieve 是恢复语义的核心,返回的是字节级精确的原文。它有两个特殊待遇:
- 豁免结果大小上限。mcp/server.go 定义了
Tool.ExemptResultCap字段:
// ExemptResultCap declares that this tool returns the exact original bytes,
// which must NEVER be truncated or rejected by maxResultBytes — recovery
// paths (caveman_retrieve) set it, because the CCR store is shared with a
// gateway that has no matching ceiling, so an original can legitimately
// exceed the cap and failing it closed would make elided content
// unrecoverable ...
ExemptResultCap bool
背景是:CCR 存储与 Caveman gateway 共享,gateway 侧没有对应的 16 MiB 上限,因此原文完全可能超过该上限;若恢复路径因此 fail-closed,被省略的内容就永久不可恢复了。在 mcp/server.go 的 handleToolCall 中可以看到豁免判断 !s.capExempt[p.Name]。
- 防风暴账本。工具输入除了必填的
recovery_handle外还有一个"强烈建议"的query字段(可选):
"recovery_handle": StringProp("Exact ccr_ handle returned by Caveman or copied from a <<ccr:HANDLE>> marker."),
"query": StringProp("Optional but strongly preferred: one broad description covering every detail you need from this handle, ..."),
query 走 BM25 收窄(eng.RetrieveQuery),空 query 则是字节级完整恢复;query 收窄视图只返回完整记录而非零散行,并在跳过处打印 … [caveman: non-adjacent] … 标记。防风暴账本见第五节。
未知 handle 的返回是 fail-closed 的工具错误:isError:true + cave_unknown_handle,绝不伪造载荷。
3.3 caveman_stats:只有 inferred,没有 verified
无输入,返回 statsPayload(mcp/engine_tools.go):
type statsPayload struct {
TokensBefore int `json:"tokens_before"`
TokensAfter int `json:"tokens_after"`
Requests int `json:"requests"`
Ratio float64 `json:"ratio"`
Basis string `json:"basis"`
Scope string `json:"scope"`
}
statsTool 中 Basis 硬编码为 engine.BasisInferred、Scope 硬编码为 "session",源码注释明确:the string "verified" never appears(PRD §11.5)。存储不可用时的错误码是 cave_stats_unavailable。
3.4 / 3.5 TOON 编解码对
caveman_toon_encode:对统一/表格化 JSON 做显式的 TOON 重编码。注意它不是代理的 best-of 门控——即使编码结果不比原输入小,只要编码有效就返回(两侧字节数input_bytes/output_bytes都给出,encoded:true),把"值不值得"的判断权留给 Agent。无法往返(round-trip)的输入原样返回并附note: "not encoded: ..."——注释强调"永不静默 no-op,永不发明编码"(mcp/engine_tools.go)。caveman_toon_decode:TOON 解码回 JSON,响亮失败:非法 TOON 返回cave_invalid_toon错误(isError:true),绝不把原始输入伪装成 JSON 输出(mcp/engine_tools.go)。
四、协议层:一个"杀不死"的 stdio JSON-RPC 服务器
4.1 stdout 即协议通道
整个进程的输出纪律是:stdout 只承载 JSON-RPC 协议,日志一律走 stderr。mcp/cmd/caveman-mcp/main.go 的包注释和 mcp/AGENTS.md 的 Conventions 都强调这一点,且有专门测试守护。
4.2 帧格式与存活性(un-killable transport)
Server.Serve 的循环设计目标是"除 EOF 外任何东西都不能结束会话"(issue #139,见 mcp/server.go):
- 行分隔帧:一条 JSON 消息一行,响应是一个以换行结尾、不含内嵌换行的紧凑 JSON。
- 超长入站消息:单条消息超过
maxInboundBytes(默认 16 MiB,defaultMaxInboundBytes)时不无限缓冲,而是排空到下一个换行后回复cave_payload_too_large(JSON-RPC-32600),然后继续服务(mcp/server.go 的readLine)。 - 畸形行:解析失败回复
-32700(parse error),流继续。 - JSON-RPC batch 数组:按规范处理,逐成员派发,非通知响应合成一个数组返回;全通知 batch 无响应(mcp/server.go)。
- 通知:无
id或"id":null的请求视为 notification,绝不回复(mcp/protocol.go 的isNotification)。 - panic 隔离:工具 handler 的 panic 被
recover捕获后降级为cave_tool_panicked工具错误(mcp/server.go 的invokeHandler);派发表层的 panic 降级为cave_internal_error(serveOne)。 - 生成侧上限:工具结果内容超过
maxResultBytes(同样默认 16 MiB)时被替换为 fail-closed 错误,但caveman_retrieve豁免(见 3.2)。
为什么"死服务器比慢服务器更糟"?mcp/AGENTS.md 的 Gotchas 给出了因果链:代理(proxy)决定是否省略内容取决于安装期的 marker 文件,而不是运行中的 Agent;如果 MCP 服务器死了而代理还在压缩,Agent 就失去了展开省略内容的 caveman_retrieve。
4.3 协议版本协商永不报错
initialize 的处理(mcp/server.go)值得单独讲,因为它记录了一次真实事故:
- 适配层实现的契约版本是
2024-11-05(supportedProtocolVersions里只列了这一个,刻意显式)。 - 若客户端请求的版本在支持集合中,回显客户端版本;否则回答自己支持的版本,让客户端按 MCP 生命周期自行决定是否继续。
- 旧代码曾直接回
-32602: unsupported protocol version,导致 Claude Code(及其他所有已更新协议的客户端)直接丢弃该服务器。事故后果比"工具不可用"严重:如 4.2 所述,caveman wrap依据安装期 marker 判断恢复可用性,于是代理继续压缩而 Agent 端没有任何恢复工具。源码总结这条经验为:"Declining to echo an unimplemented version is right; refusing to speak is not."(拒绝回显未实现的版本是对的;拒绝对话是错的。)
initialize 成功响应还携带 capabilities.tools 与 serverInfo{name, version},其中 name 固定为 caveman。
4.4 结果封装与错误码词汇表
mcp/protocol.go 定义了 MCP 工具结果的三个构造函数:
ToolText(v):把值序列化为 JSON 文本项(Agent 拿到可解析的 JSON);ToolRawText(s):裸文本(恢复出的原文字节即走这里);ToolError(code, msg):isError:true+ 一个cave_snake_code,确保主机不会把错误误认为成功载荷。
已知错误码(散布于 mcp/engine_tools.go 与 mcp/server.go)包括:cave_invalid_arguments、cave_compress_failed、cave_unknown_handle、cave_unknown_tool、cave_stats_unavailable、cave_invalid_toon、cave_tool_panicked、cave_payload_too_large、cave_internal_error。JSON-RPC 层则使用标准错误码 -32700/-32600/-32601/-32602/-32603(mcp/protocol.go)。
由此形成两条清晰的语义分界(mcp/AGENTS.md 的 Honesty Invariants):
- fail-open:引擎错误或畸形输入 → 字节级一致的直通,永不产生协议错误(对应 README 的 "never an error");
- fail-closed:未知工具/handle →
isError+cave_snake_code;未知 JSON-RPC 方法 →-32601。
五、caveman_retrieve 的防风暴账本
这是 mcp/engine_tools.go 中最有工程含量的部分。动机是:caveman_retrieve 每次调用都消耗一整个 Agent 回合——模型会重读整个对话前缀,而之前每次 retrieve 返回的内容从此都是前缀的一部分,所以 N 次 retrieve 不是 N 份载荷的成本,而是 N 个回合乘以逐次增长的转录。
源码注释引用了 2026-08-10 对 229 个本地 CaveBench stdout 文件的只读扫描:34 个会话共 534 次恢复工具调用,其中 15 个会话超过 5 次(p95 为 118,max 143),而这 15 个高调用会话中有 8 个仍通过了精确任务评分。注释非常诚实地标注:这是混杂臂/任务/重复批次的描述性证据,不是同任务配对实验,不构成对某个通用阈值的验证——因此阈值保持为历史运行时行为。
在此之上实现了两条规则,且任何一条都不得扣留本会话尚未给过的内容:
- 重复去重:完全相同的
(handle, query)(query 先做与RetrieveQuery相同的 trim," "与""视为同一请求)第二次出现时,只返回一行指回转录中已有答案的提示(repeatNote),不再重发字节。 - 超限全量兑付:当会话内不同的 retrieve 次数超过
retrieveStormThreshold(常量值 5,见 mcp/engine_tools.go)后,下一次 retrieve 返回该 handle 的完整存储原文(而非 query 收窄视图)并明确告知(fullPayoutNote),意图是"一次性交齐,之后无可翻页"。
实现上,EngineTools 构造时创建 newRetrieveSession()("一个服务器进程即一个会话"),账本是一个带互斥锁的 map[string]bool(key 为 handle + \x00 + trim(query))。*retrieveSession 为 nil 时两条规则全部禁用、行为退化为旧语义——这保证了无会话概念的调用方不受影响。
测试覆盖是双层的:
- 单元层 mcp/engine_tools_antistorm_test.go:
TestRepeatedRetrieveIsAnsweredWithAPointer(含空白填充仍算重复、不同 query 必须正常服务)、TestRetrieveStormPaysOutInFull(前 5 次不得兑付、第 6 次必须是未过滤全量、兑付后重复要指回)、TestRetrieveWithoutASessionBehavesAsBefore、以及诚实性闸门TestAntiStormNeverWithholdsUnseenContent——从未请求过的 handle 无论会话计数多高都必须服务。 - 端到端层 mcp/retrieve_integration_test.go:用真实引擎 + 内存 CCR store +
testdata下三种真实形状的页面(inventory_catalog_page.json、webhook_delivery_events_page.json、inventory_stock_page.csv)驱动真实的工具 handler,断言恢复出的内容不含任何指针形态(ccr://、<<ccr:、full:行首、__caveman_elided__、elided (caveman)五类正则,见pointerShapes),且压缩视图丢弃的每一条 SKU / delivery id / CSV 行都逐字回来。
该端到端测试的注释还记录了一次真实故障:2026-08-08,被测 Agent 报告"caveman_retrieve 对指针只返回指向另一个指针的新指针,无限循环",做了 27-97 次恢复调用后超时。根因是 CCR store 中存在两个 id 空间(压缩铸造的 blob handle ccr_… 与代理原生运行时的 typed OBJECT id),而恢复工具当时只认 blob 空间。normalizeRecoveryHandle(mcp/engine_tools.go)现在接受全部四种形态——裸 ccr_…、<<ccr:ccr_…>>、半剥离的 ccr:ccr_…、以及原生运行时掩码的 ccr://…——因为"Agent 会原样复制它看到的东西,任何无法解析的形态都会把一次恢复变成一场风暴"。TestRetrieveHandleFormsAllResolve 与 TestRetrieveResolvesNativeRuntimeObjectPointers 分别锁死了这两条契约。
六、恢复存储:共享 CCR store 与环境变量
mcp/cmd/caveman-mcp/main.go 的 openRecoveryStore 定义了存储解析顺序,这也是理解"本地 MCP 与代理如何互通"的关键:
CAVEMAN_MCP_EPHEMERAL=1→ccr.OpenMemory(),全新内存 store,不触碰磁盘(测试/无需代理恢复的会话用);CAVEMAN_CCR_DB环境变量指定的路径;- 否则
CAVEMAN_HOME目录下的ccr.db;CAVEMAN_HOME未设时为~/.caveman/ccr.db; - 连用户主目录都解析失败时,兜底退回内存 store。
默认打开的是与 Caveman gateway(代理)共享的文件 store——这正是 README 标题 "over stdio, for any MCP host" 背后更深一层的含义:代理在流式请求中省略(elide)的内容所发布的 handle,可以在本地 MCP 服务器的 caveman_retrieve 里解析回来,跨越进程边界("so a caveman_retrieve here resolves handles the proxy disclosed")。
七、边界声明与使用成本
mcp/AGENTS.md 与 mcp/CLAUDE.md 还明确了几个使用前提,部署前值得知道:
- v1 限制:仅 stdio 传输、仅字符串载荷(内容类型由引擎检测);HTTP 传输与
caveman mcp子命令属于 v2 计划,当前仓库未提供。 - 构建/测试:
make product-build PRODUCT=mcp/make product-test PRODUCT=mcp。 - 注入成本:mcp/CLAUDE.md 指出,注册该服务器会把五个工具 schema 放入被包装 Agent 的每次调用前缀——agent bench 中测得约 11,060 tokens/call(201 次调用累计约 222 万)。因此
caveman wrap把注入放在execute.mcp表面开关(auto|marker-only|true|false)之后;非 auto 表面会同时抑制两个注入点(mcp install写入、配置型 Agent 的 profilemcp.servers.caveman叠加层),但从不卸载用户自己安装的服务器。该文档还提醒:在此新增第六个工具会抬高所有被包装 Agent 的每次调用税。 - 诚实性不变量:fail-open / fail-closed / zero-egress / 协议协商永不报错,四条均已在上文对应源码位置给出证据。
八、小结:如何阅读这份实现
回到 mcp/README.md 的骨架——五个工具、stdio、local-only、inferred-only——仓库源码把每一句承诺都落实成了可验证的代码与测试:
- 想看协议帧与存活性设计:mcp/server.go(
Serve、readLine、handleBatch、handleInitialize、handleToolCall); - 想看五个工具的描述、schema 与 payload:mcp/engine_tools.go;
- 想看 JSON-RPC 与工具结果类型:mcp/protocol.go;
- 想看启动、版本打戳与 store 解析:mcp/cmd/caveman-mcp/main.go;
- 想看行为契约的测试证据:mcp/retrieve_integration_test.go、mcp/engine_tools_antistorm_test.go、mcp/tests/launcher.test.mjs、mcp/tests/package.test.mjs。
这套代码的整体设计语言可以概括为:压缩可以激进(有损 S4),记账必须诚实(inferred、fail-closed、可字节级恢复),传输必须存活(除 EOF 外任何输入都不杀服务器)。理解了这三条主轴,caveman-mcp 的每个常量、每个错误码和每条注释就都能对上了。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00