When your MCP server handles large datasets - whether listing thousands of files, database records, or search results - you need pagination to manage memory efficiently and provide responsive user experiences.
This guide covers how to implement and use pagination in MCP.
Without pagination, large responses can cause:
MCP uses cursor-based pagination for reliable, consistent paging through result sets.
---
A cursor is an opaque string that marks your position in a result set. Think of it like a bookmark in a long book.
sequenceDiagram
participant Client
participant Server
Client->>Server: tools/list (no cursor)
Server-->>Client: tools [1-10], nextCursor: "abc123"
Client->>Server: tools/list (cursor: "abc123")
Server-->>Client: tools [11-20], nextCursor: "def456"
Client->>Server: tools/list (cursor: "def456")
Server-->>Client: tools [21-25], nextCursor: null (end)
These MCP methods support pagination:
| Method | Returns | Cursor Support |
|--------|---------|----------------|
| tools/list | Tool definitions | ✅ |
| resources/list | Resource definitions | ✅ |
| prompts/list | Prompt definitions | ✅ |
| resources/templates/list | Resource templates | ✅ |
---
from mcp.server import Server
from mcp.types import Tool, ListToolsResult
import math
app = Server("paginated-server")
# Simulated large dataset
ALL_TOOLS = [
Tool(name=f"tool_{i}", description=f"Tool number {i}", inputSchema={})
for i in range(100)
]
PAGE_SIZE = 10
@app.list_tools()
async def list_tools(cursor: str | None = None) -> ListToolsResult:
"""List tools with pagination support."""
# Decode cursor to get starting index
start_index = 0
if cursor:
try:
start_index = int(cursor)
except ValueError:
start_index = 0
# Get page of results
end_index = min(start_index + PAGE_SIZE, len(ALL_TOOLS))
page_tools = ALL_TOOLS[start_index:end_index]
# Calculate next cursor
next_cursor = None
if end_index < len(ALL_TOOLS):
next_cursor = str(end_index)
return ListToolsResult(
tools=page_tools,
nextCursor=next_cursor
)
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server({
name: "paginated-server",
version: "1.0.0"
});
// Simulated large dataset
const ALL_TOOLS = Array.from({ length: 100 }, (_, i) => ({
name: `tool_${i}`,
description: `Tool number ${i}`,
inputSchema: { type: "object", properties: {} }
}));
const PAGE_SIZE = 10;
server.setRequestHandler(ListToolsResultSchema, async (request) => {
// Decode cursor
let startIndex = 0;
if (request.params?.cursor) {
startIndex = parseInt(request.params.cursor, 10) || 0;
}
// Get page of results
const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_TOOLS.length);
const pageTools = ALL_TOOLS.slice(startIndex, endIndex);
// Calculate next cursor
const nextCursor = endIndex < ALL_TOOLS.length ? String(endIndex) : undefined;
return {
tools: pageTools,
nextCursor
};
});
@Service
public class PaginatedToolService {
private static final int PAGE_SIZE = 10;
private final List<Tool> allTools;
public PaginatedToolService() {
// Initialize large dataset
this.allTools = IntStream.range(0, 100)
.mapToObj(i -> new Tool("tool_" + i, "Tool number " + i, Map.of()))
.collect(Collectors.toList());
}
@McpMethod("tools/list")
public ListToolsResult listTools(@Param("cursor") String cursor) {
// Decode cursor
int startIndex = 0;
if (cursor != null && !cursor.isEmpty()) {
try {
startIndex = Integer.parseInt(cursor);
} catch (NumberFormatException e) {
startIndex = 0;
}
}
// Get page of results
int endIndex = Math.min(startIndex + PAGE_SIZE, allTools.size());
List<Tool> pageTools = allTools.subList(startIndex, endIndex);
// Calculate next cursor
String nextCursor = endIndex < allTools.size() ? String.valueOf(endIndex) : null;
return new ListToolsResult(pageTools, nextCursor);
}
}
---
from mcp import ClientSession
async def get_all_tools(session: ClientSession) -> list:
"""Fetch all tools using pagination."""
all_tools = []
cursor = None
while True:
result = await session.list_tools(cursor=cursor)
all_tools.extend(result.tools)
if result.nextCursor is None:
break
cursor = result.nextCursor
return all_tools
# Usage
async with client_session as session:
tools = await get_all_tools(session)
print(f"Found {len(tools)} tools")
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
async function getAllTools(client: Client): Promise<Tool[]> {
const allTools: Tool[] = [];
let cursor: string | undefined = undefined;
do {
const result = await client.listTools({ cursor });
allTools.push(...result.tools);
cursor = result.nextCursor;
} while (cursor);
return allTools;
}
// Usage
const tools = await getAllTools(client);
console.log(`Found ${tools.length} tools`);
For very large datasets, load pages on-demand:
class PaginatedToolIterator:
"""Lazily iterate through paginated tools."""
def __init__(self, session: ClientSession):
self.session = session
self.cursor = None
self.buffer = []
self.exhausted = False
async def __anext__(self):
# Return from buffer if available
if self.buffer:
return self.buffer.pop(0)
# Check if we've exhausted all pages
if self.exhausted:
raise StopAsyncIteration
# Fetch next page
result = await self.session.list_tools(cursor=self.cursor)
self.buffer = list(result.tools)
self.cursor = result.nextCursor
if self.cursor is None:
self.exhausted = True
if not self.buffer:
raise StopAsyncIteration
return self.buffer.pop(0)
def __aiter__(self):
return self
# Usage - memory efficient for large datasets
async for tool in PaginatedToolIterator(session):
process_tool(tool)
---
Resources often need pagination for directories or large datasets:
from mcp.server import Server
from mcp.types import Resource, ListResourcesResult
import os
app = Server("file-server")
@app.list_resources()
async def list_resources(cursor: str | None = None) -> ListResourcesResult:
"""List files in directory with pagination."""
directory = "/data/files"
all_files = sorted(os.listdir(directory))
# Decode cursor (file index)
start_index = int(cursor) if cursor else 0
page_size = 20
end_index = min(start_index + page_size, len(all_files))
# Create resource list for this page
resources = []
for filename in all_files[start_index:end_index]:
filepath = os.path.join(directory, filename)
resources.append(Resource(
uri=f"file://{filepath}",
name=filename,
mimeType="application/octet-stream"
))
# Calculate next cursor
next_cursor = str(end_index) if end_index < len(all_files) else None
return ListResourcesResult(
resources=resources,
nextCursor=next_cursor
)
---
# Cursor is just the index
cursor = "50" # Start at item 50
Pros: Simple, stateless
Cons: Results can shift if items are added/removed
# Cursor is the last seen ID
cursor = "item_abc123" # Start after this item
Pros: Stable even if items change
Cons: Requires ordered IDs
import base64
import json
def encode_cursor(state: dict) -> str:
return base64.b64encode(json.dumps(state).encode()).decode()
def decode_cursor(cursor: str) -> dict:
return json.loads(base64.b64decode(cursor).decode())
# Cursor contains multiple state fields
cursor = encode_cursor({
"offset": 50,
"filter": "active",
"sort": "name"
})
Pros: Can encode complex state
Cons: More complex, larger cursor strings
---
# Consider the data size
PAGE_SIZE_SMALL_ITEMS = 100 # Simple metadata
PAGE_SIZE_MEDIUM_ITEMS = 20 # Richer objects
PAGE_SIZE_LARGE_ITEMS = 5 # Complex content
@app.list_tools()
async def list_tools(cursor: str | None = None) -> ListToolsResult:
try:
start_index = int(cursor) if cursor else 0
if start_index < 0 or start_index >= len(ALL_TOOLS):
start_index = 0 # Reset to beginning
except (ValueError, TypeError):
start_index = 0 # Invalid cursor, start fresh
# ...
return ListToolsResult(
tools=page_tools,
nextCursor=next_cursor,
# Some implementations include total for UI progress
_meta={"total": len(ALL_TOOLS)}
)
async def test_pagination():
# Empty result set
result = await session.list_tools()
assert result.tools == []
assert result.nextCursor is None
# Single page
result = await session.list_tools()
assert len(result.tools) <= PAGE_SIZE
# Invalid cursor
result = await session.list_tools(cursor="invalid")
assert result.tools # Should return first page
---
# BAD: Loads everything into memory
@app.list_tools()
async def list_tools() -> ListToolsResult:
all_tools = load_all_tools() # 1 million tools!
return ListToolsResult(tools=all_tools)
# GOOD: Only loads what's needed
@app.list_tools()
async def list_tools(cursor: str | None = None) -> ListToolsResult:
offset = int(cursor) if cursor else 0
tools = await db.query_tools(offset=offset, limit=PAGE_SIZE)
return ListToolsResult(tools=tools, nextCursor=...)
---
---
MCP 서버가 수천 개의 파일, 데이터베이스 레코드 또는 검색 결과와 같은 대규모 데이터셋을 처리할 때 메모리를 효율적으로 관리하고 반응성 있는 사용자 경험을 제공하려면 페이지네이션이 필요합니다. 이 가이드는 MCP에서 페이지네이션을 구현하고 사용하는 방법을 다룹니다.
페이지네이션이 없으면 대규모 응답이 다음과 같은 문제를 일으킬 수 있습니다:
MCP는 결과 집합을 안정적이고 일관되게 페이지 처리하기 위해 커서 기반 페이지네이션을 사용합니다.
---
커서는 결과 집합 내 위치를 표시하는 불투명한 문자열입니다. 긴 책에서의 북마크와 같이 생각할 수 있습니다.
sequenceDiagram
participant Client
participant Server
Client->>Server: tools/list (커서 없음)
Server-->>Client: 도구 [1-10], 다음커서: "abc123"
Client->>Server: tools/list (커서: "abc123")
Server-->>Client: 도구 [11-20], 다음커서: "def456"
Client->>Server: tools/list (커서: "def456")
Server-->>Client: 도구 [21-25], 다음커서: null (끝)
다음 MCP 메서드들이 페이지네이션을 지원합니다:
| 메서드 | 반환값 | 커서 지원 |
|--------|---------|----------------|
| tools/list | 도구 정의 | ✅ |
| resources/list | 리소스 정의 | ✅ |
| prompts/list | 프롬프트 정의 | ✅ |
| resources/templates/list | 리소스 템플릿 | ✅ |
---
from mcp.server import Server
from mcp.types import Tool, ListToolsResult
import math
app = Server("paginated-server")
# 시뮬레이션된 대용량 데이터셋
ALL_TOOLS = [
Tool(name=f"tool_{i}", description=f"Tool number {i}", inputSchema={})
for i in range(100)
]
PAGE_SIZE = 10
@app.list_tools()
async def list_tools(cursor: str | None = None) -> ListToolsResult:
"""List tools with pagination support."""
# 시작 인덱스를 얻기 위해 커서 디코딩
start_index = 0
if cursor:
try:
start_index = int(cursor)
except ValueError:
start_index = 0
# 결과 페이지 가져오기
end_index = min(start_index + PAGE_SIZE, len(ALL_TOOLS))
page_tools = ALL_TOOLS[start_index:end_index]
# 다음 커서 계산하기
next_cursor = None
if end_index < len(ALL_TOOLS):
next_cursor = str(end_index)
return ListToolsResult(
tools=page_tools,
nextCursor=next_cursor
)
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server({
name: "paginated-server",
version: "1.0.0"
});
// 시뮬레이션된 대용량 데이터셋
const ALL_TOOLS = Array.from({ length: 100 }, (_, i) => ({
name: `tool_${i}`,
description: `Tool number ${i}`,
inputSchema: { type: "object", properties: {} }
}));
const PAGE_SIZE = 10;
server.setRequestHandler(ListToolsResultSchema, async (request) => {
// 커서 디코딩
let startIndex = 0;
if (request.params?.cursor) {
startIndex = parseInt(request.params.cursor, 10) || 0;
}
// 결과 페이지 가져오기
const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_TOOLS.length);
const pageTools = ALL_TOOLS.slice(startIndex, endIndex);
// 다음 커서 계산하기
const nextCursor = endIndex < ALL_TOOLS.length ? String(endIndex) : undefined;
return {
tools: pageTools,
nextCursor
};
});
@Service
public class PaginatedToolService {
private static final int PAGE_SIZE = 10;
private final List<Tool> allTools;
public PaginatedToolService() {
// 대용량 데이터셋 초기화
this.allTools = IntStream.range(0, 100)
.mapToObj(i -> new Tool("tool_" + i, "Tool number " + i, Map.of()))
.collect(Collectors.toList());
}
@McpMethod("tools/list")
public ListToolsResult listTools(@Param("cursor") String cursor) {
// 커서 디코딩
int startIndex = 0;
if (cursor != null && !cursor.isEmpty()) {
try {
startIndex = Integer.parseInt(cursor);
} catch (NumberFormatException e) {
startIndex = 0;
}
}
// 결과 페이지 가져오기
int endIndex = Math.min(startIndex + PAGE_SIZE, allTools.size());
List<Tool> pageTools = allTools.subList(startIndex, endIndex);
// 다음 커서 계산
String nextCursor = endIndex < allTools.size() ? String.valueOf(endIndex) : null;
return new ListToolsResult(pageTools, nextCursor);
}
}
---
from mcp import ClientSession
async def get_all_tools(session: ClientSession) -> list:
"""Fetch all tools using pagination."""
all_tools = []
cursor = None
while True:
result = await session.list_tools(cursor=cursor)
all_tools.extend(result.tools)
if result.nextCursor is None:
break
cursor = result.nextCursor
return all_tools
# 사용법
async with client_session as session:
tools = await get_all_tools(session)
print(f"Found {len(tools)} tools")
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
async function getAllTools(client: Client): Promise<Tool[]> {
const allTools: Tool[] = [];
let cursor: string | undefined = undefined;
do {
const result = await client.listTools({ cursor });
allTools.push(...result.tools);
cursor = result.nextCursor;
} while (cursor);
return allTools;
}
// 사용법
const tools = await getAllTools(client);
console.log(`Found ${tools.length} tools`);
매우 큰 데이터셋의 경우 필요에 따라 페이지를 로드하세요:
class PaginatedToolIterator:
"""Lazily iterate through paginated tools."""
def __init__(self, session: ClientSession):
self.session = session
self.cursor = None
self.buffer = []
self.exhausted = False
async def __anext__(self):
# 버퍼에서 가능하면 반환
if self.buffer:
return self.buffer.pop(0)
# 모든 페이지를 다 사용했는지 확인
if self.exhausted:
raise StopAsyncIteration
# 다음 페이지 가져오기
result = await self.session.list_tools(cursor=self.cursor)
self.buffer = list(result.tools)
self.cursor = result.nextCursor
if self.cursor is None:
self.exhausted = True
if not self.buffer:
raise StopAsyncIteration
return self.buffer.pop(0)
def __aiter__(self):
return self
# 사용법 - 대용량 데이터셋에 대해 메모리 효율적임
async for tool in PaginatedToolIterator(session):
process_tool(tool)
---
리소스는 디렉터리나 대규모 데이터셋에 대해 페이지네이션이 자주 필요합니다:
from mcp.server import Server
from mcp.types import Resource, ListResourcesResult
import os
app = Server("file-server")
@app.list_resources()
async def list_resources(cursor: str | None = None) -> ListResourcesResult:
"""List files in directory with pagination."""
directory = "/data/files"
all_files = sorted(os.listdir(directory))
# 커서 디코딩 (파일 인덱스)
start_index = int(cursor) if cursor else 0
page_size = 20
end_index = min(start_index + page_size, len(all_files))
# 이 페이지에 대한 리소스 리스트 생성
resources = []
for filename in all_files[start_index:end_index]:
filepath = os.path.join(directory, filename)
resources.append(Resource(
uri=f"file://{filepath}",
name=filename,
mimeType="application/octet-stream"
))
# 다음 커서 계산
next_cursor = str(end_index) if end_index < len(all_files) else None
return ListResourcesResult(
resources=resources,
nextCursor=next_cursor
)
---
# 커서는 단지 인덱스입니다
cursor = "50" # 50번째 항목에서 시작합니다
장점: 단순하고 상태 비저장
단점: 항목이 추가/삭제되면 결과가 이동할 수 있음
# 커서는 마지막으로 본 ID입니다
cursor = "item_abc123" # 이 항목 다음부터 시작합니다
장점: 항목이 변경되어도 안정적
단점: 정렬된 ID 필요
import base64
import json
def encode_cursor(state: dict) -> str:
return base64.b64encode(json.dumps(state).encode()).decode()
def decode_cursor(cursor: str) -> dict:
return json.loads(base64.b64decode(cursor).decode())
# 커서는 여러 상태 필드를 포함합니다
cursor = encode_cursor({
"offset": 50,
"filter": "active",
"sort": "name"
})
장점: 복잡한 상태를 인코딩 가능
단점: 더 복잡하고 커서 문자열이 길어짐
---
# 데이터 크기를 고려하세요
PAGE_SIZE_SMALL_ITEMS = 100 # 간단한 메타데이터
PAGE_SIZE_MEDIUM_ITEMS = 20 # 더 풍부한 객체
PAGE_SIZE_LARGE_ITEMS = 5 # 복잡한 내용
@app.list_tools()
async def list_tools(cursor: str | None = None) -> ListToolsResult:
try:
start_index = int(cursor) if cursor else 0
if start_index < 0 or start_index >= len(ALL_TOOLS):
start_index = 0 # 처음으로 재설정
except (ValueError, TypeError):
start_index = 0 # 잘못된 커서, 새로 시작
# ...
return ListToolsResult(
tools=page_tools,
nextCursor=next_cursor,
# 일부 구현은 UI 진행 상황을 위한 전체 합계를 포함합니다
_meta={"total": len(ALL_TOOLS)}
)
async def test_pagination():
# 빈 결과 집합
result = await session.list_tools()
assert result.tools == []
assert result.nextCursor is None
# 단일 페이지
result = await session.list_tools()
assert len(result.tools) <= PAGE_SIZE
# 잘못된 커서
result = await session.list_tools(cursor="invalid")
assert result.tools # 첫 페이지를 반환해야 함
---
# 나쁨: 모든 것을 메모리에 로드함
@app.list_tools()
async def list_tools() -> ListToolsResult:
all_tools = load_all_tools() # 100만 개의 도구!
return ListToolsResult(tools=all_tools)
# 좋음: 필요한 것만 로드합니다
@app.list_tools()
async def list_tools(cursor: str | None = None) -> ListToolsResult:
offset = int(cursor) if cursor else 0
tools = await db.query_tools(offset=offset, limit=PAGE_SIZE)
return ListToolsResult(tools=tools, nextCursor=...)
---
---
---
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다.
정확성을 위해 최선을 다하고 있으나, 자동 번역에는 오류나 부정확성이 포함될 수 있음을 유의해 주시기 바랍니다.
원본 문서의 원어는 권위 있는 출처로 간주되어야 합니다.
중요한 정보의 경우 전문 인간 번역을 권장합니다.
본 번역의 사용으로 인한 오해나 오해석에 대해 당사는 어떠한 법적 책임도 지지 않습니다.