import * as fs from 'fs';
import * as path from 'path';
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
}
export class Logger {
private logLevel: LogLevel;
private logToFile: boolean;
private logFilePath: string;
constructor() {
// Read log level from environment (default: INFO)
const envLevel = process.env.LOG_LEVEL?.toUpperCase() || 'INFO';
this.logLevel = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
// Read log to file setting
this.logToFile = process.env.LOG_TO_FILE === 'true';
// Set log file path
const logDir = process.env.LOG_DIR || './logs';
if (this.logToFile && !fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const timestamp = new Date().toISOString().split('T')[0];
this.logFilePath = path.join(logDir, `mcp-server-${timestamp}.log`);
}
private shouldLog(level: LogLevel): boolean {
return level <= this.logLevel;
}
private formatMessage(level: string, message: string, data?: any): string {
const timestamp = new Date().toISOString();
let formatted = `[${timestamp}] [${level}] ${message}`;
if (data !== undefined) {
if (typeof data === 'object') {
formatted += `\n${JSON.stringify(data, null, 2)}`;
} else {
formatted += ` ${data}`;
}
}
return formatted;
}
private write(level: string, message: string, data?: any): void {
const formatted = this.formatMessage(level, message, data);
// Always write to stderr for MCP compatibility
console.error(formatted);
// Optionally write to file
if (this.logToFile) {
try {
fs.appendFileSync(this.logFilePath, formatted + '\n');
} catch (error) {
console.error(`Failed to write to log file: ${error}`);
}
}
}
error(message: string, error?: any): void {
if (this.shouldLog(LogLevel.ERROR)) {
const errorData = error instanceof Error
? { message: error.message, stack: error.stack }
: error;
this.write('ERROR', message, errorData);
}
}
warn(message: string, data?: any): void {
if (this.shouldLog(LogLevel.WARN)) {
this.write('WARN', message, data);
}
}
info(message: string, data?: any): void {
if (this.shouldLog(LogLevel.INFO)) {
this.write('INFO', message, data);
}
}
debug(message: string, data?: any): void {
if (this.shouldLog(LogLevel.DEBUG)) {
this.write('DEBUG', message, data);
}
}
// Special logging for API requests
apiRequest(method: string, url: string, headers?: any): void {
if (this.shouldLog(LogLevel.DEBUG)) {
const sanitizedHeaders = this.sanitizeHeaders(headers);
this.debug(`API Request: ${method} ${url}`, { headers: sanitizedHeaders });
}
}
// Special logging for API responses
apiResponse(method: string, url: string, status: number, data?: any): void {
if (this.shouldLog(LogLevel.DEBUG)) {
const summary = data ? this.summarizeData(data) : 'No data';
this.debug(`API Response: ${method} ${url} - ${status}`, { summary });
}
}
// Special logging for API errors
apiError(method: string, url: string, error: any): void {
const errorInfo = {
status: error.response?.status,
statusText: error.response?.statusText,
message: error.message,
data: error.response?.data,
};
this.error(`API Error: ${method} ${url}`, errorInfo);
}
// Sanitize sensitive data from headers
private sanitizeHeaders(headers?: any): any {
if (!headers) return {};
const sanitized = { ...headers };
if (sanitized.Authorization) {
sanitized.Authorization = 'Bearer ***';
}
return sanitized;
}
// Summarize response data to avoid logging huge payloads
private summarizeData(data: any): any {
if (Array.isArray(data)) {
return `Array with ${data.length} items`;
}
if (typeof data === 'object') {
return `Object with keys: ${Object.keys(data).join(', ')}`;
}
return data;
}
// Log tool invocation
toolInvoked(toolName: string, args?: any): void {
if (this.shouldLog(LogLevel.INFO)) {
this.info(`Tool invoked: ${toolName}`, { arguments: args });
}
}
// Log tool completion
toolCompleted(toolName: string, success: boolean, duration?: number): void {
if (this.shouldLog(LogLevel.INFO)) {
const status = success ? 'SUCCESS' : 'FAILED';
const message = `Tool ${status}: ${toolName}`;
const data = duration ? { duration: `${duration}ms` } : undefined;
this.info(message, data);
}
}
}
// Export singleton instance
export const logger = new Logger();