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