Skip to content

Add list_all_* helpers that drain pagination on the client#3021

Open
CJGjr wants to merge 3 commits into
modelcontextprotocol:mainfrom
CJGjr:list-all-pagination-v2
Open

Add list_all_* helpers that drain pagination on the client#3021
CJGjr wants to merge 3 commits into
modelcontextprotocol:mainfrom
CJGjr:list-all-pagination-v2

Conversation

@CJGjr

@CJGjr CJGjr commented Jun 29, 2026

Copy link
Copy Markdown

Closes #2556.

This continues #2658 by @adityasingh2400, which implemented the helpers but went stale against the v2 rework on main. I opened the original issue, so I rebased the work onto current main, added a guard, and am putting it up as a ready-to-merge PR. Aditya's authorship is preserved on the feature commit. Happy to close this in favor of an updated #2658 if he would rather carry it forward.

What this adds

  • list_all_tools / list_all_prompts / list_all_resources / list_all_resource_templates on Client: each walks next_cursor until the server reports no more pages and returns the combined list.
  • iter_all_tools / iter_all_prompts / iter_all_resources / iter_all_resource_templates: async iterators for streaming consumers that do not want to materialize every page.
  • The single-page list_* methods get docstrings pointing at the new drains.
  • ClientSessionGroup aggregation drains pagination, so multi-server consumers see the full collection instead of only page 0.

The single-page primitives are unchanged; the drains are opt-in.

Non-advancing cursor guard

A server that keeps returning the same cursor it was handed would make a naive drain loop page forever. The loops raise RuntimeError when the cursor does not advance, rather than looping or silently truncating (silent truncation being the exact failure the issue describes). Documented in each helper's Raises: section and covered by a parametrized test across tools, prompts, resources, and templates. This addresses the malformed-server concern @agaonker raised on the issue. Easy to switch to a quiet stop instead if that is preferred.

Notes on the v2 rebase

The original PR predated the v2 rework. Bringing it current meant: types import from mcp_types, the cursor-spy tests run with mode="legacy" (the default client path is now streamless, so the wire spy has nothing to observe otherwise), and the test Tool carries a valid input_schema now that type is required. The helper logic itself is unchanged.

Docs

docs/advanced/pagination.md gets a "Draining in one call" section covering list_all_*/iter_all_*, the ClientSessionGroup behavior, and the cursor guard. It is backed by a runnable docs_src/pagination/tutorial003.py snippet with matching tests, so the page keeps proving every claim against the SDK.

Verification

  • uv run pytest tests/client/ and tests/docs_src/test_pagination.py pass
  • uv run pyright src/mcp/client/client.py src/mcp/client/session_group.py clean
  • uv run ruff check . && uv run ruff format --check . clean
  • readme-snippets and markdownlint (repo config) clean
  • 100% coverage on the new lines, including all four guard branches

adityasingh2400 and others added 3 commits June 29, 2026 10:56
Currently Client.list_tools / list_prompts / list_resources /
list_resource_templates return a single page and the caller has to loop
on next_cursor manually. Add list_all_tools / list_all_prompts /
list_all_resources / list_all_resource_templates that walk next_cursor
until exhausted, plus iter_all_* async iterators for streaming
consumers. The single-page methods get a docstring update pointing at
the new drains. ClientSessionGroup switches its tool/prompt/resource
aggregation to the drain helper so its consumers always see the full
collection across multi-page servers.

Implements the helper maxisbey endorsed in modelcontextprotocol#2556.

Rebased onto the v2 rework: types import from mcp_types, the stream-spy
tests run in legacy mode, and the test Tool carries a valid input_schema.
A server that returns the same next_cursor it was given would make the
list_all_*/iter_all_* loops page forever. Raise RuntimeError when the
cursor does not advance instead of silently looping or truncating, and
document it in the Raises section of each helper. Covered by a
parametrized test across tools, prompts, resources, and templates.
Add a Draining in one call section to docs/advanced/pagination.md covering
list_all_*/iter_all_*, the ClientSessionGroup behavior, and the
non-advancing cursor guard. Backed by a runnable tutorial003 snippet and
matching tests, in keeping with the page proving every claim.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/advanced/pagination.md">

<violation number="1" location="docs/advanced/pagination.md:68">
P2: Warning overstates the non-advancing cursor guard: `ClientSessionGroup` pagination drains do not raise and can still loop forever. Scope the warning to `Client` list/iter helpers or add the same guard to the group implementation.</violation>
</file>

<file name="src/mcp/client/client.py">

<violation number="1" location="src/mcp/client/client.py:609">
P2: Track all previously seen pagination cursors, not only the immediately previous one. Alternating/cyclic cursors can still make `list_all_*`/`iter_all_*` loop forever.</violation>
</file>

<file name="src/mcp/client/session_group.py">

<violation number="1" location="src/mcp/client/session_group.py:89">
P1: Pagination draining in `ClientSessionGroup` lacks a non-advancing cursor guard, so a buggy server can cause an infinite loop during component aggregation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

next_cursor = getattr(result, "next_cursor", None)
if next_cursor is None:
return items
cursor = next_cursor

@cubic-dev-ai cubic-dev-ai Bot Jun 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Pagination draining in ClientSessionGroup lacks a non-advancing cursor guard, so a buggy server can cause an infinite loop during component aggregation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/session_group.py, line 89:

<comment>Pagination draining in `ClientSessionGroup` lacks a non-advancing cursor guard, so a buggy server can cause an infinite loop during component aggregation.</comment>

<file context>
@@ -67,6 +67,28 @@ class StreamableHttpParameters(BaseModel):
+        next_cursor = getattr(result, "next_cursor", None)
+        if next_cursor is None:
+            return items
+        cursor = next_cursor
+
+
</file context>
Suggested change
cursor = next_cursor
if next_cursor == cursor:
raise RuntimeError(
"Server returned a pagination cursor that did not advance; refusing to page forever."
)
cursor = next_cursor
Fix with cubic

Comment on lines +68 to +71
A drain trusts the server to advance the cursor. A server that keeps returning the same
`next_cursor` it was handed would page forever, so the drains stop and raise `RuntimeError`
the moment a cursor fails to move. A page that does not advance is a broken server, and a
loud failure beats a silent hang or a half-read list.

@cubic-dev-ai cubic-dev-ai Bot Jun 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Warning overstates the non-advancing cursor guard: ClientSessionGroup pagination drains do not raise and can still loop forever. Scope the warning to Client list/iter helpers or add the same guard to the group implementation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/advanced/pagination.md, line 68:

<comment>Warning overstates the non-advancing cursor guard: `ClientSessionGroup` pagination drains do not raise and can still loop forever. Scope the warning to `Client` list/iter helpers or add the same guard to the group implementation.</comment>

<file context>
@@ -50,6 +50,26 @@ Run its `main()` and it prints `100 resources`: ten pages of ten, stitched toget
+`ClientSessionGroup` aggregation drains the same way, so a group fronting several servers reports the full collection instead of each server's first page. That aggregator is **[Session groups](session-groups.md)**.
+
+!!! warning
+    A drain trusts the server to advance the cursor. A server that keeps returning the same
+    `next_cursor` it was handed would page forever, so the drains stop and raise `RuntimeError`
+    the moment a cursor fails to move. A page that does not advance is a broken server, and a
</file context>
Suggested change
A drain trusts the server to advance the cursor. A server that keeps returning the same
`next_cursor` it was handed would page forever, so the drains stop and raise `RuntimeError`
the moment a cursor fails to move. A page that does not advance is a broken server, and a
loud failure beats a silent hang or a half-read list.
The `Client` drain helpers trust the server to advance the cursor. A server that keeps returning the same
`next_cursor` it was handed would page forever, so those helpers stop and raise `RuntimeError`
the moment a cursor fails to move. A page that does not advance is a broken server, and a
loud failure beats a silent hang or a half-read list.
Fix with cubic

Comment thread src/mcp/client/client.py
@@ -2,7 +2,7 @@

@cubic-dev-ai cubic-dev-ai Bot Jun 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Track all previously seen pagination cursors, not only the immediately previous one. Alternating/cyclic cursors can still make list_all_*/iter_all_* loop forever.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/client.py, line 609:

<comment>Track all previously seen pagination cursors, not only the immediately previous one. Alternating/cyclic cursors can still make `list_all_*`/`iter_all_*` loop forever.</comment>

<file context>
@@ -566,9 +583,130 @@ async def complete(
+                yield tool
+            if result.next_cursor is None:
+                return
+            if result.next_cursor == cursor:
+                raise RuntimeError(
+                    "Server returned a pagination cursor that did not advance; refusing to page forever."
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add list_all_* helpers to drain pagination

2 participants