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 list_movies_by_genre(
    genre: str,
    page_size: int = 10,
    cursor: int = 0,
    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
    # Calculate skip value from cursor
    skip = cursor * page_size

    # 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 uses the cursor number to calculate the skip value.

Next, using the driver instance from the lifespan context, execute the query to fetch the movies using the SKIP and LIMIT clauses, and coerce the results into a list of dictionaries.

python
    try:
        # Access driver from lifespan context
        driver = ctx.request_context.lifespan_context.driver

        # 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.title ASC
            SKIP $skip
            LIMIT $limit
            """,
            genre=genre,
            skip=skip,
            limit=page_size
        )

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

Finally, calculate the next cursor and return the structured output.

python
        # Calculate next cursor
        next_cursor = None
        if len(movies) == page_size:
            next_cursor = 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
        }

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

The structured output consists of a dictionary with the following keys:

  • genre - The genre passed to the tool by the client

  • movies - A list of movies returned from the query

  • next_cursor - The cursor for the next page

  • page - The current page number

  • page_size - The number of movies per page

  • has_more - A boolean indicating if more pages 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

page_size (optional)
  Type: integer
  Enter value: 2

cursor (optional)
  Type: string
  Enter value: 1=9

You should receive a structured response similar to the following, with information about the current page and the next cursor.

json
Structured Response
{
  "genre": "Action",
  "movies": [
    {
      "title": "'Hellboy': The Seeds of Creation",
      "released": "2004-07-27",
      "rating": 6.9
    },
    {
      "title": "13 Assassins (Jûsan-nin no shikaku)",
      "released": "2010-09-25",
      "rating": 7.6
    }
  ],
  "next_cursor": 2,
  "page": 1,
  "page_size": 2,
  "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

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

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

The response should contain a different page number, next cursor and list of movies.

json
Paginated Response
{
  "genre": "Action",
  "movies": [
    {
      "title": "2 Fast 2 Furious (Fast and the Furious 2, The)",
      "released": "2003-06-06",
      "rating": 5.8
    },
    {
      "title": "2 Guns",
      "released": "2013-08-02",
      "rating": 6.7
    }
  ],
  "next_cursor": 6,
  "page": 3,
  "page_size": 2,
  "has_more": true
}

Experiment with different genres, for example Comedy, Sci-Fi, or Documentary, and change the page_size to see how it affects the results.

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?