UNPKG

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

386 lines 13 kB
"use strict"; /** * Logging utilities for MCP Maple * Provides structured logging for API operations and errors */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.requestTracker = exports.performanceMonitor = exports.defaultLogger = exports.RequestTracker = exports.PerformanceMonitor = exports.McpLogger = void 0; const winston_1 = __importDefault(require("winston")); class McpLogger { logger; constructor(component = 'mcp-maple') { // In MCP mode (no port specified), disable console logging to avoid JSON pollution const isMcpMode = !process.env.MCP_PORT && !process.argv.includes('--port'); this.logger = winston_1.default.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json(), winston_1.default.format.printf(({ timestamp, level, message, component: comp, ...meta }) => { return JSON.stringify({ timestamp, level, component: comp || component, message, ...meta, }); })), defaultMeta: { component }, silent: isMcpMode, transports: isMcpMode ? [] : [ new winston_1.default.transports.Console({ stderrLevels: ['error', 'warn', 'info', 'debug'], consoleWarnLevels: [], format: winston_1.default.format.combine(winston_1.default.format.colorize(), winston_1.default.format.simple()), }), ], }); // Add file transport in production if (process.env.NODE_ENV === 'production') { this.logger.add(new winston_1.default.transports.File({ filename: 'logs/error.log', level: 'error', })); this.logger.add(new winston_1.default.transports.File({ filename: 'logs/combined.log', })); } } info(message, context) { this.logger.info(message, context); } warn(message, context) { this.logger.warn(message, context); } error(message, context) { this.logger.error(message, context); } debug(message, context) { this.logger.debug(message, context); } // Specialized logging methods logApiRequest(endpoint, params) { this.info('API Request Started', { operation: 'api_request', endpoint, ...(params && { params }), }); } logApiResponse(endpoint, duration, success) { if (success) { this.info('API Request Completed', { operation: 'api_response', endpoint, duration, success, }); } else { this.warn('API Request Failed', { operation: 'api_response', endpoint, duration, success, }); } } logApiError(endpoint, error, duration) { this.error('API Request Error', { operation: 'api_error', endpoint, error: error.message || error, ...(duration !== undefined && { duration }), }); } logCharacterOperation(operation, characterName, context) { this.info(`Character ${operation}`, { operation: `character_${operation}`, characterName, ...context, }); } logGuildOperation(operation, guildName, worldName, context) { this.info(`Guild ${operation}`, { operation: `guild_${operation}`, guildName, worldName, ...context, }); } logMcpOperation(operation, toolName, context) { this.info(`MCP ${operation}`, { operation: `mcp_${operation}`, toolName, ...context, }); } // Security and audit logging logSecurityEvent(event, context) { this.warn(`Security Event: ${event}`, { operation: 'security_event', event, timestamp: new Date().toISOString(), ...context, }); } logAuditEvent(action, resource, user, context) { this.info(`Audit: ${action} on ${resource}`, { operation: 'audit_event', action, resource, user: user || 'anonymous', timestamp: new Date().toISOString(), ...context, }); } // Performance logging logPerformanceMetric(metric, value, unit = 'ms', context) { this.info(`Performance Metric: ${metric}`, { operation: 'performance_metric', metric, value, unit, timestamp: new Date().toISOString(), ...context, }); } // Cache operations logCacheOperation(operation, key, context) { this.debug(`Cache ${operation.toUpperCase()}: ${key}`, { operation: 'cache_operation', cacheOperation: operation, cacheKey: key, ...context, }); } // Error recovery logging logRecoveryAttempt(strategy, error, attempt, success, context) { const level = success ? 'info' : 'warn'; const message = `Recovery ${success ? 'succeeded' : 'failed'}: ${strategy} (attempt ${attempt})`; this.logger.log(level, message, { operation: 'error_recovery', strategy, attempt, success, error: error.message, ...context, }); } // Health check logging logHealthCheck(component, status, details) { const level = status === 'healthy' ? 'info' : 'warn'; this.logger.log(level, `Health Check: ${component} is ${status}`, { operation: 'health_check', component, status, timestamp: new Date().toISOString(), ...details, }); } // Rate limiting logging logRateLimit(action, context) { const level = action === 'exceeded' ? 'warn' : 'debug'; this.logger.log(level, `Rate Limit ${action}`, { operation: 'rate_limit', action, timestamp: new Date().toISOString(), ...context, }); } // Structured error logging with sanitization logError(error, context) { const sanitizedError = this.sanitizeLogData({ error }); this.error(`Error: ${error.message}`, { operation: 'error', errorType: error.constructor.name, errorMessage: error.message, stack: error.stack, ...sanitizedError, ...context, }); } // Data sanitization for logging sanitizeLogData(data) { if (!data || typeof data !== 'object') { return data; } const sanitized = Array.isArray(data) ? [] : {}; const sensitiveKeys = [ 'password', 'token', 'secret', 'key', 'auth', 'authorization', 'api_key', 'apiKey', 'accessToken', 'refreshToken', 'sessionId', ]; for (const [key, value] of Object.entries(data)) { const lowerKey = key.toLowerCase(); if (sensitiveKeys.some((sensitiveKey) => lowerKey.includes(sensitiveKey))) { sanitized[key] = '[REDACTED]'; } else if (typeof value === 'object' && value !== null) { sanitized[key] = this.sanitizeLogData(value); } else { sanitized[key] = value; } } return sanitized; } // Batch logging for performance async flushLogs() { return new Promise((resolve) => { if (this.logger.transports.length === 0) { resolve(); return; } let pending = this.logger.transports.length; this.logger.transports.forEach((transport) => { if (typeof transport.flush === 'function') { transport.flush(() => { pending--; if (pending === 0) resolve(); }); } else { pending--; if (pending === 0) resolve(); } }); }); } // Create child logger with additional context createChild(context) { const childLogger = new McpLogger(); childLogger.logger = this.logger.child(context); return childLogger; } } exports.McpLogger = McpLogger; // Performance monitoring utilities class PerformanceMonitor { metrics = new Map(); logger; constructor(logger) { this.logger = logger; } startTimer(operation) { const startTime = Date.now(); return () => { const duration = Date.now() - startTime; this.recordMetric(operation, duration); return duration; }; } recordMetric(operation, duration) { const existing = this.metrics.get(operation); if (existing) { existing.count++; existing.totalTime += duration; existing.minTime = Math.min(existing.minTime, duration); existing.maxTime = Math.max(existing.maxTime, duration); } else { this.metrics.set(operation, { count: 1, totalTime: duration, minTime: duration, maxTime: duration, }); } this.logger.logPerformanceMetric(operation, duration, 'ms', { operation: operation, }); } getMetrics() { const result = {}; for (const [operation, metric] of this.metrics.entries()) { result[operation] = { count: metric.count, avgTime: Math.round(metric.totalTime / metric.count), minTime: metric.minTime, maxTime: metric.maxTime, totalTime: metric.totalTime, }; } return result; } clearMetrics() { this.metrics.clear(); } logSummary() { const metrics = this.getMetrics(); this.logger.info('Performance Summary', { operation: 'performance_summary', metrics, timestamp: new Date().toISOString(), }); } } exports.PerformanceMonitor = PerformanceMonitor; // Request tracking for audit purposes class RequestTracker { requests = new Map(); logger; constructor(logger) { this.logger = logger; } startRequest(requestId, context) { this.requests.set(requestId, { startTime: Date.now(), context: { ...context }, }); this.logger.logAuditEvent('request_started', context.operation || 'unknown', context.user, { requestId, ...context, }); } endRequest(requestId, success, additionalContext) { const request = this.requests.get(requestId); if (!request) { this.logger.warn('Request tracking error: Unknown request ID', { requestId }); return; } const duration = Date.now() - request.startTime; this.logger.logAuditEvent(success ? 'request_completed' : 'request_failed', request.context.operation || 'unknown', request.context.user, { requestId, duration, success, ...request.context, ...additionalContext, }); this.requests.delete(requestId); } getActiveRequests() { return Array.from(this.requests.keys()); } cleanupStaleRequests(maxAge = 300000) { // 5 minutes default const now = Date.now(); for (const [requestId, request] of this.requests.entries()) { if (now - request.startTime > maxAge) { this.logger.warn('Cleaning up stale request', { requestId, age: now - request.startTime, context: request.context, }); this.requests.delete(requestId); } } } } exports.RequestTracker = RequestTracker; // Default logger instance exports.defaultLogger = new McpLogger('mcp-maple'); exports.performanceMonitor = new PerformanceMonitor(exports.defaultLogger); exports.requestTracker = new RequestTracker(exports.defaultLogger); //# sourceMappingURL=logger.js.map