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."

753 lines (660 loc) 23.8 kB
/** * DCD Plugin - Open Source Alternative to Simian * Integrates DCD (Duplicate Code Detector) with the MCP server * License: AGPL-3.0 (much more friendly than Simian's restrictive licensing) */ import { spawn } from 'child_process'; import { join } from 'path'; import { createHash } from 'crypto'; import { AnalysisPlugin } from '../../core/plugin-manager.js'; import { createLogger } from '../../utils/logger.js'; import { AnalysisCache } from '../simian/cache.js'; import { createDCDCapabilityDefinition } from '../../core/plugin-definitions.js'; import { validateAnalysisParams, getSecurityConfig } from '../../utils/security.js'; import { ResponseOptimizer } from '../../core/response-optimizer.js'; // Create logger once for this module const logger = createLogger('dcd-plugin'); /** * Dependency injection function for DCD plugin * @returns {Object} Dependencies object */ function getDeps() { return { spawn, join, createHash, logger, AnalysisCache, createDCDCapabilityDefinition, validateAnalysisParams, getSecurityConfig, ResponseOptimizer, }; } export class DCDPlugin extends AnalysisPlugin { constructor(config, _getDeps = getDeps) { super('dcd', config); const { ResponseOptimizer, createDCDCapabilityDefinition, getSecurityConfig } = _getDeps(); this.cache = null; this.responseOptimizer = new ResponseOptimizer(); this.isDCDAvailable = false; this.capabilityDefinition = createDCDCapabilityDefinition(); this.securityConfig = getSecurityConfig(process.env.NODE_ENV || 'development'); } async initialize(_getDeps = getDeps) { const { logger, AnalysisCache } = _getDeps(); logger.info('Initializing DCD plugin...'); try { this.isDCDAvailable = await this.checkDCDAvailable(); if (!this.isDCDAvailable) { logger.warn( 'DCD executable not found. Install with: go install github.com/boyter/dcd@latest' ); if (!this.config.development?.mockMode) { logger.warn('Enabling mock mode due to missing DCD executable'); this.config.development = { ...this.config.development, mockMode: true }; } } if (this.config.cache?.enabled) { this.cache = new AnalysisCache(this.config.cache); await this.cache.initialize(); } logger.info( `DCD plugin initialized (mock mode: ${this.config.development?.mockMode || false})` ); } catch (error) { logger.error('Failed to initialize DCD plugin:', error); throw error; } } async checkDCDAvailable(_getDeps = getDeps) { const { spawn } = _getDeps(); return new Promise(resolve => { // Use the configured executable path instead of hardcoded 'dcd' const executable = this.config.executable || 'dcd'; const dcd = spawn(executable, ['--version'], { stdio: 'pipe' }); dcd.on('close', code => { resolve(code === 0); }); dcd.on('error', () => { resolve(false); }); }); } getTools() { // Only expose tools if DCD is available or mock mode is enabled if (!this.isDCDAvailable && !this.config.development?.mockMode) { return []; } return this.capabilityDefinition.getToolDefinitions(); } getResources() { const baseResources = this.capabilityDefinition.getResourceDefinitions(); // Add the new capability description resource return [ ...baseResources, { uri: 'dcd://capabilities', name: 'DCD Plugin Capabilities', description: 'Comprehensive description of all DCD plugin capabilities for AI consumption', mimeType: 'application/json', }, ]; } async executeTool(toolName, params, _getDeps = getDeps) { const { logger } = _getDeps(); logger.info(`Executing DCD tool: ${toolName}`, params); try { switch (toolName) { case 'analyze_duplicates_dcd': return await this.analyzeDuplicates(params); case 'scan_repository_dcd': return await this.scanRepository(params); case 'get_optimization_info': return this.getOptimizationInfo(); default: throw new Error(`Unknown tool: ${toolName}`); } } catch (error) { logger.error(`DCD tool execution failed for ${toolName}: ${error.message}`, { toolName, params, stack: error.stack, }); throw error; } } async getResource(resourceUri) { switch (resourceUri) { case 'dcd://status': return await this.getPluginStatus(); case 'dcd://install-guide': return await this.getInstallGuide(); case 'dcd://capabilities': return await this.getCapabilityDescription(); default: throw new Error(`Unknown resource: ${resourceUri}`); } } async analyzeDuplicates(params, options = {}, _getDeps = getDeps) { const { validateAnalysisParams, logger } = _getDeps(); try { // Validate and sanitize parameters const validatedParams = validateAnalysisParams(params, { pathOptions: this.securityConfig, }); const { path, matchLength = 6, extensions, fuzziness = 0 } = validatedParams; const { context, agentMessage } = options; // Detect context for response optimization using the response optimizer const detectedContext = context || this.responseOptimizer.detectContext('analyze_duplicates_dcd', params, agentMessage); // Check regular cache first const cacheKey = this.generateCacheKey('dcd-duplicates', validatedParams); let result = null; if (this.cache) { const cached = await this.cache.get(cacheKey); if (cached) { logger.debug('Cache hit - returning cached DCD analysis result'); result = cached; } } // Execute analysis if not cached if (!result) { result = await this.executeAnalysis(path, { matchLength, extensions, fuzziness }); // Cache the result if (this.cache) { await this.cache.set(cacheKey, result); } } // Optimize response based on detected context const optimizedResult = this.responseOptimizer.optimizeResponse(result, { context: detectedContext, }); logger.debug(`Returning ${detectedContext}-optimized analysis result`); return optimizedResult; } catch (error) { logger.error(`DCD analyzeDuplicates failed: ${error.message}`, { function: 'analyzeDuplicates', params, errorType: error.constructor.name, stack: error.stack, }); // Re-throw to preserve original error context throw error; } } async scanRepository(params, _getDeps = getDeps) { const { validateAnalysisParams, logger } = _getDeps(); try { // Validate and sanitize parameters const validatedParams = validateAnalysisParams( { path: params.repository, matchLength: params.matchLength, excludePatterns: params.excludePatterns, }, { pathOptions: this.securityConfig, } ); if (this.config.development?.mockMode) { return await this.mockScanRepository(validatedParams); } return await this.runDCDAnalysis(validatedParams.path, { matchLength: validatedParams.matchLength, excludePatterns: validatedParams.excludePatterns, }); } catch (error) { logger.error(`DCD scanRepository failed: ${error.message}`, { function: 'scanRepository', params, errorType: error.constructor.name, stack: error.stack, }); // Re-throw to preserve original error context throw error; } } async runDCDAnalysis(path, options = {}, _getDeps = getDeps) { const { spawn, process } = _getDeps(); if (!this.isDCDAvailable) { throw new Error( 'DCD executable not available. Install with: go install github.com/boyter/dcd@latest' ); } const args = this.buildDCDArgs(path, options); return new Promise((resolve, reject) => { // Use the configured executable path instead of hardcoded 'dcd' const executable = this.config.executable || 'dcd'; const dcd = spawn(executable, args, { cwd: process.cwd(), timeout: this.config.timeout || 30000, stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; // Add timeout handling const timeout = setTimeout(() => { dcd.kill('SIGTERM'); reject(new Error('DCD execution timed out')); }, this.config.timeout || 30000); dcd.stdout.on('data', data => { stdout += data.toString(); }); dcd.stderr.on('data', data => { stderr += data.toString(); }); dcd.on('error', error => { clearTimeout(timeout); reject(new Error(`Failed to execute DCD: ${error.message}`)); }); dcd.on('close', code => { clearTimeout(timeout); if (code === 0) { try { const parsed = this.parseDCDOutput(stdout); resolve(parsed); } catch (parseError) { reject(new Error(`Failed to parse DCD output: ${parseError.message}`)); } } else { // Handle null/undefined exit codes const exitCode = code || 'unknown'; const errorMsg = stderr || 'No error output available'; reject(new Error(`DCD failed with code ${exitCode}: ${errorMsg}`)); } }); }); } buildDCDArgs(path, options = {}) { const args = []; if (options.matchLength) { args.push('--match-length', options.matchLength.toString()); } if (options.fuzziness) { args.push('--fuzz', options.fuzziness.toString()); } if (options.extensions && options.extensions.length > 0) { const exts = options.extensions .map(ext => { return ext.replace('.', ''); }) .join(','); args.push('--include-ext', exts); } if (options.excludePatterns && options.excludePatterns.length > 0) { args.push('--exclude-pattern', options.excludePatterns.join(',')); } args.push('--verbose'); // Get detailed output args.push(path); return args; } parseDCDOutput(output, _getDeps = getDeps) { const { logger } = _getDeps(); try { const lines = output.split('\n'); const duplicates = []; let totalFiles = 0; let duplicateLines = 0; // Parse DCD output format // Example: "Found duplicate lines in processor/cocomo_test.go:" // " lines 0-8 match 0-8 in processor/workers_tokei_test.go (length 8)" let currentDuplicate = null; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Found duplicate lines in')) { // New duplicate block starting const fileMatch = trimmed.match(/Found duplicate lines in (.+):/); if (fileMatch) { currentDuplicate = { fingerprint: this.generateFingerprint(trimmed), lineCount: 0, occurrences: [ { file: fileMatch[1], startLine: 0, endLine: 0, }, ], }; } } else if (currentDuplicate && trimmed.includes('match') && trimmed.includes('in')) { // Parse the match line: " lines 0-8 match 0-8 in processor/workers_tokei_test.go (length 8)" const matchRegex = /lines\s+(\d+)-(\d+)\s+match\s+(\d+)-(\d+)\s+in\s+(.+?)\s+\(length\s+(\d+)\)/; const match = trimmed.match(matchRegex); if (match) { const [, startLine1, endLine1, startLine2, endLine2, file2, length] = match; // Update first occurrence currentDuplicate.occurrences[0].startLine = parseInt(startLine1); currentDuplicate.occurrences[0].endLine = parseInt(endLine1); currentDuplicate.lineCount = parseInt(length); // Add second occurrence currentDuplicate.occurrences.push({ file: file2, startLine: parseInt(startLine2), endLine: parseInt(endLine2), }); duplicates.push(currentDuplicate); duplicateLines += parseInt(length); currentDuplicate = null; } } else if ( trimmed.startsWith('Found') && trimmed.includes('duplicate lines in') && trimmed.includes('files') ) { // Summary line: "Found 98634 duplicate lines in 140 files" const summaryMatch = trimmed.match(/Found (\d+) duplicate lines in (\d+) files/); if (summaryMatch) { duplicateLines = parseInt(summaryMatch[1]); totalFiles = parseInt(summaryMatch[2]); } } } return { summary: { totalFiles, duplicateLines, duplicateBlocks: duplicates.length, analysisTime: 'DCD analysis', tool: 'DCD (Open Source)', }, duplicates, rawOutput: output, }; } catch (error) { logger.error(`Failed to parse DCD output: ${error.message}`, { function: 'parseDCDOutput', outputLength: output?.length || 0, errorType: error.constructor.name, stack: error.stack, }); throw new Error(`Failed to parse DCD output: ${error.message}`); } } generateCacheKey(type, params, _getDeps = getDeps) { const { createHash } = _getDeps(); const hash = createHash('md5'); hash.update(JSON.stringify({ type, params, tool: 'dcd' })); return hash.digest('hex'); } /** * Generate unique request ID for deduplication * @param {Object} params - Request parameters * @returns {string} Request ID */ generateRequestId(params, _getDeps = getDeps) { const { createHash } = _getDeps(); const hash = createHash('md5'); hash.update(JSON.stringify({ tool: 'dcd', params, timestamp: Math.floor(Date.now() / 1000) })); return hash.digest('hex'); } /** * Detect analysis context from parameters and agent message * @param {Object} params - Analysis parameters * @param {string} agentMessage - Message from AI agent * @returns {string} Detected context */ // NOTE: Context detection is now handled by ResponseOptimizer.detectContext() // This ensures consistent behavior across all tools and plugins /** * Execute the actual analysis (abstracted for reuse) * @param {string} path - Path to analyze * @param {Object} options - Analysis options * @returns {Promise<Object>} Analysis result */ async executeAnalysis(path, options, _getDeps = getDeps) { const { logger } = _getDeps(); try { if (this.config.development?.mockMode) { return this.mockAnalyzeDuplicates({ path, ...options }); } else { return this.runDCDAnalysis(path, options); } } catch (error) { logger.error(`DCD executeAnalysis failed: ${error.message}`, { function: 'executeAnalysis', path, options, errorType: error.constructor.name, stack: error.stack, }); // Re-throw to preserve original error context throw error; } } generateFingerprint(content, _getDeps = getDeps) { const { createHash, logger } = _getDeps(); try { const hash = createHash('md5'); hash.update(content); return hash.digest('hex').substring(0, 8); } catch (error) { logger.error(`Failed to generate fingerprint: ${error.message}`, { function: 'generateFingerprint', contentLength: content?.length || 0, errorType: error.constructor.name, stack: error.stack, }); throw new Error(`Failed to generate fingerprint: ${error.message}`); } } async mockAnalyzeDuplicates(params, _getDeps = getDeps) { const { logger, join } = _getDeps(); try { logger.info('Running mock DCD duplicate analysis'); await new Promise(resolve => { return setTimeout(resolve, 300); }); return { summary: { totalFiles: 5, duplicateLines: 67, duplicateBlocks: 3, analysisTime: '0.3s', tool: 'DCD (Mock Mode)', }, duplicates: [ { fingerprint: 'dcd12345', lineCount: 23, occurrences: [ { file: join(params.path, 'utils.js'), startLine: 15, endLine: 37, }, { file: join(params.path, 'helpers.js'), startLine: 8, endLine: 30, }, ], }, ], }; } catch (error) { logger.error(`Mock DCD analysis failed: ${error.message}`, { function: 'mockAnalyzeDuplicates', params, errorType: error.constructor.name, stack: error.stack, }); // Re-throw to preserve original error context throw error; } } async mockScanRepository(_params, _getDeps = getDeps) { const { logger } = _getDeps(); try { logger.info('Running mock DCD repository scan'); await new Promise(resolve => { return setTimeout(resolve, 800); }); return { summary: { totalFiles: 89, duplicateLines: 234, duplicateBlocks: 12, analysisTime: '0.8s', tool: 'DCD (Mock Mode)', }, duplicates: [], }; } catch (error) { logger.error(`Mock DCD repository scan failed: ${error.message}`, { function: 'mockScanRepository', params: _params, errorType: error.constructor.name, stack: error.stack, }); // Re-throw to preserve original error context throw error; } } getOptimizationInfo(_getDeps = getDeps) { const { logger } = _getDeps(); logger.debug('Returning optimization system information'); return { system: 'Simple progressive disclosure optimization', usage: { default: 'All analysis tools return optimized overviews by default', detailed: 'Say "show details" in your request to get complete unfiltered analysis', examples: [ 'Normal: "analyze duplicates in src/" → optimized overview', 'Detailed: "show details of duplicates in src/" → complete analysis', ], }, benefits: { speed: 'Overview responses are 90%+ smaller and faster', relevance: 'Get key insights without information overload', simplicity: 'No complex keywords to remember - just "show details"', progressive: 'Start with overview, expand to details when needed', }, }; } async getPluginStatus() { return { plugin: 'dcd', name: 'DCD (Duplicate Code Detector)', version: '1.1.0', status: this.isDCDAvailable ? 'available' : 'unavailable', mockMode: this.config.development?.mockMode || false, executable: this.isDCDAvailable ? 'dcd (from PATH)' : 'not found', installation: { command: 'go install github.com/boyter/dcd@latest', description: 'Install DCD using Go package manager', homepage: 'https://github.com/boyter/dcd', license: 'AGPL-3.0 (open source)', }, tools: this.isDCDAvailable || this.config.development?.mockMode ? ['analyze_duplicates_dcd', 'scan_repository_dcd', 'get_optimization_info'] : [], message: this.isDCDAvailable ? 'DCD is available and ready for analysis' : this.config.development?.mockMode ? 'DCD is in mock mode - using simulated results' : 'DCD is not available. Install with: go install github.com/boyter/dcd@latest', }; } async getInstallGuide() { return { data: `# DCD Installation Guide ## Quick Install (Recommended) \`\`\`bash # Install DCD via Go (requires Go 1.19+) go install github.com/boyter/dcd@latest \`\`\` ## Manual Install 1. Download binary from: https://github.com/boyter/dcd/releases 2. Extract and place in your PATH 3. Verify: \`dcd --version\` ## Usage Examples \`\`\`bash # Basic analysis dcd src/ # With options dcd --match-length 10 --include-ext js,ts --exclude-pattern node_modules src/ # Fuzzy matching dcd --fuzz 10 src/ \`\`\` ## License DCD is licensed under AGPL-3.0 - much more permissive than Simian! ## Why DCD? - ✅ Open source (AGPL-3.0) - ✅ Fast (written in Go) - ✅ No licensing restrictions - ✅ Active development - ✅ CLI-friendly for CI/CD `, mimeType: 'text/markdown', }; } /** * Get comprehensive capability description for AI consumption * This provides detailed information about what the plugin can do */ async getCapabilityDescription() { return { mimeType: 'application/json', data: { plugin: this.capabilityDefinition.pluginName, metadata: { generatedAt: new Date().toISOString(), pluginVersion: '1.0.0', dcdVersion: '1.1.0', status: this.isDCDAvailable ? 'ready' : 'dcd-not-available', mockMode: this.config.development?.mockMode || false, license: 'AGPL-3.0', homepage: 'https://github.com/boyter/dcd', }, tools: Array.from(this.capabilityDefinition.tools.values()), resources: Array.from(this.capabilityDefinition.resources.values()), operations: Array.from(this.capabilityDefinition.operations.values()), aiInstructions: { overview: 'This plugin provides duplicate code detection using DCD (Duplicate Code Detector), an open-source alternative to Simian.', whenToUse: [ 'When you need to find duplicate code in a project', 'For code quality assessment and technical debt analysis', 'During code reviews to identify copy-paste programming', 'For refactoring planning and cleanup', 'When setting up quality gates in CI/CD', 'For open source projects without licensing restrictions', ], parameterGuidance: { matchLength: 'Lower values (3-5) catch smaller duplicates but may have more noise. Higher values (8-12) focus on significant duplicates.', fuzziness: 'Use 0 for exact matches, 1-20 for minor variations (whitespace, variable names), 50+ for structural similarities.', extensions: 'Filter by file types to focus analysis. Common: [".js", ".ts", ".jsx", ".py", ".java", ".go"]', excludePatterns: 'Always exclude: node_modules, .git, dist, build, vendor for better performance.', }, interpretingResults: { duplicateLines: 'Total lines involved in duplications - high numbers indicate significant duplication', duplicateBlocks: 'Number of distinct duplicate patterns - each may have multiple occurrences', fingerprint: 'Unique identifier for each duplicate pattern - same fingerprint = same code block', occurrences: 'All locations where the same duplicate appears - candidates for refactoring', }, }, }, }; } async shutdown(_getDeps = getDeps) { const { logger } = _getDeps(); logger.info('Shutting down DCD plugin...'); if (this.cache) { await this.cache.shutdown(); } await super.shutdown(); } }