code-auditor-mcp
Version:
TypeScript/JavaScript code quality auditor with MCP server - Analyze code for SOLID principles, DRY violations, security patterns, and more
604 lines • 28.1 kB
JavaScript
// MCP servers use stdio: stdout for protocol messages, stderr for logging
// All log messages must go to stderr to avoid interfering with MCP protocol
import chalk from 'chalk';
console.error(chalk.blue('[INFO]'), 'Loading modules...');
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
console.error(chalk.blue('[INFO]'), 'MCP SDK loaded');
import { createAuditRunner } from './auditRunner.js';
console.error(chalk.blue('[INFO]'), 'Audit runner loaded');
import { searchFunctions, findDefinition, syncFileIndex } from './codeIndexService.js';
import { ConfigGeneratorFactory } from './generators/ConfigGeneratorFactory.js';
import { DEFAULT_SERVER_URL } from './constants.js';
console.error(chalk.blue('[INFO]'), 'Code index service loaded');
import path from 'node:path';
import fs from 'node:fs/promises';
import { CodeIndexDB } from './codeIndexDB.js';
console.error(chalk.blue('[INFO]'), 'All modules loaded successfully');
const tools = [
// Core Audit Tools
{
name: 'audit',
description: 'Run a comprehensive code audit on files or directories, including React component analysis',
parameters: [
{
name: 'path',
type: 'string',
required: false,
description: 'The file or directory path to audit (defaults to current directory)',
default: process.cwd(),
},
{
name: 'analyzers',
type: 'array',
required: false,
description: 'List of analyzers to run (solid, dry, security, react, data-access)',
default: ['solid', 'dry'],
},
{
name: 'minSeverity',
type: 'string',
required: false,
description: 'Minimum severity level to report',
default: 'warning',
enum: ['info', 'warning', 'critical'],
},
{
name: 'indexFunctions',
type: 'boolean',
required: false,
description: 'Automatically index functions during audit',
default: true,
},
],
},
{
name: 'audit_health',
description: 'Quick health check of a codebase with key metrics',
parameters: [
{
name: 'path',
type: 'string',
required: false,
description: 'The directory path to check',
default: process.cwd(),
},
{
name: 'threshold',
type: 'number',
required: false,
description: 'Health score threshold (0-100) for pass/fail',
default: 70,
},
{
name: 'indexFunctions',
type: 'boolean',
required: false,
description: 'Automatically index functions during health check',
default: true,
},
],
},
// Code Index Tools
{
name: 'search_code',
description: 'Search indexed functions and React components with natural language queries. Supports operators: entity:component, component:functional|class|memo|forwardRef, hook:useState|useEffect|etc, prop:propName, dep:packageName, dependency:lodash, uses:express, calls:functionName, calledby:functionName, dependents-of:functionName, used-by:functionName, depends-on:module, imports-from:file, unused-imports, dead-imports, type:fileType, file:path, lang:language, complexity:1-10, jsdoc:true|false',
parameters: [
{
name: 'query',
type: 'string',
required: true,
description: 'Search query with natural language and/or operators. Examples: "Button component:functional", "entity:component hook:useState", "render prop:onClick", "dep:lodash", "calls:validateUser", "unused-imports", "dependents-of:authenticate"',
},
{
name: 'filters',
type: 'object',
required: false,
description: 'Optional filters (language, filePath, dependencies, componentType, entityType)',
},
{
name: 'limit',
type: 'number',
required: false,
description: 'Maximum results to return',
default: 50,
},
{
name: 'offset',
type: 'number',
required: false,
description: 'Offset for pagination',
default: 0,
},
],
},
{
name: 'find_definition',
description: 'Find the exact definition of a specific function or React component',
parameters: [
{
name: 'name',
type: 'string',
required: true,
description: 'Function or component name to find',
},
{
name: 'filePath',
type: 'string',
required: false,
description: 'Optional file path to narrow search',
},
],
},
{
name: 'sync_index',
description: 'Synchronize, cleanup, or reset the code index',
parameters: [
{
name: 'mode',
type: 'string',
required: false,
description: 'Operation mode: sync (update), cleanup (remove stale), or reset (clear all)',
default: 'sync',
enum: ['sync', 'cleanup', 'reset'],
},
{
name: 'path',
type: 'string',
required: false,
description: 'Optional specific path to sync',
},
],
},
// AI Configuration Tool
{
name: 'generate_ai_config',
description: 'Generate configuration files for AI coding assistants',
parameters: [
{
name: 'tools',
type: 'array',
required: true,
description: 'AI tools to configure (cursor, continue, copilot, claude, zed, windsurf, cody, aider, cline, pearai)',
},
{
name: 'outputDir',
type: 'string',
required: false,
description: 'Output directory for configuration files',
default: '.',
},
],
},
// Workflow Guide Tool
{
name: 'get_workflow_guide',
description: 'Get recommended workflows and best practices for using code auditor tools effectively',
parameters: [
{
name: 'scenario',
type: 'string',
required: false,
description: 'Specific scenario: initial-setup, react-development, code-review, find-patterns, maintenance. Leave empty to see all.',
},
],
},
];
async function startMcpServer() {
console.error(chalk.blue('[INFO]'), 'Starting Code Auditor MCP Server...');
const server = new Server({
name: 'code-auditor',
version: '0.1.0',
}, {
capabilities: {
tools: {},
},
});
console.error(chalk.blue('[INFO]'), 'Server instance created');
// Handle tool listing
console.error(chalk.blue('[INFO]'), 'Setting up request handlers...');
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.error(chalk.blue('[DEBUG]'), 'Handling ListTools request');
return {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: {
type: 'object',
properties: tool.parameters.reduce((acc, param) => {
acc[param.name] = {
type: param.type,
description: param.description,
...(param.default !== undefined && { default: param.default }),
...(param.enum && { enum: param.enum }),
};
return acc;
}, {}),
required: tool.parameters.filter(p => p.required).map(p => p.name),
},
})),
};
});
console.error(chalk.blue('[INFO]'), `Registered ${tools.length} tools`);
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(chalk.blue('[DEBUG]'), `Handling CallTool request for: ${name}`);
try {
let result;
switch (name) {
case 'audit': {
const auditPath = path.resolve(args.path || process.cwd());
const indexFunctions = args.indexFunctions !== false; // Default true
// Check if path is a file or directory
const stats = await fs.stat(auditPath).catch(() => null);
const isFile = stats?.isFile() || false;
const options = {
projectRoot: isFile ? path.dirname(auditPath) : auditPath,
enabledAnalyzers: args.analyzers || ['solid', 'dry', 'security'],
minSeverity: (args.minSeverity || 'warning'),
verbose: false,
indexFunctions,
...(isFile && { includePaths: [auditPath] }),
};
const runner = createAuditRunner(options);
const auditResult = await runner.run();
// Handle function indexing if enabled and functions were collected
let indexingResult = null;
if (indexFunctions && auditResult.metadata.fileToFunctionsMap) {
try {
const syncStats = { added: 0, updated: 0, removed: 0 };
// Sync each file's functions to handle additions, updates, and removals
for (const [filePath, functions] of Object.entries(auditResult.metadata.fileToFunctionsMap)) {
const fileStats = await syncFileIndex(filePath, functions);
syncStats.added += fileStats.added;
syncStats.updated += fileStats.updated;
syncStats.removed += fileStats.removed;
}
indexingResult = {
success: true,
registered: syncStats.added + syncStats.updated,
failed: 0,
syncStats
};
console.error(chalk.blue('[INFO]'), `Synced functions: ${syncStats.added} added, ${syncStats.updated} updated, ${syncStats.removed} removed`);
}
catch (error) {
console.error(chalk.yellow('[WARN]'), 'Failed to sync functions:', error);
}
}
// Format for MCP
result = {
summary: {
totalViolations: auditResult.summary.totalViolations,
criticalIssues: auditResult.summary.criticalIssues,
warnings: auditResult.summary.warnings,
suggestions: auditResult.summary.suggestions,
filesAnalyzed: auditResult.metadata.filesAnalyzed,
executionTime: auditResult.metadata.auditDuration,
healthScore: calculateHealthScore(auditResult),
},
violations: getAllViolations(auditResult).slice(0, 100), // Limit to first 100
recommendations: auditResult.recommendations,
...(indexingResult && { functionIndexing: indexingResult }),
};
break;
}
case 'audit_health': {
const auditPath = path.resolve(args.path || process.cwd());
const threshold = args.threshold || 70;
const indexFunctions = args.indexFunctions !== false; // Default true
const runner = createAuditRunner({
projectRoot: auditPath,
enabledAnalyzers: ['solid', 'dry', 'security'],
minSeverity: 'warning',
verbose: false,
indexFunctions, // Pass the flag to the runner
});
const auditResult = await runner.run();
const healthScore = calculateHealthScore(auditResult);
// Handle function indexing if enabled and functions were collected
let indexingResult = null;
if (indexFunctions && auditResult.metadata.fileToFunctionsMap) {
try {
const syncStats = { added: 0, updated: 0, removed: 0 };
// Sync each file's functions to handle additions, updates, and removals
for (const [filePath, functions] of Object.entries(auditResult.metadata.fileToFunctionsMap)) {
const fileStats = await syncFileIndex(filePath, functions);
syncStats.added += fileStats.added;
syncStats.updated += fileStats.updated;
syncStats.removed += fileStats.removed;
}
indexingResult = {
success: true,
registered: syncStats.added + syncStats.updated,
failed: 0,
syncStats
};
console.error(chalk.blue('[INFO]'), `Synced functions: ${syncStats.added} added, ${syncStats.updated} updated, ${syncStats.removed} removed`);
}
catch (error) {
console.error(chalk.yellow('[WARN]'), 'Failed to sync functions:', error);
}
}
result = {
healthScore,
threshold,
passed: healthScore >= threshold,
status: healthScore >= threshold ? 'healthy' : 'needs-attention',
metrics: {
filesAnalyzed: auditResult.metadata.filesAnalyzed,
totalViolations: auditResult.summary.totalViolations,
criticalViolations: auditResult.summary.criticalIssues,
warningViolations: auditResult.summary.warnings,
},
recommendation: getHealthRecommendation(healthScore, auditResult),
...(indexingResult && { functionIndexing: indexingResult }),
};
break;
}
case 'search_code': {
const query = args.query;
const filters = args.filters;
const limit = args.limit || 50;
const offset = args.offset || 0;
// Allow empty query to return all results
if (query !== undefined && typeof query !== 'string') {
throw new Error('query must be a string');
}
result = await searchFunctions({
query,
filters,
limit,
offset
});
break;
}
case 'find_definition': {
const name = args.name;
const filePath = args.filePath;
if (!name || typeof name !== 'string') {
throw new Error('name must be a non-empty string');
}
const definition = await findDefinition(name, filePath);
result = definition || { error: 'Function not found' };
break;
}
case 'generate_ai_config': {
const tools = args.tools;
const serverUrl = args.serverUrl || DEFAULT_SERVER_URL;
const outputDir = args.outputDir || '.';
const overwrite = args.overwrite || false;
if (!Array.isArray(tools) || tools.length === 0) {
throw new Error('tools parameter must be a non-empty array');
}
const factory = new ConfigGeneratorFactory(serverUrl);
const generatedFiles = [];
const errors = [];
for (const tool of tools) {
try {
const generator = factory.createGenerator(tool);
if (!generator) {
errors.push(`Unknown tool: ${tool}`);
continue;
}
const config = generator.generateConfig();
const outputPath = path.resolve(outputDir, config.filename);
// Check if file exists
let fileExists = false;
try {
await fs.access(outputPath);
fileExists = true;
}
catch {
// File doesn't exist
}
if (fileExists && !overwrite) {
errors.push(`File already exists: ${config.filename} (use overwrite: true to replace)`);
continue;
}
// Ensure directory exists
await fs.mkdir(path.dirname(outputPath), { recursive: true });
// Write main config file
await fs.writeFile(outputPath, config.content);
generatedFiles.push(config.filename);
// Write additional files if any
if (config.additionalFiles) {
for (const additionalFile of config.additionalFiles) {
const additionalPath = path.resolve(outputDir, additionalFile.filename);
await fs.mkdir(path.dirname(additionalPath), { recursive: true });
await fs.writeFile(additionalPath, additionalFile.content);
generatedFiles.push(additionalFile.filename);
}
}
}
catch (error) {
errors.push(`Failed to generate config for ${tool}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
result = {
success: errors.length === 0,
generatedFiles,
errors: errors.length > 0 ? errors : undefined,
totalRequested: tools.length,
totalGenerated: generatedFiles.length
};
break;
}
case 'get_workflow_guide': {
const scenario = args.scenario;
const { getWorkflowGuide, getWorkflowTips } = await import('./mcp-tools/workflowGuide.js');
try {
const workflows = getWorkflowGuide(scenario);
const tips = getWorkflowTips();
result = {
success: true,
...(scenario ? { workflow: workflows } : { workflows }),
tips
};
}
catch (error) {
result = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
availableScenarios: ['initial-setup', 'react-development', 'code-review', 'find-patterns', 'maintenance']
};
}
break;
}
case 'sync_index': {
const mode = args.mode || 'sync';
const targetPath = args.path;
const db = CodeIndexDB.getInstance();
await db.initialize();
switch (mode) {
case 'cleanup': {
const cleanupResult = await db.bulkCleanup();
result = {
mode: 'cleanup',
success: true,
scannedFiles: cleanupResult.scannedCount,
removedEntries: cleanupResult.removedCount,
removedFiles: cleanupResult.removedFiles,
errors: cleanupResult.errors,
message: `Cleaned up ${cleanupResult.removedCount} entries from ${cleanupResult.removedFiles.length} deleted files`
};
break;
}
case 'reset': {
await db.clearIndex();
result = {
mode: 'reset',
success: true,
message: 'Index cleared successfully'
};
break;
}
case 'sync':
default: {
if (targetPath) {
// Sync specific file
const syncResult = await db.synchronizeFile(path.resolve(targetPath));
result = {
mode: 'sync',
success: true,
path: targetPath,
...(syncResult || { message: 'File not found' })
};
}
else {
// Deep sync all files
const syncResult = await db.deepSync();
result = {
mode: 'sync',
success: true,
syncedFiles: syncResult.syncedFiles,
addedFunctions: syncResult.addedFunctions,
updatedFunctions: syncResult.updatedFunctions,
removedFunctions: syncResult.removedFunctions,
errors: syncResult.errors,
message: `Synced ${syncResult.syncedFiles} files: ${syncResult.addedFunctions} added, ${syncResult.updatedFunctions} updated, ${syncResult.removedFunctions} removed`
};
}
break;
}
}
break;
}
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: error instanceof Error ? error.message : 'Unknown error',
tool: name,
}),
},
],
isError: true,
};
}
});
console.error(chalk.blue('[INFO]'), 'Request handlers configured');
const transport = new StdioServerTransport();
console.error(chalk.blue('[INFO]'), 'Creating stdio transport...');
await server.connect(transport);
console.error(chalk.green('✓ Code Auditor MCP Server started'));
console.error(chalk.gray('Listening on stdio...'));
}
function getAllViolations(result) {
const violations = [];
for (const [analyzerName, analyzerResult] of Object.entries(result.analyzerResults)) {
for (const violation of analyzerResult.violations) {
violations.push({
...violation,
analyzer: analyzerName,
});
}
}
return violations;
}
function calculateHealthScore(result) {
let score = 100;
const critical = result.summary.criticalIssues;
const warning = result.summary.warnings;
score -= critical * 10;
score -= warning * 2;
return Math.max(0, Math.min(100, score));
}
function generateRecommendations(result) {
const recommendations = [];
if (result.summary.criticalIssues > 0) {
recommendations.push({
priority: 'high',
title: 'Fix critical violations immediately',
description: `${result.summary.criticalIssues} critical issues require immediate attention`,
});
}
// Add more recommendation logic based on patterns
return recommendations;
}
function getHealthRecommendation(score, result) {
if (score >= 90)
return 'Excellent code health!';
if (score >= 70)
return 'Good code health with room for improvement';
if (result.summary.criticalIssues > 0) {
return `Fix ${result.summary.criticalIssues} critical violations first`;
}
return 'Code health needs attention - run detailed audit';
}
// Error handlers
process.on('uncaughtException', (error) => {
console.error(chalk.red('[ERROR]'), 'Uncaught exception:', error);
console.error(chalk.red('[ERROR]'), 'Stack:', error.stack);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error(chalk.red('[ERROR]'), 'Unhandled rejection at:', promise);
console.error(chalk.red('[ERROR]'), 'Reason:', reason);
process.exit(1);
});
// Start server
console.error(chalk.blue('[INFO]'), 'Initializing server...');
startMcpServer().catch(error => {
console.error(chalk.red('[ERROR]'), 'Failed to start MCP server:', error);
console.error(chalk.red('[ERROR]'), 'Stack:', error.stack);
process.exit(1);
});
//# sourceMappingURL=mcp.js.map