Pagination
Handle large result sets efficiently with cursor-based pagination.
Overview
Added in: v1.7.0
Protocol: MCP 2025-11-25
Methods: tools/list, prompts/list, resources/list
Pagination allows servers to split large result sets into manageable chunks:
- ✅ Cursor-based - Efficient for large datasets
- ✅ Stateless - No server-side session required
- ✅ Flexible - Server controls page size
Quick Example
Server-Side
[McpTool("list_users")]
public JsonRpcMessage ListUsers(TypedJsonRpc<ListUsersParams> request)
{
var args = request.GetParams()!;
var cursor = args.Cursor;
var pageSize = 10;
// Parse cursor (simple offset in this example)
var offset = string.IsNullOrEmpty(cursor) ? 0 : int.Parse(cursor);
// Get page of results
var users = _database.Users
.Skip(offset)
.Take(pageSize + 1) // Take one extra to check if more exist
.ToList();
// Check if more results exist
var hasMore = users.Count > pageSize;
if (hasMore)
{
users = users.Take(pageSize).ToList();
}
// Generate next cursor
var nextCursor = hasMore ? (offset + pageSize).ToString() : null;
return ToolResponse.Success(
request.Id,
new
{
users,
nextCursor
});
}
public record ListUsersParams(string? Cursor = null);
Client-Side
async function fetchAllUsers() {
let cursor = null;
let allUsers = [];
do {
const response = await fetch('/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'list_users',
arguments: { Cursor: cursor }
},
id: Date.now()
})
});
const result = await response.json();
const data = JSON.parse(result.result.content[0].text);
allUsers.push(...data.users);
cursor = data.nextCursor;
} while (cursor !== null);
return allUsers;
}
Pagination Patterns
1. Offset-Based (Simple)
var cursor = args.Cursor ?? "0";
var offset = int.Parse(cursor);
var results = _data
.Skip(offset)
.Take(pageSize)
.ToList();
var nextCursor = results.Count == pageSize
? (offset + pageSize).ToString()
: null;
2. Keyset-Based (Efficient)
var cursor = args.Cursor; // Last seen ID
var query = _dbContext.Users.AsQueryable();
if (!string.IsNullOrEmpty(cursor))
{
query = query.Where(u => u.Id > cursor);
}
var results = query
.OrderBy(u => u.Id)
.Take(pageSize + 1)
.ToList();
var hasMore = results.Count > pageSize;
var nextCursor = hasMore ? results[pageSize - 1].Id : null;
3. Token-Based (Secure)
// Encode cursor state
var cursorData = new CursorData
{
Offset = 100,
Timestamp = DateTime.UtcNow
};
var json = JsonSerializer.Serialize(cursorData);
var bytes = Encoding.UTF8.GetBytes(json);
var cursor = Convert.ToBase64String(bytes);
// Decode cursor
var bytes = Convert.FromBase64String(args.Cursor);
var json = Encoding.UTF8.GetString(bytes);
var cursorData = JsonSerializer.Deserialize<CursorData>(json);
Built-in Pagination
MCP Gateway automatically paginates:
tools/list
{
"jsonrpc": "2.0",
"method": "tools/list",
"params": {
"cursor": "optional-cursor"
},
"id": 1
}
Response:
{
"jsonrpc": "2.0",
"result": {
"tools": [...],
"nextCursor": "next-page-cursor"
},
"id": 1
}
resources/list
{
"jsonrpc": "2.0",
"method": "resources/list",
"params": {
"cursor": "optional-cursor"
},
"id": 2
}
Best Practices
1. Consistent Page Size
private const int DefaultPageSize = 10;
private const int MaxPageSize = 100;
var pageSize = Math.Min(args.PageSize ?? DefaultPageSize, MaxPageSize);
2. Validate Cursors
if (!string.IsNullOrEmpty(cursor))
{
if (!int.TryParse(cursor, out var offset) || offset < 0)
{
throw new ToolInvalidParamsException("Invalid cursor");
}
}
3. Include Total Count (Optional)
return ToolResponse.Success(
request.Id,
new
{
users,
nextCursor,
totalCount = _database.Users.Count() // Optional
});
4. Handle Edge Cases
// Empty results
if (!results.Any())
{
return ToolResponse.Success(request.Id, new
{
users = Array.Empty<User>(),
nextCursor = (string?)null
});
}
// Last page
var hasMore = results.Count > pageSize;
if (!hasMore)
{
return ToolResponse.Success(request.Id, new
{
users = results,
nextCursor = (string?)null
});
}
Testing
[Fact]
public async Task ListUsers_FirstPage_ReturnsResults()
{
// Arrange
var request = new TypedJsonRpc<ListUsersParams>
{
Id = "1",
Params = new ListUsersParams(Cursor: null)
};
// Act
var response = await _tools.ListUsers(request);
// Assert
var result = JsonSerializer.Deserialize<ListUsersResult>(
response.Result!.ToString()!);
Assert.Equal(10, result.Users.Count);
Assert.NotNull(result.NextCursor);
}
[Fact]
public async Task ListUsers_LastPage_ReturnsNullCursor()
{
// Arrange - cursor pointing to last page
var request = new TypedJsonRpc<ListUsersParams>
{
Id = "1",
Params = new ListUsersParams(Cursor: "90")
};
// Act
var response = await _tools.ListUsers(request);
// Assert
var result = JsonSerializer.Deserialize<ListUsersResult>(
response.Result!.ToString()!);
Assert.True(result.Users.Count <= 10);
Assert.Null(result.NextCursor);
}
See Also
- Tools API - Complete Tools API reference
- Resources API - Resources pagination
- Pagination Example - Complete example