Skip to content

feat(client): add list_all_* helpers that drain pagination#3083

Open
sanjibani wants to merge 2 commits into
modelcontextprotocol:mainfrom
sanjibani:feat/list-all-helpers
Open

feat(client): add list_all_* helpers that drain pagination#3083
sanjibani wants to merge 2 commits into
modelcontextprotocol:mainfrom
sanjibani:feat/list-all-helpers

Conversation

@sanjibani

Copy link
Copy Markdown

Closes #2556.

The single-page list_tools / list_resources / list_resource_templates /
list_prompts methods on ClientSession return the server's first page plus
a next_cursor; callers that don't loop on the cursor silently see only
page zero. Four new list_all_* helpers drain the cursor until the server
stops sending one and return the flattened list:

list_all_tools() -> list[Tool]
list_all_resources() -> list[Resource]
list_all_resource_templates() -> list[ResourceTemplate]
list_all_prompts() -> list[Prompt]

Each helper reuses the existing public list_* method, so per-page
post-processing keeps running across the drain (notably the 2026
x-mcp-header filter inside _absorb_tool_listing). Single-page and
empty-list cases work without looping.

Tests cover multi-page drain, single-page short-circuit, empty
result, helpers-don't-share-cursor-state, terminal-empty-page-stops
the loop, and cursor-roundtrip (server's next_cursor comes back
unchanged as params.cursor on the next request). 9 new tests in
tests/client/test_client.py; full client test suite (121 tests) passes,
ruff lint + format clean.

Addresses the design sketch in @agaonker's earlier comment on #2556
(same return type, helper naming, cursor-drain loop, malformed-cursor
safety via None-termination).

Closes modelcontextprotocol#2556.

The single-page list_tools / list_resources / list_resource_templates /
list_prompts methods return the server's first page plus a next_cursor;
callers that don't loop on the cursor silently see only page zero.
Four list_all_* helpers on ClientSession drain the cursor until the
server stops sending one and return the flattened list:

  list_all_tools()             -> list[Tool]
  list_all_resources()         -> list[Resource]
  list_all_resource_templates() -> list[ResourceTemplate]
  list_all_prompts()           -> list[Prompt]

Each helper reuses the existing public list_* method, so per-page
post-processing (e.g. the 2026 x-mcp-header filter in _absorb_tool_listing)
keeps running across the drain. Single-page and empty-list cases work
without a loop.

Tests cover: multi-page drain, single-page short-circuit, empty result,
helpers-dont-share-cursor-state, terminal-empty-page-stops-loop, and
cursor-roundtrip (the server's next_cursor comes back unchanged as
params.cursor on the next request). 9 new tests pass alongside the
existing 112 in tests/client.

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

All reported issues were addressed across 2 files

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

Re-trigger cubic

Comment thread src/mcp/client/session.py Outdated
Cubic P2 on PR modelcontextprotocol#3083: a multi-page list_all_tools() drain called
list_tools() with a non-None cursor on every page, so
_absorb_tool_listing(complete=True) was never reached and the
per-tool cache (_x_mcp_header_maps, _tool_output_schemas) was
left in an additive-only state. Tools that had been present in
a prior list_tools() call but were absent from the drained
listing kept their cache entries indefinitely.

Run _absorb_tool_listing once more with complete=True on the
merged result after the loop ends. The merged listing is the
drained universe, so complete=True prunes cache entries for
tools no longer present, mirroring the single-page complete
listing semantic.

Test: seed the cache with stale-tool via a single-page complete
listing, then drain survivor across two pages, assert the
cache reflects only the drained universe. A second test asserts
the inverse: if every drained tool is in the union, all cache
entries stay.
@sanjibani sanjibani force-pushed the feat/list-all-helpers branch from ceabb7d to 6f114fc Compare July 10, 2026 05:00

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

19 issues found across 32 files (changes from recent commits).

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="tests/interaction/lowlevel/test_flows.py">

<violation number="1">
P0: The keyword argument was renamed from `message_handler` to `message_callback`, but no `connect_*` function or the `Connect` protocol defines a `message_callback` parameter. This will raise a `TypeError` at runtime. Use `message_handler=collect` instead.</violation>
</file>

<file name="tests/interaction/lowlevel/test_resources.py">

<violation number="1">
P1: This call uses `message_callback=collect`, but the `Connect` protocol and every `connect_*` implementation name the parameter `message_handler`. This will raise `TypeError` at runtime — switch back to `message_handler` so the callback reaches `ClientSession`.</violation>
</file>

<file name="tests/interaction/lowlevel/test_list_changed.py">

<violation number="1">
P0: Renamed `message_handler` to `message_callback` in the `connect()` call, but the `connect` fixture's factory functions still expect `message_handler` as the keyword argument. This will raise `TypeError: ... got an unexpected keyword argument 'message_callback'` at runtime. Keep the original `message_handler=collect` invocation to match the `Connect` protocol signature.</violation>
</file>

<file name="tests/client/test_session.py">

<violation number="1">
P1: The renamed test fixtures now call `ClientSession`/`Client` with `message_callback`, but the implementation still only accepts `message_handler`. As written these tests fail before exercising the behavior under test; either the production API needs the same rename/alias, or the tests should keep using the existing `message_handler` parameter.</violation>
</file>

<file name="tests/interaction/transports/test_streamable_http.py">

<violation number="1">
P0: `connect_over_streamable_http` accepts `message_handler`, not `message_callback` — passing `message_callback=collect` will raise `TypeError` at runtime.</violation>
</file>

<file name="tests/interaction/mcpserver/test_context.py">

<violation number="1">
P0: `message_callback` is not a recognized parameter for `connect()` — this will raise `TypeError` at runtime. The protocol and all transport implementations use `message_handler`. Replace with `message_handler=collect`.</violation>
</file>

<file name="tests/server/mcpserver/test_integration.py">

<violation number="1">
P0: Passes `message_callback` to `Client(...)` but the param is named `message_handler`. This will raise `TypeError` at runtime.</violation>
</file>

<file name="tests/interaction/mcpserver/test_tools.py">

<violation number="1">
P1: `message_callback=collect` will raise `TypeError` at runtime — the `Connect` protocol and all `connect_*` factories expect `message_handler`. Switch back to `message_handler=collect`.</violation>
</file>

<file name="tests/client/test_notification_response.py">

<violation number="1">
P0: `ClientSession.__init__` expects `message_handler` (not `message_callback`) — passing `message_callback=message_callback` will raise `TypeError` at runtime and break this test.</violation>
</file>

<file name="tests/client/test_session_group.py">

<violation number="1">
P1: Assertion keyword `message_callback` doesn't match the `ClientSession.__init__` parameter name (`message_handler`). The mock will be called with `message_handler`, so this assertion will always fail. Revert to `message_handler`.</violation>
</file>

<file name="tests/interaction/lowlevel/test_progress.py">

<violation number="1">
P0: The `connect` fixture's protocol and all concrete factories accept `message_handler` as the keyword parameter, not `message_callback`. Passing `message_callback=message_callback` will raise `TypeError` at runtime because the parameter name does not match. Keep the local function name as `message_callback` if desired, but pass it as `message_handler=message_callback`.</violation>
</file>

<file name="tests/shared/test_streamable_http.py">

<violation number="1">
P0: `ClientSession.__init__` accepts `message_handler`, not `message_callback`. Passing the renamed keyword argument raises `TypeError` at session construction, causing all 8 affected tests to crash. Rename only the local function body and keep the keyword as `message_handler=message_callback`, or keep the old kwarg name.</violation>
</file>

<file name="tests/docs_src/test_elicitation.py">

<violation number="1">
P0: `Client` accepts `message_handler`, not `message_callback`. Passing `message_callback=on_message` will raise `TypeError: Client.__init__() got an unexpected keyword argument 'message_callback'`. Revert to `message_handler=on_message`.</violation>
</file>

<file name="tests/interaction/lowlevel/test_cancellation.py">

<violation number="1">
P0: `ClientSession(client_read, client_write, message_callback=message_callback)` passes a keyword argument `message_callback` that the constructor does not accept — the parameter is named `message_handler`. This will raise `TypeError` at runtime. Fix: use `message_handler=message_callback` to pass the renamed local function to the existing keyword.</violation>
</file>

<file name="tests/docs_src/test_subscriptions.py">

<violation number="1">
P0: `Client()` has no `message_callback` parameter — it's `message_handler`. This will raise `TypeError: unexpected keyword argument 'message_callback'` at construction.</violation>
</file>

<file name="tests/docs_src/test_legacy_clients.py">

<violation number="1">
P0: Passing `message_callback` to `Client()` will raise `TypeError` at init — the dataclass field is named `message_handler`, not `message_callback`. Keep `message_handler=on_message` to match the class definition.</violation>
</file>

<file name="tests/docs_src/test_context.py">

<violation number="1">
P1: `message_callback` is not a valid keyword argument for `Client`. The parameter is named `message_handler`. Passing `message_callback` would raise `TypeError` at runtime.</violation>
</file>

<file name="tests/docs_src/test_deploy.py">

<violation number="1">
P0: Using `message_callback` on `Client()` will raise `TypeError` at runtime — the dataclass only accepts `message_handler`. Change `message_callback` back to `message_handler`.</violation>
</file>

<file name="tests/interaction/_connect.py">

<violation number="1">
P1: The interaction tests that use `connect(..., message_callback=...)` will fail before they can exercise the transport, because the helper now forwards `message_callback` into `Client(...)` while the actual client API still names this parameter `message_handler`. Keeping the helper’s forwarded keyword aligned with `Client`/`ClientSession` (or renaming the production API in the same change) would avoid the unexpected-keyword error.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger 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

1 participant