maplestorysea-mcp-server
Version:
NEXON MapleStory SEA API MCP Server for Claude Desktop - Complete character info, union details, guild data, rankings optimized for SEA servers
929 lines • 40.8 kB
JavaScript
"use strict";
/**
* Custom error classes for MCP Maple
* Provides structured error handling for different failure scenarios
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorAggregator = exports.defaultErrorRecovery = exports.seaWorldValidationStrategy = exports.seaApiUnsupportedFeatureStrategy = exports.cacheBypassStrategy = exports.fallbackStrategy = exports.retryStrategy = exports.ErrorRecoveryManager = exports.ToolExecutionError = exports.AuthorizationError = exports.AuthenticationError = exports.DatabaseError = exports.SeaConnectionError = exports.SeaValidationError = exports.SeaQuotaExceededError = exports.SeaDataUnavailableError = exports.SeaMaintenanceError = exports.SeaGuildNameError = exports.SeaCharacterNameError = exports.SeaWorldNotFoundError = exports.SeaApiUnsupportedFeatureError = exports.ServiceUnavailableError = exports.ConfigurationError = exports.CacheError = exports.RankingTimeoutError = exports.TimeoutError = exports.NetworkError = exports.ValidationError = exports.RateLimitError = exports.InvalidApiKeyError = exports.GuildNotFoundError = exports.CharacterNotFoundError = exports.NexonApiError = exports.McpMapleError = void 0;
exports.createNexonApiError = createNexonApiError;
exports.createSeaApiError = createSeaApiError;
exports.isRetryableError = isRetryableError;
exports.getRetryDelay = getRetryDelay;
exports.shouldRetryError = shouldRetryError;
exports.getRetryDelayForError = getRetryDelayForError;
exports.sanitizeErrorForLogging = sanitizeErrorForLogging;
exports.createDetailedErrorLog = createDetailedErrorLog;
exports.formatErrorForUser = formatErrorForUser;
class McpMapleError extends Error {
code;
statusCode;
context;
constructor(message, code, statusCode, context) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.context = context;
this.name = 'McpMapleError';
Object.setPrototypeOf(this, McpMapleError.prototype);
}
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
statusCode: this.statusCode,
context: this.context,
stack: this.stack,
};
}
}
exports.McpMapleError = McpMapleError;
class NexonApiError extends McpMapleError {
constructor(message, statusCode, endpoint, params) {
super(message, 'NEXON_API_ERROR', statusCode, { endpoint, params });
this.name = 'NexonApiError';
Object.setPrototypeOf(this, NexonApiError.prototype);
}
}
exports.NexonApiError = NexonApiError;
class CharacterNotFoundError extends McpMapleError {
constructor(characterName) {
super(`Character '${characterName}' not found`, 'CHARACTER_NOT_FOUND', 404, { characterName });
this.name = 'CharacterNotFoundError';
Object.setPrototypeOf(this, CharacterNotFoundError.prototype);
}
}
exports.CharacterNotFoundError = CharacterNotFoundError;
class GuildNotFoundError extends McpMapleError {
constructor(guildName, worldName) {
super(`Guild '${guildName}' not found${worldName ? ` in world '${worldName}'` : ''}`, 'GUILD_NOT_FOUND', 404, { guildName, worldName });
this.name = 'GuildNotFoundError';
Object.setPrototypeOf(this, GuildNotFoundError.prototype);
}
}
exports.GuildNotFoundError = GuildNotFoundError;
class InvalidApiKeyError extends McpMapleError {
constructor() {
super('Invalid NEXON API key provided for MapleStory SEA', 'INVALID_API_KEY', 401);
this.name = 'InvalidApiKeyError';
Object.setPrototypeOf(this, InvalidApiKeyError.prototype);
}
}
exports.InvalidApiKeyError = InvalidApiKeyError;
class RateLimitError extends McpMapleError {
constructor(retryAfter) {
super('API rate limit exceeded', 'RATE_LIMIT_EXCEEDED', 429, { retryAfter });
this.name = 'RateLimitError';
Object.setPrototypeOf(this, RateLimitError.prototype);
}
}
exports.RateLimitError = RateLimitError;
class ValidationError extends McpMapleError {
constructor(field, value, expectedType) {
super(`Validation failed for field '${field}': ${value}${expectedType ? ` (expected ${expectedType})` : ''}`, 'VALIDATION_ERROR', 400, { field, value, expectedType });
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
exports.ValidationError = ValidationError;
class NetworkError extends McpMapleError {
constructor(originalError) {
super('Network request failed', 'NETWORK_ERROR', undefined, {
originalError: originalError.message,
});
this.name = 'NetworkError';
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
exports.NetworkError = NetworkError;
class TimeoutError extends McpMapleError {
constructor(timeout, endpoint) {
const endpointInfo = endpoint ? ` for endpoint ${endpoint}` : '';
super(`Request timed out after ${timeout}ms${endpointInfo}`, 'TIMEOUT_ERROR', 408, {
timeout,
endpoint,
});
this.name = 'TimeoutError';
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
exports.TimeoutError = TimeoutError;
class RankingTimeoutError extends McpMapleError {
constructor(timeout, endpoint, queryParams) {
const message = `Ranking request timed out after ${timeout}ms. Ranking queries may take longer than other API calls.`;
super(message, 'RANKING_TIMEOUT_ERROR', 408, {
timeout,
endpoint,
queryParams,
isRankingEndpoint: true,
});
this.name = 'RankingTimeoutError';
Object.setPrototypeOf(this, RankingTimeoutError.prototype);
}
}
exports.RankingTimeoutError = RankingTimeoutError;
class CacheError extends McpMapleError {
constructor(operation, originalError) {
super(`Cache operation failed: ${operation}`, 'CACHE_ERROR', undefined, {
operation,
originalError: originalError?.message,
});
this.name = 'CacheError';
Object.setPrototypeOf(this, CacheError.prototype);
}
}
exports.CacheError = CacheError;
class ConfigurationError extends McpMapleError {
constructor(setting, value) {
super(`Invalid configuration for '${setting}'${value ? `: ${value}` : ''}`, 'CONFIGURATION_ERROR', 500, {
setting,
value,
});
this.name = 'ConfigurationError';
Object.setPrototypeOf(this, ConfigurationError.prototype);
}
}
exports.ConfigurationError = ConfigurationError;
class ServiceUnavailableError extends McpMapleError {
constructor(service, reason) {
super(`Service '${service}' is unavailable${reason ? `: ${reason}` : ''}`, 'SERVICE_UNAVAILABLE', 503, {
service,
reason,
});
this.name = 'ServiceUnavailableError';
Object.setPrototypeOf(this, ServiceUnavailableError.prototype);
}
}
exports.ServiceUnavailableError = ServiceUnavailableError;
/**
* SEA API specific errors
*/
class SeaApiUnsupportedFeatureError extends McpMapleError {
constructor(feature) {
super(`Feature '${feature}' is not supported by MapleStory SEA API`, 'SEA_API_UNSUPPORTED_FEATURE', 501, { feature });
this.name = 'SeaApiUnsupportedFeatureError';
Object.setPrototypeOf(this, SeaApiUnsupportedFeatureError.prototype);
}
}
exports.SeaApiUnsupportedFeatureError = SeaApiUnsupportedFeatureError;
class SeaWorldNotFoundError extends McpMapleError {
constructor(worldName) {
super(`World '${worldName}' is not available in MapleStory SEA. Available worlds: Aquila, Bootes, Cassiopeia, Draco`, 'SEA_WORLD_NOT_FOUND', 400, { worldName, availableWorlds: ['Aquila', 'Bootes', 'Cassiopeia', 'Draco'] });
this.name = 'SeaWorldNotFoundError';
Object.setPrototypeOf(this, SeaWorldNotFoundError.prototype);
}
}
exports.SeaWorldNotFoundError = SeaWorldNotFoundError;
class SeaCharacterNameError extends McpMapleError {
constructor(characterName, reason = 'Invalid character name format') {
super(`${reason}. Character names in SEA must contain only English letters and numbers: '${characterName}'`, 'SEA_CHARACTER_NAME_ERROR', 400, { characterName, reason });
this.name = 'SeaCharacterNameError';
Object.setPrototypeOf(this, SeaCharacterNameError.prototype);
}
}
exports.SeaCharacterNameError = SeaCharacterNameError;
class SeaGuildNameError extends McpMapleError {
constructor(guildName, reason = 'Invalid guild name format') {
super(`${reason}. Guild names in SEA must contain only English letters, numbers, and spaces: '${guildName}'`, 'SEA_GUILD_NAME_ERROR', 400, { guildName, reason });
this.name = 'SeaGuildNameError';
Object.setPrototypeOf(this, SeaGuildNameError.prototype);
}
}
exports.SeaGuildNameError = SeaGuildNameError;
class SeaMaintenanceError extends McpMapleError {
constructor(estimatedEndTime) {
const message = estimatedEndTime
? `MapleStory SEA servers are currently under maintenance. Estimated completion: ${estimatedEndTime}`
: 'MapleStory SEA servers are currently under maintenance. Please try again later';
super(message, 'SEA_MAINTENANCE_ERROR', 503, { estimatedEndTime });
this.name = 'SeaMaintenanceError';
Object.setPrototypeOf(this, SeaMaintenanceError.prototype);
}
}
exports.SeaMaintenanceError = SeaMaintenanceError;
class SeaDataUnavailableError extends McpMapleError {
constructor(dataType, characterName, additionalInfo) {
const characterInfo = characterName ? ` for character '${characterName}'` : '';
const extraInfo = additionalInfo ? ` ${additionalInfo}` : '';
const message = `${dataType} data is temporarily unavailable${characterInfo}.${extraInfo} Please try again later.`;
super(message, 'SEA_DATA_UNAVAILABLE', 503, { dataType, characterName, additionalInfo });
this.name = 'SeaDataUnavailableError';
Object.setPrototypeOf(this, SeaDataUnavailableError.prototype);
}
}
exports.SeaDataUnavailableError = SeaDataUnavailableError;
class SeaQuotaExceededError extends McpMapleError {
constructor(quotaType, resetTime) {
const resetInfo = resetTime ? ` Quota resets at ${resetTime}.` : ' Please try again later.';
const message = `Your ${quotaType} API quota has been exceeded.${resetInfo}`;
super(message, 'SEA_QUOTA_EXCEEDED', 429, { quotaType, resetTime });
this.name = 'SeaQuotaExceededError';
Object.setPrototypeOf(this, SeaQuotaExceededError.prototype);
}
}
exports.SeaQuotaExceededError = SeaQuotaExceededError;
class SeaValidationError extends McpMapleError {
constructor(field, value, requirement, suggestion) {
const suggestionText = suggestion ? ` Suggestion: ${suggestion}` : '';
const message = `Invalid ${field}: '${value}'. ${requirement}.${suggestionText}`;
super(message, 'SEA_VALIDATION_ERROR', 400, { field, value, requirement, suggestion });
this.name = 'SeaValidationError';
Object.setPrototypeOf(this, SeaValidationError.prototype);
}
}
exports.SeaValidationError = SeaValidationError;
class SeaConnectionError extends McpMapleError {
constructor(type, duration, endpoint) {
let message = '';
switch (type) {
case 'timeout':
message = duration
? `Connection timed out after ${duration}ms while accessing MapleStory SEA API`
: 'Connection timeout while accessing MapleStory SEA API';
break;
case 'network':
message = 'Network connection failed while accessing MapleStory SEA API';
break;
case 'gateway':
message = 'Gateway error from MapleStory SEA API servers';
break;
}
if (endpoint) {
message += `. Endpoint: ${endpoint}`;
}
super(message, 'SEA_CONNECTION_ERROR', type === 'timeout' ? 408 : 502, {
type,
duration,
endpoint,
});
this.name = 'SeaConnectionError';
Object.setPrototypeOf(this, SeaConnectionError.prototype);
}
}
exports.SeaConnectionError = SeaConnectionError;
class DatabaseError extends McpMapleError {
constructor(operation, originalError) {
super(`Database operation failed: ${operation}`, 'DATABASE_ERROR', 500, {
operation,
originalError: originalError?.message,
});
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
exports.DatabaseError = DatabaseError;
class AuthenticationError extends McpMapleError {
constructor(reason) {
super(`Authentication failed${reason ? `: ${reason}` : ''}`, 'AUTHENTICATION_ERROR', 401, {
reason,
});
this.name = 'AuthenticationError';
Object.setPrototypeOf(this, AuthenticationError.prototype);
}
}
exports.AuthenticationError = AuthenticationError;
class AuthorizationError extends McpMapleError {
constructor(resource, action) {
super(`Access denied: insufficient permissions for '${action}' on '${resource}'`, 'AUTHORIZATION_ERROR', 403, {
resource,
action,
});
this.name = 'AuthorizationError';
Object.setPrototypeOf(this, AuthorizationError.prototype);
}
}
exports.AuthorizationError = AuthorizationError;
class ToolExecutionError extends McpMapleError {
constructor(toolName, originalError) {
super(`Tool execution failed: ${toolName}`, 'TOOL_EXECUTION_ERROR', 500, {
toolName,
originalError: originalError.message,
originalStack: originalError.stack,
});
this.name = 'ToolExecutionError';
Object.setPrototypeOf(this, ToolExecutionError.prototype);
}
}
exports.ToolExecutionError = ToolExecutionError;
// Error factory functions
function createNexonApiError(statusCode, message, endpoint, params) {
switch (statusCode) {
case 401:
if (message.toLowerCase().includes('expired') || message.toLowerCase().includes('expire')) {
return new McpMapleError('Your NEXON API key has expired. Please renew your API key', 'API_KEY_EXPIRED', 401);
}
if (message.toLowerCase().includes('missing') || message.toLowerCase().includes('required')) {
return new McpMapleError('NEXON API key is required to access MapleStory SEA data', 'API_KEY_MISSING', 401);
}
return new InvalidApiKeyError();
case 403:
if (message.toLowerCase().includes('insufficient') ||
message.toLowerCase().includes('permission')) {
return new McpMapleError('Your API key does not have sufficient permissions for this operation', 'INSUFFICIENT_PERMISSIONS', 403);
}
if (message.toLowerCase().includes('access denied')) {
return new AuthorizationError('NEXON API', 'access data with provided API key');
}
return new AuthorizationError('API endpoint', 'access');
case 404:
if (endpoint?.includes('character')) {
const characterName = params?.character_name || 'unknown';
// Check if it's actually a data unavailable issue vs character not found
if (message.toLowerCase().includes('unavailable') ||
message.toLowerCase().includes('temporary')) {
return new SeaDataUnavailableError('Character', characterName);
}
return new CharacterNotFoundError(characterName);
}
if (endpoint?.includes('guild')) {
const guildName = params?.guild_name || 'unknown';
const worldName = params?.world_name;
if (message.toLowerCase().includes('unavailable') ||
message.toLowerCase().includes('temporary')) {
return new SeaDataUnavailableError('Guild', guildName);
}
return new GuildNotFoundError(guildName, worldName);
}
if (endpoint?.includes('ranking')) {
return new McpMapleError('No ranking data available for the specified criteria', 'RANKING_DATA_NOT_FOUND', 404, { endpoint, params });
}
return new McpMapleError(message, 'NOT_FOUND', statusCode, { endpoint, params });
case 408:
const timeout = params?.timeout || 'unknown';
return new SeaConnectionError('timeout', typeof timeout === 'number' ? timeout : undefined, endpoint);
case 429:
// Check for different types of rate limiting
if (message.toLowerCase().includes('daily') || message.toLowerCase().includes('quota')) {
return new SeaQuotaExceededError('daily');
}
if (message.toLowerCase().includes('concurrent')) {
return new SeaQuotaExceededError('concurrent');
}
return new RateLimitError();
case 500:
return new McpMapleError('Internal server error occurred. Please try again later', 'INTERNAL_SERVER_ERROR', 500, { endpoint, params });
case 501:
// Handle unsupported features for SEA API
if (message.toLowerCase().includes('notice') ||
message.toLowerCase().includes('probability') ||
message.toLowerCase().includes('server status') ||
endpoint?.includes('/notice/') ||
endpoint?.includes('/probability/') ||
endpoint?.includes('/server/')) {
const feature = endpoint?.split('/').pop() || 'unknown';
return new SeaApiUnsupportedFeatureError(feature);
}
return new NexonApiError(message, statusCode, endpoint, params);
case 502:
return new SeaConnectionError('gateway', undefined, endpoint);
case 503:
if (message.toLowerCase().includes('maintenance')) {
return new SeaMaintenanceError();
}
return new ServiceUnavailableError('MapleStory SEA API', message);
case 504:
return new McpMapleError('Gateway timeout while processing your request', 'GATEWAY_TIMEOUT', 504, { endpoint, params });
case 400:
// Handle SEA-specific validation errors with detailed feedback
if (endpoint?.includes('/maplestorysea/') && params) {
// Check for invalid world names
if (params.world_name &&
!['Aquila', 'Bootes', 'Cassiopeia', 'Draco'].includes(params.world_name)) {
return new SeaWorldNotFoundError(params.world_name);
}
// Check for invalid character names
if (params.character_name) {
const characterName = params.character_name;
if (/[가-힣]/.test(characterName)) {
return new SeaCharacterNameError(characterName, 'Korean characters not supported in SEA API');
}
if (characterName.length < 2) {
return new SeaValidationError('character name', characterName, 'Must be at least 2 characters long');
}
if (characterName.length > 13) {
return new SeaValidationError('character name', characterName, 'Must be 13 characters or less');
}
if (!/^[a-zA-Z0-9]+$/.test(characterName)) {
return new SeaCharacterNameError(characterName, 'Contains invalid characters');
}
}
// Check for invalid guild names
if (params.guild_name) {
const guildName = params.guild_name;
if (/[가-힣]/.test(guildName)) {
return new SeaGuildNameError(guildName, 'Korean characters not supported in SEA API');
}
if (guildName.length < 2) {
return new SeaValidationError('guild name', guildName, 'Must be at least 2 characters long');
}
if (guildName.length > 12) {
return new SeaValidationError('guild name', guildName, 'Must be 12 characters or less');
}
}
// Check for invalid dates
if (params.date) {
const date = params.date;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return new SeaValidationError('date', date, 'Must be in YYYY-MM-DD format', 'Use format like 2024-01-15');
}
const parsedDate = new Date(date);
const today = new Date();
if (parsedDate > today) {
return new SeaValidationError('date', date, 'Cannot be in the future', "Use today's date or earlier");
}
const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
if (parsedDate < thirtyDaysAgo) {
return new SeaValidationError('date', date, 'Date is too old', 'Data is only available from the last 30 days');
}
}
// Check for invalid page numbers
if (params.page) {
const page = parseInt(params.page);
if (isNaN(page) || page < 1 || page > 200) {
return new SeaValidationError('page number', params.page, 'Must be between 1 and 200');
}
}
}
return new NexonApiError(message, statusCode, endpoint, params);
default:
return new NexonApiError(message, statusCode, endpoint, params);
}
}
/**
* SEA API specific error factory
*/
function createSeaApiError(type, details) {
switch (type) {
case 'unsupported_feature':
return new SeaApiUnsupportedFeatureError(details.feature || 'unknown');
case 'invalid_world':
return new SeaWorldNotFoundError(details.worldName || 'unknown');
case 'invalid_character_name':
return new SeaCharacterNameError(details.characterName || 'unknown', details.reason);
default:
return new McpMapleError('Unknown SEA API error', 'SEA_API_ERROR');
}
}
// Error utilities
function isRetryableError(error) {
if (error instanceof NexonApiError) {
// Retry on server errors and rate limits (with backoff)
return error.statusCode ? error.statusCode >= 500 || error.statusCode === 429 : false;
}
if (error instanceof RateLimitError) {
return true;
}
if (error instanceof NetworkError || error instanceof TimeoutError) {
return true;
}
return false;
}
function getRetryDelay(attemptNumber, baseDelay = 1000, errorType) {
// SEA API optimized retry delays based on error type
let adjustedBaseDelay = baseDelay;
switch (errorType) {
case 'rate_limit':
adjustedBaseDelay = 5000; // 5 seconds for rate limit errors
break;
case 'server_error':
adjustedBaseDelay = 2000; // 2 seconds for server errors
break;
case 'network_error':
adjustedBaseDelay = 1500; // 1.5 seconds for network errors
break;
case 'timeout':
adjustedBaseDelay = 3000; // 3 seconds for timeout errors
break;
default:
adjustedBaseDelay = baseDelay;
}
// Exponential backoff with jitter for SEA API stability
const backoffFactor = 2;
const delay = adjustedBaseDelay * Math.pow(backoffFactor, attemptNumber - 1);
// Add jitter to prevent thundering herd effect
const jitterFactor = 0.1;
const jitter = Math.random() * jitterFactor * delay;
// Cap at maximum delay for stability
return Math.min(delay + jitter, 30000);
}
function shouldRetryError(error, attemptNumber) {
// Don't retry if we've exceeded max attempts
if (attemptNumber >= 4) {
// Max 3 retries (attempt 4 is final)
return false;
}
// Always retry for retryable errors
if (isRetryableError(error)) {
return true;
}
// Special retry logic for SEA API specific errors
if (error instanceof SeaConnectionError) {
return error.context?.type !== 'gateway'; // Don't retry gateway errors
}
if (error instanceof SeaDataUnavailableError) {
return attemptNumber <= 2; // Only retry twice for data availability
}
if (error instanceof SeaMaintenanceError) {
return false; // Don't retry during maintenance
}
if (error instanceof SeaQuotaExceededError) {
return error.context?.quotaType === 'concurrent'; // Only retry concurrent limits
}
return false;
}
function getRetryDelayForError(error, attemptNumber) {
if (error instanceof RateLimitError || error instanceof SeaQuotaExceededError) {
return getRetryDelay(attemptNumber, 1000, 'rate_limit');
}
if (error instanceof TimeoutError || error instanceof SeaConnectionError) {
const errorType = error instanceof SeaConnectionError
? error.context?.type === 'timeout'
? 'timeout'
: 'network_error'
: 'timeout';
return getRetryDelay(attemptNumber, 1000, errorType);
}
if (error instanceof NexonApiError && error.statusCode && error.statusCode >= 500) {
return getRetryDelay(attemptNumber, 1000, 'server_error');
}
if (error instanceof NetworkError) {
return getRetryDelay(attemptNumber, 1000, 'network_error');
}
return getRetryDelay(attemptNumber);
}
function sanitizeErrorForLogging(error) {
if (error instanceof McpMapleError) {
const errorData = error.toJSON();
// Remove sensitive information from context
if (errorData.context) {
const sanitizedContext = { ...errorData.context };
// Remove API keys, passwords, tokens
const sensitiveKeys = ['api_key', 'apiKey', 'password', 'token', 'secret', 'authorization'];
sensitiveKeys.forEach((key) => {
if (sanitizedContext[key]) {
sanitizedContext[key] = '[REDACTED]';
}
});
// Sanitize URLs to remove query parameters that might contain sensitive data
if (sanitizedContext.endpoint && typeof sanitizedContext.endpoint === 'string') {
try {
const url = new URL(sanitizedContext.endpoint);
url.search = ''; // Remove query parameters
sanitizedContext.endpoint = url.toString();
}
catch {
// If URL parsing fails, leave as is
}
}
errorData.context = sanitizedContext;
}
return errorData;
}
// Handle plain objects with context property
const sanitizedError = {
name: error.name || 'UnknownError',
message: error.message || 'Unknown error occurred',
stack: error.stack,
};
// Also sanitize context for plain objects
if (error.context && typeof error.context === 'object') {
const sanitizedContext = { ...error.context };
// Remove API keys, passwords, tokens
const sensitiveKeys = ['api_key', 'apiKey', 'password', 'token', 'secret', 'authorization'];
sensitiveKeys.forEach((key) => {
if (sanitizedContext[key]) {
sanitizedContext[key] = '[REDACTED]';
}
});
sanitizedError.context = sanitizedContext;
}
return sanitizedError;
}
/**
* Enhanced error logging for SEA API debugging
*/
function createDetailedErrorLog(error, context = {}) {
const timestamp = context.timestamp || new Date();
const sanitizedError = sanitizeErrorForLogging(error);
const logEntry = {
timestamp: timestamp.toISOString(),
level: 'error',
operation: context.operation || 'unknown',
// Error information
error: {
type: error.constructor.name,
code: error instanceof McpMapleError ? error.code : 'UNKNOWN',
message: error.message,
statusCode: error instanceof McpMapleError ? error.statusCode : undefined,
},
// Request context
request: {
endpoint: context.endpoint,
params: context.params ? sanitizeParams(context.params) : undefined,
userAgent: context.userAgent,
duration: context.duration,
},
// Retry information
retry: {
attemptNumber: context.attemptNumber || 1,
isRetryable: isRetryableError(error),
nextRetryDelay: context.attemptNumber
? getRetryDelayForError(error, context.attemptNumber)
: undefined,
},
// SEA API specific context
seaApiContext: getSeaApiContextForError(error),
// Debugging information
debug: {
stack: error.stack,
fullError: sanitizedError,
},
};
return logEntry;
}
/**
* Sanitize request parameters for logging
*/
function sanitizeParams(params) {
const sanitized = { ...params };
// Remove sensitive parameter values
const sensitiveKeys = ['api_key', 'apiKey', 'key', 'password', 'token', 'secret'];
sensitiveKeys.forEach((key) => {
if (sanitized[key]) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
/**
* Get SEA API specific context for error logging
*/
function getSeaApiContextForError(error) {
const context = {
apiRegion: 'SEA',
supportedWorlds: ['Aquila', 'Bootes', 'Cassiopeia', 'Draco'],
};
if (error instanceof SeaWorldNotFoundError) {
context.errorType = 'invalid_world';
context.providedWorld = error.context?.worldName;
context.suggestion = 'Use one of the supported SEA worlds';
}
if (error instanceof SeaCharacterNameError) {
context.errorType = 'invalid_character_name';
context.providedName = error.context?.characterName;
context.requirement = 'English letters and numbers only';
}
if (error instanceof SeaGuildNameError) {
context.errorType = 'invalid_guild_name';
context.providedName = error.context?.guildName;
context.requirement = 'English letters, numbers, and spaces only';
}
if (error instanceof SeaApiUnsupportedFeatureError) {
context.errorType = 'unsupported_feature';
context.feature = error.context?.feature;
context.reason = 'Feature not available in SEA API';
}
if (error instanceof SeaConnectionError) {
context.errorType = 'connection_issue';
context.connectionType = error.context?.type;
context.suggestion = 'Check network connectivity and SEA API status';
}
if (error instanceof SeaMaintenanceError) {
context.errorType = 'maintenance';
context.estimatedEndTime = error.context?.estimatedEndTime;
context.suggestion = 'Wait for maintenance to complete';
}
if (error instanceof SeaDataUnavailableError) {
context.errorType = 'data_unavailable';
context.dataType = error.context?.dataType;
context.characterName = error.context?.characterName;
context.suggestion = 'Try again later or check if character/data exists';
}
if (error instanceof SeaQuotaExceededError) {
context.errorType = 'quota_exceeded';
context.quotaType = error.context?.quotaType;
context.resetTime = error.context?.resetTime;
context.suggestion = 'Reduce request frequency or wait for quota reset';
}
return context;
}
/**
* Format error for user display (customer-friendly message)
*/
function formatErrorForUser(error) {
if (error instanceof SeaWorldNotFoundError) {
return `Invalid world name. Please use one of these SEA worlds: Aquila, Bootes, Cassiopeia, or Draco.`;
}
if (error instanceof SeaCharacterNameError) {
return `Invalid character name format. Character names in MapleStory SEA must contain only English letters and numbers.`;
}
if (error instanceof SeaGuildNameError) {
return `Invalid guild name format. Guild names in MapleStory SEA must contain only English letters, numbers, and spaces.`;
}
if (error instanceof SeaMaintenanceError) {
return `MapleStory SEA servers are currently under maintenance. Please try again later.`;
}
if (error instanceof SeaDataUnavailableError) {
return `The requested data is temporarily unavailable. Please try again in a few moments.`;
}
if (error instanceof SeaQuotaExceededError) {
const quotaType = error.context?.quotaType || 'API';
return `Your ${quotaType} request limit has been reached. Please wait before making more requests.`;
}
if (error instanceof SeaConnectionError) {
return `Connection issue with MapleStory SEA servers. Please check your internet connection and try again.`;
}
if (error instanceof CharacterNotFoundError) {
return `Character not found in MapleStory SEA servers. Please check the character name and world.`;
}
if (error instanceof GuildNotFoundError) {
return `Guild not found in MapleStory SEA servers. Please check the guild name and world.`;
}
if (error instanceof RateLimitError || error instanceof SeaQuotaExceededError) {
return `Too many requests. Please wait a moment before trying again.`;
}
if (error instanceof InvalidApiKeyError) {
return `Invalid API key. Please check your NEXON API key configuration.`;
}
if (error instanceof SeaApiUnsupportedFeatureError) {
return `This feature is not available in MapleStory SEA. Please use only SEA-supported features.`;
}
// Generic fallback
return `An error occurred while accessing MapleStory SEA data. Please try again later.`;
}
class ErrorRecoveryManager {
strategies = [];
registerStrategy(strategy) {
this.strategies.push(strategy);
}
async attemptRecovery(error, context) {
for (const strategy of this.strategies) {
if (strategy.canHandle(error)) {
try {
return await strategy.recover(error, context);
}
catch (recoveryError) {
// Recovery failed, try next strategy or throw original error
continue;
}
}
}
// No strategy could handle the error
throw error;
}
}
exports.ErrorRecoveryManager = ErrorRecoveryManager;
// Built-in recovery strategies
exports.retryStrategy = {
name: 'retry',
canHandle: (error) => isRetryableError(error),
recover: async (error, context) => {
if (!context?.operation) {
throw error;
}
const maxAttempts = context.maxAttempts || 3;
let lastError = error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
if (attempt > 1) {
// Wait before retry
const delay = getRetryDelay(attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
return await context.operation();
}
catch (retryError) {
lastError = retryError;
if (!isRetryableError(lastError) || attempt === maxAttempts) {
break;
}
}
}
throw lastError;
},
};
exports.fallbackStrategy = {
name: 'fallback',
canHandle: (error) => true, // Can handle any error
recover: async (error, context) => {
if (context?.fallbackOperation) {
return await context.fallbackOperation();
}
if (context?.fallbackValue !== undefined) {
return context.fallbackValue;
}
// Default fallback behavior
if (error instanceof CharacterNotFoundError || error instanceof GuildNotFoundError) {
return null;
}
throw error;
},
};
exports.cacheBypassStrategy = {
name: 'cache-bypass',
canHandle: (error) => error instanceof CacheError,
recover: async (error, context) => {
if (!context?.operation) {
throw error;
}
// Bypass cache and execute operation directly
return await context.operation();
},
};
exports.seaApiUnsupportedFeatureStrategy = {
name: 'sea-api-unsupported-feature',
canHandle: (error) => error instanceof SeaApiUnsupportedFeatureError,
recover: async (error, context) => {
// Log warning through proper logger if available
if (context?.logger) {
context.logger.warn('SEA API unsupported feature', {
feature: error instanceof SeaApiUnsupportedFeatureError ? error.context?.feature : 'unknown',
message: error.message,
operation: 'error_recovery',
});
}
// Return fallback value or empty result
return context?.fallbackValue || null;
},
};
exports.seaWorldValidationStrategy = {
name: 'sea-world-validation',
canHandle: (error) => error instanceof SeaWorldNotFoundError,
recover: async (error, context) => {
if (context?.suggestClosest && error instanceof SeaWorldNotFoundError && context?.logger) {
// Log suggestion through proper logger
const availableWorlds = ['Aquila', 'Bootes', 'Cassiopeia', 'Draco'];
context.logger.warn('Invalid world name for SEA API', {
invalidWorld: error.context?.worldName,
availableWorlds,
operation: 'validation_error_recovery',
});
}
throw error; // Don't recover from validation errors, let user fix input
},
};
// Default error recovery manager with built-in strategies
exports.defaultErrorRecovery = new ErrorRecoveryManager();
exports.defaultErrorRecovery.registerStrategy(exports.retryStrategy);
exports.defaultErrorRecovery.registerStrategy(exports.cacheBypassStrategy);
exports.defaultErrorRecovery.registerStrategy(exports.seaApiUnsupportedFeatureStrategy);
exports.defaultErrorRecovery.registerStrategy(exports.seaWorldValidationStrategy);
exports.defaultErrorRecovery.registerStrategy(exports.fallbackStrategy);
// Error aggregation for batch operations
class ErrorAggregator {
errors = [];
addError(operation, error, context) {
this.errors.push({ operation, error, context });
}
hasErrors() {
return this.errors.length > 0;
}
getErrors() {
return [...this.errors];
}
getErrorCount() {
return this.errors.length;
}
getSummary() {
const byType = {};
const byCode = {};
this.errors.forEach(({ error }) => {
const type = error.constructor.name;
const code = error instanceof McpMapleError ? error.code : 'UNKNOWN';
byType[type] = (byType[type] || 0) + 1;
byCode[code] = (byCode[code] || 0) + 1;
});
return {
total: this.errors.length,
byType,
byCode,
};
}
clear() {
this.errors = [];
}
createAggregateError() {
if (this.errors.length === 0) {
throw new Error('No errors to aggregate');
}
if (this.errors.length === 1) {
const errorEntry = this.errors[0];
if (errorEntry) {
const { error } = errorEntry;
return error instanceof McpMapleError
? error
: new McpMapleError(error.message, 'UNKNOWN_ERROR');
}
}
const summary = this.getSummary();
return new McpMapleError(`Multiple operations failed: ${summary.total} errors`, 'AGGREGATE_ERROR', 500, {
summary,
errors: this.errors.map(({ operation, error }) => ({
operation,
error: sanitizeErrorForLogging(error),
})),
});
}
}
exports.ErrorAggregator = ErrorAggregator;
//# sourceMappingURL=errors.js.map