import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; import request from 'supertest'; import { createMCPServer, InMemoryStore, ResourceDefinition, AuthContext, ResourceReadOptions } from '../index'; describe('Pagination Support', () => { let server: any; let app: any; // Mock data const mockItems = Array.from({ length: 50 }, (_, i) => ({ id: `item-${i + 1}`, name: `Item ${i + 1}`, data: `Data for item ${i + 1}`, })); beforeAll(async () => { const paginatedResource: ResourceDefinition = { uri: 'test://items', name: 'test-items', description: 'Test resource with pagination', mimeType: 'application/json', list: async (context: AuthContext, options?: ResourceReadOptions) => { const page = options?.pagination?.page || 1; const limit = options?.pagination?.limit || 10; const offset = (page - 1) * limit; const paginatedItems = mockItems.slice(offset, offset + limit); return { resources: paginatedItems.map(item => ({ uri: `test://items/${item.id}`, name: item.name, })), pagination: { page, limit, total: mockItems.length, hasMore: offset + limit < mockItems.length, }, }; }, read: async (uri: string, context: AuthContext, options?: ResourceReadOptions) => { const itemId = uri.split('/').pop(); const item = mockItems.find(i => i.id === itemId); if (!item) { throw new Error('Item not found'); } // Support pagination for large content const page = options?.pagination?.page || 1; const limit = options?.pagination?.limit || 100; return { contents: item, pagination: options?.pagination ? { page, limit, total: 1, hasMore: false, } : undefined, }; }, }; const cursorResource: ResourceDefinition = { uri: 'test://cursor', name: 'test-cursor', description: 'Test resource with cursor pagination', mimeType: 'application/json', list: async (_context: AuthContext) => { return { resources: [{ uri: 'test://cursor', name: 'Cursor Resource' }], }; }, read: async (uri: string, context: AuthContext, options?: ResourceReadOptions) => { const cursor = options?.pagination?.cursor; const limit = options?.pagination?.limit || 10; const cursorIndex = cursor ? parseInt(cursor, 10) : 0; const paginatedItems = mockItems.slice(cursorIndex, cursorIndex + limit); const hasMore = cursorIndex + limit < mockItems.length; return { contents: paginatedItems, pagination: { page: 1, limit, total: mockItems.length, hasMore, nextCursor: hasMore ? String(cursorIndex + limit) : undefined, prevCursor: cursorIndex > 0 ? String(Math.max(0, cursorIndex - limit)) : undefined, }, }; }, }; server = createMCPServer({ name: 'pagination-test', version: '1.0.0', publicUrl: 'http://localhost:3001', port: 3001, store: new InMemoryStore(), tools: [], resources: [paginatedResource, cursorResource], }); await server.start(); app = server.getApp(); }); afterAll(async () => { await server.stop(); }); describe('List with Pagination', () => { it('should return first page by default', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/list', params: {}, }); expect(response.status).toBe(200); expect(response.body.result).toBeDefined(); expect(response.body.result.resources).toHaveLength(10); // Default limit expect(response.body.result._meta?.pagination).toMatchObject({ page: 1, limit: 10, total: 50, hasMore: true, }); }); it('should return specific page with custom limit', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/list', params: {}, _meta: { page: 2, limit: 5, }, }); expect(response.status).toBe(200); expect(response.body.result.resources).toHaveLength(5); expect(response.body.result._meta?.pagination).toMatchObject({ page: 2, limit: 5, total: 50, hasMore: true, }); }); it('should indicate no more pages on last page', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/list', params: {}, _meta: { page: 5, limit: 10, }, }); expect(response.status).toBe(200); expect(response.body.result.resources).toHaveLength(10); expect(response.body.result._meta?.pagination.hasMore).toBe(false); }); }); describe('Read with Pagination', () => { it('should read without pagination by default', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'test://items/item-1', }, }); expect(response.status).toBe(200); expect(response.body.result.contents).toBeDefined(); expect(response.body.result._meta?.pagination).toBeUndefined(); }); it('should support pagination when requested', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'test://items/item-1', }, _meta: { page: 1, limit: 50, }, }); expect(response.status).toBe(200); expect(response.body.result.contents).toBeDefined(); expect(response.body.result._meta?.pagination).toMatchObject({ page: 1, limit: 50, total: 1, hasMore: false, }); }); }); describe('Cursor-based Pagination', () => { it('should support cursor pagination', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'test://cursor', }, _meta: { limit: 10, }, }); expect(response.status).toBe(200); expect(response.body.result.contents).toHaveLength(10); expect(response.body.result._meta?.pagination).toMatchObject({ page: 1, limit: 10, total: 50, hasMore: true, }); expect(response.body.result._meta?.pagination.nextCursor).toBe('10'); expect(response.body.result._meta?.pagination.prevCursor).toBeUndefined(); }); it('should navigate using cursor', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'test://cursor', }, _meta: { cursor: '20', limit: 10, }, }); expect(response.status).toBe(200); expect(response.body.result.contents).toHaveLength(10); expect(response.body.result._meta?.pagination.nextCursor).toBe('30'); expect(response.body.result._meta?.pagination.prevCursor).toBe('10'); }); it('should handle last page with cursor', async () => { const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/read', params: { uri: 'test://cursor', }, _meta: { cursor: '45', limit: 10, }, }); expect(response.status).toBe(200); expect(response.body.result.contents).toHaveLength(5); expect(response.body.result._meta?.pagination.hasMore).toBe(false); expect(response.body.result._meta?.pagination.nextCursor).toBeUndefined(); expect(response.body.result._meta?.pagination.prevCursor).toBe('35'); }); }); describe('Backward Compatibility', () => { it('should handle resources without pagination support', async () => { // This tests that old resources still work const response = await request(app) .post('/mcp') .send({ jsonrpc: '2.0', id: 1, method: 'resources/list', params: {}, }); expect(response.status).toBe(200); expect(response.body.result.resources).toBeDefined(); }); }); });