Skip to main content
Glama
2389-research

MCP Agent Social Media Server

logger.ts9.95 kB
// ABOUTME: Enhanced logging utility for the MCP Agent Social Media Server // ABOUTME: Provides structured logging with levels, context, and performance tracking import { appendFileSync, existsSync, writeFileSync } from 'node:fs'; import { mkdirSync } from 'node:fs'; import { basename, dirname } from 'node:path'; import { ENV_KEYS } from './config.js'; export enum LogLevel { SILENT = -1, ERROR = 0, WARN = 1, INFO = 2, DEBUG = 3, } export interface LogContext { tool?: string; sessionId?: string; agentName?: string; requestId?: string; [key: string]: unknown; } export class Logger { private static instance: Logger; private logLevel: LogLevel; private startTime: number; private isStdioMode: boolean; private logFile: string | null; private instanceId: string; private constructor() { this.logLevel = this.parseLogLevel(process.env[ENV_KEYS.LOG_LEVEL] || 'INFO'); this.startTime = Date.now(); this.isStdioMode = process.env[ENV_KEYS.MCP_TRANSPORT] !== 'http'; this.logFile = process.env.LOG_FILE || null; // Create instance identifier from current working directory + process ID const dirName = basename(process.cwd()) || 'unknown'; this.instanceId = `${dirName}:${process.pid}`; // Initialize log file if specified if (this.logFile) { try { // Ensure directory exists const logDir = dirname(this.logFile); if (!existsSync(logDir)) { mkdirSync(logDir, { recursive: true }); } // Write startup banner to log file const banner = `\n=== MCP Agent Social Server [${this.instanceId}] Started at ${new Date().toISOString()} ===\n`; if (existsSync(this.logFile)) { appendFileSync(this.logFile, banner); } else { writeFileSync(this.logFile, banner); } } catch (error) { // If file logging fails, continue without it but log to stderr process.stderr.write(`Failed to initialize log file ${this.logFile}: ${error}\n`); this.logFile = null; } } } static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } private parseLogLevel(level: string): LogLevel { switch (level.toUpperCase()) { case 'SILENT': return LogLevel.SILENT; case 'ERROR': return LogLevel.ERROR; case 'WARN': return LogLevel.WARN; case 'INFO': return LogLevel.INFO; case 'DEBUG': return LogLevel.DEBUG; default: return LogLevel.INFO; } } private formatMessage(level: string, message: string, context?: LogContext): string { const timestamp = new Date().toISOString(); const uptime = Math.floor((Date.now() - this.startTime) / 1000); let contextStr = ''; if (context) { try { contextStr = ` ${JSON.stringify(context)}`; } catch (error) { // Handle circular references or other JSON serialization errors contextStr = ` ${JSON.stringify( { ...context, _jsonError: error instanceof Error ? error.message : 'Unknown JSON error', }, (_key, value) => { if (typeof value === 'object' && value !== null) { // Simple circular reference detection if ( typeof value.toString === 'function' && value.toString().includes('[object Object]') ) { return '[Object]'; } } return value; }, )}`; } } return `[${timestamp}] [${level}] [${this.instanceId}] [uptime:${uptime}s] ${message}${contextStr}`; } private log(level: LogLevel, levelStr: string, message: string, context?: LogContext): void { if (level <= this.logLevel) { const formattedMessage = this.formatMessage(levelStr, message, context); // Always write to file if configured if (this.logFile) { try { appendFileSync(this.logFile, `${formattedMessage}\n`); } catch (error) { // If file logging fails, try stderr but don't create infinite loops try { process.stderr.write(`File logging failed: ${error}\n`); } catch (_stderrError) { // If both file and stderr fail, silently continue - avoid infinite loops // This prevents EPIPE cascades when stdio is completely broken } } } try { if (this.isStdioMode) { // In stdio mode, write to stderr to avoid polluting JSON-RPC stream process.stderr.write(`${formattedMessage}\n`); } else { // In HTTP mode, use console logging if (level === LogLevel.ERROR) { console.error(formattedMessage); } else { console.log(formattedMessage); } } } catch (error) { // Completely ignore EPIPE errors to prevent infinite loops // These happen when stdout/stderr are closed (e.g., when Claude Desktop disconnects) if (error instanceof Error && 'code' in error && error.code === 'EPIPE') { // Silent fail on EPIPE - don't try to log this error as it creates infinite loops return; } // Only rethrow non-EPIPE errors, but also protect against infinite loops if (this.logFile) { try { appendFileSync(this.logFile, `Logger stdio error: ${error}\n`); } catch (_fileError) { // If both stdio and file fail, we're in a bad state - just return return; } } } } } error(message: string, context?: LogContext): void { this.log(LogLevel.ERROR, 'ERROR', message, context); } warn(message: string, context?: LogContext): void { this.log(LogLevel.WARN, 'WARN', message, context); } info(message: string, context?: LogContext): void { this.log(LogLevel.INFO, 'INFO', message, context); } debug(message: string, context?: LogContext): void { this.log(LogLevel.DEBUG, 'DEBUG', message, context); } // Tool-specific logging helpers toolStart(toolName: string, args: unknown, context?: LogContext): void { this.info(`Tool ${toolName} started`, { tool: toolName, args: args, ...context, }); } toolSuccess(toolName: string, duration: number, context?: LogContext): void { this.info(`Tool ${toolName} completed`, { tool: toolName, duration: `${duration}ms`, status: 'success', ...context, }); } toolError(toolName: string, error: Error, duration: number, context?: LogContext): void { this.error(`Tool ${toolName} failed`, { tool: toolName, duration: `${duration}ms`, status: 'error', error: error.message, stack: error.stack, ...context, }); } // Session-specific logging sessionCreated(sessionId: string, agentName: string): void { this.info('Session created', { sessionId, agentName, event: 'session_created' }); } sessionDeleted(sessionId: string, agentName?: string): void { this.info('Session deleted', { sessionId, agentName, event: 'session_deleted' }); } sessionValidationFailed(sessionId: string, reason: string): void { this.warn('Session validation failed', { sessionId, reason, event: 'session_validation_failed', }); } // API-specific logging apiRequest(method: string, url: string, context?: LogContext): void { this.debug(`API request: ${method} ${url}`, { method, url, event: 'api_request', ...context, }); } apiResponse( method: string, url: string, status: number, duration: number, context?: LogContext, ): void { const logMethod = status >= 400 ? this.warn.bind(this) : this.debug.bind(this); logMethod(`API response: ${method} ${url} - ${status}`, { method, url, status, duration: `${duration}ms`, event: 'api_response', ...context, }); } apiError(method: string, url: string, error: Error, context?: LogContext): void { this.error(`API error: ${method} ${url}`, { method, url, error: error.message, event: 'api_error', ...context, }); } // Performance logging performance(operation: string, duration: number, context?: LogContext): void { const logMethod = duration > 1000 ? this.warn.bind(this) : this.info.bind(this); logMethod(`Performance: ${operation}`, { operation, duration: `${duration}ms`, slow: duration > 1000, ...context, }); } // Shutdown/death logging serverShutdown(reason: string, context?: LogContext): void { const shutdownMessage = `=== SERVER SHUTDOWN: ${reason} at ${new Date().toISOString()} ===`; // Always write shutdown to file if configured, even if log level is low if (this.logFile) { try { appendFileSync(this.logFile, `${shutdownMessage}\n`); if (context) { appendFileSync(this.logFile, `Context: ${JSON.stringify(context)}\n`); } appendFileSync( this.logFile, `Uptime: ${Math.floor((Date.now() - this.startTime) / 1000)}s\n\n`, ); } catch (error) { // Even if file logging fails, try to write to stderr but avoid infinite loops try { process.stderr.write(`File logging failed during shutdown: ${error}\n`); } catch (_stderrError) { // If both fail during shutdown, we can't do much - just continue } } } // Also log normally this.error(`Server shutting down: ${reason}`, { reason, uptime: Math.floor((Date.now() - this.startTime) / 1000), timestamp: new Date().toISOString(), ...context, }); } } // Export singleton instance export const logger = Logger.getInstance();

Latest Blog Posts

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/2389-research/mcp-socialmedia'

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