import { createMCPServer, InMemoryStore, ResourceDefinition, PaginationMetadata } from '../src'; /** * Example: Pagination Support in MCP Resources * * This example demonstrates how to implement pagination in both list() and read() * methods for MCP resources. Pagination helps handle large datasets efficiently. */ // Mock data for demonstration const allItems = Array.from({ length: 100 }, (_, i) => ({ id: `item-${i + 1}`, name: `Item ${i + 1}`, description: `Description for item ${i + 1}`, timestamp: Date.now() - i * 1000, })); // Helper function to paginate array data function paginateArray( items: T[], page: number = 1, limit: number = 10 ): { items: T[]; pagination: PaginationMetadata } { const offset = (page - 1) * limit; const paginatedItems = items.slice(offset, offset + limit); return { items: paginatedItems, pagination: { page, limit, total: items.length, hasMore: offset + limit < items.length, }, }; } // Example 1: Paginated List Resource const paginatedListResource: ResourceDefinition = { uri: 'items://list', name: 'items-list', description: 'List of items with pagination support', mimeType: 'application/json', // List handler with pagination list: async (context, options) => { const page = options?.pagination?.page || 1; const limit = options?.pagination?.limit || 10; console.log(`Listing items: page=${page}, limit=${limit}`); const { items, pagination } = paginateArray(allItems, page, limit); return { resources: items.map(item => ({ uri: `items://list/${item.id}`, name: item.name, description: item.description, })), pagination, }; }, // Read handler (single item, no pagination needed) read: async (uri, context) => { const itemId = uri.split('/').pop(); const item = allItems.find(i => i.id === itemId); if (!item) { throw new Error(`Item not found: ${itemId}`); } return { contents: item, }; }, }; // Example 2: Paginated Read Resource (for large content) const paginatedReadResource: ResourceDefinition = { uri: 'logs://stream/{streamId}', name: 'log-stream', description: 'Log stream with paginated read', mimeType: 'text/plain', list: async (context) => { return { resources: [ { uri: 'logs://stream/app-logs', name: 'Application Logs' }, { uri: 'logs://stream/error-logs', name: 'Error Logs' }, { uri: 'logs://stream/access-logs', name: 'Access Logs' }, ], }; }, // Read handler with pagination for large log files read: async (uri, context, options) => { const streamId = uri.split('/').pop(); const page = options?.pagination?.page || 1; const limit = options?.pagination?.limit || 50; console.log(`Reading logs: stream=${streamId}, page=${page}, limit=${limit}`); // Simulate fetching log lines const allLogLines = Array.from( { length: 500 }, (_, i) => `[${new Date(Date.now() - i * 1000).toISOString()}] Log entry ${i + 1}` ); const { items: logLines, pagination } = paginateArray(allLogLines, page, limit); return { contents: logLines.join('\n'), pagination, }; }, }; // Example 3: Cursor-based Pagination (for real-time data) interface CursorData { items: any[]; nextCursor?: string; prevCursor?: string; } const cursorPaginatedResource: ResourceDefinition = { uri: 'feed://updates', name: 'updates-feed', description: 'Real-time updates with cursor-based pagination', mimeType: 'application/json', list: async (context) => { return { resources: [ { uri: 'feed://updates', name: 'Updates Feed' }, ], }; }, read: async (uri, context, options) => { const cursor = options?.pagination?.cursor; const limit = options?.pagination?.limit || 20; console.log(`Reading feed: cursor=${cursor}, limit=${limit}`); // In a real implementation, you would: // 1. Decode the cursor to get the last seen item ID/timestamp // 2. Query items after that cursor // 3. Generate a new cursor for the next page // Simulate cursor-based pagination const cursorIndex = cursor ? parseInt(cursor, 10) : 0; const feedItems = allItems.slice(cursorIndex, cursorIndex + limit); const hasMore = cursorIndex + limit < allItems.length; const nextCursor = hasMore ? String(cursorIndex + limit) : undefined; const prevCursor = cursorIndex > 0 ? String(Math.max(0, cursorIndex - limit)) : undefined; return { contents: feedItems, pagination: { page: 1, // Not used in cursor pagination limit, total: allItems.length, hasMore, nextCursor, prevCursor, }, }; }, }; // Create and start the server async function main() { const server = createMCPServer({ name: 'pagination-example', version: '1.0.0', publicUrl: 'http://localhost:3000', port: 3000, store: new InMemoryStore(), tools: [], resources: [ paginatedListResource, paginatedReadResource, cursorPaginatedResource, ], }); await server.start(); console.log('🚀 Pagination example server started!'); console.log('\nTry these requests:'); console.log('\n1. List items with pagination:'); console.log(' POST http://localhost:3000/mcp'); console.log(' Body: {"method": "resources/list", "_meta": {"page": 1, "limit": 5}}'); console.log('\n2. Read logs with pagination:'); console.log(' POST http://localhost:3000/mcp'); console.log(' Body: {"method": "resources/read", "params": {"uri": "logs://stream/app-logs"}, "_meta": {"page": 2, "limit": 10}}'); console.log('\n3. Feed with cursor pagination:'); console.log(' POST http://localhost:3000/mcp'); console.log(' Body: {"method": "resources/read", "params": {"uri": "feed://updates"}, "_meta": {"cursor": "20", "limit": 20}}'); } // Usage notes: /** * PAGINATION REQUEST FORMAT: * * Clients should send pagination parameters in the _meta field: * * { * "jsonrpc": "2.0", * "id": 1, * "method": "resources/list", * "params": {}, * "_meta": { * "page": 1, // Page number (1-based) * "limit": 10, // Items per page * "cursor": "..." // For cursor-based pagination * } * } * * PAGINATION RESPONSE FORMAT: * * The response will include pagination metadata in _meta: * * { * "jsonrpc": "2.0", * "id": 1, * "result": { * "resources": [...], * "_meta": { * "pagination": { * "page": 1, * "limit": 10, * "total": 100, * "hasMore": true, * "nextCursor": "...", * "prevCursor": "..." * } * } * } * } * * IMPLEMENTATION TIPS: * * 1. Always provide sensible defaults (e.g., page=1, limit=10) * 2. Set maximum limits to prevent abuse (e.g., max 100 items per page) * 3. Return total count when possible (helps clients show "X of Y") * 4. Use cursor-based pagination for real-time/streaming data * 5. Use offset-based pagination for stable, sorted datasets * 6. Include hasMore flag to indicate if more data is available * 7. Consider caching paginated results for frequently accessed pages */ if (require.main === module) { main().catch(console.error); } export { paginatedListResource, paginatedReadResource, cursorPaginatedResource };