AI 技术
#MCP#JSON-RPC#分页游标#工具发现#客户端实现

MCP 工具列表分页游标:为什么工具数量超过一页后客户端只看到部分工具

远程 MCP Server 注册的工具超过一页时,tools/list 会返回 nextCursor,客户端必须带着游标继续拉取才能拿到完整工具集。本文解释分页契约、客户端漏跟游标和服务端游标失效这两类故障,并给出校验点与可观测指标。

一个远程 MCP Server 注册了 14 个工具,页大小设为 8。客户端连上后,设置面板里只显示 8 个可用工具,另外 6 个既不在列表里,模型也调用不到。服务端日志显示它只收到过一次 tools/list 请求,之后没有任何带游标的后续请求。

这不是工具没注册成功,也不是权限过滤。问题出在分页契约上:tools/list 一次只返回一页,响应里带一个 nextCursor,客户端必须拿着这个游标再发一次请求,直到响应不再出现 nextCursor 为止。任何一环没接上,工具集就是残缺的。

下面用一个企业知识库场景贯穿全文。这个远程 MCP Server 把检索、写入、建索引、权限查询等能力拆成 14 个工具,页大小 8,分两页返回。

分页契约到底约定了什么

MCP 的列表类操作采用游标分页。规范把 Cursor 定义为一个不透明字符串,客户端不能解析它的内部结构,只能原样回传。tools/list 的请求参数里有一个可选的 cursor 字段,响应结果里有一个可选的 nextCursor 字段。

契约可以拆成三条:

  • 服务端返回结果时,如果还有下一页,就在结果里放 nextCursor;如果没有下一页,就省略这个字段。
  • 客户端收到带 nextCursor 的响应,必须再发一次 tools/list,把该值放进请求的 cursor 参数。
  • 客户端重复这个过程,直到某次响应的结果里不再有 nextCursor,此时才把各页工具合并成完整集合。

关键点在于“不透明”。游标不是页码,也不是偏移量。规范没有要求 cursor 必须是数字,也没有要求第 N 页的游标等于 N 乘以页大小。客户端把它当成一个需要原样带回的句柄即可。

在这个知识库场景里,第一次请求返回 8 个工具,结果里带 nextCursor。第二次请求带上这个游标,返回剩余 6 个工具,结果里没有 nextCursor。两次合并才是完整的 14 个。

客户端漏跟游标会看到什么

最常见的故障是客户端只发一次 tools/list,拿到第一页就当成全部。它把 8 个工具注册进本地缓存,设置面板显示“8 tools enabled”,模型能看到的工具描述也只有这 8 个。

这个故障有几个容易误判的特征。

第一,服务端看起来完全正常。它按契约返回了第一页和 nextCursor,没有报错,也没有超时。

第二,重启客户端、切换服务器开关都不解决问题。因为每次连接都只发一次请求,结果永远是同一页。

第三,直接对服务端发探测请求能看到全部工具。用命令行按 JSON-RPC 顺序发 initialize、notifications/initialized、tools/list,再发一次带 cursor 的 tools/list,两次结果相加就是 14 个。这说明服务端没问题,是客户端没有跟进。

社区里已有这类报告:某个 MCP 客户端对分页服务端只注册第一页,14 个工具只显示 8 个,维护者确认是客户端只发一次 tools/list、没有跟随 nextCursor,并把它记为客户端缺陷。这属于实现问题,不是协议问题。

判断方法很直接:抓客户端与服务端之间的 JSON-RPC 消息,数一数一次连接里出现了几次 tools/list 请求。如果只有一次,而第一次响应里带了 nextCursor,那工具集必然不完整。

服务端游标不稳定会怎样

客户端正确跟进了游标,工具集仍可能缺项,这时问题往往在服务端。游标分页对服务端有一个隐含要求:同一个分页序列内,游标必须能稳定地指向“下一页从哪里开始”。

服务端常见的不稳定实现有三类。

第一类是把游标编码成数组下标或偏移量,但工具集合在两次请求之间发生了变化。比如第二次请求到来前,管理员正好注册了一个新工具,或者某个工具被禁用。偏移量对应到变化后的列表,就会跳过或重复某些工具。

第二类是游标里带了会话状态,但服务端在两次请求之间重建了状态。远程 Server 常见于多实例部署:第一次请求打到实例 A,游标由 A 生成;第二次请求被负载均衡打到实例 B,B 不认识这个游标,可能返回空页或报参数错误。

第三类是把游标做成短时效令牌,过期后失效。客户端拉取慢、网络重试、或者中间隔了一次用户操作,游标就过期了,后续页拿不回来。

这三类问题的共同表现是:客户端确实在循环请求,但某一次响应提前没有了 nextCursor,或者返回了错误。最终注册的工具数少于服务端实际数量,而且数量不稳定,随部署和时序变化。

一次完整拉取的数据流

把上面的机制串起来,一次正确的工具发现流程如下。

flowchart TD
    A[客户端发送 initialize] --> B[服务端返回能力与 serverInfo]
    B --> C[客户端发送 notifications/initialized]
    C --> D[客户端发送 tools/list 不带 cursor]
    D --> E{响应是否含 nextCursor}
    E -->|是| F[客户端把 nextCursor 放入 cursor 再请求]
    F --> E
    E -->|否| G[合并所有页的工具]
    G --> H[注册到本地工具缓存]
    H --> I[模型按需调用工具]

流程里有两个转折点值得注意。

第一个转折在 E 节点。判断依据是响应结果里有没有 nextCursor,而不是“这一页是不是满的”。如果服务端返回了不足一页的工具但仍有 nextCursor,客户端也必须继续拉。反过来,如果返回了满页但没有 nextCursor,说明这就是最后一页。

第二个转折在 F 到 E 的循环。循环必须有终止条件,否则服务端游标实现有 bug 时可能返回同一个游标,客户端就会无限请求。工程上通常给循环加一个最大页数上限,超过就报错并记录,而不是一直转下去。

合并阶段也有细节。不同页之间不应出现重复工具名。如果出现,说明服务端游标不稳定,客户端要么去重,要么把这个异常上报,不能静默接受。

全量返回与检索式筛选的取舍

既然分页会带来这些麻烦,一个自然的想法是让服务端一次性返回全部工具,不要分页。另一个方向是干脆不返回全量列表,改成按需检索工具。两种方案各有适用边界。

维度游标分页一次性全量返回检索式筛选
单次响应大小受页大小约束,稳定随工具数线性增长只返回命中的少量工具
客户端实现复杂度需要循环与合并逻辑最低需要查询构造与结果处理
工具集完整性依赖客户端正确跟进天然完整依赖检索召回质量
对客户端缺陷的容忍度低,漏跟即缺项高中,召回不足会漏工具
工具数量很大时可扩展响应膨胀,上下文占用高可扩展
服务端状态要求游标需稳定、可跨请求无需要索引与检索能力
典型失败模式只拉第一页、游标失效大列表撑爆上下文召回不足、排序偏差

这张表里最需要权衡的是“工具集完整性”和“上下文占用”。工具数量在几十个以内时,一次性全量返回最省事,客户端不容易出错,模型也能看到全部工具描述。工具数量上百后,全量描述会占用大量上下文,还会稀释模型的注意力,这时分页或检索式筛选更有价值。

检索式筛选解决的是另一个问题:不是“怎么把列表拉全”,而是“怎么只把当前任务相关的工具给模型”。它把完整性责任从客户端转移到了检索质量上。如果检索召回不足,模型同样看不到该用的工具,只是失败方式从“缺页”变成了“缺召回”。

在知识库场景里,如果 14 个工具都常用,全量返回是合理选择。如果工具增长到几百个,且每次任务只涉及少数几个,检索式筛选更合适。分页处在中间:它保证协议层面能扩展,但把正确性责任压在了客户端的循环逻辑上。

校验点与可观测指标

要判断工具集是否完整,可以从客户端和服务端两侧各设校验点。

客户端侧:

  • 记录一次连接内 tools/list 的请求次数。如果第一次响应含 nextCursor 而请求次数为 1,直接判定为漏跟游标。
  • 比较本地注册的工具数与服务端声明的工具总数。若服务端不提供总数,至少在合并后检查是否有重复工具名和页数异常。
  • 给分页循环设最大页数上限,触顶时记录游标值和已拉取页数。

服务端侧:

  • 记录每次 tools/list 请求是否带 cursor,以及返回的 nextCursor 是否为空。带游标请求的占比长期为零,说明客户端没有跟进。
  • 监控游标解析失败率。多实例部署下,这个指标能暴露游标未共享的问题。
  • 记录游标从签发到使用的间隔,超过预期时效的请求单独统计。

几个适合长期观察的指标:单次连接的平均 tools/list 请求数、游标解析错误率、合并后工具数与服务端实际工具数的差值、分页循环触顶次数。差值持续大于零,就说明有工具没被发现。

什么时候这套机制会退化

分页契约在几种前提下才成立,前提被破坏时它会退化甚至失效。

客户端不实现循环逻辑时,分页直接退化成“只返回第一页”。这是当前最现实的故障来源,社区报告里已经出现。对这类客户端,服务端能做的临时缓解是把页大小调大到覆盖全部工具,或者用一个代理把多页合并成单页返回。这两种做法都绕开了客户端缺陷,但代价是放弃了分页带来的扩展性。

服务端游标不能跨请求稳定复现时,客户端即使正确循环也会缺项。多实例部署尤其要注意:游标要么编码成自包含的信息,要么把状态放到所有实例都能访问的共享存储里。把游标状态放在单个实例的内存里,是远程 Server 最常见的坑。

工具集合在分页过程中频繁变动时,任何基于偏移量的游标都会漂移。工具注册表如果支持热更新,服务端需要保证游标对变更有明确的语义,比如基于稳定排序键而不是数组下标。

还有一个容易被忽略的边界:工具描述本身很大时,即使工具数量不多,单页也可能撑爆上下文。这时问题不在分页,而在工具描述的体积控制。

仍未解决的问题是,协议层面没有强制客户端必须跟进游标,也没有规定服务端游标的最小稳定性要求。这两点目前依赖实现自觉。在客户端生态补齐之前,服务端侧的工具总数校验和代理合并仍是比较可靠的兜底手段。

资料来源

  1. Model Context Protocol Specification (2025-06-18)
  2. Model Context Protocol Schema (TypeScript definitions)
  3. JSON-RPC 2.0 Specification
  4. MCP: Cursor only registers the first page of a paginated tools/list (doesn't follow nextCursor) — 14-tool server shows 8 - Bug Reports - Cursor - Community Forum