/** * MCP servers communicate over stdio, so ALL logs must go to stderr. * Never write logs to stdout — it corrupts the JSON-RPC stream. */ type Level = 'debug' | 'info' | 'warn' | 'error'; const LEVELS: Record = { debug: 10, info: 20, warn: 30, error: 40 }; function currentThreshold(): number { const env = (process.env.NEXUS_LOG_LEVEL || 'info').toLowerCase() as Level; return LEVELS[env] ?? LEVELS.info; } function emit(level: Level, scope: string, message: string, extra?: unknown): void { if (LEVELS[level] < currentThreshold()) return; const ts = new Date().toISOString(); let line = `[${ts}] ${level.toUpperCase()} (${scope}) ${message}`; if (extra !== undefined) { try { line += ` ${typeof extra === 'string' ? extra : JSON.stringify(extra)}`; } catch { line += ' [unserializable extra]'; } } process.stderr.write(line + '\n'); } export interface Logger { debug(message: string, extra?: unknown): void; info(message: string, extra?: unknown): void; warn(message: string, extra?: unknown): void; error(message: string, extra?: unknown): void; } export function createLogger(scope: string): Logger { return { debug: (m, e) => emit('debug', scope, m, e), info: (m, e) => emit('info', scope, m, e), warn: (m, e) => emit('warn', scope, m, e), error: (m, e) => emit('error', scope, m, e), }; }