首页
/ caveman-mcp:把 Caveman 压缩引擎以 stdio MCP 服务暴露给任意 Agent 主机的完整实现解析

caveman-mcp:把 Caveman 压缩引擎以 stdio MCP 服务暴露给任意 Agent 主机的完整实现解析

2026-09-04 10:58:15作者:冯梦姬Eddie

本篇以仓库中的 mcp/README.md 为核心,完整讲解 Caveman 的 MCP 服务:如何在 Claude Code 等任意 MCP 主机上一行配置接入 caveman-mcp,五个压缩工具(caveman_compresscaveman_retrievecaveman_statscaveman_toon_encodecaveman_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/engineengine/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-mcp Go source and official binary are governed by BSL 1.1; see repository LICENSE.BSL and LICENSING.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.mjsbin/binary-installer.generated.mjsbin/release.generated.mjs 三个文件。

命令行与版本探测

二进制本身只支持一个显式子命令(见 mcp/cmd/caveman-mcp/main.go):

usage: caveman-mcp [version --json]

version --json 输出 caveman.mcp.version.v1 schema 的 JSON,其中 capabilities 声明了 mcp_recoverybuild_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) 压缩后文本、推断的 ratiorecovery_handle(直通时为 null)
caveman_retrieve recovery_handle (string) 字节级精确的原文;未知 handle 返回错误
caveman_stats 会话级合计:前后 token 数、ratiobasis:"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(可选):引擎内容类型,如 jsontoon
  • typecontent_type 的别名,两者都传时以 content_type 优先(见 compressTool 的取值逻辑)。

返回是一个 JSON 文本项(MCP 的 ToolText 会把结果序列化为 Agent 可解析的 JSON),字段结构为 compressPayloadmcp/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 是恢复语义的核心,返回的是字节级精确的原文。它有两个特殊待遇:

  1. 豁免结果大小上限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.gohandleToolCall 中可以看到豁免判断 !s.capExempt[p.Name]

  1. 防风暴账本。工具输入除了必填的 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

无输入,返回 statsPayloadmcp/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"`
}

statsToolBasis 硬编码为 engine.BasisInferredScope 硬编码为 "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 协议,日志一律走 stderrmcp/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.goreadLine)。
  • 畸形行:解析失败回复 -32700(parse error),流继续。
  • JSON-RPC batch 数组:按规范处理,逐成员派发,非通知响应合成一个数组返回;全通知 batch 无响应(mcp/server.go)。
  • 通知:无 id"id":null 的请求视为 notification,绝不回复(mcp/protocol.goisNotification)。
  • panic 隔离:工具 handler 的 panic 被 recover 捕获后降级为 cave_tool_panicked 工具错误(mcp/server.goinvokeHandler);派发表层的 panic 降级为 cave_internal_errorserveOne)。
  • 生成侧上限:工具结果内容超过 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-05supportedProtocolVersions只列了这一个,刻意显式)。
  • 若客户端请求的版本在支持集合中,回显客户端版本;否则回答自己支持的版本,让客户端按 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.toolsserverInfo{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.gomcp/server.go)包括:cave_invalid_argumentscave_compress_failedcave_unknown_handlecave_unknown_toolcave_stats_unavailablecave_invalid_tooncave_tool_panickedcave_payload_too_largecave_internal_error。JSON-RPC 层则使用标准错误码 -32700/-32600/-32601/-32602/-32603mcp/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 个仍通过了精确任务评分。注释非常诚实地标注:这是混杂臂/任务/重复批次的描述性证据,不是同任务配对实验,不构成对某个通用阈值的验证——因此阈值保持为历史运行时行为。

在此之上实现了两条规则,且任何一条都不得扣留本会话尚未给过的内容

  1. 重复去重:完全相同的 (handle, query)(query 先做与 RetrieveQuery 相同的 trim," """ 视为同一请求)第二次出现时,只返回一行指回转录中已有答案的提示(repeatNote),不再重发字节。
  2. 超限全量兑付:当会话内不同的 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.goTestRepeatedRetrieveIsAnsweredWithAPointer(含空白填充仍算重复、不同 query 必须正常服务)、TestRetrieveStormPaysOutInFull(前 5 次不得兑付、第 6 次必须是未过滤全量、兑付后重复要指回)、TestRetrieveWithoutASessionBehavesAsBefore、以及诚实性闸门 TestAntiStormNeverWithholdsUnseenContent——从未请求过的 handle 无论会话计数多高都必须服务。
  • 端到端层 mcp/retrieve_integration_test.go:用真实引擎 + 内存 CCR store + testdata 下三种真实形状的页面(inventory_catalog_page.jsonwebhook_delivery_events_page.jsoninventory_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 空间。normalizeRecoveryHandlemcp/engine_tools.go)现在接受全部四种形态——裸 ccr_…<<ccr:ccr_…>>、半剥离的 ccr:ccr_…、以及原生运行时掩码的 ccr://…——因为"Agent 会原样复制它看到的东西,任何无法解析的形态都会把一次恢复变成一场风暴"。TestRetrieveHandleFormsAllResolveTestRetrieveResolvesNativeRuntimeObjectPointers 分别锁死了这两条契约。

六、恢复存储:共享 CCR store 与环境变量

mcp/cmd/caveman-mcp/main.goopenRecoveryStore 定义了存储解析顺序,这也是理解"本地 MCP 与代理如何互通"的关键:

  1. CAVEMAN_MCP_EPHEMERAL=1ccr.OpenMemory(),全新内存 store,不触碰磁盘(测试/无需代理恢复的会话用);
  2. CAVEMAN_CCR_DB 环境变量指定的路径;
  3. 否则 CAVEMAN_HOME 目录下的 ccr.dbCAVEMAN_HOME 未设时为 ~/.caveman/ccr.db
  4. 连用户主目录都解析失败时,兜底退回内存 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.mdmcp/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 的 profile mcp.servers.caveman 叠加层),但从不卸载用户自己安装的服务器。该文档还提醒:在此新增第六个工具会抬高所有被包装 Agent 的每次调用税。
  • 诚实性不变量:fail-open / fail-closed / zero-egress / 协议协商永不报错,四条均已在上文对应源码位置给出证据。

八、小结:如何阅读这份实现

回到 mcp/README.md 的骨架——五个工具、stdio、local-only、inferred-only——仓库源码把每一句承诺都落实成了可验证的代码与测试:

这套代码的整体设计语言可以概括为:压缩可以激进(有损 S4),记账必须诚实(inferred、fail-closed、可字节级恢复),传输必须存活(除 EOF 外任何输入都不杀服务器)。理解了这三条主轴,caveman-mcp 的每个常量、每个错误码和每条注释就都能对上了。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341