Skip to content

Conversation

maxisbey
Copy link
Contributor

@maxisbey maxisbey commented Aug 20, 2025

Summary

This PR adds pagination support for listing prompts, resources, and tools using cursor-based pagination as defined in the MCP spec.

Design Evolution

Initial Implementation (Updated)

Originally, this PR introduced separate paginated decorators (list_prompts_paginated, list_resources_paginated, list_tools_paginated) to maintain backward compatibility. The rationale was:

  • Backward compatibility: Modifying existing decorators would break existing code
  • Clean separation: Users could opt-in to pagination
  • Type safety: Different signatures for paginated vs non-paginated handlers

Current Implementation (After Discussion)

Based on feedback and discussion, the implementation has evolved to use unified decorators with automatic callback inspection:

  • Single list_prompts(), list_resources(), and list_tools() decorators handle both paginated and non-paginated callbacks
  • Uses the new accepts_cursor() function to inspect callback signatures at runtime
  • Maintains full backward compatibility while allowing pagination support
  • Cleaner API with no need for separate decorator methods

How It Works

The decorators now automatically detect whether a callback accepts a cursor parameter:

# Non-paginated (existing code continues to work)
@server.list_resources()
async def list_resources() -> list[Resource]:
    return [...]

# Paginated (new capability)
@server.list_resources()
async def list_resources(cursor: Cursor | None) -> ListResourcesResult:
    return ListResourcesResult(resources=[...], nextCursor="...")

Changes

  • Add accepts_cursor() function for callback signature inspection
  • Modify list_prompts(), list_resources(), and list_tools() decorators to support both signatures
  • Add comprehensive unit tests for pagination and signature detection
  • Add examples demonstrating pagination usage
  • Tool cache properly maintained for both paginated and non-paginated modes

Test plan

  • Unit tests pass for all three decorators with both signatures
  • Tests verify cursor passthrough for both None and string values
  • Function inspection tests cover various callback signatures
  • Backward compatibility verified with existing non-paginated handlers
  • Manually tested via the inspector and with example client code

🤖 Generated with Claude Code

👨 Below generated by the human, Max Isbey

Manual testing

I spun up the MCP inspector and ran the example pagination server in both stdio mode and SSE mode. Below is some testing evidence using the SSE mode:

Clicking "List More Resources" correctly lists all 30 resources:

Note: this works for prompts and tools as well

Requesting a resource works:

Running the pagination client example code works listing all 30 resources:

Copy link
Member

@jerome3o-anthropic jerome3o-anthropic left a comment

Choose a reason for hiding this comment

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

Can we have a some docs with examples? not sure if we have good docs for the lowlevel api but some usage examples would be great (if not just to help with reviewing).

Otherwise this looks good to me. happy with the separate functions to avoid breaking changes. keen to know how you think we should approach this in the high level interface.

@maxisbey maxisbey force-pushed the feat/list-pagination branch from 91fa584 to 9d72703 Compare August 20, 2025 14:02
@maxisbey
Copy link
Contributor Author

Can we have a some docs with examples? not sure if we have good docs for the lowlevel api but some usage examples would be great (if not just to help with reviewing).

Otherwise this looks good to me. happy with the separate functions to avoid breaking changes. keen to know how you think we should approach this in the high level interface.

Updated the docs, added example code, and included testing in the PR desription

@maxisbey maxisbey force-pushed the feat/list-pagination branch from 66ee42f to 83d4890 Compare August 20, 2025 15:43
@jerome3o-anthropic jerome3o-anthropic self-requested a review August 20, 2025 15:44
@maxisbey maxisbey force-pushed the feat/list-pagination branch 3 times, most recently from 70435e5 to 5d823c8 Compare August 21, 2025 13:51
@jerome3o-anthropic jerome3o-anthropic self-requested a review August 21, 2025 14:02
@maxisbey maxisbey enabled auto-merge (squash) August 21, 2025 14:02
@maxisbey maxisbey requested a review from pcarleton August 21, 2025 14:17
Copy link
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

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

Backward compatibility: Modifying the existing decorators to require a cursor parameter in the callback would break existing uses of these decorators.

You can make it optional, can't you?

maxisbey and others added 5 commits August 27, 2025 10:07
Add list_prompts_paginated, list_resources_paginated, and list_tools_paginated
decorators to support cursor-based pagination for listing endpoints.

These decorators:
- Accept a cursor parameter (can be None for first page)
- Return the respective ListResult type directly
- Maintain backward compatibility with existing non-paginated decorators
- Update tool cache for list_tools_paginated

Also includes simplified unit tests that verify cursor passthrough.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Apply automatic formatting from ruff to ensure code meets project standards:
- Remove trailing whitespace
- Adjust line breaks for consistency
- Format function arguments according to line length limits
- Create mcp_simple_pagination example server demonstrating all three paginated endpoints
- Add pagination snippets for both server and client implementations
- Update README to use snippet-source pattern for pagination examples
- Move mutually exclusive note to blockquote format for better visibility
- Complete example shows tools, resources, and prompts pagination with different page sizes
@maxisbey maxisbey force-pushed the feat/list-pagination branch from 47c3f19 to 2b2e80c Compare August 27, 2025 14:16
@maxisbey maxisbey requested a review from Kludex August 27, 2025 14:19
The previous change to run tools directly (without uv run) broke the CI
pipeline because the tools are installed in uv's virtual environment
which isn't in PATH during pre-commit execution.

Reverting to use `uv run --frozen` ensures the tools are found both
locally and in CI environments.
],
)

if transport == "sse":
Copy link
Member

Choose a reason for hiding this comment

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

Why are we doing this every time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just following the pattern of all the other sample servers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to leave this as is, and maybe chnage in a different PR dedicated to simplifying the example servers or something if that's alright with you?

Comment on lines 24 to 26
if method:
params.pop("self", None)
params.pop("cls", None)
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't rely on parameter names.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh fair, turns out this isn't needed anyway as inspect.signature.parameters doesn't contain these by default. Updated the unit tests to reflect this.

cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]], func)

async def cursor_handler(req: types.ListToolsRequest):
result = await cursor_func(req.params.cursor if req.params is not None else None)
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to pass the ListToolRequest instead? What does that object contains?

I'm on my phone only this week.

Copy link
Member

Choose a reason for hiding this comment

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

Because then you, as a user don't need to type it as Cursor | None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm well the only thing in the ListToolsRequest is the cursor. Unless you think in the future there's a chance more would be added to it?

Copy link
Member

Choose a reason for hiding this comment

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

My point is more that it makes more sense to receive a request as parameter.

But answering the question... Yes, if something is added in the future, it will be there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alrighty, changed from pasing cursors to passing request objects

@maxisbey maxisbey requested a review from Kludex September 1, 2025 12:56
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.

3 participants