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

924 lines (818 loc) 27.5 kB
/** * Simian Plugin * Integrates Simian Similarity Analyzer with the MCP server */ import { spawn } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; import { createHash } from 'crypto'; import { homedir } from 'os'; import { AnalysisPlugin } from '../../core/plugin-manager.js'; import { createLogger } from '../../utils/logger.js'; import { SimianOutputParser } from './output-parser.js'; import { AnalysisCache } from './cache.js'; import { createSimianCapabilityDefinition } from '../../core/plugin-definitions.js'; import { validateAnalysisParams, getSecurityConfig, SecurityValidationError, } from '../../utils/security.js'; // Create logger once for this module const logger = createLogger('simian-plugin'); /** * Dependency injection function for Simian plugin */ function getDeps() { return { spawn, existsSync, join, createHash, homedir, logger, SimianOutputParser, AnalysisCache, createSimianCapabilityDefinition, validateAnalysisParams, getSecurityConfig, SecurityValidationError, }; } /** * Simian Plugin - provides code similarity analysis via Simian */ export class SimianPlugin extends AnalysisPlugin { constructor(config, _getDeps = getDeps) { super('simian', config); const { SimianOutputParser, createSimianCapabilityDefinition, getSecurityConfig } = _getDeps(); this.parser = new SimianOutputParser(); this.cache = null; this.isSimianAvailable = false; this.capabilityDefinition = createSimianCapabilityDefinition(); this.securityConfig = getSecurityConfig(process.env.NODE_ENV || 'development'); // Use ~/lib/simian/ as default path if not specified if (!this.config.executable) { this.config.executable = this.getDefaultSimianPath(); } } getDefaultSimianPath(_getDeps = getDeps) { const { homedir, join, existsSync, logger } = _getDeps(); const homeDir = homedir(); // Check common Simian installation paths (ordered by preference) const possiblePaths = [ // Version-specific installations (recommended) join(homeDir, 'lib', 'simian-4.0.0', 'simian-4.0.0.jar'), join(homeDir, 'lib', 'simian-2.5.10', 'simian-2.5.10.jar'), join(homeDir, 'lib', 'simian', 'simian.jar'), // Binary installations join(homeDir, 'lib', 'simian', 'bin', 'simian'), join(homeDir, 'lib', 'simian', 'bin', 'simian.bat'), join(homeDir, 'lib', 'simian', 'simian'), join(homeDir, 'lib', 'simian', 'simian.bat'), // Legacy locations join(homeDir, 'lib', 'simian-4.0.0', 'bin', 'simian'), join(homeDir, 'lib', 'simian-2.5.10', 'bin', 'simian'), ]; for (const path of possiblePaths) { if (existsSync(path)) { logger.debug(`Found Simian at: ${path}`); return path; } } // Return preferred default if none found logger.debug('No Simian installation found, using default path'); return possiblePaths[0]; // ~/lib/simian-4.0.0/simian-4.0.0.jar } isJarFile(path) { return path.toLowerCase().endsWith('.jar'); } getExecutionCommand(path) { if (this.isJarFile(path)) { // For .jar files, use Java return { executable: this.config.javaExecutable || 'java', args: ['-jar', path], }; } else { // For native executables return { executable: path, args: [], }; } } async initialize(_getDeps = getDeps) { const { existsSync, AnalysisCache, logger } = _getDeps(); logger.info('Initializing Simian plugin...'); try { this.isSimianAvailable = existsSync(this.config.executable); if (!this.isSimianAvailable) { logger.warn(`Simian executable not found at: ${this.config.executable}`); if (!this.config.development?.mockMode) { logger.warn('Enabling mock mode due to missing Simian executable'); this.config.development = { ...this.config.development, mockMode: true }; } } // Initialize cache if (this.config.cache?.enabled) { this.cache = new AnalysisCache(this.config.cache); await this.cache.initialize(); } logger.info( `Simian plugin initialized (mock mode: ${this.config.development?.mockMode || false})` ); } catch (error) { logger.error('Failed to initialize Simian plugin:', error); throw error; } } getTools() { // Only expose tools if Simian is available or mock mode is enabled if (!this.isSimianAvailable && !this.config.development?.mockMode) { return []; } return this.capabilityDefinition.getToolDefinitions(); } getResources() { const resources = this.capabilityDefinition.getResourceDefinitions(); // Add the additional resources not in the definition resources.push({ uri: 'simian://analysis/history', name: 'Analysis History', description: 'Historical analysis results and trends', mimeType: 'application/json', }); // Add capabilities resource for AI consumption resources.push({ uri: 'simian://capabilities', name: 'Simian Plugin Capabilities', description: 'Comprehensive capability information for AI consumption', mimeType: 'application/json', }); return resources; } getPrompts() { return [ { name: 'analyze_codebase_quality', description: 'Analyze codebase for technical debt and quality issues', arguments: [ { name: 'codebase_path', description: 'Path to the codebase to analyze', required: true, }, { name: 'focus_areas', description: 'Specific areas to focus on (duplicates, complexity, maintainability)', required: false, }, ], }, { name: 'generate_refactoring_plan', description: 'Generate a comprehensive refactoring plan based on duplicate analysis', arguments: [ { name: 'analysis_path', description: 'Path to analyze for refactoring opportunities', required: true, }, { name: 'priority_level', description: 'Priority level for refactoring (high, medium, low)', required: false, }, ], }, ]; } async executeTool(toolName, params, _getDeps = getDeps) { const { logger } = _getDeps(); logger.info(`Executing tool: ${toolName}`, params); try { switch (toolName) { case 'analyze_duplicates': return await this.analyzeDuplicates(params); case 'scan_repository': return await this.scanRepository(params); case 'suggest_refactoring': return await this.suggestRefactoring(params); default: throw new Error(`Unknown tool: ${toolName}`); } } catch (error) { logger.error(`Simian tool execution failed for ${toolName}:`, error); throw error; } } async getResource(resourceUri) { switch (resourceUri) { case 'simian://analysis/history': return await this.getAnalysisHistory(); case 'simian://config': return await this.getCurrentConfig(); case 'simian://status': return await this.getPluginStatus(); case 'simian://capabilities': return this.getCapabilityDescription(); default: throw new Error(`Unknown resource: ${resourceUri}`); } } getCapabilityDescription() { return { plugin: { name: 'Simian Similarity Analyzer', version: '1.0.0', description: 'Commercial code similarity analysis tool for detecting duplicate code', provider: 'Red Hill Consulting', type: 'analysis', category: 'code-quality', license: 'Commercial', homepage: 'https://www.harukizaemon.com/simian/', }, features: { languageSupport: [ 'Java', 'C#', 'C++', 'C', 'JavaScript', 'Python', 'Ruby', 'Objective-C', 'PHP', 'VB.NET', 'ActionScript', 'Swift', 'Go', 'Kotlin', 'Scala', 'Groovy', 'PLSQL', 'ColdFusion', 'COBOL', 'Fortran', 'Text files', ], analysisTypes: ['code-duplication', 'similarity-detection', 'clone-detection'], outputFormats: ['xml', 'text', 'html'], thresholdControl: true, ignoreOptions: { strings: true, numbers: true, characters: true, curlyBraces: true, identifiers: true, }, }, installation: this.getInstallationGuide(), configuration: { required: ['executable'], optional: ['javaExecutable', 'defaultThreshold', 'timeout', 'cache'], defaults: { defaultThreshold: 6, timeout: 30000, javaExecutable: 'java', }, }, status: { available: this.isSimianAvailable, executable: this.config.executable, mockMode: this.config.development?.mockMode || false, cacheEnabled: this.config.cache?.enabled || false, }, aiGuidance: { description: 'Simian is a commercial similarity analyzer that detects duplication in code by comparing text blocks across files.', parameterRecommendations: { threshold: 'Use 6-12 for general code analysis. Lower values (2-5) detect more duplicates but may include false positives. Higher values (13+) are more conservative.', extensions: 'Specify file extensions to focus analysis on relevant code files (e.g., [".js", ".java", ".py"])', ignoreOptions: 'Use ignore options to reduce noise: ignore strings/numbers for data-heavy code, ignore identifiers for similar logic patterns', }, resultInterpretation: { duplicateBlocks: 'Each block shows exact duplicate code locations with line numbers and file paths', similarity: 'Higher line counts indicate more significant duplication that should be prioritized for refactoring', actionableInsights: 'Focus on duplicates with 10+ lines first, consider extracting common functions or creating shared modules', }, }, }; } getInstallationGuide() { return { overview: 'Simian is a commercial similarity analyzer that detects duplication in code.', requirements: { license: 'Commercial license required for production use', java: 'Java Runtime Environment (JRE) 8 or higher for .jar files', platforms: ['Windows', 'macOS', 'Linux'], }, steps: [ { step: 1, title: 'Download Simian', description: 'Download from https://www.harukizaemon.com/simian/download.html', notes: ['Commercial license required', 'Free trial available'], }, { step: 2, title: 'Extract to ~/lib/simian/', description: 'Extract the downloaded archive to your home lib directory', commands: [ 'mkdir -p ~/lib', 'cd ~/lib', 'unzip simian-4.0.0.zip', 'mv simian-4.0.0 simian', ], }, { step: 3, title: 'Configure MCP Server', description: 'Set executable path in configuration', example: { plugins: { simian: { enabled: true, executable: '~/lib/simian/simian-4.0.0.jar', javaExecutable: 'java', defaultThreshold: 6, }, }, }, }, { step: 4, title: 'Verify Installation', description: 'Test the installation', commands: ['java -jar ~/lib/simian/simian-4.0.0.jar -help'], }, ], troubleshooting: [ { issue: 'Java not found', solution: 'Install Java 8+ and ensure "java" is in your PATH', verification: 'java -version', }, { issue: 'License not found', solution: 'Ensure simian.lic is in the same directory as the .jar file', }, { issue: 'Permission denied', solution: 'Make sure the executable has proper permissions', command: 'chmod +x ~/lib/simian/bin/simian', }, ], alternatives: [ { name: 'DCD', description: 'Open-source duplicate code detector', availability: 'Free alternative with similar functionality', }, { name: 'PMD CPD', description: 'Open-source copy-paste detector', availability: 'Free alternative included with PMD', }, ], }; } async getPrompt(promptName, params) { switch (promptName) { case 'analyze_codebase_quality': return await this.generateCodebaseQualityPrompt(params); case 'generate_refactoring_plan': return await this.generateRefactoringPlanPrompt(params); default: throw new Error(`Unknown prompt: ${promptName}`); } } async analyzeDuplicates(params, _getDeps = getDeps) { const { validateAnalysisParams, SecurityValidationError, logger } = _getDeps(); try { // Validate and sanitize parameters const validatedParams = validateAnalysisParams(params, { pathOptions: this.securityConfig, }); const { path, threshold = this.config.defaultThreshold, extensions, options, } = validatedParams; // Check cache first const cacheKey = this.generateCacheKey('duplicates', validatedParams); if (this.cache) { const cached = await this.cache.get(cacheKey); if (cached) { logger.debug('Returning cached analysis result'); return cached; } } let result; if (this.config.development?.mockMode) { result = await this.mockAnalyzeDuplicates(validatedParams); } else { result = await this.runSimianAnalysis(path, { threshold, extensions, options, formatter: 'xml', }); } // Cache the result if (this.cache) { await this.cache.set(cacheKey, result); } return result; } catch (error) { if (error instanceof SecurityValidationError) { logger.warn(`Security validation failed for Simian analyzeDuplicates: ${error.message}`); throw new Error(`Invalid parameters: ${error.message}`); } throw error; } } /** * Scan entire repository for duplicates */ async scanRepository(params, _getDeps = getDeps) { const { validateAnalysisParams, SecurityValidationError, logger } = _getDeps(); try { // Validate and sanitize parameters const validatedParams = validateAnalysisParams( { path: params.repository, threshold: params.threshold || this.config.defaultThreshold, includePatterns: params.includePatterns, excludePatterns: params.excludePatterns, }, { pathOptions: this.securityConfig, } ); const { path, threshold, includePatterns, excludePatterns } = validatedParams; if (this.config.development?.mockMode) { return await this.mockScanRepository(validatedParams); } return await this.runSimianAnalysis(path, { threshold, includePatterns, excludePatterns, extensions: ['.js'], // Start with JavaScript files for testing formatter: 'xml', recursive: true, }); } catch (error) { if (error instanceof SecurityValidationError) { logger.warn(`Security validation failed for Simian scanRepository: ${error.message}`); throw new Error(`Invalid parameters: ${error.message}`); } throw error; } } /** * Suggest refactoring opportunities */ async suggestRefactoring(params) { // First analyze duplicates const analysis = await this.analyzeDuplicates(params); // Generate refactoring suggestions based on analysis return this.generateRefactoringSuggestions(analysis, params); } /** * Run Simian analysis with specified options */ async runSimianAnalysis(path, options = {}, _getDeps = getDeps) { const { spawn, logger } = _getDeps(); if (!this.isSimianAvailable) { throw new Error('Simian executable not available'); } const simianArgs = this.buildSimianArgs(path, options); const { executable, args } = this.getExecutionCommand(this.config.executable); const fullArgs = [...args, ...simianArgs]; logger.debug(`Executing: ${executable} ${fullArgs.join(' ')}`); return new Promise((resolve, reject) => { const simian = spawn(executable, fullArgs, { cwd: process.cwd(), timeout: this.config.timeout, }); let stdout = ''; let stderr = ''; simian.stdout.on('data', data => { stdout += data.toString(); }); simian.stderr.on('data', data => { stderr += data.toString(); }); simian.on('close', code => { if (code === 0 || code === 1) { // Simian returns 1 when duplicates are found try { const parsed = this.parser.parse(stdout, options.formatter || 'xml'); resolve(parsed); } catch (parseError) { reject(new Error(`Failed to parse Simian output: ${parseError.message}`)); } } else { reject(new Error(`Simian failed with code ${code}: ${stderr}`)); } }); simian.on('error', error => { if (this.isJarFile(this.config.executable)) { reject( new Error( `Failed to execute Simian Java .jar: ${error.message}. Make sure Java is installed and 'java' is in your PATH.` ) ); } else { reject(new Error(`Failed to execute Simian: ${error.message}`)); } }); }); } /** * Build Simian command line arguments */ buildSimianArgs(path, options = {}) { const args = []; // Add threshold if (options.threshold) { args.push(`-threshold=${options.threshold}`); } // Add formatter (default to XML for better parsing) const formatter = options.formatter || 'xml'; args.push(`-formatter=${formatter}`); // Add analysis options if (options.options) { const simianOptions = options.options; if (simianOptions.ignoreStrings) { args.push('-ignoreStrings'); } if (simianOptions.ignoreNumbers) { args.push('-ignoreNumbers'); } if (simianOptions.ignoreCharacters) { args.push('-ignoreCharacters'); } if (simianOptions.ignoreCurlyBraces) { args.push('-ignoreCurlyBraces'); } if (simianOptions.ignoreIdentifiers) { args.push('-ignoreIdentifiers'); } } // Add include patterns for file extensions if (options.extensions && options.extensions.length > 0) { const patterns = options.extensions .map(ext => { return `*${ext}`; }) .join(','); args.push(`-includes=${patterns}`); } // Add the path to analyze args.push(path); // Add include patterns if (options.includePatterns && options.includePatterns.length > 0) { for (const pattern of options.includePatterns) { args.push(pattern); } } // Add exclude patterns if (options.excludePatterns && options.excludePatterns.length > 0) { for (const pattern of options.excludePatterns) { args.push(`-excludes=${pattern}`); } } return args; } generateCacheKey(type, params, _getDeps = getDeps) { const { createHash } = _getDeps(); const key = `simian:${type}:${JSON.stringify(params)}`; return createHash('md5').update(key).digest('hex'); } async mockAnalyzeDuplicates(params, _getDeps = getDeps) { const { logger } = _getDeps(); logger.info('Running mock Simian duplicate analysis'); // Simulate analysis delay await new Promise(resolve => { return setTimeout(resolve, 100); }); return { tool: 'simian', version: '4.0.0', analysis: { type: 'duplicates', timestamp: new Date().toISOString(), path: params.path, threshold: params.threshold || this.config.defaultThreshold, options: params.options || {}, }, summary: { totalFiles: 42, filesWithDuplicates: 8, duplicateBlocks: 15, duplicateLines: 324, duplicationPercentage: 12.5, }, duplicates: [ { id: 1, lineCount: 23, tokenCount: 156, occurrences: [ { file: 'src/utils/helper.js', startLine: 45, endLine: 67, }, { file: 'src/components/validator.js', startLine: 123, endLine: 145, }, ], }, { id: 2, lineCount: 18, tokenCount: 98, occurrences: [ { file: 'src/models/user.js', startLine: 78, endLine: 95, }, { file: 'src/models/admin.js', startLine: 34, endLine: 51, }, { file: 'src/models/guest.js', startLine: 12, endLine: 29, }, ], }, ], recommendations: [ 'Extract common functionality from src/utils/helper.js and src/components/validator.js into a shared utility', 'Consider creating a base class for user models to eliminate duplication', 'Review threshold settings - current setting may be too sensitive', ], }; } async mockScanRepository(_params, _getDeps = getDeps) { const { logger } = _getDeps(); logger.info('Running mock repository scan'); // Simulate longer analysis await new Promise(resolve => { return setTimeout(resolve, 200); }); return { tool: 'simian', version: '4.0.0', analysis: { type: 'repository-scan', timestamp: new Date().toISOString(), scope: 'full-repository', }, summary: { totalFiles: 156, linesOfCode: 12453, filesWithDuplicates: 23, duplicateBlocks: 67, duplicateLines: 1234, duplicationPercentage: 9.9, }, hotspots: [ { directory: 'src/models/', duplicationPercentage: 25.3 }, { directory: 'src/utils/', duplicationPercentage: 18.7 }, { directory: 'src/components/', duplicationPercentage: 12.1 }, ], }; } generateRefactoringSuggestions(analysis, params) { const suggestions = []; if (analysis.duplicates && analysis.duplicates.length > 0) { for (const duplicate of analysis.duplicates) { if (duplicate.lineCount > (params.complexityThreshold || 10)) { suggestions.push({ type: 'extract-method', priority: 'high', description: `Extract ${duplicate.lineCount} duplicate lines into a shared method`, files: duplicate.occurrences.map(occ => { return occ.file; }), effort: 'medium', impact: 'high', }); } } } return { analysis: analysis.analysis, suggestions, metrics: { totalSuggestions: suggestions.length, highPriority: suggestions.filter(s => { return s.priority === 'high'; }).length, }, }; } async getAnalysisHistory() { // Mock implementation - in real version would query cache/database return []; } async getCurrentConfig() { return { executable: this.config.executable, available: this.isSimianAvailable, mockMode: this.config.development?.mockMode || false, }; } async getPluginStatus() { return { plugin: 'simian', name: 'Simian Similarity Analyzer', version: '2.5.10', status: this.isSimianAvailable ? 'available' : 'unavailable', mockMode: this.config.development?.mockMode || false, executable: this.isSimianAvailable ? this.config.executable : 'not found', installation: { path: '~/lib/simian-4.0.0/simian-4.0.0.jar', description: 'Install Simian JAR file in user library directory', homepage: 'https://simian.quandarypeak.com/', license: 'Commercial License Required', javaRequired: true, }, tools: this.isSimianAvailable || this.config.development?.mockMode ? ['analyze_duplicates', 'scan_repository', 'suggest_refactoring'] : [], message: this.isSimianAvailable ? 'Simian is available and ready for analysis' : this.config.development?.mockMode ? 'Simian is in mock mode - using simulated results' : 'Simian is not available. Install at ~/lib/simian-4.0.0/simian-4.0.0.jar (requires commercial license)', }; } async generateCodebaseQualityPrompt(params) { const analysis = await this.analyzeDuplicates({ path: params.codebase_path, threshold: 6, }); return `Based on the duplicate code analysis of ${params.codebase_path}: **Code Quality Assessment:** - Total duplication: ${analysis.summary?.duplicationPercentage || 0}% - Files with duplicates: ${analysis.summary?.filesWithDuplicates || 0} - Duplicate blocks found: ${analysis.summary?.duplicateBlocks || 0} **Focus Areas: ${params.focus_areas || 'duplicates, maintainability'}** Please analyze this codebase for: 1. Code duplication patterns and their impact 2. Maintainability concerns based on duplication 3. Technical debt indicators 4. Recommendations for improvement ${JSON.stringify(analysis, null, 2)}`; } async generateRefactoringPlanPrompt(params) { const suggestions = await this.suggestRefactoring({ path: params.analysis_path, complexityThreshold: 10, }); return `Generate a comprehensive refactoring plan for ${params.analysis_path}: **Priority Level: ${params.priority_level || 'medium'}** **Current State:** - Total refactoring suggestions: ${suggestions.metrics?.totalSuggestions || 0} - High priority items: ${suggestions.metrics?.highPriority || 0} **Refactoring Opportunities:** ${JSON.stringify(suggestions.suggestions, null, 2)} Please create a detailed refactoring plan that includes: 1. Prioritized list of refactoring tasks 2. Estimated effort and impact for each task 3. Dependencies between refactoring tasks 4. Risk assessment for each change 5. Recommended implementation order`; } async shutdown(_getDeps = getDeps) { const { logger } = _getDeps(); logger.info('Shutting down Simian plugin...'); try { if (this.cache) { await this.cache.close(); } } catch (error) { logger.warn('Error during cache shutdown:', error); } logger.info('Simian plugin shutdown complete'); } }