evolution-api-mcp
Version:
MCP Server for Evolution API v2 - Integrate WhatsApp functionality with Claude Desktop and other MCP clients
677 lines (676 loc) • 25 kB
JavaScript
"use strict";
/**
* Comprehensive error handling system for Evolution API MCP Server
*
* This module provides:
* - Error type definitions and classes
* - HTTP status code to user-friendly message mapping
* - Authentication failure handling
* - Timeout and network error handling with retry suggestions
* - Validation error messages with parameter correction hints
* - Error logging and debugging information
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorUtils = exports.globalErrorHandler = exports.ErrorHandler = exports.HTTP_STATUS_ERROR_MAP = exports.InstanceError = exports.RateLimitError = exports.TimeoutError = exports.NetworkError = exports.ValidationError = exports.AuthenticationError = exports.ConfigurationError = exports.McpError = exports.ErrorSeverity = exports.ErrorType = void 0;
/**
* Error types for different categories of errors
*/
var ErrorType;
(function (ErrorType) {
ErrorType["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
ErrorType["AUTHENTICATION_ERROR"] = "AUTHENTICATION_ERROR";
ErrorType["API_ERROR"] = "API_ERROR";
ErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
ErrorType["VALIDATION_ERROR"] = "VALIDATION_ERROR";
ErrorType["TIMEOUT_ERROR"] = "TIMEOUT_ERROR";
ErrorType["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
ErrorType["INSTANCE_ERROR"] = "INSTANCE_ERROR";
ErrorType["PERMISSION_ERROR"] = "PERMISSION_ERROR";
ErrorType["RESOURCE_NOT_FOUND"] = "RESOURCE_NOT_FOUND";
ErrorType["INTERNAL_ERROR"] = "INTERNAL_ERROR";
})(ErrorType || (exports.ErrorType = ErrorType = {}));
/**
* Error severity levels for logging and handling
*/
var ErrorSeverity;
(function (ErrorSeverity) {
ErrorSeverity["LOW"] = "LOW";
ErrorSeverity["MEDIUM"] = "MEDIUM";
ErrorSeverity["HIGH"] = "HIGH";
ErrorSeverity["CRITICAL"] = "CRITICAL";
})(ErrorSeverity || (exports.ErrorSeverity = ErrorSeverity = {}));
/**
* Base error class with comprehensive error information
*/
class McpError extends Error {
constructor(type, message, options = {}) {
super(message);
this.name = 'McpError';
this.type = type;
this.code = options.code;
this.statusCode = options.statusCode;
this.severity = options.severity || this.getDefaultSeverity(type);
this.timestamp = new Date();
this.details = options.details;
this.suggestions = options.suggestions || [];
this.retryable = options.retryable ?? this.getDefaultRetryable(type);
this.context = options.context;
if (options.cause) {
this.cause = options.cause;
}
// Capture stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, McpError);
}
}
getDefaultSeverity(type) {
switch (type) {
case ErrorType.CONFIGURATION_ERROR:
case ErrorType.AUTHENTICATION_ERROR:
return ErrorSeverity.HIGH;
case ErrorType.INTERNAL_ERROR:
return ErrorSeverity.CRITICAL;
case ErrorType.VALIDATION_ERROR:
case ErrorType.RESOURCE_NOT_FOUND:
return ErrorSeverity.MEDIUM;
default:
return ErrorSeverity.LOW;
}
}
getDefaultRetryable(type) {
switch (type) {
case ErrorType.NETWORK_ERROR:
case ErrorType.TIMEOUT_ERROR:
case ErrorType.RATE_LIMIT_ERROR:
return true;
case ErrorType.AUTHENTICATION_ERROR:
case ErrorType.VALIDATION_ERROR:
case ErrorType.CONFIGURATION_ERROR:
case ErrorType.PERMISSION_ERROR:
return false;
default:
return false;
}
}
/**
* Convert error to JSON for logging and API responses
*/
toJSON() {
return {
type: this.type,
message: this.message,
code: this.code,
statusCode: this.statusCode,
severity: this.severity,
timestamp: this.timestamp.toISOString(),
details: this.details,
suggestions: this.suggestions,
retryable: this.retryable,
context: this.context,
stack: this.stack
};
}
/**
* Get user-friendly error message for Claude Desktop
*/
getUserMessage() {
const baseMessage = this.message;
const suggestions = this.suggestions.length > 0
? `\n\nSuggestions:\n${this.suggestions.map(s => `• ${s}`).join('\n')}`
: '';
return `${baseMessage}${suggestions}`;
}
}
exports.McpError = McpError;
/**
* Specific error classes for different error types
*/
class ConfigurationError extends McpError {
constructor(message, options = {}) {
super(ErrorType.CONFIGURATION_ERROR, message, {
...options,
severity: ErrorSeverity.HIGH,
retryable: false
});
}
}
exports.ConfigurationError = ConfigurationError;
class AuthenticationError extends McpError {
constructor(message, options = {}) {
const suggestions = options.suggestions || [
'Verify your Evolution API key is correct',
'Check if the API key has the required permissions',
'Ensure the Evolution API server is accessible'
];
super(ErrorType.AUTHENTICATION_ERROR, message, {
...options,
severity: ErrorSeverity.HIGH,
retryable: false,
suggestions
});
}
}
exports.AuthenticationError = AuthenticationError;
class ValidationError extends McpError {
constructor(message, validationDetails = [], options = {}) {
const details = { validationDetails, ...options.details };
super(ErrorType.VALIDATION_ERROR, message, {
...options,
severity: ErrorSeverity.MEDIUM,
retryable: false,
details
});
this.validationDetails = validationDetails;
}
}
exports.ValidationError = ValidationError;
class NetworkError extends McpError {
constructor(message, networkDetails = {}, options = {}) {
const details = { networkDetails, ...options.details };
const suggestions = options.suggestions || [
'Check your internet connection',
'Verify the Evolution API server is running',
'Try again in a few moments'
];
super(ErrorType.NETWORK_ERROR, message, {
...options,
severity: ErrorSeverity.MEDIUM,
retryable: true,
details,
suggestions
});
this.networkDetails = networkDetails;
}
}
exports.NetworkError = NetworkError;
class TimeoutError extends McpError {
constructor(message, timeout, options = {}) {
const details = { timeout, ...options.details };
const suggestions = options.suggestions || [
`Request timed out after ${timeout}ms`,
'Try increasing the timeout value',
'Check if the Evolution API server is responding slowly'
];
super(ErrorType.TIMEOUT_ERROR, message, {
...options,
severity: ErrorSeverity.MEDIUM,
retryable: true,
details,
suggestions
});
}
}
exports.TimeoutError = TimeoutError;
class RateLimitError extends McpError {
constructor(message, retryAfter, options = {}) {
const defaultSuggestions = retryAfter
? [`Wait ${retryAfter} seconds before retrying`]
: ['Wait a moment before making more requests'];
const details = { retryAfter, ...options.details };
const suggestions = options.suggestions || defaultSuggestions;
super(ErrorType.RATE_LIMIT_ERROR, message, {
...options,
severity: ErrorSeverity.LOW,
retryable: true,
details,
suggestions
});
}
}
exports.RateLimitError = RateLimitError;
class InstanceError extends McpError {
constructor(message, instanceName, options = {}) {
const details = { instanceName, ...options.details };
const suggestions = options.suggestions || [
'Verify the instance name is correct',
'Check if the instance exists and is connected',
'Try creating the instance if it doesn\'t exist'
];
super(ErrorType.INSTANCE_ERROR, message, {
...options,
severity: ErrorSeverity.MEDIUM,
retryable: false,
details,
suggestions
});
}
}
exports.InstanceError = InstanceError;
/**
* HTTP status code to error type mapping
*/
exports.HTTP_STATUS_ERROR_MAP = {
400: {
type: ErrorType.VALIDATION_ERROR,
getMessage: (status, data) => data?.message || 'Bad request - invalid parameters provided',
getSuggestions: () => [
'Check the request parameters for correct format',
'Verify all required fields are provided',
'Review the API documentation for parameter requirements'
]
},
401: {
type: ErrorType.AUTHENTICATION_ERROR,
getMessage: () => 'Authentication failed - invalid or missing API key',
getSuggestions: () => [
'Verify your Evolution API key is correct',
'Check if the API key has expired',
'Ensure the API key has the required permissions'
]
},
403: {
type: ErrorType.PERMISSION_ERROR,
getMessage: () => 'Access forbidden - insufficient permissions',
getSuggestions: () => [
'Check if your API key has the required permissions',
'Verify you have access to the requested resource',
'Contact your administrator for permission updates'
]
},
404: {
type: ErrorType.RESOURCE_NOT_FOUND,
getMessage: (status, data) => data?.message || 'Resource not found',
getSuggestions: () => [
'Verify the resource identifier is correct',
'Check if the resource exists',
'Ensure you\'re using the correct endpoint'
]
},
409: {
type: ErrorType.API_ERROR,
getMessage: (status, data) => data?.message || 'Conflict - resource already exists or is in use',
getSuggestions: () => [
'Check if the resource already exists',
'Try using a different identifier',
'Verify the current state of the resource'
]
},
422: {
type: ErrorType.VALIDATION_ERROR,
getMessage: (status, data) => data?.message || 'Unprocessable entity - validation failed',
getSuggestions: () => [
'Review the validation errors in the response',
'Correct the invalid fields and try again',
'Check the API documentation for field requirements'
]
},
429: {
type: ErrorType.RATE_LIMIT_ERROR,
getMessage: () => 'Rate limit exceeded - too many requests',
getSuggestions: (status, data) => {
const retryAfter = data?.retryAfter || 60;
return [
`Wait ${retryAfter} seconds before making more requests`,
'Reduce the frequency of your requests',
'Consider implementing request queuing'
];
}
},
500: {
type: ErrorType.API_ERROR,
getMessage: () => 'Internal server error - something went wrong on the Evolution API server',
getSuggestions: () => [
'Try the request again in a few moments',
'Check the Evolution API server status',
'Contact support if the problem persists'
]
},
502: {
type: ErrorType.NETWORK_ERROR,
getMessage: () => 'Bad gateway - Evolution API server is unreachable',
getSuggestions: () => [
'Check if the Evolution API server is running',
'Verify the server URL is correct',
'Try again in a few moments'
]
},
503: {
type: ErrorType.API_ERROR,
getMessage: () => 'Service unavailable - Evolution API server is temporarily down',
getSuggestions: () => [
'Wait a few moments and try again',
'Check the Evolution API server status',
'Contact your administrator if the issue persists'
]
},
504: {
type: ErrorType.TIMEOUT_ERROR,
getMessage: () => 'Gateway timeout - Evolution API server took too long to respond',
getSuggestions: () => [
'Try the request again',
'Check if the Evolution API server is responding slowly',
'Consider increasing the timeout value'
]
}
};
/**
* Error handler class for comprehensive error processing
*/
class ErrorHandler {
constructor(options = {}) {
this.enableLogging = options.enableLogging ?? true;
this.logLevel = options.logLevel ?? 'error';
}
/**
* Handle HTTP errors from axios responses
*/
handleHttpError(error, context) {
// If it's already our error type, return it
if (error instanceof McpError) {
return error;
}
// Handle axios errors
if (error.isAxiosError) {
return this.handleAxiosError(error, context);
}
// Handle generic errors
return new McpError(ErrorType.INTERNAL_ERROR, error.message || 'Unknown error occurred', {
details: error,
context,
cause: error
});
}
/**
* Handle axios-specific errors
*/
handleAxiosError(axiosError, context) {
const { response, request, code, message } = axiosError;
// Network errors (no response received)
if (!response && request) {
if (code === 'ECONNABORTED' || message.includes('timeout')) {
return new TimeoutError('Request timeout - the Evolution API did not respond in time', axiosError.timeout || 30000, { context, cause: axiosError });
}
return new NetworkError(`Network error: ${message}`, { url: request.responseURL }, { context, cause: axiosError, code });
}
// HTTP response errors
if (response) {
const status = response.status;
const data = response.data;
const errorMapping = exports.HTTP_STATUS_ERROR_MAP[status];
if (errorMapping) {
const errorMessage = errorMapping.getMessage(status, data);
const suggestions = errorMapping.getSuggestions(status, data);
// Create specific error types based on status
switch (errorMapping.type) {
case ErrorType.AUTHENTICATION_ERROR:
return new AuthenticationError(errorMessage, {
statusCode: status,
details: data,
suggestions,
context
});
case ErrorType.VALIDATION_ERROR:
return new ValidationError(errorMessage, this.extractValidationDetails(data), {
statusCode: status,
details: data,
suggestions,
context
});
case ErrorType.RATE_LIMIT_ERROR:
return new RateLimitError(errorMessage, data?.retryAfter, {
statusCode: status,
details: data,
suggestions,
context
});
case ErrorType.NETWORK_ERROR:
return new NetworkError(errorMessage, { url: response.config?.url }, {
statusCode: status,
details: data,
suggestions,
context
});
case ErrorType.TIMEOUT_ERROR:
return new TimeoutError(errorMessage, response.config?.timeout || 30000, {
statusCode: status,
details: data,
suggestions,
context
});
default:
return new McpError(errorMapping.type, errorMessage, {
statusCode: status,
details: data,
suggestions,
context
});
}
}
// Fallback for unmapped status codes
return new McpError(ErrorType.API_ERROR, `HTTP ${status}: ${data?.message || message}`, {
statusCode: status,
details: data,
context
});
}
// Fallback for other axios errors
return new McpError(ErrorType.NETWORK_ERROR, message, {
details: axiosError,
context,
cause: axiosError
});
}
/**
* Extract validation details from error response
*/
extractValidationDetails(data) {
if (!data)
return [];
const details = [];
// Handle different validation error formats
if (data.errors && Array.isArray(data.errors)) {
data.errors.forEach((error) => {
details.push({
field: error.field || error.path || 'unknown',
value: error.value,
message: error.message || 'Validation failed',
code: error.code || 'VALIDATION_ERROR',
suggestion: this.getValidationSuggestion(error)
});
});
}
else if (data.message && typeof data.message === 'string') {
// Try to parse validation messages from string
const validationMatch = data.message.match(/(\w+):\s*(.+)/);
if (validationMatch) {
details.push({
field: validationMatch[1],
value: undefined,
message: validationMatch[2],
code: 'VALIDATION_ERROR',
suggestion: this.getValidationSuggestion({ field: validationMatch[1], message: validationMatch[2] })
});
}
}
return details;
}
/**
* Generate validation suggestions based on field and error
*/
getValidationSuggestion(error) {
const field = error.field || error.path || '';
const message = error.message || '';
// Common validation suggestions
if (field.includes('email')) {
return 'Provide a valid email address (e.g., user@example.com)';
}
if (field.includes('phone') || field.includes('number')) {
return 'Use the format: country code + number (e.g., 5511999999999)';
}
if (field.includes('url')) {
return 'Provide a valid URL starting with http:// or https://';
}
if (field.includes('instance')) {
return 'Use a valid instance name (alphanumeric characters and hyphens only)';
}
if (message.includes('required')) {
return `The field '${field}' is required and cannot be empty`;
}
if (message.includes('format')) {
return `Check the format of the '${field}' field`;
}
return `Please check the '${field}' field and try again`;
}
/**
* Handle validation errors from Zod schemas
*/
handleValidationError(zodError, context) {
const validationDetails = zodError.errors.map(error => ({
field: error.path.join('.'),
value: error.received || undefined,
message: error.message,
code: error.code,
suggestion: this.getZodValidationSuggestion(error)
}));
const message = `Validation failed: ${validationDetails.map(d => `${d.field} - ${d.message}`).join(', ')}`;
return new ValidationError(message, validationDetails, { context });
}
/**
* Generate suggestions for Zod validation errors
*/
getZodValidationSuggestion(error) {
switch (error.code) {
case 'invalid_type':
return `Expected ${error.expected}, but received ${error.received}`;
case 'too_small':
if (error.type === 'string') {
return `Must be at least ${error.minimum} characters long`;
}
return `Must be at least ${error.minimum}`;
case 'too_big':
if (error.type === 'string') {
return `Must be no more than ${error.maximum} characters long`;
}
return `Must be no more than ${error.maximum}`;
case 'invalid_string':
if (error.validation === 'email') {
return 'Must be a valid email address';
}
if (error.validation === 'url') {
return 'Must be a valid URL';
}
if (error.validation === 'regex') {
return 'Value contains invalid characters. Please check the format requirements.';
}
return 'Invalid string format';
case 'custom':
return error.message;
default:
return 'Please check the value and try again';
}
}
/**
* Log error with appropriate level
*/
logError(error) {
if (!this.enableLogging)
return;
const logData = {
timestamp: error.timestamp.toISOString(),
type: error.type,
message: error.message,
code: error.code,
statusCode: error.statusCode,
severity: error.severity,
context: error.context,
details: error.details
};
switch (error.severity) {
case ErrorSeverity.CRITICAL:
console.error('[CRITICAL ERROR]', logData);
break;
case ErrorSeverity.HIGH:
console.error('[HIGH ERROR]', logData);
break;
case ErrorSeverity.MEDIUM:
console.warn('[MEDIUM ERROR]', logData);
break;
case ErrorSeverity.LOW:
if (this.logLevel === 'info' || this.logLevel === 'debug') {
console.info('[LOW ERROR]', logData);
}
break;
}
// Log stack trace for debugging
if (this.logLevel === 'debug' && error.stack) {
console.debug('[ERROR STACK]', error.stack);
}
}
/**
* Create error response for MCP tools
*/
createToolErrorResponse(error) {
this.logError(error);
return {
success: false,
error: {
type: error.type,
message: error.getUserMessage(),
code: error.code,
suggestions: error.suggestions,
retryable: error.retryable
}
};
}
/**
* Create success response for MCP tools
*/
createToolSuccessResponse(data) {
return {
success: true,
data
};
}
}
exports.ErrorHandler = ErrorHandler;
/**
* Global error handler instance
*/
exports.globalErrorHandler = new ErrorHandler();
/**
* Utility functions for common error scenarios
*/
exports.ErrorUtils = {
/**
* Create configuration error
*/
configurationError(message, suggestions) {
return new ConfigurationError(message, { suggestions });
},
/**
* Create authentication error
*/
authenticationError(message) {
return new AuthenticationError(message || 'Authentication failed');
},
/**
* Create validation error from Zod error
*/
validationError(zodError, context) {
return exports.globalErrorHandler.handleValidationError(zodError, context);
},
/**
* Create instance error
*/
instanceError(instanceName, message) {
return new InstanceError(message || `Instance '${instanceName}' not found or not connected`, instanceName);
},
/**
* Create network error
*/
networkError(message, url) {
return new NetworkError(message, { url });
},
/**
* Create timeout error
*/
timeoutError(timeout) {
return new TimeoutError(`Request timed out after ${timeout}ms`, timeout);
},
/**
* Create rate limit error
*/
rateLimitError(retryAfter) {
return new RateLimitError('Rate limit exceeded', retryAfter);
}
};