/** * Centralized logging for MCP UI operations. * Provides structured, contextual logs with levels. */ export type LogLevel = "debug" | "info" | "warn" | "error"; export interface LogContext { server?: string; session?: string; tool?: string; uri?: string; [key: string]: unknown; } export interface LogEntry { level: LogLevel; message: string; context?: LogContext; error?: Error; timestamp: Date; } type LogHandler = (entry: LogEntry) => void; const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3, }; const LEVEL_PREFIX: Record = { debug: "[MCP-UI:DEBUG]", info: "[MCP-UI]", warn: "[MCP-UI:WARN]", error: "[MCP-UI:ERROR]", }; class Logger { private minLevel: LogLevel = "info"; private handlers: LogHandler[] = []; private defaultContext: LogContext = {}; setLevel(level: LogLevel): void { this.minLevel = level; } setDefaultContext(context: LogContext): void { this.defaultContext = context; } addHandler(handler: LogHandler): void { this.handlers.push(handler); } clearHandlers(): void { this.handlers = []; } private shouldLog(level: LogLevel): boolean { return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.minLevel]; } private emit(level: LogLevel, message: string, context?: LogContext, error?: Error): void { if (!this.shouldLog(level)) return; const entry: LogEntry = { level, message, context: { ...this.defaultContext, ...context }, error, timestamp: new Date(), }; // Default console output const prefix = LEVEL_PREFIX[level]; const contextStr = formatContext(entry.context); const fullMessage = contextStr ? `${prefix} ${message} ${contextStr}` : `${prefix} ${message}`; if (level === "error") { console.error(fullMessage, error ?? ""); } else if (level === "warn") { console.warn(fullMessage); } else if (level === "debug") { console.debug(fullMessage); } else { console.log(fullMessage); } // Custom handlers for (const handler of this.handlers) { try { handler(entry); } catch { // Ignore handler errors } } } debug(message: string, context?: LogContext): void { this.emit("debug", message, context); } info(message: string, context?: LogContext): void { this.emit("info", message, context); } warn(message: string, context?: LogContext): void { this.emit("warn", message, context); } error(message: string, error?: Error, context?: LogContext): void { this.emit("error", message, context, error); } /** * Create a child logger with additional default context. */ child(context: LogContext): ChildLogger { return new ChildLogger(this, context); } } class ChildLogger { constructor( private parent: Logger, private context: LogContext ) {} debug(message: string, context?: LogContext): void { this.parent.debug(message, { ...this.context, ...context }); } info(message: string, context?: LogContext): void { this.parent.info(message, { ...this.context, ...context }); } warn(message: string, context?: LogContext): void { this.parent.warn(message, { ...this.context, ...context }); } error(message: string, error?: Error, context?: LogContext): void { this.parent.error(message, error, { ...this.context, ...context }); } child(context: LogContext): ChildLogger { return new ChildLogger(this.parent, { ...this.context, ...context }); } } function formatContext(context?: LogContext): string { if (!context || Object.keys(context).length === 0) return ""; const parts: string[] = []; for (const [key, value] of Object.entries(context)) { if (value !== undefined && value !== null) { parts.push(`${key}=${typeof value === "string" ? value : JSON.stringify(value)}`); } } return parts.length > 0 ? `(${parts.join(", ")})` : ""; } // Singleton instance export const logger = new Logger(); // Enable debug mode via environment variable if (process.env.MCP_UI_DEBUG === "1" || process.env.MCP_UI_DEBUG === "true") { logger.setLevel("debug"); }