UNPKG

@alcyone-labs/simple-mcp-logger

Version:

Logging solution for MCP servers. Prevents console output from corrupting MCP protocol communication. Drop-in replacement for console, Winston, and Pino with automatic STDOUT suppression in MCP mode.

427 lines (426 loc) 11.5 kB
"use strict"; const fs = require("node:fs"); const path = require("node:path"); function _interopNamespaceDefault(e) { const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } }); if (e) { for (const k in e) { if (k !== "default") { const d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: () => e[k] }); } } } n.default = e; return Object.freeze(n); } const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs); const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path); class Logger { constructor(config = {}) { this.config = { level: "info", mcpMode: false, prefix: "", ...config }; if (this.config.logToFile) { this.initFileLogging(this.config.logToFile); } } /** * Initialize file logging */ initFileLogging(filePath) { try { const dir = path__namespace.dirname(filePath); fs__namespace.mkdirSync(dir, { recursive: true }); this.fileStream = fs__namespace.createWriteStream(filePath, { flags: "a" }); this.fileStream.on("error", (error) => { console.error( `SimpleMcpLogger: File stream error for '${filePath}': ${error.message}` ); console.error( `SimpleMcpLogger: File logging will be disabled. Check file permissions and disk space.` ); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error( `SimpleMcpLogger: Failed to initialize file logging for '${filePath}': ${errorMessage}` ); console.error( `SimpleMcpLogger: Possible causes: invalid path, insufficient permissions, or disk full.` ); this.fileStream = void 0; } } /** * Write to file if file logging is enabled */ writeToFile(level, message, ...args) { if (this.fileStream && !this.fileStream.destroyed) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const formattedArgs = args.length > 0 ? " " + args.map( (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg) ).join(" ") : ""; const logLine = `[${timestamp}] ${level.toUpperCase()}: ${message}${formattedArgs} `; this.fileStream.write(logLine); } } /** * Set MCP mode - when true, all console output is suppressed */ setMcpMode(enabled) { this.config.mcpMode = enabled; } /** * Set log level */ setLevel(level) { this.config.level = level; } /** * Set prefix for all log messages */ setPrefix(prefix) { this.config.prefix = prefix; } /** * Set or change the log file path * * @param filePath Path to the log file. Directory will be created if it doesn't exist. * * @example * ```typescript * await logger.setLogFile('./logs/app.log'); * logger.info('This goes to the new file'); * ``` */ async setLogFile(filePath) { await this.close(); this.config.logToFile = filePath; this.initFileLogging(filePath); } /** * Close file stream */ close() { return new Promise((resolve) => { if (this.fileStream && !this.fileStream.destroyed) { this.fileStream.end(() => { this.fileStream = void 0; resolve(); }); } else { resolve(); } }); } /** * Check if logging is enabled for a given level */ shouldLog(level) { if (this.config.mcpMode && !this.config.logToFile) { return false; } if (this.config.level === "silent") { return false; } const levels = ["debug", "info", "warn", "error"]; const currentLevelIndex = levels.indexOf(this.config.level); const messageLevelIndex = levels.indexOf(level); return messageLevelIndex >= currentLevelIndex; } /** * Check if console logging should be used (not in MCP mode or file logging disabled) */ shouldLogToConsole(level) { return this.shouldLog(level) && (!this.config.mcpMode || !this.config.logToFile); } /** * Format message with prefix */ formatMessage(message) { return this.config.prefix ? `${this.config.prefix} ${message}` : message; } /** * Debug logging */ debug(message, ...args) { if (this.shouldLog("debug")) { if (this.config.logToFile) { this.writeToFile("debug", this.formatMessage(message), ...args); } if (this.shouldLogToConsole("debug")) { console.debug(this.formatMessage(message), ...args); } } } /** * Environment-aware debug logging - only outputs if DEBUG environment variable is truthy * This method respects the DEBUG environment variable and will only log when the DEBUG env var is truthy. * It works with all configured transports (console, file, etc.) and respects MCP mode settings. * Treats "false", "0", and empty string as falsy values. */ envDebug(message, ...args) { const debugEnv = process.env.DEBUG; if (!debugEnv || debugEnv === "false" || debugEnv === "0") { return; } if (this.shouldLog("debug")) { if (this.config.logToFile) { this.writeToFile( "debug", this.formatMessage(`[ENV-DEBUG] ${message}`), ...args ); } if (this.shouldLogToConsole("debug")) { console.debug(this.formatMessage(`[ENV-DEBUG] ${message}`), ...args); } } } /** * Info logging - uses stderr for MCP compliance */ info(message, ...args) { if (this.shouldLog("info")) { if (this.config.logToFile) { this.writeToFile("info", this.formatMessage(message), ...args); } if (this.shouldLogToConsole("info")) { console.error(this.formatMessage(message), ...args); } } } /** * Warning logging */ warn(message, ...args) { if (this.shouldLog("warn")) { if (this.config.logToFile) { this.writeToFile("warn", this.formatMessage(message), ...args); } if (this.shouldLogToConsole("warn")) { console.warn(this.formatMessage(message), ...args); } } } /** * Error logging - uses stderr which is allowed in MCP mode for debugging */ error(message, ...args) { if (this.shouldLog("error")) { if (this.config.logToFile) { this.writeToFile("error", this.formatMessage(message), ...args); } if (this.shouldLogToConsole("error")) { console.error(this.formatMessage(message), ...args); } } } /** * Log method - alias for info to match console.log behavior */ log(message, ...args) { this.info(message, ...args); } /** * Trace logging - uses console.trace for stack traces */ trace(message, ...args) { if (this.shouldLog("debug")) { if (message) { console.trace(this.formatMessage(message), ...args); } else { console.trace(); } } } /** * Table logging - uses console.table for structured data */ table(data, columns) { if (this.shouldLog("info")) { console.table(data, columns); } } /** * Group logging - creates a collapsible group */ group(label) { if (this.shouldLog("info")) { if (label) { console.group(this.formatMessage(label)); } else { console.group(); } } } /** * Collapsed group logging */ groupCollapsed(label) { if (this.shouldLog("info")) { if (label) { console.groupCollapsed(this.formatMessage(label)); } else { console.groupCollapsed(); } } } /** * End group logging */ groupEnd() { if (this.shouldLog("info")) { console.groupEnd(); } } /** * Time logging - starts a timer */ time(label) { if (this.shouldLog("debug")) { const timerLabel = label ? this.formatMessage(label) : void 0; console.time(timerLabel); } } /** * Time end logging - ends a timer and logs the duration */ timeEnd(label) { if (this.shouldLog("debug")) { const timerLabel = label ? this.formatMessage(label) : void 0; console.timeEnd(timerLabel); } } /** * Time log - logs current timer value without ending it */ timeLog(label, ...args) { if (this.shouldLog("debug")) { const timerLabel = label ? this.formatMessage(label) : void 0; console.timeLog(timerLabel, ...args); } } /** * Count logging - maintains a counter for the label */ count(label) { if (this.shouldLog("debug")) { const countLabel = label ? this.formatMessage(label) : void 0; console.count(countLabel); } } /** * Count reset - resets the counter for the label */ countReset(label) { if (this.shouldLog("debug")) { const countLabel = label ? this.formatMessage(label) : void 0; console.countReset(countLabel); } } /** * Assert logging - logs an error if assertion fails */ assert(condition, message, ...args) { if (this.shouldLog("error")) { if (message) { console.assert(condition, this.formatMessage(message), ...args); } else { console.assert(condition, ...args); } } } /** * Clear console - clears the console if supported */ clear() { if (this.shouldLog("debug") && console.clear) { console.clear(); } } /** * Dir logging - displays an interactive list of object properties */ dir(obj, options) { if (this.shouldLog("info")) { console.dir(obj, options); } } /** * DirXML logging - displays XML/HTML element representation */ dirxml(obj) { if (this.shouldLog("info")) { console.dirxml(obj); } } /** * MCP-safe error logging - always uses STDERR even in MCP mode * * STDERR is safe for MCP servers because the MCP protocol only uses STDOUT * for JSON-RPC messages. STDERR output appears in client logs without * interfering with protocol communication. * * Use this for critical errors, debugging info, and monitoring data that * needs to be visible even when the logger is in MCP mode. */ mcpError(message, ...args) { console.error(this.formatMessage(message), ...args); } /** * Create a child logger with additional prefix */ child(prefix) { return new Logger({ ...this.config, prefix: this.config.prefix ? `${this.config.prefix}:${prefix}` : prefix }); } } const logger = new Logger(); function createMcpLogger(prefixOrOptions, logToFile, options) { if (typeof prefixOrOptions === "object" && prefixOrOptions !== null) { const opts = prefixOrOptions; return new Logger({ level: opts.level ?? "error", mcpMode: opts.mcpMode ?? true, prefix: opts.prefix, logToFile: opts.logToFile }); } const prefix = prefixOrOptions; const mergedOptions = { level: "error", mcpMode: true, prefix, logToFile, ...options }; return new Logger({ level: mergedOptions.level, mcpMode: mergedOptions.mcpMode, prefix: mergedOptions.prefix, logToFile: mergedOptions.logToFile }); } function createCliLogger(level = "info", prefix) { return new Logger({ level, mcpMode: false, prefix }); } exports.Logger = Logger; exports.createCliLogger = createCliLogger; exports.createMcpLogger = createMcpLogger; exports.logger = logger; //# sourceMappingURL=SimpleMcpLogger-D6nWOVH5.cjs.map