/** * Centralized logging for MCP UI operations. * Provides structured, contextual logs with levels. */ import { emit as emitEvent } from "../visual/event-sink.ts"; 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(), }; // Route through the event sink → Working sub-line (no console output). // Context is appended for usefulness; the console-style [MCP-UI:*] // prefix is dropped (the sub-line glyph carries the level). const contextStr = formatContext(entry.context); const errStr = error ? `: ${error.message}` : ""; const text = contextStr ? `${message}${errStr} ${contextStr}` : `${message}${errStr}`; if (level === "error") { emitEvent(text, "error"); } else if (level === "warn") { emitEvent(text, "warning"); } else if (level === "debug") { // Debug is too verbose for a single-line toolbar slot; drop unless // a custom handler (e.g. file logger) is registered. } else { emitEvent(text, "info"); } // 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"); }