modelcontextprotocol / modelcontextprotocol/python-sdk
Add `list_all_*` helpers to drain pagination
まだ誰も着手していません。
- 主要言語
- Python
- スター
- 24.3k
- フォーク
- 4k
- 平均マージ
- 1日 1時間
- マージ済み PR(30日)
- 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 にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- 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