夜雨聆风学习资料网

ARTICLE · 1155440

MCP 说明文字的预算在客户端,超出部分被静默裁掉

MCP 说明文字的预算在客户端,超出部分被静默裁掉

一个 MCP 服务器的作者能控制自己发出去的那段文字,控制不了它到达模型时还剩多少。该做的都做了——在初始化响应里给出 instructions,给每个工具写清楚描述——客户端仍然可能把后面一半切掉,而且默认不会在标准输出或流式结果里留下任何提示。

8 月 18 日和 8 月 31 日,两个服务器先后撞上这条线。open-design 的 od mcp 那段说明有 5,859 个字符,报告者写了一段读源码、量长度的脚本,算出 3,811 个字符、约 65% 从来没有到过模型,切口停在句子中间,正好赶在运行流程的说明之前:技能与插件清单、start_run 之前的 list_agents、确认流程、requestId 的幂等约定、运行查询与取消,全在被切掉的那一段里;维护者复核后确认了这个切口。markdown-vault-mcp 的组成式说明是 2,358 个字符,只丢了 310 个字符,但丢掉的是实例的读写权限声明和运维通过环境变量追加的那段;后果是同一次会话里的三个服务器送出了逐字节相同的说明块,因为活下来的只剩一句硬编码的身份常量。两次在客户端都没有报错,能指望的只有打开 debug 时的日志行,和正文末尾那个截断标记。

上限不在协议里

在能查到的规范文本里,这个字段没有长度上限。下面出现的每个数字都来自客户端,不来自协议。

Claude Code 面向 MCP 服务器作者的那一段写的是:它把每个工具描述和每个服务器的说明截断为默认 2,048 个字符,接着建议保持简洁、把关键细节放在开头。单位是字符不是字节,环境变量条目里的写法一致(默认 2,048)。唯一的偏差出现在一处讨论里:markdown-vault-mcp 那个 issue 下,有位评论者把它理解为 InitializeResult.instructions 的每服务器上限、按 UTF-16 单元计数。那是条评论不是文档,但值得记一笔——这个数字在二手记录里转写时容易变形,抬预算前先确认自己那台客户端按哪个口径算。

上限按项、按服务器生效,不是全局共享的一份。文档把这项限制描述成「每个工具描述和每个服务器的说明」,并写明要改就是改「会话中每个 MCP 服务器」的限制;环境变量条目同样写成每个工具描述、每个服务器的说明。所以话多的服务器挤不掉话少的;反过来,一个会话里挂了几十个话多的服务器,这些说明加起来占掉的上下文,也不会因为预算按份分而变少。

切口在尾部,标记只有 13 个字符

截断保留开头、砍掉尾部,并且在被砍的位置补一个标记。一次针对这个行为的实测把标记量了出来:一个省略号、一个空格、一个方括号里的词,一共 13 个字符。issue 里能对上这个形态:markdown-vault-mcp 的组成式说明在 OKF 那段话中间被切断,最后一个完整片段是「prefer root-relative mar…」;open-design 那条停得更直接,切口正好落在「To make Open」之后。

除了那 13 个字符,剩下的提示几乎没有。实测记录里标准错误输出是空的,流式输出从不提这件事,只有 debug 日志留一行,形如 MCP server "open-design": Server instructions truncated from 5859 to 2048 chars。也就是说,判断自己有没有被截,得主动去看 debug 日志,或者在测试里直接量服务端拼出来的字符串长度。

同一段工具描述,走两条通道两个预算

如果被切的是工具描述而不是服务器说明,还要多问一句:它走哪条通道。

Claude Code 默认开启工具搜索(tool search)。开启时完整工具定义不进上下文,开局只送工具名和服务器说明,模型需要某个能力时再去搜索、按需取回。按需取回的那份完整定义,和一次性预加载的定义,走的是两条路径。

更新日志里两条相邻的记录各改了一条路径的预算。2.1.295 把模型通过工具搜索加载的 MCP 工具描述的切割长度从 2,048 改到 16,384 个字符。2.1.296 把预加载的 MCP 工具描述和服务器说明的默认上限从 2,048 改到 4,096。同一段工具描述,挂在按需路径上有 16,384 的预算,走预加载路径只有 4,096。服务器说明始终在开局进上下文,所以它按预加载那份算。

走哪条路径不在服务器手里。客户端用 ENABLE_TOOL_SEARCH 决定:不设值时默认开,auto 和 auto:N 按可延迟定义的 token 占上下文窗口的比例决定何时启用,false 则每次请求都发全量。单台服务器可以用 alwaysLoad 免于延迟。服务器作者能直接做的,是把承重信息写在开头,让它在任何一条预算下都还在。

这里还有一处口径不一致要单独说:更新日志说 2.1.296 把默认上限从 2,048 改成 4,096,而设置项文档这一刻仍然写着默认 2,048。两处至少有一处没跟上。写方案时不要把任何一个具体数字当稳定常量,尤其是本来就要按最小那份预算设计的时候。

这段文字落在用户回合,不在系统提示

MCP 规范给服务器作者的示例把 instructions 当成放进系统提示的材料。Claude Code 不放在那里。一次 20 轮的实测记录了投递形态:这段说明以第一条用户回合里的一段提醒到达,附件类型是 mcp_instructions_delta;20 轮里系统提示都是 6,662 个字符,从头到尾没有出现服务器的任何一个标记;工具搜索开或关都不改变这个位置。

位置影响的也不只是可见范围。系统提示里的文字每轮都在,是前缀里最稳定的一段;作为用户回合附件到达的文字,从第一条回合起就留在对话历史里,和当轮内容混在同一层。把一段长期约束放在哪一层,本身就是决定它活多久的一部分。

抬预算的开关在客户端,写错不报错

这个上限可以在客户端抬高。CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH 从 2.1.280 起可用,同时作用于每个 MCP 工具描述和每个服务器的 instructions。文档写明它接受纯数字的正整数,其它一律忽略、默认值生效。

实测里这条规则咬人:把它写成 25,000,带千分位逗号,没有被当成非法配置报出来,默认值悄悄生效,文本照旧切在 2,048,标准错误输出和 debug 日志都没有提示。上限真的抬上去也不是免费的:同一次实测里把 20,000 个字符的说明放进去,首个请求多了 6,578 个 token;而被砍到 2,048 字符的那份说明本身也占着 721 个 token,同样的字符数换成日文是 1,727 个。

既然这是用户侧的变量,服务器作者就不该假设对面抬过。文档给作者的建议也是照这个前提写的:假定只有开头那一段存在。

两端各自能改的部分

对服务器作者,能改的是顺序和内容。open-design 的修复提议就是重排:把运行流程、启动和查询的说明提到只读工具目录之前,因为逐工具的细节本来就在工具 schema 里,说明段不必再抄一遍;超长的正文挪进 resource,用一行指路。这类改动不需要客户端配合,也不会因为对面版本升级而失效。

对客户端运维,能改的是把变量送到每台机器上,而不是只在服务器侧加配置。抬预算换来的是首个请求固定多出来的 token 开销,以及一段更长、从第一条用户回合就进入历史的文字。这两个代价都随会话推进被一起带着走。

预算数字在最近两个版本里动过两次,方向都是放宽,而文档里那一项还停在旧值。更稳的做法是按「对面只保证最小那份预算」来组织这段文字,而不是按当前的默认值来切分内容。

相关学习资料