Use this file to discover all available pages before exploring further.
MCP uses cursor-based pagination for all list operations that may return large result sets. The MCP C# SDK provides both convenience methods for automatic pagination and raw methods for manual control.
Instead of offset-based pagination (page 1, page 2, etc.), MCP uses opaque cursor tokens. Each paginated response may include a NextCursor value. If present, pass it in the next request to retrieve the next page of results.
The cursor format is completely opaque to clients. Servers can use any encoding scheme (numeric offsets, base64 tokens, database cursors, etc.) as long as they can parse their own cursors.
For more control (processing page-by-page, limiting results, showing progress), use the raw methods:
string? cursor = null;int pageCount = 0;do{ var result = await client.ListToolsAsync(new ListToolsRequestParams { Cursor = cursor }); pageCount++; Console.WriteLine($"Page {pageCount}: {result.Tools.Count} tools"); // Process this page of results foreach (var tool in result.Tools) { Console.WriteLine($" {tool.Name}: {tool.Description}"); } // Get the cursor for the next page (null when no more pages) cursor = result.NextCursor;} while (cursor is not null);Console.WriteLine($"Total pages: {pageCount}");
.WithListToolsHandler(async (ctx, ct) =>{ const int pageSize = 20; int? lastId = null; // Parse cursor as the last ID from the previous page if (ctx.Params?.Cursor is { } cursor) { lastId = int.Parse(cursor); } var query = dbContext.Tools .OrderBy(t => t.Id) .AsQueryable(); if (lastId.HasValue) { query = query.Where(t => t.Id > lastId.Value); } var tools = await query .Take(pageSize + 1) // Fetch one extra to check if more exist .ToListAsync(ct); var hasMore = tools.Count > pageSize; if (hasMore) { tools.RemoveAt(tools.Count - 1); // Remove the extra item } return new ListToolsResult { Tools = tools.Select(MapToMcpTool).ToList(), NextCursor = hasMore ? tools[^1].Id.ToString() : null };});
Return an error or restart from the beginning if cursor is invalid:
if (!TryParseCursor(cursor, out var state)){ // Start from beginning or return error state = new CursorState();}
Because the cursor format is opaque, any value (including empty string) signals more results are available. If a server erroneously sends an empty string cursor with the final page, clients can implement pagination workarounds.