modelcontextprotocol / modelcontextprotocol/python-sdk
Add `list_all_*` helpers to drain pagination
Nessuno ha ancora preso questa issue.
- Lingua principale
- Python
- Stelle
- 24.3k
- Fork
- 4k
- Merge medio
- 1g 1h
- PR unite (30g)
- 31
Descrizione
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.
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia da Client.list_tools() e dai metodi correlati in src/mcp/client/client.py, quindi confronta il comportamento su una singola pagina e le asserzioni del cursore in test/client/test_list_methods_cursor.py. Decidi l'ambito previsto, limitato a Client, e la denominazione, aggiungi una copertura che mostri che ogni helper esaurisce tutte le pagine e valuta se includere ClientSessionGroup prima di considerare concluso il lavoro.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- python
- Ambito
- api
- Tipo di issue
- Funzionalità
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Stato di attività
- Tranquilla
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 52/100