/**
* Debug Utility for MCP Server
*
* This module provides debug logging that only outputs when DEBUG mode is enabled.
* This prevents console.log from interfering with MCP STDIO communication.
*/
import type { DebugInfo } from "@/types/index.js";
import { config } from "@/config/Config.js";
// Log levels
export type LogLevel = "debug" | "info" | "warn" | "error";
// Logger interface
export interface Logger {
log(...args: unknown[]): void;
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;
}
// Enhanced logger with structured logging
export interface StructuredLogger extends Logger {
logStructured(info: DebugInfo): void;
child(context: Record<string, unknown>): StructuredLogger;
}
import { ConfigHelpers } from "@/config/Config.js";
// Check if debug mode is enabled
const isDebugMode = (): boolean => ConfigHelpers.shouldDebug();
// Get current timestamp
const getTimestamp = (): string => new Date().toISOString();
// Format log message with timestamp and level
const formatMessage = (level: LogLevel, args: unknown[]): string => {
const timestamp = getTimestamp();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
return `${prefix} ${args
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)))
.join(" ")}`;
};
// Handle circular references in objects
const safeStringify = (obj: unknown): string => {
try {
return JSON.stringify(
obj,
(key, value) => {
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack,
};
}
return value;
},
2,
);
} catch (_error) {
return "[Object with circular reference]";
}
};
/**
* Debug logger that only outputs in debug mode
*/
export const debug: Logger = {
log: (...args: unknown[]): void => {
if (isDebugMode()) {
console.error(formatMessage("debug", args)); // Use stderr for debug to avoid STDIO interference
}
},
info: (...args: unknown[]): void => {
if (isDebugMode()) {
console.error(formatMessage("info", args));
}
},
warn: (...args: unknown[]): void => {
if (isDebugMode()) {
console.error(formatMessage("warn", args));
}
},
error: (...args: unknown[]): void => {
if (isDebugMode()) {
console.error(formatMessage("error", args));
}
},
};
/**
* Silent logger for production use
*/
export const silent: Logger = {
log: (): void => {},
warn: (): void => {},
error: (): void => {},
info: (): void => {},
};
/**
* Enhanced structured logger
*/
class StructuredLoggerImpl implements StructuredLogger {
private context: Record<string, unknown>;
private enabled: boolean;
constructor(context: Record<string, unknown> = {}, enabled = isDebugMode()) {
this.context = context;
this.enabled = enabled;
}
private output(level: LogLevel, args: unknown[]): void {
if (!this.enabled) return;
const debugInfo: DebugInfo = {
timestamp: Date.now(),
level,
message: args.map((arg) => (typeof arg === "object" ? safeStringify(arg) : String(arg))).join(" "),
...(Object.keys(this.context).length > 0 && { context: this.context }),
};
console.error(safeStringify(debugInfo));
}
log(...args: unknown[]): void {
this.output("debug", args);
}
info(...args: unknown[]): void {
this.output("info", args);
}
warn(...args: unknown[]): void {
this.output("warn", args);
}
error(...args: unknown[]): void {
this.output("error", args);
}
logStructured(info: DebugInfo): void {
if (!this.enabled) return;
const enhancedInfo: DebugInfo = {
...info,
context: { ...this.context, ...info.context },
};
console.error(safeStringify(enhancedInfo));
}
child(context: Record<string, unknown>): StructuredLogger {
return new StructuredLoggerImpl({ ...this.context, ...context }, this.enabled);
}
}
/**
* Create a structured logger instance
*/
export const createStructuredLogger = (context: Record<string, unknown> = {}): StructuredLogger => {
return new StructuredLoggerImpl(context);
};
/**
* Default logger instance
*/
export const logger: Logger = debug;
/**
* Create a logger with context
*/
export const createLogger = (context: Record<string, unknown> = {}): StructuredLogger => {
return createStructuredLogger(context);
};
/**
* Performance measurement utility
*/
export interface PerformanceTimer {
end(): number;
endWithLog(message?: string): number;
}
export const startTimer = (label?: string): PerformanceTimer => {
const start = Date.now();
return {
end(): number {
return Date.now() - start;
},
endWithLog(message = "Operation"): number {
const duration = Date.now() - start;
debug.info(`${message} completed in ${duration}ms${label ? ` [${label}]` : ""}`);
return duration;
},
};
};
/**
* Log error with stack trace
*/
export const logError = (error: Error | string, context?: Record<string, unknown>): void => {
if (typeof error === "string") {
debug.error(error, context);
} else {
debug.error(`${error.name}: ${error.message}`, {
stack: error.stack,
...context,
});
}
};
/**
* Conditional logging
*/
export const logIf = (condition: boolean, level: LogLevel = "debug") => {
if (!condition) return silent;
const loggers = {
log: debug.log,
info: debug.info,
warn: debug.warn,
error: debug.error,
};
return loggers[level as keyof typeof loggers] || debug.log;
};
/**
* Type-safe environment variable getter with security logging
*/
export const getEnvVar = (key: string, defaultValue?: string): string | undefined => {
const value = process.env[key];
if (value === undefined && defaultValue !== undefined) {
// Don't log sensitive environment variable names in production
const isSecretVar = /password|secret|token|key|auth/i.test(key);
if (!isSecretVar || !config().app.isProduction) {
debug.warn(`Environment variable ${key} not found, using default`);
}
return defaultValue;
}
return value;
};
/**
* Validate required environment variables with secure error messages
*/
export const validateEnvVars = (required: string[]): void => {
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
// In production, don't expose which specific env vars are missing
const errorMessage = config().app.isProduction
? `Missing ${missing.length} required environment variable(s)`
: `Missing required environment variables: ${missing.join(", ")}`;
const error = new Error(errorMessage);
logError(error);
throw error;
}
};
/**
* Sanitize environment variable for logging
*/
export const sanitizeEnvValue = (key: string, value: string): string => {
const sensitiveKeys = /password|secret|token|key|auth|credential/i;
if (sensitiveKeys.test(key)) {
if (!value || value.length === 0) {
return "[EMPTY]";
}
return `[REDACTED:${value.length}chars]`;
}
return value;
};
/**
* Get sanitized environment summary for debugging
*/
export const getEnvSummary = (keys: string[]): Record<string, string> => {
const summary: Record<string, string> = {};
for (const key of keys) {
const value = process.env[key];
if (value !== undefined) {
summary[key] = sanitizeEnvValue(key, value);
} else {
summary[key] = "[NOT_SET]";
}
}
return summary;
};