Skip to main content
Glama
pagination.tsโ€ข21.6 kB
export interface PaginationParams { limit?: number; starting_after?: string; offset?: number; // For offset-based pagination (get-all-campaigns) } export interface PaginatedResponse<T> { data: T[]; total?: number; limit: number; hasMore: boolean; next_starting_after?: string; pagination_info?: string; } export interface CompletePaginationOptions { maxPages?: number; defaultLimit?: number; progressCallback?: (current: number, total: number) => void; useOffsetPagination?: boolean; } export interface InstantlyAPICall { (endpoint: string, method?: string, data?: any): Promise<any>; } export interface ReusablePaginationOptions { maxPages?: number; batchSize?: number; additionalParams?: string[]; progressCallback?: (pageCount: number, totalItems: number) => void; enablePerformanceMonitoring?: boolean; operationType?: 'accounts' | 'campaigns' | 'emails' | 'leads'; useClientDetection?: boolean; // Enable automatic client-based timeout adjustment } export interface PaginationMetadata { returned_count: number; has_more: boolean; next_starting_after?: string; limit: number; pages_retrieved: number; request_time_ms: number; timeout_occurred: boolean; note?: string; } export interface PaginatedResult<T> { data: T[]; pagination: PaginationMetadata; filters_applied?: Record<string, any>; metadata: { request_time_ms: number; note: string; timeout_occurred: boolean; }; } export function buildInstantlyPaginationQuery(params: PaginationParams): URLSearchParams { const query = new URLSearchParams(); if (params.limit !== undefined) { query.append('limit', params.limit.toString()); } if (params.starting_after !== undefined) { query.append('starting_after', params.starting_after); } return query; } export function buildQueryParams(args: any, additionalParams: string[] = []): URLSearchParams { const query = new URLSearchParams(); // Add pagination parameters if (args?.limit) query.append('limit', String(args.limit)); if (args?.starting_after) query.append('starting_after', String(args.starting_after)); // Add additional parameters additionalParams.forEach(param => { if (args?.[param]) { query.append(param, String(args[param])); } }); return query; } export function parsePaginatedResponse<T>(response: any, requestedLimit?: number): PaginatedResponse<T> { // Handle Instantly API response format - check for both 'data' and 'items' arrays if (response.data && Array.isArray(response.data)) { return { data: response.data as T[], total: response.total, limit: response.limit || requestedLimit || response.data.length, hasMore: !!response.next_starting_after, next_starting_after: response.next_starting_after, }; } // Handle Instantly API 'items' format (used by campaigns, accounts, etc.) if (response.items && Array.isArray(response.items)) { return { data: response.items as T[], total: response.total, limit: response.limit || requestedLimit || response.items.length, hasMore: !!response.next_starting_after, next_starting_after: response.next_starting_after, }; } // Handle array response if (Array.isArray(response)) { return { data: response as T[], limit: requestedLimit || response.length, hasMore: false, }; } // Default case - preserve requested limit even if no data return { data: [], limit: requestedLimit || 0, hasMore: false, }; } export function createPaginationInfo( currentLimit: number, totalItems?: number, hasMore?: boolean, nextStartingAfter?: string ): string { let info = `Showing ${currentLimit} items`; if (totalItems !== undefined) { info += ` of ${totalItems} total`; } if (hasMore) { info += ` (more available)`; } if (nextStartingAfter) { info += ` - Next page token: ${nextStartingAfter}`; } return info; } /** * Complete pagination function that retrieves ALL data following Instantly API pagination rules * * @param apiCall Function that makes the API request * @param initialParams Initial parameters for the first request * @param options Pagination options including max pages and progress callback * @returns Promise<T[]> All items retrieved through pagination */ export async function getAllDataWithPagination<T>( apiCall: (params: any) => Promise<any>, initialParams: any = {}, options: CompletePaginationOptions = {} ): Promise<T[]> { const { maxPages = 20, defaultLimit = 100, progressCallback, useOffsetPagination = false } = options; const allItems: T[] = []; let pageCount = 0; let startingAfter: string | undefined = undefined; let offset = 0; console.log(`๐Ÿ”„ Starting complete pagination retrieval (max ${maxPages} pages, limit ${defaultLimit})...`); while (pageCount < maxPages) { try { // Prepare parameters for this page const pageParams = { ...initialParams }; if (useOffsetPagination) { // Offset-based pagination (for get-all-campaigns) pageParams.limit = defaultLimit; pageParams.offset = offset; } else { // Token-based pagination (standard Instantly API) pageParams.limit = defaultLimit; if (startingAfter) { pageParams.starting_after = startingAfter; } } console.log(`๐Ÿ“„ Fetching page ${pageCount + 1}...`, pageParams); // Make the API call const response = await apiCall(pageParams); // Extract items from response let items: T[] = []; let nextStartingAfter: string | undefined = undefined; if (response.items && Array.isArray(response.items)) { items = response.items; nextStartingAfter = response.next_starting_after; } else if (response.data && Array.isArray(response.data)) { items = response.data; nextStartingAfter = response.next_starting_after; } else if (Array.isArray(response)) { items = response; nextStartingAfter = undefined; } else { console.warn(`โš ๏ธ Unexpected response format on page ${pageCount + 1}:`, typeof response); break; } // Add items to our collection if (items.length > 0) { allItems.push(...items); console.log(`โœ… Retrieved ${items.length} items (total: ${allItems.length})`); // Call progress callback if provided if (progressCallback) { progressCallback(allItems.length, allItems.length); } } // Check termination conditions if (useOffsetPagination) { // For offset pagination: stop if we got fewer items than requested if (items.length < defaultLimit) { console.log(`๐Ÿ Reached end of data (got ${items.length} < ${defaultLimit})`); break; } offset += defaultLimit; } else { // For token pagination: stop if no next_starting_after or empty items if (!nextStartingAfter || items.length === 0) { console.log(`๐Ÿ Reached end of data (next_starting_after: ${nextStartingAfter}, items: ${items.length})`); break; } startingAfter = nextStartingAfter; } pageCount++; // Safety check for infinite loops if (pageCount >= maxPages) { console.warn(`โš ๏ธ Reached maximum page limit (${maxPages}). Total items: ${allItems.length}`); break; } } catch (error) { console.error(`โŒ Error during pagination on page ${pageCount + 1}:`, error); throw error; } } console.log(`โœ… Pagination complete: ${allItems.length} total items retrieved in ${pageCount} pages`); return allItems; } /** * Instantly API specific pagination helper * Handles the specific patterns used by Instantly API endpoints */ export async function getInstantlyDataWithPagination<T>( makeRequest: (endpoint: string, method?: string, data?: any) => Promise<any>, endpoint: string, params: any = {}, options: CompletePaginationOptions = {} ): Promise<{ items: T[], totalRetrieved: number, pagesUsed: number }> { const apiCall = async (pageParams: any) => { // Build query string for GET requests const queryParams = new URLSearchParams(); Object.entries(pageParams).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)); } }); const fullEndpoint = queryParams.toString() ? `${endpoint}?${queryParams.toString()}` : endpoint; return await makeRequest(fullEndpoint); }; const allItems = await getAllDataWithPagination<T>(apiCall, params, { maxPages: 20, defaultLimit: 100, progressCallback: (current, total) => { console.log(`๐Ÿ“Š Progress: ${current} items retrieved...`); }, ...options }); return { items: allItems, totalRetrieved: allItems.length, pagesUsed: Math.ceil(allItems.length / (options.defaultLimit || 100)) }; } /** * Validates pagination results and reports discrepancies */ export function validatePaginationResults<T>( items: T[], expectedCount?: number, context: string = 'items' ): string { let message = `๐Ÿ“Š Retrieved ${items.length} ${context}`; if (expectedCount && expectedCount !== items.length) { if (items.length < expectedCount) { message += `\nโš ๏ธ Note: Expected ${expectedCount} ${context} but retrieved ${items.length}. `; message += `This may be due to API pagination limitations or access restrictions.`; } else { message += `\nโœ… Retrieved more ${context} than expected (${items.length} vs ${expectedCount}).`; } } else if (expectedCount) { message += `\nโœ… Count matches expectation (${expectedCount}).`; } return message; } /** * Reusable pagination function for Instantly API endpoints * Handles the complete pagination flow with proper termination logic * NOW WITH: Client-aware timeout protection, adaptive limits, partial results */ export async function paginateInstantlyAPI( endpoint: string, apiCall: InstantlyAPICall, params: any = {}, options: ReusablePaginationOptions = {} ): Promise<any[]> { const { maxPages: userMaxPages, batchSize = 100, additionalParams = [], progressCallback, enablePerformanceMonitoring = true, operationType = 'accounts', useClientDetection = true // Enable by default } = options; // ADDED: Client-aware timeout protection // UPDATED: Increased timeout to 30 seconds for better reliability with slow API responses let TOTAL_TIMEOUT_MS = 30000; // Default: 30 seconds (increased from 20s for intermittent hanging fix) let TIMEOUT_BUFFER_MS = 3000; // Default: 3 second buffer (was 5s) let DELAY_BETWEEN_REQUESTS_MS = 0; // No delay - rate limiter handles API limits let maxPages = userMaxPages || 2; // Default: 2 pages (was 5) for faster response // Import and use client detection if enabled if (useClientDetection) { try { const { globalClientManager } = await import('./client-detection.js'); const clientConfig = globalClientManager.getTimeoutConfig(); TOTAL_TIMEOUT_MS = clientConfig.totalTimeoutMs; TIMEOUT_BUFFER_MS = clientConfig.bufferMs; DELAY_BETWEEN_REQUESTS_MS = clientConfig.delayBetweenRequestsMs; maxPages = userMaxPages || clientConfig.maxPages; console.error(`[Pagination] Using ${clientConfig.clientName} config: ${TOTAL_TIMEOUT_MS}ms timeout, ${maxPages} max pages`); } catch (error) { console.error('[Pagination] Client detection unavailable, using defaults:', error); } } const startTime = Date.now(); // Initialize performance monitoring if enabled let performanceMonitor: any = null; if (enablePerformanceMonitoring) { try { const { createPerformanceMonitor } = await import('./performance-monitor.js'); performanceMonitor = createPerformanceMonitor(operationType); } catch (error) { console.error('[Instantly MCP] Performance monitoring unavailable:', error); } } console.error(`[Instantly MCP] Starting reusable pagination for ${endpoint}...`); // Support starting_after parameter from params if (params?.starting_after) { console.error(`[Instantly MCP] Starting pagination from: ${params.starting_after}`); } const allItems: any[] = []; let pageCount = 0; let startingAfter: string | undefined = params?.starting_after; let hasMore = true; let timeoutOccurred = false; let nextStartingAfter: string | undefined = undefined; try { while (hasMore && pageCount < maxPages) { // ADDED: Check timeout before each page request const elapsedTime = Date.now() - startTime; if (elapsedTime > TOTAL_TIMEOUT_MS - TIMEOUT_BUFFER_MS) { console.error(`[Instantly MCP] โฑ๏ธ Approaching timeout (${elapsedTime}ms elapsed), stopping pagination gracefully`); timeoutOccurred = true; break; } pageCount++; // Build query parameters for this page const queryParams = new URLSearchParams(); queryParams.append('limit', batchSize.toString()); if (startingAfter) { queryParams.append('starting_after', startingAfter); } // Add additional parameters additionalParams.forEach(param => { if (params?.[param]) { queryParams.append(param, String(params[param])); } }); const fullEndpoint = `${endpoint}${queryParams.toString() ? `?${queryParams}` : ''}`; console.error(`[Instantly MCP] Page ${pageCount}: Fetching up to ${batchSize} items...`); // Make the API call for this page let response: any; let hasError = false; let isRateLimited = false; console.error(`[Instantly MCP] ๐Ÿ” DEBUG: About to call apiCall for ${fullEndpoint}`); try { response = await apiCall(fullEndpoint); console.error(`[Instantly MCP] ๐Ÿ” DEBUG: apiCall completed, response type: ${typeof response}`); console.error(`[Instantly MCP] ๐Ÿ” DEBUG: response keys: ${response ? Object.keys(response) : 'response is null/undefined'}`); } catch (error: any) { hasError = true; // Check if it's a rate limit error if (error.message?.includes('rate limit') || error.status === 429) { isRateLimited = true; console.error(`[Instantly MCP] Rate limit hit on page ${pageCount}, retrying...`); // Wait and retry once await new Promise(resolve => setTimeout(resolve, 2000)); try { response = await apiCall(fullEndpoint); hasError = false; } catch (retryError) { throw retryError; } } else { throw error; } } // Extract items from response (handle different response formats) let pageItems: any[] = []; if (Array.isArray(response)) { // Direct array response pageItems = response; hasMore = false; // Array response means no pagination } else if (response && response.data && Array.isArray(response.data)) { // Standard paginated response with data array pageItems = response.data; nextStartingAfter = response.next_starting_after; } else if (response && response.items && Array.isArray(response.items)) { // Alternative response format with items array (official API format) pageItems = response.items; nextStartingAfter = response.next_starting_after; } else { console.error(`[Instantly MCP] Unexpected response format in page ${pageCount}:`, typeof response); throw new Error(`Unexpected API response format in page ${pageCount}`); } // Record performance metrics if (performanceMonitor) { performanceMonitor.recordApiCall(pageItems.length, isRateLimited, hasError); // Check if we should abort due to performance issues const abortCheck = performanceMonitor.shouldAbort(); if (abortCheck.abort) { console.error(`[Instantly MCP] Aborting pagination: ${abortCheck.reason}`); break; } } // Add this page to our accumulated results if (pageItems.length > 0) { allItems.push(...pageItems); console.error(`[Instantly MCP] Page ${pageCount}: Retrieved ${pageItems.length} items (total: ${allItems.length})`); // Call progress callback if provided if (progressCallback) { progressCallback(pageCount, allItems.length); } } else { console.error(`[Instantly MCP] Page ${pageCount}: No items returned, ending pagination`); hasMore = false; } // Check termination conditions - ONLY terminate when no next_starting_after token // DO NOT terminate based on batch size as API may return fewer items per page if (!nextStartingAfter) { console.error(`[Instantly MCP] Pagination complete: No next_starting_after token`); hasMore = false; } else { startingAfter = nextStartingAfter; console.error(`[Instantly MCP] Continuing pagination with token: ${nextStartingAfter.substring(0, 20)}...`); } // Safety check to prevent infinite loops if (pageCount >= maxPages) { console.error(`[Instantly MCP] Reached maximum page limit (${maxPages}), stopping pagination`); break; } // ADDED: Delay between requests to prevent rate limiting if (hasMore && pageCount < maxPages) { await new Promise(resolve => setTimeout(resolve, DELAY_BETWEEN_REQUESTS_MS)); } } const totalTime = Date.now() - startTime; console.error(`[Instantly MCP] Reusable pagination complete: ${allItems.length} total items retrieved in ${pageCount} pages (${totalTime}ms)`); // ADDED: Log timeout warning if occurred if (timeoutOccurred) { console.error(`[Instantly MCP] โš ๏ธ TIMEOUT: Partial results returned. Use starting_after="${nextStartingAfter}" to retrieve remaining items.`); } // Finalize performance monitoring if (performanceMonitor) { performanceMonitor.finalize(); const recommendations = performanceMonitor.getRecommendations(); if (recommendations.length > 0) { console.error(`[Instantly MCP] Performance recommendations:`); recommendations.forEach((rec: string) => console.error(` - ${rec}`)); } } // Validate results if (allItems.length === 0) { console.error(`[Instantly MCP] Warning: No items found for ${endpoint}`); } else { console.error(`[Instantly MCP] โœ… Successfully retrieved complete dataset: ${allItems.length} items`); } // ADDED: Store metadata for caller to access (allItems as any).__pagination_metadata = { returned_count: allItems.length, has_more: !!nextStartingAfter, next_starting_after: nextStartingAfter, limit: batchSize, pages_retrieved: pageCount, request_time_ms: totalTime, timeout_occurred: timeoutOccurred, note: nextStartingAfter ? `Retrieved ${pageCount} pages (${allItems.length} items). More results available. To retrieve additional pages, call this tool again with starting_after parameter set to: ${nextStartingAfter}` : `Retrieved all available data (${allItems.length} items in ${pageCount} pages).` }; return allItems; } catch (error) { console.error(`[Instantly MCP] Error during reusable pagination at page ${pageCount}:`, error); // Finalize performance monitoring even on error if (performanceMonitor) { performanceMonitor.finalize(); } // CHANGED: Return partial results instead of throwing error if (allItems.length > 0) { const totalTime = Date.now() - startTime; console.error(`[Instantly MCP] โš ๏ธ Error occurred, but returning ${allItems.length} items retrieved before error`); // Store metadata for partial results (allItems as any).__pagination_metadata = { returned_count: allItems.length, has_more: true, // Assume more data exists since we errored next_starting_after: nextStartingAfter, limit: batchSize, pages_retrieved: pageCount, request_time_ms: totalTime, timeout_occurred: true, note: `Partial results returned due to error. Retrieved ${allItems.length} items before error occurred.` }; return allItems; } throw error; } } /** * Helper function to apply client-side date filtering * Used when API doesn't support server-side date filtering */ export function applyDateFilters<T extends Record<string, any>>( items: T[], createdAfter?: string, createdBefore?: string, dateField: string = 'created_at' ): T[] { if (!createdAfter && !createdBefore) { return items; } return items.filter(item => { const itemDate = item[dateField]; if (!itemDate) return true; // Keep items without date field const itemTimestamp = new Date(itemDate).getTime(); if (createdAfter) { const afterTimestamp = new Date(createdAfter).getTime(); if (itemTimestamp < afterTimestamp) return false; } if (createdBefore) { const beforeTimestamp = new Date(createdBefore).getTime(); // Add 1 day to include the entire "before" date const beforeEndOfDay = beforeTimestamp + (24 * 60 * 60 * 1000); if (itemTimestamp > beforeEndOfDay) return false; } return true; }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bcharleson/Instantly-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server