Skip to main content
Glama
Logger.ts6.25 kB
import { createWriteStream, WriteStream } from 'fs'; import { mkdir } from 'fs/promises'; import path from 'path'; /** * Configurable logging system for XcodeMCP * Supports log levels: DEBUG, INFO, WARN, ERROR, SILENT * Logs to stderr by default, with optional file logging * Environment variables: * - LOG_LEVEL: Sets the minimum log level (default: INFO) * - XCODEMCP_LOG_FILE: Optional file path for logging * - XCODEMCP_CONSOLE_LOGGING: Enable/disable console output (default: true) */ export class Logger { public static readonly LOG_LEVELS = { SILENT: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4 } as const; public static readonly LOG_LEVEL_NAMES: Record<number, string> = { 0: 'SILENT', 1: 'ERROR', 2: 'WARN', 3: 'INFO', 4: 'DEBUG' }; private static instance: Logger | null = null; private logLevel: number; private consoleLogging: boolean; private logFile: string | undefined; private fileStream: WriteStream | null = null; constructor() { this.logLevel = this.parseLogLevel(process.env.LOG_LEVEL || 'INFO'); this.consoleLogging = process.env.XCODEMCP_CONSOLE_LOGGING !== 'false'; this.logFile = process.env.XCODEMCP_LOG_FILE; this.setupFileLogging(); } /** * Get or create the singleton logger instance */ public static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } /** * Parse log level from string (case insensitive) */ private parseLogLevel(levelStr: string): number { const level = levelStr.toUpperCase(); const logLevelValue = Logger.LOG_LEVELS[level as keyof typeof Logger.LOG_LEVELS]; return logLevelValue !== undefined ? logLevelValue : Logger.LOG_LEVELS.INFO; } /** * Setup file logging if specified */ private async setupFileLogging(): Promise<void> { if (!this.logFile) { return; } try { // Create parent directories if they don't exist const dir = path.dirname(this.logFile); await mkdir(dir, { recursive: true }); // Create write stream this.fileStream = createWriteStream(this.logFile, { flags: 'a' }); this.fileStream.on('error', (error: Error) => { // Fallback to stderr if file logging fails if (this.consoleLogging) { process.stderr.write(`Logger: Failed to write to log file: ${error.message}\n`); } }); } catch (error) { // Fallback to stderr if file setup fails if (this.consoleLogging) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`Logger: Failed to setup log file: ${errorMessage}\n`); } } } /** * Format log message with timestamp and level */ private formatMessage(level: number, message: string, ...args: unknown[]): string { const timestamp = new Date().toISOString(); const levelName = Logger.LOG_LEVEL_NAMES[level]; const formattedArgs = args.length > 0 ? ' ' + args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg) ).join(' ') : ''; return `[${timestamp}] [${levelName}] XcodeMCP: ${message}${formattedArgs}`; } /** * Write log message to configured outputs */ private writeLog(level: number, message: string, ...args: unknown[]): void { if (level > this.logLevel) { return; // Skip if below configured log level } const formattedMessage = this.formatMessage(level, message, ...args); // Always write to stderr for MCP protocol compatibility (unless console logging disabled) if (this.consoleLogging) { process.stderr.write(formattedMessage + '\n'); } // Write to file if configured if (this.fileStream && this.fileStream.writable) { this.fileStream.write(formattedMessage + '\n'); } } /** * Log at DEBUG level */ public debug(message: string, ...args: unknown[]): void { this.writeLog(Logger.LOG_LEVELS.DEBUG, message, ...args); } /** * Log at INFO level */ public info(message: string, ...args: unknown[]): void { this.writeLog(Logger.LOG_LEVELS.INFO, message, ...args); } /** * Log at WARN level */ public warn(message: string, ...args: unknown[]): void { this.writeLog(Logger.LOG_LEVELS.WARN, message, ...args); } /** * Log at ERROR level */ public error(message: string, ...args: unknown[]): void { this.writeLog(Logger.LOG_LEVELS.ERROR, message, ...args); } /** * Flush any pending log writes (important for process exit) */ public async flush(): Promise<void> { return new Promise<void>((resolve) => { if (this.fileStream && this.fileStream.writable) { this.fileStream.end(resolve); } else { resolve(); } }); } /** * Get current log level as string */ public getLogLevel(): string { return Logger.LOG_LEVEL_NAMES[this.logLevel] || 'UNKNOWN'; } /** * Check if a log level is enabled */ public isLevelEnabled(level: number): boolean { return level <= this.logLevel; } // Static convenience methods public static debug(message: string, ...args: unknown[]): void { Logger.getInstance().debug(message, ...args); } public static info(message: string, ...args: unknown[]): void { Logger.getInstance().info(message, ...args); } public static warn(message: string, ...args: unknown[]): void { Logger.getInstance().warn(message, ...args); } public static error(message: string, ...args: unknown[]): void { Logger.getInstance().error(message, ...args); } public static async flush(): Promise<void> { if (Logger.instance) { await Logger.instance.flush(); } } public static getLogLevel(): string { return Logger.getInstance().getLogLevel(); } public static isLevelEnabled(level: number): boolean { return Logger.getInstance().isLevelEnabled(level); } } // Ensure proper cleanup on process exit process.on('exit', async () => { await Logger.flush(); }); process.on('SIGINT', async () => { await Logger.flush(); process.exit(0); }); process.on('SIGTERM', async () => { await Logger.flush(); process.exit(0); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lapfelix/XcodeMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server