UNPKG

mcp-ynab

Version:

Model Context Protocol server for YNAB integration

199 lines (172 loc) 5.47 kB
import logger from './logger.js'; class ErrorHandler { constructor() { this.retryDelays = [1000, 2000, 4000, 8000]; // Exponential backoff in milliseconds } /** * Handle YNAB API errors with appropriate retries and user-friendly messages */ async handleApiError(error, operation = 'API call', attempt = 1, maxAttempts = 3) { // Parse YNAB-specific errors const ynabError = this.parseYnabError(error); // Determine if we should retry if (this.shouldRetry(ynabError, attempt, maxAttempts)) { const delay = this.retryDelays[Math.min(attempt - 1, this.retryDelays.length - 1)]; await logger.error(`${operation} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`, { operation, attempt, maxAttempts, delay, error: ynabError.message }); await this.delay(delay); throw new RetryableError(ynabError.message, attempt); } // No more retries, throw final error throw new YnabError(ynabError.message, ynabError.code, ynabError.type); } /** * Parse YNAB API error into structured format */ parseYnabError(error) { let message = 'Unknown error occurred'; let code = 'UNKNOWN_ERROR'; let type = 'general'; if (error.error) { // YNAB SDK error format const ynabError = error.error; if (ynabError.detail) { message = ynabError.detail; } else if (ynabError.name) { message = ynabError.name; } // Handle specific YNAB error types if (error.status === 401) { code = 'AUTHENTICATION_ERROR'; type = 'auth'; message = 'Invalid or expired YNAB API key. Please check your YNAB_API_KEY environment variable.'; } else if (error.status === 403) { code = 'AUTHORIZATION_ERROR'; type = 'auth'; message = 'Access denied. Your API key may not have permission to access this resource.'; } else if (error.status === 404) { code = 'NOT_FOUND'; type = 'resource'; message = 'The requested budget or resource was not found.'; } else if (error.status === 429) { code = 'RATE_LIMITED'; type = 'throttle'; message = 'YNAB API rate limit exceeded. Please wait before making more requests.'; } else if (error.status >= 500) { code = 'SERVER_ERROR'; type = 'server'; message = 'YNAB API server error. Please try again later.'; } } else if (error.message) { // Generic error message = error.message; // Try to categorize generic errors if (message.includes('YNAB_API_KEY')) { code = 'MISSING_API_KEY'; type = 'config'; } else if (message.includes('network') || message.includes('timeout')) { code = 'NETWORK_ERROR'; type = 'network'; } } return { message, code, type }; } /** * Determine if an error should be retried */ shouldRetry(errorInfo, attempt, maxAttempts) { if (attempt >= maxAttempts) { return false; } // Don't retry authentication or configuration errors if (errorInfo.type === 'auth' || errorInfo.type === 'config') { return false; } // Don't retry 404 errors if (errorInfo.code === 'NOT_FOUND') { return false; } // Retry network, server, and throttling errors return ['network', 'server', 'throttle'].includes(errorInfo.type); } /** * Create a delay (for retry logic) */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Wrap an async function with error handling and retry logic */ async withRetry(asyncFn, operation = 'operation', maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await asyncFn(); } catch (error) { if (error instanceof RetryableError) { // This error came from handleApiError, continue retrying continue; } // Handle the error and determine if we should retry await this.handleApiError(error, operation, attempt, maxAttempts); } } } /** * Format error for MCP response */ formatForMCP(error) { if (error instanceof YnabError) { return { error: 'tool_error', message: error.message, code: error.code, type: error.type }; } return { error: 'tool_error', message: error.message || 'An unexpected error occurred', code: 'UNKNOWN_ERROR', type: 'general' }; } /** * Validate required environment variables */ validateEnvironment() { const required = ['YNAB_API_KEY']; const missing = required.filter(key => !process.env[key]); if (missing.length > 0) { throw new YnabError( `Missing required environment variables: ${missing.join(', ')}. Please check your .env file.`, 'MISSING_ENV_VARS', 'config' ); } } } /** * Custom error classes */ class YnabError extends Error { constructor(message, code = 'UNKNOWN_ERROR', type = 'general') { super(message); this.name = 'YnabError'; this.code = code; this.type = type; } } class RetryableError extends Error { constructor(message, attempt) { super(message); this.name = 'RetryableError'; this.attempt = attempt; } } export { ErrorHandler, YnabError, RetryableError };