@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
JavaScript
"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