Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
index.ts9.12 kB
// Copyright 2025 Chris Bunting // Brief: Configuration management system for MCP Code Analysis & Quality Server // Scope: Centralized configuration handling for all MCP servers import { ConfigInterface, ServerConfig, LogLevel } from '@mcp-code-analysis/shared-types'; import { EventEmitter } from 'events'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; export interface ConfigSource { name: string; priority: number; get<T>(key: string): Promise<T | undefined>; set<T>(key: string, value: T): Promise<void>; has(key: string): Promise<boolean>; } export class FileConfigSource implements ConfigSource { name = 'file'; priority = 100; private configPath: string; private config: Record<string, unknown> = {}; constructor(configPath?: string) { this.configPath = configPath ?? path.join(process.cwd(), 'mcp-config.json'); this.loadConfig(); } private loadConfig(): void { try { if (fs.existsSync(this.configPath)) { const configData = fs.readFileSync(this.configPath, 'utf8'); this.config = JSON.parse(configData); } } catch (error) { console.warn(`Failed to load config from ${this.configPath}:`, error); this.config = {}; } } async get<T>(key: string): Promise<T | undefined> { return this.config[key] as T; } async set<T>(key: string, value: T): Promise<void> { this.config[key] = value; await this.saveConfig(); } async has(key: string): Promise<boolean> { return key in this.config; } private async saveConfig(): Promise<void> { try { const configDir = path.dirname(this.configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2)); } catch (error) { console.error(`Failed to save config to ${this.configPath}:`, error); } } } export class EnvironmentConfigSource implements ConfigSource { name = 'environment'; priority = 200; async get<T>(key: string): Promise<T | undefined> { const envKey = key.toUpperCase().replace(/\./g, '_'); const value = process.env[envKey]; if (value !== undefined) { try { return JSON.parse(value) as T; } catch { return value as T; } } return undefined; } async set<T>(_key: string, _value: T): Promise<void> { throw new Error('Environment config source is read-only'); } async has(key: string): Promise<boolean> { const envKey = key.toUpperCase().replace(/\./g, '_'); return envKey in process.env; } } export class DefaultConfigSource implements ConfigSource { name = 'default'; priority = 0; private defaults: Record<string, any>; constructor(defaults: Record<string, any>) { this.defaults = { ...defaults }; } async get<T>(key: string): Promise<T | undefined> { return this.defaults[key] as T; } async set<T>(_key: string, _value: T): Promise<void> { throw new Error('Default config source is read-only'); } async has(key: string): Promise<boolean> { return key in this.defaults; } } export class ConfigManager extends EventEmitter implements ConfigInterface { private sources: ConfigSource[] = []; private cache: Map<string, any> = new Map(); private watchers: fs.FSWatcher[] = []; constructor() { super(); } addSource(source: ConfigSource): void { this.sources.push(source); this.sources.sort((a, b) => b.priority - a.priority); this.clearCache(); } async get<T>(key: string, defaultValue?: T): Promise<T> { if (this.cache.has(key)) { return this.cache.get(key) as T; } for (const source of this.sources) { try { const value = await source.get<T>(key); if (value !== undefined) { this.cache.set(key, value); return value; } } catch (error) { console.warn(`Failed to get config key "${key}" from source "${source.name}":`, error); } } if (defaultValue !== undefined) { this.cache.set(key, defaultValue); return defaultValue; } throw new Error(`Configuration key "${key}" not found`); } async set<T>(key: string, value: T): Promise<void> { // Find the first writable source for (const source of this.sources) { try { await source.set(key, value); break; } catch (error) { console.warn(`Failed to set config key "${key}" in source "${source.name}":`, error); } } this.cache.set(key, value); this.emit('change', { key, value }); } async has(key: string): Promise<boolean> { if (this.cache.has(key)) { return true; } for (const source of this.sources) { try { if (await source.has(key)) { return true; } } catch (error) { console.warn(`Failed to check config key "${key}" in source "${source.name}":`, error); } } return false; } async reload(): Promise<void> { this.clearCache(); this.emit('reload'); } private clearCache(): void { this.cache.clear(); } watchFile(filePath: string): void { if (fs.existsSync(filePath)) { const watcher = fs.watch(filePath, (eventType) => { if (eventType === 'change') { this.reload().catch(error => { console.error('Failed to reload config:', error); }); } }); this.watchers.push(watcher); } } dispose(): void { this.watchers.forEach(watcher => watcher.close()); this.watchers = []; this.removeAllListeners(); } } export function createDefaultConfig(): ServerConfig { return { name: 'mcp-code-analysis-server', version: '1.0.0', host: 'localhost', port: 3000, logLevel: LogLevel.INFO, cache: { enabled: true, ttl: 3600, // 1 hour maxSize: 1000, storage: 'memory' }, plugins: [] }; } export function createConfigManager(configPath?: string): ConfigManager { const manager = new ConfigManager(); // Add sources in priority order (highest first) manager.addSource(new EnvironmentConfigSource()); manager.addSource(new FileConfigSource(configPath)); manager.addSource(new DefaultConfigSource(createDefaultConfig())); // Watch for file changes if (configPath) { manager.watchFile(configPath); } return manager; } // Configuration validation export function validateServerConfig(config: Partial<ServerConfig>): ServerConfig { const defaults = createDefaultConfig(); return { name: config.name || defaults.name, version: config.version || defaults.version, host: config.host || defaults.host, port: config.port || defaults.port, logLevel: config.logLevel || defaults.logLevel, cache: { ...defaults.cache, ...config.cache }, plugins: config.plugins || defaults.plugins }; } // Configuration schema for validation export const ConfigSchema = { type: 'object', properties: { name: { type: 'string', minLength: 1 }, version: { type: 'string', pattern: '^\\d+\\.\\d+\\.\\d+$' }, host: { type: 'string', format: 'hostname' }, port: { type: 'integer', minimum: 1, maximum: 65535 }, logLevel: { type: 'string', enum: Object.values(LogLevel) }, cache: { type: 'object', properties: { enabled: { type: 'boolean' }, ttl: { type: 'integer', minimum: 0 }, maxSize: { type: 'integer', minimum: 1 }, storage: { type: 'string', enum: ['memory', 'redis', 'file'] } }, required: ['enabled', 'ttl', 'maxSize', 'storage'] }, plugins: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', minLength: 1 }, enabled: { type: 'boolean' }, config: { type: 'object' } }, required: ['name', 'enabled', 'config'] } } }, required: ['name', 'version', 'logLevel', 'cache', 'plugins'] }; // Configuration utilities export class ConfigUtils { static resolvePath(configPath: string): string { if (path.isAbsolute(configPath)) { return configPath; } return path.resolve(process.cwd(), configPath); } static mergeConfigs(...configs: Partial<ServerConfig>[]): ServerConfig { const result: Partial<ServerConfig> = {}; for (const config of configs) { Object.assign(result, config); } return validateServerConfig(result); } static getConfigPaths(): string[] { const paths: string[] = []; // Current directory paths.push(path.join(process.cwd(), 'mcp-config.json')); // Home directory paths.push(path.join(os.homedir(), '.mcp', 'config.json')); // Global config paths.push(path.join('/etc', 'mcp', 'config.json')); return paths.filter(fs.existsSync); } static async findConfigFile(): Promise<string | null> { const paths = this.getConfigPaths(); return paths.length > 0 ? paths[0] : null; } }

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/cbunting99/mcp-code-analysis-server'

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