Skip to main content
Glama

Klaviyo MCP Server

klaviyo-client.js7.88 kB
import axios from 'axios'; import { API_CONFIG, RATE_LIMIT_CONFIG } from './config.js'; import logger from './utils/logger.js'; import { getCache, setCache, hasCache } from './utils/cache.js'; // Create a client with the API key const apiKey = process.env.KLAVIYO_API_KEY; if (!apiKey) { logger.error('KLAVIYO_API_KEY environment variable is not set. API calls will fail.'); throw new Error('KLAVIYO_API_KEY environment variable is required. Please set it before starting the server.'); } const client = axios.create({ baseURL: API_CONFIG.baseURL, headers: { 'Authorization': `Klaviyo-API-Key ${apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json', 'Revision': API_CONFIG.revision } }); /** * Sleep for a specified number of milliseconds * @param {number} ms - Milliseconds to sleep * @returns {Promise} - Promise that resolves after the specified time */ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); /** * Calculate backoff delay for retries * @param {number} retryCount - Current retry attempt * @returns {number} - Delay in milliseconds */ function calculateBackoffDelay(retryCount) { const delay = Math.min( RATE_LIMIT_CONFIG.initialDelayMs * Math.pow(RATE_LIMIT_CONFIG.backoffFactor, retryCount), RATE_LIMIT_CONFIG.maxDelayMs ); // Add some jitter to prevent all clients retrying simultaneously return delay * (0.8 + Math.random() * 0.4); } /** * Check if error is due to rate limiting * @param {Error} error - Error to check * @returns {boolean} - True if rate limited */ function isRateLimitError(error) { return ( error.response && (error.response.status === 429 || (error.response.status === 400 && error.response.data?.errors?.some(e => e.detail?.toLowerCase().includes('rate limit') || e.title?.toLowerCase().includes('rate limit') ) ) ) ); } /** * Execute a request with retry logic * @param {Function} requestFn - Function that returns a promise for the request * @param {string} method - HTTP method * @param {string} endpoint - API endpoint * @param {Object} [requestData] - Request data or params * @param {Function} [fallbackFn] - Optional fallback function to call if all retries fail * @returns {Promise} - Promise that resolves with the response data */ async function executeWithRetry(requestFn, method, endpoint, requestData, fallbackFn) { let retries = 0; // For debugging only - don't use directly in API calls const debugData = requestData; // Check cache first if it's a GET request const cacheKey = method === 'GET' ? `${endpoint}` : null; if (method === 'GET' && hasCache(cacheKey)) { logger.debug(`Cache hit for ${method} ${endpoint}`); return getCache(cacheKey); } while (true) { try { logger.request(method, endpoint, debugData); const response = await requestFn(); logger.response(method, endpoint, response.status, response.data); // Cache the response if it's a GET request if (method === 'GET' && cacheKey) { setCache(cacheKey, response.data); } return response.data; } catch (error) { if (isRateLimitError(error) && retries < RATE_LIMIT_CONFIG.maxRetries) { retries++; const delay = calculateBackoffDelay(retries); logger.warn(`Rate limit exceeded for ${method} ${endpoint}. Retrying in ${Math.round(delay / 1000)}s (attempt ${retries}/${RATE_LIMIT_CONFIG.maxRetries})`); await sleep(delay); continue; } logger.apiError(method, endpoint, error); // Try fallback if provided if (fallbackFn) { try { logger.info(`Attempting fallback for ${method} ${endpoint}`); const fallbackResponse = await fallbackFn(error); logger.info(`Fallback successful for ${method} ${endpoint}`); return fallbackResponse; } catch (fallbackError) { logger.error(`Fallback failed for ${method} ${endpoint}`, { originalError: error.message, fallbackError: fallbackError.message }); // Continue to throw the original error since fallback also failed } } handleError(error); } } } // Generic request methods export async function get(endpoint, params = {}, fallbackFn) { // Build the URL with query parameters according to Klaviyo API specs let url = endpoint; const queryParams = []; // Special handling for campaign endpoint - add required filter if missing if (endpoint === '/campaigns/' && !params.filter) { logger.debug('Adding default email filter for campaigns endpoint'); params.filter = "equals(messages.channel,'email')"; } // Handle filter parameter if provided if (params.filter) { queryParams.push(`filter=${encodeURIComponent(params.filter)}`); } // Handle include parameter if provided if (params.include) { queryParams.push(`include=${encodeURIComponent(params.include)}`); } // Handle page_size parameter if provided if (params.page_size) { queryParams.push(`page[size]=${params.page_size}`); } // Handle pagination cursor if provided if (params.page_cursor) { queryParams.push(`page[cursor]=${params.page_cursor}`); } // Add query parameters to URL if (queryParams.length > 0) { url = `${endpoint}?${queryParams.join('&')}`; } logger.debug(`Prepared GET request to: ${url}`); return executeWithRetry( () => client.get(url), 'GET', endpoint, params, fallbackFn ); } export async function post(endpoint, data, fallbackFn) { return executeWithRetry( () => client.post(endpoint, data), 'POST', endpoint, data, fallbackFn ); } export async function patch(endpoint, data, fallbackFn) { return executeWithRetry( () => client.patch(endpoint, data), 'PATCH', endpoint, data, fallbackFn ); } export async function del(endpoint, data, fallbackFn) { return executeWithRetry( () => { const config = data ? { data } : undefined; return client.delete(endpoint, config); }, 'DELETE', endpoint, data, fallbackFn ).then(response => { // For DELETE requests that return 204 No Content if (response === undefined) { return { success: true }; } return response; }); } function handleError(error) { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx const errorData = error.response.data; // Enhanced error handling for JSON:API format let errorMessage = 'Unknown API error'; if (errorData.errors && Array.isArray(errorData.errors)) { errorMessage = errorData.errors.map(e => { // Extract the most detailed error information available const detail = e.detail || e.title || ''; const source = e.source?.pointer ? ` (source: ${e.source.pointer})` : ''; const code = e.code ? ` [code: ${e.code}]` : ''; return `${detail}${source}${code}`; }).join(', '); } else if (errorData.message) { // Fallback for non-standard error formats errorMessage = errorData.message; } // Include endpoint and status in error message const statusText = error.response.statusText ? ` ${error.response.statusText}` : ''; throw new Error(`Klaviyo API Error (${error.response.status}${statusText}): ${errorMessage}`); } else if (error.request) { // The request was made but no response was received throw new Error('No response received from Klaviyo API. This could indicate network issues or an invalid endpoint.'); } else { // Something happened in setting up the request that triggered an Error throw new Error(`Error setting up request: ${error.message}`); } }

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/ivan-rivera-projects/Klaviyo-MCP-Server-Enhanced'

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