modelcontextprotocol / modelcontextprotocol/python-sdk

Add `list_all_*` helpers to drain pagination

Offen
#2,556 3 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

enhancement P3 v2
Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 1 Std.
Gemergte PRs (30 T.)
31

Beschreibung

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):

  1. 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.

  2. 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.

  3. 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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit Client.list_tools() und den zugehörigen Methoden in src/mcp/client/client.py und vergleiche anschließend das Verhalten bei einer einzelnen Seite sowie die Cursor-Assertions in test/client/test_list_methods_cursor.py. Entscheide über den vorgesehenen, auf Client beschränkten Umfang und die Benennung, füge Tests hinzu, die zeigen, dass jeder Helper alle Seiten abruft, und prüfe, ob ClientSessionGroup einbezogen wird, bevor du die Arbeit als abgeschlossen betrachtest.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
api
Issue-Typ
Feature
Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Aktivitätsstatus
Ruhig
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
52/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.