modelcontextprotocol / modelcontextprotocol/python-sdk
Add `list_all_*` helpers to drain pagination
Ninguém assumiu esta issue ainda.
- Linguagem predominante
- Python
- Estrelas
- 24.3k
- Forks
- 4k
- Merge médio
- 1d 1h
- PRs com merge (30d)
- 31
Descrição
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.
Guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Direção de pesquisa
Comece com Client.list_tools() e os métodos relacionados em src/mcp/client/client.py; em seguida, compare o comportamento de uma única página e as asserções de cursor em test/client/test_list_methods_cursor.py. Decida o escopo pretendido, limitado a Client, e a nomenclatura, adicione cobertura mostrando que cada helper percorre todas as páginas e considere se ClientSessionGroup deve ser incluído antes de considerar o trabalho concluído.
Escrita pelo modelo de indexação a partir do texto da issue.
Avaliação
- Stack de tecnologia
- python
- Domínio
- api
- Tipo de issue
- Funcionalidade
- Dificuldade
- 3/5
- Tempo estimado
- 1-2 dias
- Status de atividade
- Pouca atividade
- Clareza
- Razoavelmente clara
- Facilidade para iniciantes
- 52/100