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
JavaScript
/**
* 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 };