Skip to main content
Glama

Filesystem MCP Server

logger.ts19.3 kB
/** * @fileoverview Provides a singleton Logger class that wraps Winston for file logging * and supports sending MCP (Model Context Protocol) `notifications/message`. * It handles different log levels compliant with RFC 5424 and MCP specifications. * @module src/utils/internal/logger */ import fs from "fs"; import path from "path"; import winston from "winston"; import TransportStream from "winston-transport"; import { config } from "../../config/index.js"; import { RequestContext } from "./requestContext.js"; /** * Defines the supported logging levels based on RFC 5424 Syslog severity levels, * as used by the Model Context Protocol (MCP). * Levels are: 'debug'(7), 'info'(6), 'notice'(5), 'warning'(4), 'error'(3), 'crit'(2), 'alert'(1), 'emerg'(0). * Lower numeric values indicate higher severity. */ export type McpLogLevel = | "debug" | "info" | "notice" | "warning" | "error" | "crit" | "alert" | "emerg"; /** * Numeric severity mapping for MCP log levels (lower is more severe). * @private */ const mcpLevelSeverity: Record<McpLogLevel, number> = { emerg: 0, alert: 1, crit: 2, error: 3, warning: 4, notice: 5, info: 6, debug: 7, }; /** * Maps MCP log levels to Winston's core levels for file logging. * @private */ const mcpToWinstonLevel: Record< McpLogLevel, "debug" | "info" | "warn" | "error" > = { debug: "debug", info: "info", notice: "info", warning: "warn", error: "error", crit: "error", alert: "error", emerg: "error", }; /** * Interface for a more structured error object, primarily for formatting console logs. * @private */ interface ErrorWithMessageAndStack { message?: string; stack?: string; [key: string]: any; } /** * Interface for the payload of an MCP log notification. * This structure is used when sending log data via MCP `notifications/message`. */ export interface McpLogPayload { message: string; context?: RequestContext; error?: { message: string; stack?: string; }; [key: string]: any; } /** * Type for the `data` parameter of the `McpNotificationSender` function. */ export type McpNotificationData = McpLogPayload | Record<string, unknown>; /** * Defines the signature for a function that can send MCP log notifications. * This function is typically provided by the MCP server instance. * @param level - The severity level of the log message. * @param data - The payload of the log notification. * @param loggerName - An optional name or identifier for the logger/server. */ export type McpNotificationSender = ( level: McpLogLevel, data: McpNotificationData, loggerName?: string, ) => void; // The logsPath from config is already resolved and validated by src/config/index.ts const resolvedLogsDir = config.logsPath; const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic. /** * Creates the Winston console log format. * @returns The Winston log format for console output. * @private */ function createWinstonConsoleFormat(): winston.Logform.Format { return winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.printf(({ timestamp, level, message, ...meta }) => { let metaString = ""; const metaCopy = { ...meta }; if (metaCopy.error && typeof metaCopy.error === "object") { const errorObj = metaCopy.error as ErrorWithMessageAndStack; if (errorObj.message) metaString += `\n Error: ${errorObj.message}`; if (errorObj.stack) metaString += `\n Stack: ${String(errorObj.stack) .split("\n") .map((l: string) => ` ${l}`) .join("\n")}`; delete metaCopy.error; } if (Object.keys(metaCopy).length > 0) { try { const remainingMetaJson = JSON.stringify(metaCopy, null, 2); if (remainingMetaJson !== "{}") metaString += `\n Meta: ${remainingMetaJson}`; } catch (stringifyError: unknown) { const errorMessage = stringifyError instanceof Error ? stringifyError.message : String(stringifyError); metaString += `\n Meta: [Error stringifying metadata: ${errorMessage}]`; } } return `${timestamp} ${level}: ${message}${metaString}`; }), ); } /** * Singleton Logger class that wraps Winston for robust logging. * Supports file logging, conditional console logging, and MCP notifications. */ export class Logger { private static instance: Logger; private winstonLogger?: winston.Logger; private initialized = false; private mcpNotificationSender?: McpNotificationSender; private currentMcpLevel: McpLogLevel = "info"; private currentWinstonLevel: "debug" | "info" | "warn" | "error" = "info"; private readonly MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH = 1024; private readonly LOG_FILE_MAX_SIZE = 5 * 1024 * 1024; // 5MB private readonly LOG_MAX_FILES = 5; /** @private */ private constructor() {} /** * Initializes the Winston logger instance. * Should be called once at application startup. * @param level - The initial minimum MCP log level. */ public async initialize(level: McpLogLevel = "info"): Promise<void> { if (this.initialized) { this.warning("Logger already initialized.", { loggerSetup: true, requestId: "logger-init", timestamp: new Date().toISOString(), }); return; } this.currentMcpLevel = level; this.currentWinstonLevel = mcpToWinstonLevel[level]; let logsDirCreatedMessage: string | null = null; // This message is now informational as creation is handled by config if (isLogsDirSafe) { // Directory creation is handled by config/index.ts ensureDirectory. // We can log if it was newly created by checking if it existed before config ran, // but that's complex. For now, we assume config handled it. // If resolvedLogsDir is set, config ensures it exists. if (!fs.existsSync(resolvedLogsDir)) { // This case should ideally not be hit if config.logsPath is correctly set up and validated. // However, if it somehow occurs (e.g. dir deleted after config init but before logger init), // we attempt to create it. try { await fs.promises.mkdir(resolvedLogsDir, { recursive: true }); logsDirCreatedMessage = `Re-created logs directory (should have been created by config): ${resolvedLogsDir}`; } catch (err: unknown) { if (process.stdout.isTTY) { const errorMessage = err instanceof Error ? err.message : String(err); console.error( `Error creating logs directory at ${resolvedLogsDir}: ${errorMessage}. File logging disabled.`, ); } throw err; // Critical if logs dir cannot be ensured } } } const fileFormat = winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json(), ); const transports: TransportStream[] = []; const fileTransportOptions = { format: fileFormat, maxsize: this.LOG_FILE_MAX_SIZE, maxFiles: this.LOG_MAX_FILES, tailable: true, }; if (isLogsDirSafe) { transports.push( new winston.transports.File({ filename: path.join(resolvedLogsDir, "error.log"), level: "error", ...fileTransportOptions, }), new winston.transports.File({ filename: path.join(resolvedLogsDir, "warn.log"), level: "warn", ...fileTransportOptions, }), new winston.transports.File({ filename: path.join(resolvedLogsDir, "info.log"), level: "info", ...fileTransportOptions, }), new winston.transports.File({ filename: path.join(resolvedLogsDir, "debug.log"), level: "debug", ...fileTransportOptions, }), new winston.transports.File({ filename: path.join(resolvedLogsDir, "combined.log"), ...fileTransportOptions, }), ); } else { if (process.stdout.isTTY) { console.warn( "File logging disabled as logsPath is not configured or invalid.", ); } } this.winstonLogger = winston.createLogger({ level: this.currentWinstonLevel, transports, exitOnError: false, }); // Configure console transport after Winston logger is created const consoleStatus = this._configureConsoleTransport(); const initialContext: RequestContext = { loggerSetup: true, requestId: "logger-init-deferred", timestamp: new Date().toISOString(), }; if (logsDirCreatedMessage) { // Log if we had to re-create it this.info(logsDirCreatedMessage, initialContext); } if (consoleStatus.message) { this.info(consoleStatus.message, initialContext); } this.initialized = true; this.info( `Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, { loggerSetup: true, requestId: "logger-post-init", timestamp: new Date().toISOString(), logsPathUsed: resolvedLogsDir, }, ); } /** * Sets the function used to send MCP 'notifications/message'. * @param sender - The function to call for sending notifications, or undefined to disable. */ public setMcpNotificationSender( sender: McpNotificationSender | undefined, ): void { this.mcpNotificationSender = sender; const status = sender ? "enabled" : "disabled"; this.info(`MCP notification sending ${status}.`, { loggerSetup: true, requestId: "logger-set-sender", timestamp: new Date().toISOString(), }); } /** * Dynamically sets the minimum logging level. * @param newLevel - The new minimum MCP log level to set. */ public setLevel(newLevel: McpLogLevel): void { const setLevelContext: RequestContext = { loggerSetup: true, requestId: "logger-set-level", timestamp: new Date().toISOString(), }; if (!this.ensureInitialized()) { if (process.stdout.isTTY) { console.error("Cannot set level: Logger not initialized."); } return; } if (!(newLevel in mcpLevelSeverity)) { this.warning( `Invalid MCP log level provided: ${newLevel}. Level not changed.`, setLevelContext, ); return; } const oldLevel = this.currentMcpLevel; this.currentMcpLevel = newLevel; this.currentWinstonLevel = mcpToWinstonLevel[newLevel]; if (this.winstonLogger) { // Ensure winstonLogger is defined this.winstonLogger.level = this.currentWinstonLevel; } const consoleStatus = this._configureConsoleTransport(); if (oldLevel !== newLevel) { this.info( `Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, setLevelContext, ); if ( consoleStatus.message && consoleStatus.message !== "Console logging status unchanged." ) { this.info(consoleStatus.message, setLevelContext); } } } /** * Configures the console transport based on the current log level and TTY status. * Adds or removes the console transport as needed. * @returns {{ enabled: boolean, message: string | null }} Status of console logging. * @private */ private _configureConsoleTransport(): { enabled: boolean; message: string | null; } { if (!this.winstonLogger) { return { enabled: false, message: "Cannot configure console: Winston logger not initialized.", }; } const consoleTransport = this.winstonLogger.transports.find( (t) => t instanceof winston.transports.Console, ); const shouldHaveConsole = this.currentMcpLevel === "debug" && process.stdout.isTTY; let message: string | null = null; if (shouldHaveConsole && !consoleTransport) { const consoleFormat = createWinstonConsoleFormat(); this.winstonLogger.add( new winston.transports.Console({ level: "debug", // Console always logs debug if enabled format: consoleFormat, }), ); message = "Console logging enabled (level: debug, stdout is TTY)."; } else if (!shouldHaveConsole && consoleTransport) { this.winstonLogger.remove(consoleTransport); message = "Console logging disabled (level not debug or stdout not TTY)."; } else { message = "Console logging status unchanged."; } return { enabled: shouldHaveConsole, message }; } /** * Gets the singleton instance of the Logger. * @returns The singleton Logger instance. */ public static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } /** * Ensures the logger has been initialized. * @returns True if initialized, false otherwise. * @private */ private ensureInitialized(): boolean { if (!this.initialized || !this.winstonLogger) { if (process.stdout.isTTY) { console.warn("Logger not initialized; message dropped."); } return false; } return true; } /** * Centralized log processing method. * @param level - The MCP severity level of the message. * @param msg - The main log message. * @param context - Optional request context for the log. * @param error - Optional error object associated with the log. * @private */ private log( level: McpLogLevel, msg: string, context?: RequestContext, error?: Error, ): void { if (!this.ensureInitialized()) return; if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) { return; // Do not log if message level is less severe than currentMcpLevel } const logData: Record<string, unknown> = { ...context }; const winstonLevel = mcpToWinstonLevel[level]; if (error) { this.winstonLogger!.log(winstonLevel, msg, { ...logData, error }); } else { this.winstonLogger!.log(winstonLevel, msg, logData); } if (this.mcpNotificationSender) { const mcpDataPayload: McpLogPayload = { message: msg }; if (context && Object.keys(context).length > 0) mcpDataPayload.context = context; if (error) { mcpDataPayload.error = { message: error.message }; // Include stack trace in debug mode for MCP notifications, truncated for brevity if (this.currentMcpLevel === "debug" && error.stack) { mcpDataPayload.error.stack = error.stack.substring( 0, this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH, ); } } try { const serverName = config?.mcpServerName ?? "MCP_SERVER_NAME_NOT_CONFIGURED"; this.mcpNotificationSender(level, mcpDataPayload, serverName); } catch (sendError: unknown) { const errorMessage = sendError instanceof Error ? sendError.message : String(sendError); const internalErrorContext: RequestContext = { requestId: context?.requestId || "logger-internal-error", timestamp: new Date().toISOString(), originalLevel: level, originalMessage: msg, sendError: errorMessage, mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500), // Log a preview }; this.winstonLogger!.error( "Failed to send MCP log notification", internalErrorContext, ); } } } /** Logs a message at the 'debug' level. */ public debug(msg: string, context?: RequestContext): void { this.log("debug", msg, context); } /** Logs a message at the 'info' level. */ public info(msg: string, context?: RequestContext): void { this.log("info", msg, context); } /** Logs a message at the 'notice' level. */ public notice(msg: string, context?: RequestContext): void { this.log("notice", msg, context); } /** Logs a message at the 'warning' level. */ public warning(msg: string, context?: RequestContext): void { this.log("warning", msg, context); } /** * Logs a message at the 'error' level. * @param msg - The main log message. * @param err - Optional. Error object or RequestContext. * @param context - Optional. RequestContext if `err` is an Error. */ public error( msg: string, err?: Error | RequestContext, context?: RequestContext, ): void { const errorObj = err instanceof Error ? err : undefined; const actualContext = err instanceof Error ? context : err; this.log("error", msg, actualContext, errorObj); } /** * Logs a message at the 'crit' (critical) level. * @param msg - The main log message. * @param err - Optional. Error object or RequestContext. * @param context - Optional. RequestContext if `err` is an Error. */ public crit( msg: string, err?: Error | RequestContext, context?: RequestContext, ): void { const errorObj = err instanceof Error ? err : undefined; const actualContext = err instanceof Error ? context : err; this.log("crit", msg, actualContext, errorObj); } /** * Logs a message at the 'alert' level. * @param msg - The main log message. * @param err - Optional. Error object or RequestContext. * @param context - Optional. RequestContext if `err` is an Error. */ public alert( msg: string, err?: Error | RequestContext, context?: RequestContext, ): void { const errorObj = err instanceof Error ? err : undefined; const actualContext = err instanceof Error ? context : err; this.log("alert", msg, actualContext, errorObj); } /** * Logs a message at the 'emerg' (emergency) level. * @param msg - The main log message. * @param err - Optional. Error object or RequestContext. * @param context - Optional. RequestContext if `err` is an Error. */ public emerg( msg: string, err?: Error | RequestContext, context?: RequestContext, ): void { const errorObj = err instanceof Error ? err : undefined; const actualContext = err instanceof Error ? context : err; this.log("emerg", msg, actualContext, errorObj); } /** * Logs a message at the 'emerg' (emergency) level, typically for fatal errors. * @param msg - The main log message. * @param err - Optional. Error object or RequestContext. * @param context - Optional. RequestContext if `err` is an Error. */ public fatal( msg: string, err?: Error | RequestContext, context?: RequestContext, ): void { const errorObj = err instanceof Error ? err : undefined; const actualContext = err instanceof Error ? context : err; this.log("emerg", msg, actualContext, errorObj); } } /** * The singleton instance of the Logger. * Use this instance for all logging operations. */ export const logger = Logger.getInstance();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/filesystem-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server