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 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:
# 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:
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:
# 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)}")
raiseThe 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:
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
cursor (optional, default: 0)
Type: string
Enter value: 0
page_size (optional, default: 10)
Type: integer
Enter value: 10The response should contain:
-
10 Action movies
-
A
next_cursorvalue (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: 10The response should contain:
-
The next 10 Action movies
-
A new
next_cursorvalue (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_cursorisnullor not present -
has_moreisfalse -
Fewer than
page_sizemovies 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.