/** * Memento MCP Server - Configuration Management * * Centralized configuration with validation and defaults. * Validates all required environment variables on startup. */ /** * Parse boolean from string or value */ function parseBoolean(value, defaultValue = false) { if (value === undefined || value === null || value === '') { return defaultValue; } if (typeof value === 'boolean') return value; const str = String(value).toLowerCase(); return ['true', '1', 'yes', 'on'].includes(str); } /** * Parse integer with default */ function parseInt(value, defaultValue, min, max) { if (value === undefined || value === null || value === '') { return defaultValue; } const parsed = Number.parseInt(value, 10); if (Number.isNaN(parsed)) return defaultValue; if (min !== undefined && parsed < min) return min; if (max !== undefined && parsed > max) return max; return parsed; } /** * Parse array from comma-separated string */ function parseArray(value, defaultValue = []) { if (!value) return defaultValue; if (Array.isArray(value)) return value; return String(value).split(',').map((s) => s.trim()).filter(Boolean); } /** * Get environment variable with fallback */ function env(key, fallback = '') { return process.env[key] || fallback; } /** * Validate database URL format */ function validateDatabaseUrl(url) { if (!url) { return { valid: false, error: 'DATABASE_URL is required' }; } const isValid = url.startsWith('postgresql://') || url.startsWith('postgres://') || url.startsWith('file:') || url.includes('.db') || url.includes('.sqlite'); if (!isValid) { return { valid: false, error: 'DATABASE_URL must be a valid PostgreSQL or SQLite connection string', }; } return { valid: true }; } /** * Validate port number */ function validatePort(port) { if (port < 1 || port > 65535) { return { valid: false, error: `PORT must be between 1 and 65535, got ${port}` }; } return { valid: true }; } /** * Validate log level */ function validateLogLevel(level) { const validLevels = ['debug', 'info', 'warn', 'error', 'silent']; if (!validLevels.includes(level)) { return { valid: false, error: `LOG_LEVEL must be one of: ${validLevels.join(', ')}`, }; } return { valid: true }; } /** * Validate timeout values */ function validateTimeout(timeout, name) { const min = 1000; // 1 second minimum const max = 300000; // 5 minutes maximum if (timeout < min || timeout > max) { return { valid: false, error: `${name} must be between ${min}ms and ${max}ms, got ${timeout}ms`, }; } return { valid: true }; } /** * Configuration object with validation */ export const config = { // Server port: parseInt(env('PORT', '3001'), 3001, 1, 65535), nodeEnv: env('NODE_ENV', 'development'), // Database databaseUrl: env('DATABASE_URL', ''), isPostgres: env('DATABASE_URL', '').startsWith('postgresql://') || env('DATABASE_URL', '').startsWith('postgres://'), connectionLimit: parseInt(env('DB_CONNECTION_LIMIT', '10'), 10, 1, 100), poolTimeout: parseInt(env('DB_POOL_TIMEOUT', '10'), 10, 1, 60), // Application appBaseUrl: env('APP_BASE_URL', 'http://localhost:3000'), userId: env('USER_ID', null), // Optional user filter // Authentication requireAuth: parseBoolean(env('MCP_REQUIRE_AUTH'), false), staticApiKey: env('MCP_API_KEY', null), // Logging logLevel: env('MCP_LOG_LEVEL', 'info').toLowerCase(), logToFile: parseBoolean(env('MCP_LOG_TO_FILE'), false), // Performance requestTimeout: parseInt(env('MCP_REQUEST_TIMEOUT', '30000'), 30000, 1000, 300000), rateLimit: parseInt(env('MCP_RATE_LIMIT', '100'), 100, 1, 10000), rateLimitWindow: parseInt(env('MCP_RATE_LIMIT_WINDOW', '60000'), 60000, 1000, 3600000), // Session management maxSessions: parseInt(env('MCP_MAX_SESSIONS', '500'), 500, 10, 10000), sessionTtl: parseInt(env('MCP_SESSION_TTL', '3600000'), 3600000, 60000, 86400000), sessionCleanupInterval: parseInt(env('MCP_SESSION_CLEANUP_INTERVAL', '300000'), 300000, 60000, 3600000), // Caching enableCache: parseBoolean(env('MCP_ENABLE_CACHE'), true), cacheTtl: parseInt(env('MCP_CACHE_TTL', '60000'), 60000, 0, 3600000), cacheMaxSize: parseInt(env('MCP_CACHE_MAX_SIZE', '1000'), 1000, 100, 10000), // Features enableMetrics: parseBoolean(env('MCP_ENABLE_METRICS'), true), enableAuditLog: parseBoolean(env('MCP_ENABLE_AUDIT_LOG'), true), enableTools: parseArray(env('MCP_ENABLE_TOOLS'), null), // null = all tools enabled disableTools: parseArray(env('MCP_DISABLE_TOOLS'), []), // Security maxRequestSize: parseInt(env('MCP_MAX_REQUEST_SIZE', '10485760'), 10485760, 1024, 104857600), // 10MB default maxResponseSize: parseInt(env('MCP_MAX_RESPONSE_SIZE', '52428800'), 52428800, 1024, 524288000), // 50MB default allowedOrigins: parseArray(env('MCP_ALLOWED_ORIGINS'), '*'), // Observability metricsPath: env('MCP_METRICS_PATH', '/metrics'), healthPath: env('MCP_HEALTH_PATH', '/health'), debugPath: env('MCP_DEBUG_PATH', '/debug'), // Timeouts databaseQueryTimeout: parseInt(env('MCP_DB_QUERY_TIMEOUT', '30000'), 30000, 1000, 120000), toolExecutionTimeout: parseInt(env('MCP_TOOL_TIMEOUT', '60000'), 60000, 5000, 300000), }; /** * Validate all configuration values * Returns array of validation errors (empty if valid) */ export function validateConfig() { const errors = []; // Required fields if (!config.databaseUrl) { errors.push({ key: 'DATABASE_URL', message: 'DATABASE_URL is required', critical: true, }); } else { const dbValidation = validateDatabaseUrl(config.databaseUrl); if (!dbValidation.valid) { errors.push({ key: 'DATABASE_URL', message: dbValidation.error, critical: true }); } } // Port validation const portValidation = validatePort(config.port); if (!portValidation.valid) { errors.push({ key: 'PORT', message: portValidation.error, critical: true }); } // Log level validation const logLevelValidation = validateLogLevel(config.logLevel); if (!logLevelValidation.valid) { errors.push({ key: 'MCP_LOG_LEVEL', message: logLevelValidation.error, critical: false }); } // Timeout validations const requestTimeoutValidation = validateTimeout(config.requestTimeout, 'REQUEST_TIMEOUT'); if (!requestTimeoutValidation.valid) { errors.push({ key: 'MCP_REQUEST_TIMEOUT', message: requestTimeoutValidation.error, critical: false, }); } const dbTimeoutValidation = validateTimeout(config.databaseQueryTimeout, 'DB_QUERY_TIMEOUT'); if (!dbTimeoutValidation.valid) { errors.push({ key: 'MCP_DB_QUERY_TIMEOUT', message: dbTimeoutValidation.error, critical: false, }); } // Auth configuration if (config.requireAuth && !config.staticApiKey) { // Warning: auth required but no static key set // This is OK - we'll use database-stored keys // Just log a warning in development if (config.nodeEnv === 'development') { errors.push({ key: 'MCP_REQUIRE_AUTH', message: 'Auth is required but no MCP_API_KEY is set. Database API keys will be used.', critical: false, level: 'warning', }); } } // Check for conflicting tool enable/disable if (config.enableTools && config.disableTools.length > 0) { const conflicts = config.enableTools.filter((t) => config.disableTools.includes(t)); if (conflicts.length > 0) { errors.push({ key: 'MCP_ENABLE_TOOLS / MCP_DISABLE_TOOLS', message: `Tools both enabled and disabled: ${conflicts.join(', ')}. Disabled takes precedence.`, critical: false, level: 'warning', }); } } return errors; } /** * Get configuration for display (sanitized) * Removes sensitive values like API keys and database URLs */ export function getPublicConfig() { return { port: config.port, nodeEnv: config.nodeEnv, isPostgres: config.isPostgres, appBaseUrl: config.appBaseUrl, userId: config.userId || null, requireAuth: config.requireAuth, hasStaticKey: Boolean(config.staticApiKey), logLevel: config.logLevel, requestTimeout: config.requestTimeout, rateLimit: config.rateLimit, maxSessions: config.maxSessions, sessionTtl: config.sessionTtl, enableCache: config.enableCache, cacheTtl: config.cacheTtl, enableMetrics: config.enableMetrics, enableAuditLog: config.enableAuditLog, enabledToolCount: config.enableTools?.length || 'all', disabledToolCount: config.disableTools.length, maxRequestSize: config.maxRequestSize, allowedOrigins: config.allowedOrigins, }; } /** * Get database URL for logging (sanitized) */ export function getSafeDatabaseUrl() { if (!config.databaseUrl) return ''; try { const url = new URL(config.databaseUrl); // Mask password if (url.password) { url.password = '***'; } return url.toString(); } catch { // If not a valid URL, return partially masked version const url = config.databaseUrl; if (url.includes(':')) { const parts = url.split('@'); if (parts.length > 1) { return `***@${parts[1]}`; } } return url.substring(0, 20) + '...'; } } /** * Print configuration on startup */ export function printConfig() { const errors = validateConfig(); console.log(` ╔═══════════════════════════════════════════════════════╗ ║ Memento MCP Server Configuration ║ ╚═══════════════════════════════════════════════════════╝ Environment: ${config.nodeEnv.toUpperCase()} Port: ${config.port} Database: ${config.isPostgres ? 'PostgreSQL' : 'SQLite'} Database URL: ${getSafeDatabaseUrl()} Authentication: ${config.requireAuth ? 'ENABLED' : 'DISABLED'} Static Key: ${config.staticApiKey ? 'SET' : 'NOT SET'} Rate Limit: ${config.rateLimit} requests / ${config.rateLimitWindow}ms Sessions: Max: ${config.maxSessions} TTL: ${config.sessionTtl}ms Cleanup: ${config.sessionCleanupInterval}ms Timeouts: Request: ${config.requestTimeout}ms DB Query: ${config.databaseQueryTimeout}ms Tool: ${config.toolExecutionTimeout}ms Cache: ${config.enableCache ? 'ENABLED' : 'DISABLED'} TTL: ${config.cacheTtl}ms Max Size: ${config.cacheMaxSize} Features: Metrics: ${config.enableMetrics ? 'ENABLED' : 'DISABLED'} Audit Log: ${config.enableAuditLog ? 'ENABLED' : 'DISABLED'} ${errors.length > 0 ? `⚠️ CONFIGURATION WARNINGS/ERRORS: ${errors.map((e) => ` ${e.critical ? '❌' : '⚠️'} ${e.key}: ${e.message}`).join('\n')} ` : ''} `); } export default config;