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
JavaScript
;
/**
* 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