ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

python-sdk 分页(Pagination)实战:在低层 Server 上实现基于游标的分页

python-sdk 分页(Pagination)实战:在低层 Server 上实现基于游标的分页 python-sdk 分页Pagination实战在低层 Server 上实现基于游标的分页【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk本指南讲解 Model Context ProtocolMCPPython SDK 中分页的完整实现方案何时需要分页、为什么MCPServer默认不分页、如何在低层Server上编写分页的resources/list处理器以及客户端如何用cursor循环拉取全部页面。读完本文你将掌握游标cursor协议的全部约定——从服务端自定义游标格式到客户端循环的三种规则并能在自己的 MCP 服务器上落地可运行的分页代码。先决条件绝大多数服务器根本不需要分页MCPServer对每个list_*请求tools/list、resources/list、resources/templates/list、prompts/list都会把当前拥有的全部内容一次性放进单页响应返回next_cursorNone。对于几十个工具、资源或提示词prompt这种量级这就是正确且无需任何配置的答案。这一点在仓库测试中有明确的验证tests/docs_src/test_pagination.py 中的test_mcpserver_never_pages直接断言向注册了 100 个资源的MCPServer发起list_resources()返回的result.resources长度为 100 且result.next_cursor is None。分页真正服务的场景是服务器上的资源列表本质上是一张数据库表有成千上万行服务器拒绝把整张表一次性序列化进一个响应。此时协议给出的答案是游标cursor服务器返回一页内容并附上一个不透明opaque令牌客户端把这个令牌原样发回换取下一页。关键点mcp.resource()装饰器没有任何钩子支持分页。要分页你必须自己在低层 Server 上编写 list 处理器。一个会分页的服务器完整示例位于 docs_src/pagination/tutorial001.py核心代码如下from typing import Any from mcp.server import Server, ServerRequestContext from mcp.types import ListResourcesResult, PaginatedRequestParams, Resource BOOKS [fbook-{n} for n in range(1, 101)] PAGE_SIZE 10 async def list_books(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) - ListResourcesResult: start 0 if params is None or params.cursor is None else int(params.cursor) end start PAGE_SIZE page [Resource(urifbooks://catalog/{name}, namename) for name in BOOKS[start:end]] next_cursor str(end) if end len(BOOKS) else None return ListResourcesResult(resourcespage, next_cursornext_cursor) server Server(Bookshop, on_list_resourceslist_books) app server.streamable_http_app()对照源码理解这段代码的几个要点处理器是构造器参数不是装饰器。在低层Server上处理器通过Server(Bookshop, on_list_resourceslist_books)传入构造函数参见 src/mcp/server/lowlevel/server.py。on_list_resources会应答每一次resources/list请求这就是全部接线工作。类似的钩子还有on_list_tools、on_list_prompts、on_list_resource_templates。每个分页处理器都带类型标注params: PaginatedRequestParams | None。协议层面对应的请求模型定义在 src/mcp-types/mcp_types/_types.pyPaginatedRequestParams继承自RequestParams只有一个字段cursor: str | None None其 docstring 明确写道这是一个不透明令牌代表当前分页位置若提供服务器应返回该游标之后的结果。示例代码对None和模型两种形态都做了兼容但要注意在真实连接上SDK 永远不会把None传给你——一个没有params成员的请求到达处理器时会被还原成带默认值的模型对象。因此真正有意义的信号是params.cursor is None从头开始。游标长什么样由你决定。这里它是一个被渲染成字符串的偏移量offsetint(params.cursor)还原为起始位置str(end)生成下一页游标。你也可以用时间戳、主键、base64 编码的 blob——任何你能在出站时铸造、在入站时识别的字符串都行。next_cursorNone就是这是最后一页的表达。协议里没有 count、没有 total、没有has_more。这一点在结果模型上有直接体现src/mcp-types/mcp_types/_types.py 中的PaginatedResult基类只有next_cursor: str | None None一个分页字段docstring 注明若存在则后面可能还有更多结果ListResourcesResult、ListResourceTemplatesResult、ListPromptsResult、ListToolsResult均继承自它。!!! tipPAGE_SIZE 10只是为了让示例易读。请按端点自行选择一行一行的短小资源列表可以用 500 的页大小而承载大段 prompt 模板的列表就不行。客户端在页大小上没有发言权这是有意设计——协议中不存在limit参数。亲自试一试mcp run只接受MCPServer所以这个低层服务器需要你自己来托管。server.py的最后一行app server.streamable_http_app()把Server包装成一个普通的 ASGI 应用交给 uvicorn 运行uvicorn server:app --port 8000然后让任意客户端SDK 客户端或 MCP Inspector指向http://localhost:8000/mcp不带任何参数调用list_resources()。你会得到十个资源从book-1到book-10且next_cursor是字符串10。把这个游标原样传回去list_resources(cursor10)第一个资源变成book-11新的next_cursor是20。第十页返回时next_cursor为None。分页结束。以上行为在 tests/docs_src/test_pagination.py 中被逐条验证test_first_page_has_ten_resources_and_a_cursor第一页十个资源且next_cursor 10、test_the_cursor_resumes_where_the_last_page_stopped游标10续接出book-11无重叠、test_the_last_page_carries_no_cursornext_cursor是唯一的列表结束信号。客户端循环Client上的每一个list_*方法——list_tools、list_resources、list_resource_templates、list_prompts——都接受命名参数cursor。在 src/mcp/client/client.py 中可以看到这四个方法list_resources在第 592 行、list_resource_templates在第 608 行、list_prompts在第 826 行、list_tools在第 924 行签名一致内部都通过PaginatedRequestParams(cursorcursor, ...)构造请求参数并交给 session 层发送。会话层对应实现见 src/mcp/client/session.py。拉空一个分页列表只需一个while Trueimport anyio from mcp import Client from mcp.types import Resource async def list_all_resources(client: Client) - list[Resource]: resources: list[Resource] [] cursor: str | None None while True: page await client.list_resources(cursorcursor) resources.extend(page.resources) if page.next_cursor is None: break cursor page.next_cursor return resources async def main() - None: async with Client(http://localhost:8000/mcp) as client: resources await list_all_resources(client) print(f{len(resources)} resources) if __name__ __main__: anyio.run(main)这段代码完整版见 docs_src/pagination/tutorial002.py有三个关键约定cursor初始为None所以第一次请求不携带任何游标服务器返回第一页。先extend再判断next_cursor最后一页同样有资源必须先收集再检查退出条件否则会丢掉最后一页。next_cursor is None是唯一的退出条件除此之外的任何值都原封不动地塞回cursor继续循环。在 uvicorn 仍服务着server.py时于第二个终端运行python client.py。它会打印100 resources——十页、每页十个由一个从头到尾都不知道存在十页的循环拼接而成。这与 客户端文档 中对每个list_*动词展示的循环是同一条而且面对不分页的服务器它零成本第一次响应next_cursor就是None循环只执行一次。测试test_the_client_loop_collects_all_one_hundred_in_order与test_the_client_loop_runs_once_against_a_server_that_does_not_page见 tests/docs_src/test_pagination.py分别验证了这两种情形循环能把 100 个资源按顺序无缺口、无重复地拼回完整目录而面对不分页的MCPServer一次遍历即返回全部 100 个资源。三条规则游标是不透明的。客户端永远不应该解析、构造或猜测游标。一个游标的唯一合法来源就是上一页响应的next_cursor必须原样使用。页大小由服务器决定。协议里没有limit参数。如果你需要不同的页大小改的是服务器而不是客户端请求。忽略分页的客户端照样能工作。它只调用一次list_resources()拿到前十个资源永远不会注意到自己丢掉了next_cursor。什么都不会坏只是它看到的内容更少。!!! check 不透明就是不透明。伪造一个游标比如list_resources(cursorpage-2)协议不会替你兜底。上面那个服务器会对它执行int(page-2)处理器抛出异常客户端收到的响应是text MCPError(-32603, Internal server error, None) 从服务器之外得到的游标是一个 bug而不是一个 feature request。这一点在 [tests/docs_src/test_pagination.py](https://link.gitcode.com/i/49041a54ec23173a076b48a275535396) 的 test_an_invented_cursor_is_an_error 中有端到端验证客户端调用 list_resources(cursorpage-2) 会抛出 MCPError其 code -32603消息为 Internal server error。小结MCPServer把一切内容单页返回。分页是可选能力在低层Server上按需开启。on_list_resources以及on_list_tools、on_list_prompts、on_list_resource_templates收到的是PaginatedRequestParams | None第一页的params.cursor为None。你返回一页内容 next_cursor任意一个你稍后能识别的字符串或者在没有剩余内容时返回None。客户端循环传入cursor累积结果重复直到next_cursor is None。游标不透明、页大小归服务器所有、不分页的客户端至少能拿到第一页。手写Server的其余 APIon_call_tool、input_schema字典、_meta参见低层 Server 文档。对应的英文原文文档位于 docs/advanced/pagination.md本页即其法语翻译版。【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表