@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
123 lines (107 loc) • 3.76 kB
text/typescript
/**
* Logger utility for the Debug MCP Server.
*/
import * as winston from 'winston';
import type { Logger as WinstonLoggerType } from 'winston';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
/**
* Logger configuration options.
*/
export interface LoggerOptions {
/** The log level to use (error, warn, info, debug) */
level?: string;
/** Optional file path to log to */
file?: string;
}
let defaultLogger: WinstonLoggerType | null = null;
/**
* Create a winston logger with the given namespace.
*
* @param namespace - The logger namespace
* @param options - Logger configuration options
* @returns A configured winston logger instance
*/
export function createLogger(namespace: string, options: LoggerOptions = {}): WinstonLoggerType {
const level = options.level || 'info';
const transports: winston.transport[] = [];
// In stdio mode, we MUST NOT write to stdout as it corrupts the MCP protocol
// Check if we're running in stdio mode by looking for specific command line args
const isStdioMode = process.argv.includes('stdio');
if (!isStdioMode) {
// Only add console transport when NOT in stdio mode
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message, ...rest }) => {
return `${timestamp} [${level}] [${namespace}]: ${message} ${
Object.keys(rest).length ? JSON.stringify(rest, null, 2) : ''
}`;
})
)
})
);
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRootDefaultLogPath = path.resolve(__dirname, '../../logs/debug-mcp-server.log');
const logFilePath = options.file || projectRootDefaultLogPath;
try {
const logDir = path.dirname(logFilePath);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
} catch (e) {
// In stdio mode, we must not write to console as it corrupts MCP protocol
if (!isStdioMode) {
console.error(`[Logger Init Error] Failed to ensure log directory for ${logFilePath}:`, e);
}
}
try {
transports.push(
new winston.transports.File({
filename: logFilePath,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
)
})
);
} catch (fileTransportError) {
// In stdio mode, we must not write to console as it corrupts MCP protocol
if (!isStdioMode) {
console.error(`[Logger Init Error] Failed to create file transport for ${logFilePath}:`, fileTransportError);
}
}
const logger = winston.createLogger({
level,
transports,
defaultMeta: { namespace },
exitOnError: false
});
logger.on('error', (error: Error) => {
// In stdio mode, we must not write to console as it corrupts MCP protocol
if (!isStdioMode) {
console.error('[Winston Logger Internal Error] Failed to write to a transport:', error);
}
});
// If this is the root logger, set it as the default
if (namespace === 'debug-mcp') {
defaultLogger = logger;
}
return logger;
}
/**
* Get the default logger instance. If no root logger has been created, a fallback logger is created.
* @returns The default logger instance.
*/
export function getLogger(): WinstonLoggerType {
if (!defaultLogger) {
defaultLogger = createLogger('debug-mcp:default-fallback', { level: 'info' });
defaultLogger.warn('[Logger] getLogger() called before root logger was initialized. Using fallback logger.');
}
return defaultLogger;
}