feat(client): add list_all_* helpers that drain pagination#3083
Open
sanjibani wants to merge 2 commits into
Open
feat(client): add list_all_* helpers that drain pagination#3083sanjibani wants to merge 2 commits into
sanjibani wants to merge 2 commits into
Conversation
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.
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
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.
ceabb7d to
6f114fc
Compare
There was a problem hiding this comment.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).