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:
-
Create a tool that lists movies by genre with pagination support
-
Use cursor-based pagination with Neo4j’s
SKIPandLIMIT -
Return structured output with movies and pagination metadata
-
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:
uv --directory solutions/10c-paginated-tool run main.pyStep 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:
@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:
# 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.
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.
# 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)}")
raiseThe 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:
uv --directory server run main.pyIn a separate terminal, run the interactive client from the project root:
uv --directory client run main.pySelect 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=9You should receive a structured response similar to the following, with information about the current page and the next cursor.
{
"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: 2The response should contain a different page number, next cursor and list of movies.
{
"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.