MCP 分页的是目录,不是调用。一个可能返回大量数据的工具,必须自带分页契约,而且要按最小的那个宿主来写。
协议里的分页规定得很清楚:不透明游标、由服务端决定的页大小、游标无效时返回 -32602。但它恰好只覆盖四个方法,tools/call 不在其中。无论在 2025-11-25 还是 2026-07-28 里,工具结果都没有游标、没有大小上限、也没有分块。规范里唯一能表达「数据在别处」的原语,是 resource_link 加上 resources/read,而后者不支持按范围读取,许多宿主也根本没给模型留出跟进这个链接的途径。于是,一个大结果最终会怎样,取决于用户用的是哪个宿主,而各家宿主的做法并不一致。想要行为可预期的服务端作者,只能在工具自己的参数里定义一份结果契约,并按最不宽容的那个客户端来定尺寸。
步骤 1规范分页的是什么,以及值得照搬的游标规则。
分页这一节开头写的是「Model Context Protocol(MCP)支持对可能返回大量结果的列表操作进行分页」,关键词是列表操作。它点名了四个:tools/list、resources/list、resources/templates/list 和 prompts/list。这份清单在 2025-11-25 和 2026-07-28 里完全一样。2026-07-28 修订版重写了协议的大部分其余内容,这里却一项没加、一项没减。
在这四个方法之内,规则很短,拿来做你自己工具的模板正合适:
- 游标是不透明的。它是「一个不透明的字符串令牌,代表结果集中的某个位置」。客户端「MUST 把游标当作不透明令牌」:不得解析或修改它,除了「有没有给」之外,也不得从中推断任何东西。2026-07-28 补充了一句「空字符串是合法的游标」,不能把它理解为结果已到末尾。
- 页大小由服务端决定。「页大小由服务端决定,客户端 MUST NOT 假定页大小固定。」规范里没有页大小参数,也没有上限。
- 结束靠「缺席」来表示。「如果还有更多结果」,就会带上
nextCursor;客户端 SHOULD 把它的缺失视为结束。 - 坏游标要大声失败。服务端 SHOULD 提供稳定的游标,并妥善处理无效游标。无效游标 SHOULD 返回错误
-32602(Invalid params)。
缓存让各页之间的一致性比看上去更差。在 2026-07-28 下,每一页都是「一个可独立缓存的响应」,各自带着 ttlMs,而且「不存在跨页一致性保证」:客户端在遍历一个很长的 tools/list 时,「可能会看到重复或缺口」。一旦某个游标失效,客户端 SHOULD 丢弃所有已缓存的页,从头再来。ttlMs 和 cacheScope 只存在于 server/discover、这四个列表方法以及 resources/read 上,所以缓存能帮到一个庞大的目录,却帮不到一个庞大的工具结果。
工具调用的分页很早就有人提了,却一直没人接手。Issue #229「Pagination for tool/call」自 2025-03-26 起就一直开着。Issue #798 提议加一个 paginationHint 注解,并在工具结果里加一个 pagination: {hasMore, nextCursor} 块,结果在提交当天就被关闭了。
步骤 2tools/call 给你的替代品:内容块、提示和一个判别字段,没有一样能约束大小。
一个 2026-07-28 的工具结果包含 content、structuredContent、isError,以及新增的必填字段 resultType。内容块的类型有 text、image、audio、resource_link 和内嵌的 resource。没有任何字段说明结果可以多大,或者太大时该怎么办。看起来相关的四样东西,各有各的失灵方式:
structuredContent可能让你的账单翻倍。自 SEP-2106 起,它可以是任意 JSON 值。规范还写着「返回结构化内容的工具 SHOULD 同时在一个 TextContent 块里返回序列化后的 JSON」,以保持向后兼容。如果宿主把两份都交给模型,这份负载你就付了两次钱。(OpenAI Agents SDK 在use_structured_content=True时只用结构化的那一份。)在结构化数据旁边放一句简短的文字摘要,而不是把同一份 JSON 发两遍。- 注解只是提示,而大多数宿主会忽略它们。内容块可以带
audience("user"、"assistant")以及一个 0.0 到 1.0 的priority。但没有任何 MUST 要求宿主对模型隐藏audience: ["user"]的内容。路线图自己也承认「大多数实现者都没有采用这些注解」,并提出了弃用它们的设想;试图澄清内容可见性的 SEP-2200 也已关闭。别指望priority: 0能把一个块挡在上下文窗口之外。 resultType不是分块。它的取值是"complete"、用于多轮往返请求(Multi Round-Trip Request)模式的"input_required",以及来自 Tasks 扩展的"task"。input_required是为引导(elicitation)与采样的往返而设的,不是用来说「这是第一部分」的。- Tasks 是异步,不是分块。任务句柄让一次慢调用能活过一个 HTTP 响应。但最终结果仍然通过
tasks/get整个送达,扩展的摘要也没有为它规定任何大小或分页规则。一个又慢又大的结果,只是变成了一个来得晚的大结果。
步骤 3按最严格的宿主来定尺寸,而不是按你测试用的那一个。
规范对超大结果只字未提,所以每个宿主都自己拿主意,而且连单位都不统一:有的宿主按 token 限制,有的按字符限制,两者之间的换算又取决于内容。
拿一个 60,000 token 的结果去过一遍今天的各家宿主,取决于用户用的是哪一个,它会被落盘成文件、在某个字符阈值处被切断、被裁到按工具设定的 token 预算、被完整保留,或者被截断后附上一个智能体没法照做的提示。同一个服务器、同一次调用,六种不同的结果。各宿主的上限以及超过之后的行为,统一维护在当 MCP 结果撑爆上下文的 Step 2。对一份契约来说,决定尺寸的是两个数字:Claude Code 25,000 token 的默认上限,和 Gemini CLI 40,000 字符的截断线。唯一能让这些结果一致的是服务端:永远不返回超过最小宿主承受能力的内容。
Claude Code 自己的失败提示会让模型「使用分页、过滤或限制参数」,而这只有在你的工具真有这些参数时才管用。
步骤 4工具级的分页契约:参数里放游标,预算按 token 算,截断标志放进结果里。
既然协议把这件事留给了你,那就定义一次,然后用在每一个可能返回大量记录的工具上。通用的结果信封(状态、摘要、投影后的数据、省略计数、下一页游标)在塑造工具结果里讲过。下面是 MCP 特有的部分:
- 把规范的游标规则照搬进你的参数。用一个由模型原样传回的不透明
cursor字符串、一个由服务端选定的默认页大小,以及在游标过期时一个指明修复办法的isError结果。GitHub 的服务器在它基于游标的工具上用的是after+perPage,而它的一个 bug 正是 schema 里缺了描述所承诺的游标——所以要拿 schema 去对照描述检查一遍。 - 页的大小按 token 定,而不只是按记录数。Datadog 发现「一条 Datadog 日志消息可能只有 100 个字符,也可能有 1 MB」,所以 N 条记录的一页并没有大小上限。他们的服务器「在达到一定 token 数后截断响应,并返回一个用于获取更多内容的游标」。记录数上限照样保留,但让 token 或字符预算来结束这一页。
- 多取一条记录来判断还有没有更多。MotherDuck 的服务器会取
max_rows+1行(默认 1,024),以此判断结果是否被截断,设置truncated: true并附上警告,然后每次丢弃约 10% 的行,直到 JSON 低于max_chars(默认 50,000)。多取一行比跑一次计数查询便宜。 - 把标志放进结果里,紧挨着修复办法。Grafana 的 Loki 工具直接在响应里返回
resultsTruncated和maxLinesAllowed。模型看不见的截断,看起来就像一个完整的答案。 - 保持排序稳定。规范对它自己的列表都不提供跨页一致性,你的工具也不会白白得到它。按唯一键排序,并把位置编码进游标,这样遍历途中插入的一条记录,就不会把后面每一页都挤偏一位。
- 先减少字段,再加分页。GitHub 正是这样把每个项目条目的输出削减了约 93%,GitHub 服务器的案例研究梳理了这段历史。一条记录本身就有 17KB,什么页大小都救不了它。不过,标识符要留着:Anthropic 的 Slack 示例用一种简洁格式把一次响应从 206 token 压到 72,而这个简洁格式恰恰丢掉了后续调用需要的 ID。
// tools/call arguments: the contract lives here, not in the protocol {"name": "search_logs", "arguments": {"query": "service:checkout status:error", "fields": ["ts", "host", "message"], "max_records": 200, // whichever budget is hit first ends the page "max_tokens": 8000, "cursor": "c2VhcmNoOjE3Mjk0MDAwMDA6NDQx"}} // result: the flag and the remedy are in-band {"resultType": "complete", "content": [{"type": "text", "text": "141 of 3,912 matching lines (stopped at the 8,000-token budget). Pass next_cursor for more, or add host:… to narrow."}], "structuredContent": { "records": [/* 141 projected records */], "truncated": true, "total_estimate": 3912, "stopped_by": "token_budget", "next_cursor": "c2VhcmNoOjE3Mjk0MDAwMDA6NTgy"}}
规范要求返回结构化内容的工具「应当」同时把序列化后的 JSON 放进一个文本块,而这个选择决定了谁能看到什么:有些客户端只转发文本,文本块若只写一句摘要,这些客户端就看不到记录;而同时转发两者的客户端,会让模型把数据读两遍。其中的取舍,以及测量你实际发出内容的代码,见代码层面的那一页。max_tokens 的默认值由你决定,而最小的宿主定下了它的天花板:要远低于 Gemini CLI 的 40,000 字符和 Claude Code 的 10,000 token 警告线。返回之前先测量,正如 Block 的 MCP 实践手册所建议的那样(字节数、字符数、估算 token 数)。
步骤 5句柄与落盘文件:用对 resource_link,并输出宿主可以 grep 的内容。
当数据大到没法在对话里分页时,规范只提供了一个引用。「工具 MAY 返回指向 Resources 的链接……工具会返回一个可由客户端订阅或获取的 URI。」一个 resource_link 带有 uri、name、description 和 mimeType。用对它,意味着:
- 一个稳定的 URI,等模型终于去读它时仍然能解析,因为工具返回的链接「不保证出现在
resources/list请求的结果里」; - Resource 可选的
size(字节数)和准确的mimeType,让客户端能决定到底要不要去取; - 以及一段说明里面有什么的描述:「3,912 行日志,14:00–15:00 UTC,最新的在前」。
然后要做好链接通向死胡同的准备。resources/read 没有范围读取,也没有分页(范围读取只作为 File Uploads 工作组的一个议题存在),所以获取一个 2MB 的资源就是返回 2MB。许多宿主根本没给模型读取 URI 的途径。尚未合并的 PR #2527 会加一条 SHOULD,要求客户端把资源读取能力暴露给模型;在类似的东西落地之前,resource_link 在许多客户端里都是一条死路。把它当作分页工具的补充,而不是替代。
更常见的情况是,不管你要不要,宿主都会把你的结果落盘成文件。Claude Code 和 Cursor 都这么做。而这只有在智能体能读取文件的一部分时才有用:
- 让它按行组织。一个挤在几行巨长内容里的落盘文件,按行偏移分页的读取器没法只读其中一部分,而整份读取又会撞上宿主的上限。JSON Lines、Markdown 或每行一条记录,能让落盘文件依然可以用
grep、head和偏移量来读。 - 别让往返编辑依赖模型复述正文。如果读取的结果被落盘了,模型就从未见过正文,也就没法为写回重建它。编辑类工具应该接收补丁、章节 id 或服务端句柄,而不是要求把整份文档传回来。
这两条规则背后的公开报告来自 Notion 和 Atlassian 的服务器,作为宿主侧的测试用例收录在当 MCP 结果撑爆上下文的 Step 3。
_meta["anthropic/maxResultSizeChars"] 是权宜之计,不是解决方案。把它声明在某个工具的 tools/list 条目上,就能单独调高 Claude Code 对该工具文本结果的阈值。把它用在那些大输出本身就是目的的工具上,比如用户明确要求读取整个文件的文件读取工具。它对其他任何宿主都不起作用,所以依赖它的工具在更严格的宿主上照样会溢出。
步骤 6什么会来,什么不会来。别等它。
协议层面的修复在路线图上,但不在规范里。路线图最近一次更新于 2026-08-22,在一个仍在组建中的 Core Primitives 工作组名下列了三个相关条目。没有一项已经发布,也没有一项有日期:
- 对
tools/call接口的重新设计,「以解决不同返回类型之间的保真度差异,并简化结构化与非结构化输出的处理」; - 渐进式发现,让客户端「在需要时才了解工具和资源,而不是一开始就吞下整个目录」——这解决的是目录这一侧,不是结果这一侧;
- 用于缓存的 ETags,「尤其是工具调用」,这样客户端可以重新验证一个结果,而不必再取一遍。
流式相关的提案都被关闭了。SEP-2664(通过带分块的进度通知流式返回工具结果)于 2026-07-02 关闭。SEP-2998(部分工具结果)于 2026-09-22 关闭,维护者的说明是「每一个新 SEP 都应在工作组的协助下推进」,也就是说它是因流程原因被关闭的,这个想法还可能通过某个工作组卷土重来。关于流式工具结果的 Issue #117 自 2024 年 12 月起一直开着。没有任何已接受或在推进中的 SEP 定义了结果大小上限或分块的 tools/call 结果。
按协议的现状来构建。每个可能返回无上限记录数的工具,都需要一个不透明游标、一个能结束当前页的 token 预算、一个紧挨着补救说明的 truncated 标志,以及稳定的排序。先减少返回字段,再加分页;凡是宿主可能落盘的内容,都要按行组织。如果 tools/call 的重新设计真的发布了,这样构建的工具只需换一个信封;而依赖宿主来兜底的工具则需要重新设计。GitHub 的服务器是怎样吃了苦头才学会这些的,见那一页是按记录数计算的。到底该构建哪些工具,见设计 MCP 工具。对于不归你管的服务器,如何在宿主一侧控制溢出的内容,见当 MCP 结果撑爆上下文。