UNPKG

quality-mcp

Version:

An MCP server that analyzes to your codebase, with plugin support for DCD and Simian. 🏍️ "The only Zen you find on the tops of mountains is the Zen you bring up there."

453 lines (398 loc) 12.2 kB
/** * Configuration management utility * Loads and manages configuration for the Quality MCP server */ import { readFile } from 'fs/promises'; import { existsSync } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; import { createLogger } from './logger.js'; import { validateConfiguration, ConfigValidationError } from './config-validator.js'; // Get the project root directory const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const PROJECT_ROOT = join(__dirname, '..', '..'); const logger = createLogger('config'); /** * Dependency injection for testability * @returns {Object} Dependencies */ function getDeps() { return { readFile, existsSync, logger, validateConfiguration, ConfigValidationError, validateAndSanitizeConfig, join, }; } /** * Default configuration */ const DEFAULT_CONFIG = { server: { name: 'quality-mcp-server', version: '0.1.0', timeout: 30000, }, plugins: { dcd: { enabled: true, executable: 'dcd', // DCD should be in PATH after 'go install github.com/boyter/dcd@latest' defaultMatchLength: 6, defaultFuzziness: 0, timeout: 30000, cache: { enabled: true, directory: join(PROJECT_ROOT, 'cache'), ttl: 3600, // 1 hour }, supportedExtensions: [ '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.py', '.rb', '.php', '.html', '.xml', '.css', '.scss', '.sass', '.go', '.rs', '.swift', '.kt', ], }, simian: { enabled: true, executable: join(homedir(), 'lib', 'simian-4.0.0', 'simian-4.0.0.jar'), defaultThreshold: 6, defaultFormatter: 'xml', timeout: 30000, javaExecutable: 'java', // Java executable (usually 'java' in PATH) javaOptions: ['-jar'], // Java options for running .jar files cache: { enabled: true, directory: join(PROJECT_ROOT, 'cache'), ttl: 3600, // 1 hour }, supportedExtensions: [ '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.py', '.rb', '.php', '.html', '.xml', '.css', '.scss', '.sass', '.go', '.rs', '.swift', '.kt', ], options: { ignoreStrings: false, ignoreNumbers: false, ignoreCharacters: false, ignoreCurlyBraces: false, ignoreIdentifiers: false, }, }, }, logging: { level: 'info', colors: true, }, development: { mockMode: false, testDataPath: join(PROJECT_ROOT, 'test/fixtures'), }, }; /** * Configuration file paths to check (in order of preference) */ function getConfigPaths(_getDeps = getDeps) { const { join } = _getDeps(); return [ join(PROJECT_ROOT, 'config/simian.json'), join(PROJECT_ROOT, 'simian.config.json'), join(PROJECT_ROOT, '.simianrc'), join(process.env.HOME || process.env.USERPROFILE || '', '.simianrc'), ].filter(Boolean); } /** * Deep merge two objects * @param {Object} target - Target object * @param {Object} source - Source object * @returns {Object} Merged object */ function deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = deepMerge(result[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } /** * Load configuration from a file * @param {string} filePath - Path to config file * @param {Function} _getDeps - Dependency injection * @returns {Promise<Object|null>} Configuration object or null if file doesn't exist */ async function loadConfigFile(filePath, _getDeps = getDeps) { const { readFile, existsSync, logger } = _getDeps(); try { if (!existsSync(filePath)) { return null; } const content = await readFile(filePath, 'utf-8'); const config = JSON.parse(content); logger.debug(`Loaded configuration from: ${filePath}`); return config; } catch (error) { logger.warn(`Failed to load config file ${filePath}:`, error.message); return null; } } /** * Load configuration from environment variables * @param {Function} _getDeps - Dependency injection * @returns {Object} Configuration overrides from environment */ function loadEnvironmentConfig(_getDeps = getDeps) { const { logger } = _getDeps(); const envConfig = {}; // Server configuration if (process.env.MCP_SERVER_TIMEOUT) { const timeout = parseInt(process.env.MCP_SERVER_TIMEOUT, 10); if (isNaN(timeout)) { logger.warn( `Invalid MCP_SERVER_TIMEOUT value: ${process.env.MCP_SERVER_TIMEOUT} (must be a number)` ); } else { envConfig.server = { timeout }; } } // DCD configuration const dcdConfig = {}; if (process.env.DCD_EXECUTABLE) { dcdConfig.executable = process.env.DCD_EXECUTABLE; } if (process.env.DCD_MATCH_LENGTH) { const matchLength = parseInt(process.env.DCD_MATCH_LENGTH, 10); if (isNaN(matchLength)) { logger.warn( `Invalid DCD_MATCH_LENGTH value: ${process.env.DCD_MATCH_LENGTH} (must be a number)` ); } else { dcdConfig.defaultMatchLength = matchLength; } } if (process.env.DCD_FUZZINESS) { const fuzziness = parseInt(process.env.DCD_FUZZINESS, 10); if (isNaN(fuzziness)) { logger.warn(`Invalid DCD_FUZZINESS value: ${process.env.DCD_FUZZINESS} (must be a number)`); } else { dcdConfig.defaultFuzziness = fuzziness; } } if (process.env.DCD_TIMEOUT) { const timeout = parseInt(process.env.DCD_TIMEOUT, 10); if (isNaN(timeout)) { logger.warn(`Invalid DCD_TIMEOUT value: ${process.env.DCD_TIMEOUT} (must be a number)`); } else { dcdConfig.timeout = timeout; } } if (Object.keys(dcdConfig).length > 0) { envConfig.dcd = dcdConfig; } // Simian configuration const simianConfig = {}; // Simian executable path (most important for Cursor MCP configuration) if (process.env.SIMIAN_EXECUTABLE || process.env.SIMIAN_PATH) { simianConfig.executable = process.env.SIMIAN_EXECUTABLE || process.env.SIMIAN_PATH; } // Java executable (for .jar files) if (process.env.SIMIAN_JAVA_EXECUTABLE) { simianConfig.javaExecutable = process.env.SIMIAN_JAVA_EXECUTABLE; } if (process.env.SIMIAN_THRESHOLD) { const threshold = parseInt(process.env.SIMIAN_THRESHOLD, 10); if (isNaN(threshold)) { logger.warn( `Invalid SIMIAN_THRESHOLD value: ${process.env.SIMIAN_THRESHOLD} (must be a number)` ); } else { simianConfig.defaultThreshold = threshold; } } if (process.env.SIMIAN_FORMATTER) { const validFormatters = ['xml', 'plain', 'emacs', 'vs', 'yaml']; if (validFormatters.includes(process.env.SIMIAN_FORMATTER)) { simianConfig.defaultFormatter = process.env.SIMIAN_FORMATTER; } else { logger.warn( `Invalid SIMIAN_FORMATTER value: ${process.env.SIMIAN_FORMATTER} (must be one of: ${validFormatters.join(', ')})` ); } } if (process.env.SIMIAN_TIMEOUT) { const timeout = parseInt(process.env.SIMIAN_TIMEOUT, 10); if (isNaN(timeout)) { logger.warn(`Invalid SIMIAN_TIMEOUT value: ${process.env.SIMIAN_TIMEOUT} (must be a number)`); } else { simianConfig.timeout = timeout; } } if (process.env.SIMIAN_CACHE_ENABLED) { simianConfig.cache = { enabled: process.env.SIMIAN_CACHE_ENABLED === 'true', }; } if (process.env.SIMIAN_CACHE_DIR) { simianConfig.cache = { ...simianConfig.cache, directory: process.env.SIMIAN_CACHE_DIR, }; } if (Object.keys(simianConfig).length > 0) { envConfig.simian = simianConfig; } // Logging configuration if (process.env.LOG_LEVEL) { envConfig.logging = { level: process.env.LOG_LEVEL, }; } if (process.env.NO_COLOR) { envConfig.logging = { ...envConfig.logging, colors: process.env.NO_COLOR !== '1', }; } // Development configuration if (process.env.SIMIAN_MOCK_MODE) { envConfig.development = { mockMode: process.env.SIMIAN_MOCK_MODE === 'true', }; } if (Object.keys(envConfig).length > 0) { logger.debug('Loaded configuration from environment variables'); } return envConfig; } /** * Validate and sanitize configuration with comprehensive security checks * @param {Object} config - Configuration to validate * @param {Function} _getDeps - Dependency injection * @returns {Object} Validated and sanitized configuration * @throws {ConfigValidationError} If configuration is invalid */ function validateAndSanitizeConfig(config, _getDeps = getDeps) { const { validateConfiguration, ConfigValidationError, logger } = _getDeps(); logger.debug('Starting comprehensive configuration validation...'); const validationResult = validateConfiguration(config, { securityChecks: true, }); if (!validationResult.valid) { throw new ConfigValidationError( validationResult.error, validationResult.field, validationResult.value ); } // Log any security warnings if (validationResult.warnings && validationResult.warnings.length > 0) { logger.warn(`Configuration loaded with ${validationResult.warnings.length} security warnings`); for (const warning of validationResult.warnings) { logger.warn(`- ${warning.field}: ${warning.message}`); } } logger.debug('Configuration validation and sanitization completed successfully'); return validationResult.config; } /** * Load complete configuration from all sources * @param {Function} _getDeps - Dependency injection * @returns {Promise<Object>} Complete configuration object */ export async function loadConfig(_getDeps = getDeps) { const { logger, ConfigValidationError, validateAndSanitizeConfig } = _getDeps(); logger.info('Loading configuration...'); try { let config = { ...DEFAULT_CONFIG }; // Load from config files const configPaths = getConfigPaths(); for (const configPath of configPaths) { const fileConfig = await loadConfigFile(configPath); if (fileConfig) { config = deepMerge(config, fileConfig); } } // Load from environment variables const envConfig = loadEnvironmentConfig(); if (Object.keys(envConfig).length > 0) { config = deepMerge(config, envConfig); } // Validate and sanitize the final configuration const sanitizedConfig = validateAndSanitizeConfig(config); logger.info('Configuration loaded and validated successfully'); logger.info('Final sanitized configuration:', sanitizedConfig); return sanitizedConfig; } catch (error) { logger.error('Failed to load configuration:', error); if (error instanceof ConfigValidationError) { // Provide helpful error message for configuration issues logger.error(`Configuration validation failed for field '${error.field}': ${error.message}`); if (error.value !== null && error.value !== undefined) { logger.error(`Invalid value: ${JSON.stringify(error.value)}`); } } throw error; } } /** * Get the default configuration * @returns {Object} Default configuration object */ export function getDefaultConfig() { return { ...DEFAULT_CONFIG }; } /** * Check if Simian is available at the configured path * @param {Object} config - Configuration object * @param {Function} _getDeps - Dependency injection * @returns {boolean} Whether Simian is available */ export function isSimianAvailable(config, _getDeps = getDeps) { const { existsSync } = _getDeps(); return existsSync(config.simian.executable); } export { PROJECT_ROOT, ConfigValidationError, getDeps };