Build a Paginated Tool

In the previous lesson, you learned about pagination and how to use Neo4j’s SKIP and LIMIT clauses to fetch data in manageable chunks.

In this challenge, you will implement a paginated tool that allows users to browse through movies in a specific genre, page by page.

Challenge Goals

To complete this challenge, you will:

  1. Create a tool that lists movies by genre with pagination support

  2. Use cursor-based pagination with Neo4j’s SKIP and LIMIT

  3. Return structured output with movies and pagination metadata

  4. Test pagination by fetching multiple pages

Solution Available

If you get stuck, you can review the complete solution in the repository at solutions/10c-paginated-tool/main.py.

To see the solution in action, run:

bash
uv --directory solutions/10c-paginated-tool run main.py

Step 1: Implement the Paginated Tool

Add this tool to your server/main.py. Let’s break it down into parts:

First, define the tool function with its parameters:

python
server/main.py
@mcp.tool()
async def browse_movies_by_genre(
    genre: str,
    cursor: str = "0",
    page_size: int = 10,
    ctx: Context = None
) -> dict:
    """
    Browse movies in a genre with pagination support.

    Args:
        genre: Genre name (e.g., "Action", "Comedy", "Drama")
        cursor: Pagination cursor - position in the result set (default "0")
        page_size: Number of movies to return per page (default 10)

    Returns:
        Dictionary containing:
        - movies: List of movie objects with title, released, and rating
        - next_cursor: Cursor for the next page (null if no more pages)
        - page: Current page number (1-indexed)
        - has_more: Boolean indicating if more pages are available
    """

The function takes a required genre parameter and optional cursor and page_size parameters. The cursor defaults to "0" (start of the list), and page_size defaults to 10 items per page.

Next, handle cursor validation and setup:

python
    # Parse cursor to get skip value
    try:
        skip = int(cursor)
    except ValueError:
        await ctx.error(f"Invalid cursor: {cursor}")
        skip = 0

    # Access driver from lifespan context
    driver = ctx.request_context.lifespan_context.driver

    # Log the request
    page_num = (skip // page_size) + 1
    await ctx.info(f"Fetching {genre} movies, page {page_num} (showing {page_size} per page)...")

This section converts the cursor string to a number and calculates the current page number. If the cursor is invalid, it defaults to the start (skip = 0).

The query execution uses Neo4j’s SKIP and LIMIT for pagination:

python
    try:
        # Execute paginated query
        records, summary, keys = await driver.execute_query(
            """
            MATCH (m:Movie)-[:IN_GENRE]->(g:Genre {name: $genre})
            RETURN m.title AS title,
                   m.released AS released,
                   m.imdbRating AS rating
            ORDER BY m.imdbRating DESC, m.title ASC
            SKIP $skip
            LIMIT $limit
            """,
            genre=genre,
            skip=skip,
            limit=page_size
        )

The Cypher query finds movies in the specified genre, ordered by rating (highest first) and title. SKIP and LIMIT handle the pagination.

Finally, process the results and return the paginated response:

python
        # Convert to list of dictionaries
        movies = [record.data() for record in records]

        # Calculate next cursor
        next_cursor = None
        if len(movies) == page_size:
            next_cursor = str(skip + page_size)

        # Log results
        await ctx.info(f"Returned {len(movies)} movies from page {page_num}")
        if next_cursor is None:
            await ctx.info("This is the last page")

        # Return structured response
        return {
            "genre": genre,
            "movies": movies,
            "next_cursor": next_cursor,
            "page": page_num,
            "page_size": page_size,
            "has_more": next_cursor is not None,
            "count": len(movies)
        }

    except Exception as e:
        await ctx.error(f"Query failed: {str(e)}")
        raise

The response includes the movies list and pagination metadata. The next_cursor is only set if a full page was returned, indicating more results are available.

Step 2: Test with the Interactive Client

Start your server in one terminal:

bash
uv --directory server run main.py

In a separate terminal, run the interactive client from the project root:

bash
uv --directory client run main.py

Select the browse_movies_by_genre tool from the menu and test pagination:

Fetch the First Page

The client will prompt you for parameters:

genre (required)
  Type: string
  Enter value: Action

cursor (optional, default: 0)
  Type: string
  Enter value: 0

page_size (optional, default: 10)
  Type: integer
  Enter value: 10

The response should contain:

  • 10 Action movies

  • A next_cursor value (e.g., "10")

  • page: 1

  • has_more: true

Fetch the Second Page

Use the next_cursor from the first response. When the menu returns, select the same tool again and enter:

genre (required)
  Type: string
  Enter value: Action

cursor (optional, default: 0)
  Type: string
  Enter value: 10

page_size (optional, default: 10)
  Type: integer
  Enter value: 10

The response should contain:

  • The next 10 Action movies

  • A new next_cursor value (e.g., "20")

  • page: 2

  • has_more: true (if more pages exist)

Continue to the Last Page

Keep using the next_cursor until you reach a response where:

  • next_cursor is null or not present

  • has_more is false

  • Fewer than page_size movies are returned

Step 3: Test with Different Genres

Try different genres to see how pagination behaves:

  • "Comedy" - Might have many pages

  • "Sci-Fi" - Moderate number of pages

  • "Documentary" - Might fit in a single page

Notice how some genres have more movies than others!

Summary

In this challenge, you successfully implemented cursor-based pagination:

  • Cursor parameter - Accepted a cursor string to track position in results

  • SKIP and LIMIT - Used Neo4j’s pagination clauses for efficient queries

  • Next cursor calculation - Determined when more pages are available

  • Structured response - Returned movies with rich pagination metadata

  • Error handling - Handled invalid cursors and query errors

  • Logging - Provided informative feedback during pagination

Your tool can now handle large datasets efficiently, providing a great user experience when browsing through collections.

In the next lesson, you’ll learn about building prompts to provide pre-defined templates to MCP clients.

Chatbot

How can I help you today?