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