UNPKG

amazon-seller-mcp

Version:

Model Context Protocol (MCP) client for Amazon Selling Partner API

711 lines 24.3 kB
/** * Error handling utilities for Amazon Seller MCP Client * * This file contains error classes, error translation functions, and error recovery strategies * for handling errors from the Amazon Selling Partner API and translating them to MCP errors. */ import { ApiErrorType } from '../types/api.js'; import * as logger from './logger.js'; /** * Base error class for Amazon Seller MCP Client */ export class AmazonSellerMcpError extends Error { /** * Error code */ code; /** * Error details */ details; /** * Original error */ cause; /** * Create a new Amazon Seller MCP error * * @param message Error message * @param code Error code * @param details Error details * @param cause Original error */ constructor(message, code, details, cause) { super(message); this.name = 'AmazonSellerMcpError'; this.code = code; this.details = details; this.cause = cause; } } /** * Authentication error */ export class AuthenticationError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'AUTHENTICATION_ERROR', details, cause); this.name = 'AuthenticationError'; } } /** * Authorization error */ export class AuthorizationError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'AUTHORIZATION_ERROR', details, cause); this.name = 'AuthorizationError'; } } /** * Validation error */ export class ValidationError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'VALIDATION_ERROR', details, cause); this.name = 'ValidationError'; } } /** * Resource not found error */ export class ResourceNotFoundError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'RESOURCE_NOT_FOUND', details, cause); this.name = 'ResourceNotFoundError'; } } /** * Rate limit exceeded error */ export class RateLimitExceededError extends AmazonSellerMcpError { /** * Time to wait before retrying */ retryAfterMs; constructor(message, retryAfterMs, details, cause) { super(message, 'RATE_LIMIT_EXCEEDED', details, cause); this.name = 'RateLimitExceededError'; this.retryAfterMs = retryAfterMs; } } /** * Server error */ export class ServerError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'SERVER_ERROR', details, cause); this.name = 'ServerError'; } } /** * Network error */ export class NetworkError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'NETWORK_ERROR', details, cause); this.name = 'NetworkError'; } } /** * Throttling error */ export class ThrottlingError extends AmazonSellerMcpError { /** * Time to wait before retrying */ retryAfterMs; constructor(message, retryAfterMs, details, cause) { super(message, 'THROTTLING_ERROR', details, cause); this.name = 'ThrottlingError'; this.retryAfterMs = retryAfterMs; } } /** * Marketplace error */ export class MarketplaceError extends AmazonSellerMcpError { constructor(message, details, cause) { super(message, 'MARKETPLACE_ERROR', details, cause); this.name = 'MarketplaceError'; } } /** * Translate an API error to a specific Amazon Seller MCP error * * @param error API error * @returns Amazon Seller MCP error */ export function translateApiError(error) { // Extract error details from the API error const { message, type, statusCode, details, cause } = error; let translatedError; // Translate based on error type switch (type) { case ApiErrorType.AUTH_ERROR: // Check if it's an authentication or authorization error if (statusCode === 401) { translatedError = new AuthenticationError(`Authentication failed: ${message}`, details, cause); logger.error('Authentication error', { statusCode, errorType: type, errorCode: translatedError.code, errorDetails: details, }); } else { translatedError = new AuthorizationError(`Authorization failed: ${message}`, details, cause); logger.error('Authorization error', { statusCode, errorType: type, errorCode: translatedError.code, errorDetails: details, }); } break; case ApiErrorType.VALIDATION_ERROR: translatedError = new ValidationError(`Validation error: ${message}`, details, cause); logger.error('Validation error', { errorType: type, errorCode: translatedError.code, errorDetails: details, }); break; case ApiErrorType.RATE_LIMIT_EXCEEDED: { // Extract retry-after header if available let retryAfterMs = 1000; // Default to 1 second if (details?.headers?.['retry-after']) { const retryAfter = parseInt(details.headers['retry-after'], 10); if (!isNaN(retryAfter)) { retryAfterMs = retryAfter * 1000; // Convert to milliseconds } } translatedError = new RateLimitExceededError(`Rate limit exceeded: ${message}`, retryAfterMs, details, cause); logger.warn('Rate limit exceeded', { errorType: type, errorCode: translatedError.code, retryAfterMs, errorDetails: details, }); break; } case ApiErrorType.SERVER_ERROR: translatedError = new ServerError(`Server error: ${message}`, details, cause); logger.error('Server error', { statusCode, errorType: type, errorCode: translatedError.code, errorDetails: details, }); break; case ApiErrorType.NETWORK_ERROR: translatedError = new NetworkError(`Network error: ${message}`, details, cause); logger.error('Network error', { errorType: type, errorCode: translatedError.code, errorDetails: details, }); break; case ApiErrorType.CLIENT_ERROR: // Check if it's a resource not found error if (statusCode === 404) { translatedError = new ResourceNotFoundError(`Resource not found: ${message}`, details, cause); logger.warn('Resource not found', { statusCode, errorType: type, errorCode: translatedError.code, errorDetails: details, }); } // Check if it's a throttling error else if (statusCode === 429 || details?.code === 'QuotaExceeded') { // Extract retry-after header if available let retryAfterMs = 1000; // Default to 1 second if (details?.headers?.['retry-after']) { const retryAfter = parseInt(details.headers['retry-after'], 10); if (!isNaN(retryAfter)) { retryAfterMs = retryAfter * 1000; // Convert to milliseconds } } translatedError = new ThrottlingError(`Throttling error: ${message}`, retryAfterMs, details, cause); logger.warn('Throttling error', { statusCode, errorType: type, errorCode: translatedError.code, retryAfterMs, errorDetails: details, }); } // Default to a generic error else { translatedError = new AmazonSellerMcpError(`Client error: ${message}`, 'CLIENT_ERROR', details, cause); logger.error('Client error', { statusCode, errorType: type, errorCode: translatedError.code, errorDetails: details, }); } break; default: // Default to a generic error translatedError = new AmazonSellerMcpError(`Unknown error: ${message}`, 'UNKNOWN_ERROR', details, cause); logger.error('Unknown error', { errorType: type, errorCode: translatedError.code, errorDetails: details, }); break; } return translatedError; } /** * Translate an Amazon Seller MCP error to an MCP error response * * @param error Amazon Seller MCP error * @returns MCP error response */ export function translateToMcpErrorResponse(error) { // If it's an Amazon Seller MCP error, use its properties if (error instanceof AmazonSellerMcpError) { return { content: [ { type: 'text', text: error.message, }, ], isError: true, errorDetails: { code: error.code, message: error.message, details: error.details, }, }; } // For other errors, create a generic error response return { content: [ { type: 'text', text: error.message || 'An unknown error occurred', }, ], isError: true, errorDetails: { code: 'UNKNOWN_ERROR', message: error.message || 'An unknown error occurred', }, }; } /** * Retry recovery strategy */ export class RetryRecoveryStrategy { /** * Maximum number of retries */ maxRetries; /** * Base delay in milliseconds */ baseDelayMs; /** * Maximum delay in milliseconds */ maxDelayMs; /** * Create a new retry recovery strategy * * @param maxRetries Maximum number of retries * @param baseDelayMs Base delay in milliseconds * @param maxDelayMs Maximum delay in milliseconds */ constructor(maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 30000) { this.maxRetries = maxRetries; this.baseDelayMs = baseDelayMs; this.maxDelayMs = maxDelayMs; } /** * Whether the error can be recovered from * * @param error Error to check * @returns Whether the error can be recovered from */ canRecover(error) { // Can recover from network errors, server errors, rate limit errors, and throttling errors return (error instanceof NetworkError || error instanceof ServerError || error instanceof RateLimitExceededError || error instanceof ThrottlingError); } /** * Recover from the error * * @param error Error to recover from * @param context Recovery context * @returns Promise resolving to the recovery result */ async recover(error, context) { const retryCount = context.retryCount ?? 0; const maxRetries = context.maxRetries ?? this.maxRetries; const operation = context.operation; if (!operation) { throw new Error('Operation function is required for retry recovery'); } // Check if we've exceeded the maximum number of retries if (retryCount >= maxRetries) { logger.error(`Retry failed after ${retryCount} attempts`, { errorMessage: error.message, errorName: error.name, maxRetries, }); throw error; } // Calculate delay let delayMs; if (error instanceof RateLimitExceededError || error instanceof ThrottlingError) { // Use the retry-after value if available delayMs = error.retryAfterMs; } else { // Use exponential backoff with jitter const exponentialDelay = Math.min(this.maxDelayMs, this.baseDelayMs * Math.pow(2, retryCount)); // Add jitter (random delay between 0% and 25% of the exponential delay) const jitter = Math.random() * 0.25 * exponentialDelay; delayMs = exponentialDelay + jitter; } logger.info(`Retrying operation after error (attempt ${retryCount + 1}/${maxRetries})`, { errorMessage: error.message, errorName: error.name, delayMs, retryCount: retryCount + 1, maxRetries, }); // Wait before retrying await new Promise((resolve) => setTimeout(resolve, delayMs)); // Retry the operation try { return await operation(); } catch (retryError) { // If the retry fails, recursively call recover with incremented count return this.recover(retryError, { ...context, retryCount: retryCount + 1, }); } } } /** * Fallback recovery strategy */ export class FallbackRecoveryStrategy { /** * Fallback function */ fallbackFn; /** * Error types that can be recovered from */ recoverableErrors; /** * Create a new fallback recovery strategy * * @param fallbackFn Fallback function * @param recoverableErrors Error types that can be recovered from */ constructor(fallbackFn, recoverableErrors = []) { this.fallbackFn = fallbackFn; this.recoverableErrors = recoverableErrors; } /** * Whether the error can be recovered from * * @param error Error to check * @returns Whether the error can be recovered from */ canRecover(error) { // Check if the error is one of the recoverable error types return this.recoverableErrors.some((errorType) => error instanceof errorType); } /** * Recover from the error * * @param error Error to recover from * @param context Recovery context * @returns Promise resolving to the recovery result */ async recover(error, context) { return this.fallbackFn(error, context); } } /** * Circuit breaker state */ var CircuitBreakerState; (function (CircuitBreakerState) { /** * Circuit is closed (normal operation) */ CircuitBreakerState[CircuitBreakerState["CLOSED"] = 0] = "CLOSED"; /** * Circuit is open (failing fast) */ CircuitBreakerState[CircuitBreakerState["OPEN"] = 1] = "OPEN"; /** * Circuit is half-open (testing if it can be closed) */ CircuitBreakerState[CircuitBreakerState["HALF_OPEN"] = 2] = "HALF_OPEN"; })(CircuitBreakerState || (CircuitBreakerState = {})); /** * Circuit breaker recovery strategy */ export class CircuitBreakerRecoveryStrategy { /** * Circuit breaker state */ state = CircuitBreakerState.CLOSED; /** * Failure count */ failureCount = 0; /** * Last failure time */ lastFailureTime = 0; /** * Failure threshold */ failureThreshold; /** * Reset timeout in milliseconds */ resetTimeoutMs; /** * Error types that can trip the circuit breaker */ tripErrors; /** * Create a new circuit breaker recovery strategy * * @param failureThreshold Failure threshold * @param resetTimeoutMs Reset timeout in milliseconds * @param tripErrors Error types that can trip the circuit breaker */ constructor(failureThreshold = 5, resetTimeoutMs = 60000, tripErrors = [ServerError, NetworkError]) { this.failureThreshold = failureThreshold; this.resetTimeoutMs = resetTimeoutMs; this.tripErrors = tripErrors; } /** * Whether the error can be recovered from * * @param error Error to check * @returns Whether the error can be recovered from */ canRecover(error) { // Check if the error is one of the trip error types const isRecoverableError = this.tripErrors.some((errorType) => error instanceof errorType); // If the error is not recoverable, don't try to recover if (!isRecoverableError) { return false; } // Update circuit breaker state this.updateState(error); // Can recover if the circuit is closed or half-open return this.state !== CircuitBreakerState.OPEN; } /** * Recover from the error * * @param error Error to recover from * @param context Recovery context * @returns Promise resolving to the recovery result */ async recover(error, context) { const operation = context.operation; if (!operation) { throw new Error('Operation function is required for circuit breaker recovery'); } // If the circuit is open, fail fast if (this.state === CircuitBreakerState.OPEN) { const resetAfterMs = this.resetTimeoutMs - (Date.now() - this.lastFailureTime); logger.warn('Operation rejected by circuit breaker (circuit is open)', { circuitState: 'OPEN', resetAfterMs, errorMessage: error.message, errorName: error.name, }); throw new AmazonSellerMcpError('Circuit breaker is open', 'CIRCUIT_BREAKER_OPEN', { resetAfterMs, }, error); } try { // Try the operation const result = await operation(); // If successful and the circuit is half-open, close it if (this.state === CircuitBreakerState.HALF_OPEN) { this.state = CircuitBreakerState.CLOSED; this.failureCount = 0; logger.info('Circuit breaker closed after successful test request', { previousState: 'HALF_OPEN', newState: 'CLOSED', }); } return result; } catch (operationError) { // If the operation failed, update the circuit breaker state // Convert unknown error to Error or AmazonSellerMcpError const error = operationError instanceof Error ? operationError : new Error(String(operationError)); this.updateState(error); // Rethrow the error throw operationError; } } /** * Update the circuit breaker state * * @param error Error that occurred */ updateState(error) { // Check if the error is one of the trip error types const isRecoverableError = this.tripErrors.some((errorType) => error instanceof errorType); // If the error is not recoverable, don't update the state if (!isRecoverableError) { return; } // Update state based on current state switch (this.state) { case CircuitBreakerState.CLOSED: // Increment failure count this.failureCount++; this.lastFailureTime = Date.now(); logger.debug(`Circuit breaker failure count increased to ${this.failureCount}/${this.failureThreshold}`, { circuitState: 'CLOSED', failureCount: this.failureCount, failureThreshold: this.failureThreshold, errorMessage: error.message, errorName: error.name, }); // If failure count exceeds threshold, open the circuit if (this.failureCount >= this.failureThreshold) { this.state = CircuitBreakerState.OPEN; logger.warn(`Circuit breaker opened after ${this.failureCount} failures`, { previousState: 'CLOSED', newState: 'OPEN', failureCount: this.failureCount, failureThreshold: this.failureThreshold, resetTimeoutMs: this.resetTimeoutMs, errorMessage: error.message, errorName: error.name, }); // Schedule reset to half-open setTimeout(() => { this.state = CircuitBreakerState.HALF_OPEN; logger.info('Circuit breaker state changed to half-open', { previousState: 'OPEN', newState: 'HALF_OPEN', resetTimeoutMs: this.resetTimeoutMs, }); }, this.resetTimeoutMs); } break; case CircuitBreakerState.HALF_OPEN: // If a failure occurs in half-open state, open the circuit again this.state = CircuitBreakerState.OPEN; this.lastFailureTime = Date.now(); logger.warn('Circuit breaker reopened after test request failed', { previousState: 'HALF_OPEN', newState: 'OPEN', resetTimeoutMs: this.resetTimeoutMs, errorMessage: error.message, errorName: error.name, }); // Schedule reset to half-open setTimeout(() => { this.state = CircuitBreakerState.HALF_OPEN; logger.info('Circuit breaker state changed to half-open', { previousState: 'OPEN', newState: 'HALF_OPEN', resetTimeoutMs: this.resetTimeoutMs, }); }, this.resetTimeoutMs); break; case CircuitBreakerState.OPEN: // Update last failure time this.lastFailureTime = Date.now(); logger.debug('Circuit breaker received error while open', { circuitState: 'OPEN', errorMessage: error.message, errorName: error.name, }); break; } } } /** * Error recovery manager */ export class ErrorRecoveryManager { /** * Recovery strategies */ strategies = []; /** * Create a new error recovery manager * * @param strategies Recovery strategies */ constructor(strategies = []) { this.strategies = strategies; } /** * Add a recovery strategy * * @param strategy Recovery strategy */ addStrategy(strategy) { this.strategies.push(strategy); } /** * Execute an operation with error recovery * * @param operation Operation to execute * @param context Recovery context * @returns Promise resolving to the operation result */ async executeWithRecovery(operation, context = {}) { try { // Try the operation return await operation(); } catch (error) { // Try to recover from the error for (const strategy of this.strategies) { if (strategy.canRecover(error)) { return strategy.recover(error, { ...context, operation, }); } } // If no strategy can recover, rethrow the error throw error; } } } /** * Create a default error recovery manager * * @returns Default error recovery manager */ export function createDefaultErrorRecoveryManager() { const manager = new ErrorRecoveryManager(); // Add retry strategy manager.addStrategy(new RetryRecoveryStrategy()); // Add circuit breaker strategy manager.addStrategy(new CircuitBreakerRecoveryStrategy()); return manager; } //# sourceMappingURL=error-handler.js.map