modelcontextprotocol / modelcontextprotocol/python-sdk
Add `list_all_*` helpers to drain pagination
還沒有人認領這個 Issue。
- 主要語言
- Python
- 星號
- 24.3k
- 分支
- 4k
- 平均合併
- 1 天 1 小時
- 30 天內合併 PR
- 31
描述
Description
Spent a few days debugging why a handful of MCP clients (Claude Code, Cursor, the Inspector CLI mode) silently lose tools when connected to a server that paginates tools/list. Tracing
through this SDK I'm pretty convinced the root cause isn't really in those clients - it's how the SDK shapes the list API.
What I hit
Connected Claude Code to an AWS Bedrock AgentCore Gateway with 31 tools registered across 7 targets. Only 14 showed up. The gateway returns 14 on page 0 with a nextCursor, then 17 on
page 1. Claude Code calls tools/list once and drops the cursor.
Same shape with Cursor (drops the cursor too, lol pun :x) and the Inspector CLI (mcp-inspector --cli ...). The Inspector UI is the only client I tested that actually shows everything, and that's because it exposes the cursor as React state - so you click "List Tools" again for each page :(
What's actually happening in the SDK
Three layers:
- ClientSession.list_tools() (src/mcp/client/session.py:393) makes a single request and returns one page. The # Note: don't clear the cache, as we may be using a cursor comment shows this would be called repeatedly with cursors. Reasonable for a low-level primitive i think.
- Client.list_tools() (src/mcp/client/client.py:301) is a thin wrapper around the session method. Its docstring is just
"""List available tools from the server.""" with no mention of pagination. Most application code hits this layer. - ClientSessionGroup (src/mcp/client/session_group.py:369), the multi-server aggregator, calls session.list_tools() once per server with no cursor handling either. Worth flagging because aggregation across servers is the scenario where high tool counts are most likely, mcp gateways/proxies etc.
Why I think the SDK is the right place to fix this
The pagination work already landed in this repo has been about exposing the cursor to callers - the kwarg, the wire format fix, the recent params overload. Sensible additions, the cursor had to exist before anything else could use it. What's missing is a helper that actually drains across pages. So a downstream client wrapping client.list_tools() either reads the docstring ("List available tools"), assumes they got everything, and ships single-page, or notices the cursor kwarg and writes thier own loop. Claude Code, Cursor, and the Inspector CLI all shipped the first option. Hard to argue thats their bug.
#2441 is honestly what pushed me to file this. Looks like you're already adding a drain loop in _validate_tool_result over there for internal correctness reasons - same bug shape, just one method up.
Proposal
Thinking additive convenience methods on Client:
async def list_all_tools(self) -> list[Tool]: ...
async def list_all_resources(self) -> list[Resource]: ...
async def list_all_resource_templates(self) -> list[ResourceTemplate]...
async def list_all_prompts(self) -> list[Prompt]: ...
Caller side would just be:
async with Client(server) as client:
tools = await client.list_all_tools() # drains all pages
print(f"Got {len(tools)} tools")
Each helper is a four-line drain loop calling the existing list_*(cursor=...). Existing methods unchanged, the len(requests) == 1 contract in tests/client/test_list_methods_cursor.py stays valid, anything depending on cursor-as-state (Inspector UI's manual advance) keeps working.
A few things to scope before writing a PR (ehh, idk if these are the right calls, happy to be told otherwise):
-
Naming. list_all_* reads obvious to me but I'm not married to it. iter_* would suggest an async iterator, which is probably worth a separate variant for memory-constrained / streaming cases anyway.
-
Should ClientSessionGroup's tool aggregation also switch to a drain loop? Feels like yes but it's a behavior change to an existing class and maybe deserves its own issue.
-
Client only, or also ClientSession? Honestly not sure...I'd lean Client only since the session is the primitive layer where single-page-per-call seems intentional, but if there's a use case for the lower level then maybe both.
Happy to put up a PR if this lands a ready for work label. Also totally fine if it's not where you want the SDK to go. figured better to surface the problem with something concrete to react to than a vague ask.
References
- anthropics/claude-code#39586 - downstream Claude Code bug filed against this same behavior. Open since March, no movement. Part of why I'm filing here instead.
貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
研究方向
從 Client.list_tools() 和 src/mcp/client/client.py 中的相關方法開始,接著比較 test/client/test_list_methods_cursor.py 中的單頁行為和游標斷言。決定僅限於 Client 的預期範圍和命名,新增涵蓋測試以顯示每個 helper 都會耗盡所有頁面,並在將工作視為完成之前考慮是否包含 ClientSessionGroup。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- python
- 領域
- api
- Issue 類型
- 功能
- 難度
- 3/5
- 預估耗時
- 1-2 天
- 活躍度
- 冷清
- 描述清晰度
- 基本清楚
- 新手友好度
- 52/100