@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,246 lines (1,122 loc) • 41.5 kB
text/typescript
import { JSONSchema7 } from 'json-schema';
import { randomUUID } from 'crypto';
import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js';
import { ToolRegistration, RequestContext } from '../../core/types.js';
import { promises as fs } from 'fs';
import path from 'path';
/**
* Code Analysis Tools - 12-Factor MCP Implementation
*
* Implements Factor 2: Deterministic Execution with structured outputs
* Implements Factor 3: Stateless Processes with RequestContext
* Implements Factor 4: Structured Outputs for LLM consumption
*/
// Input type interfaces
interface AnalyzeCodeQualityInput {
files?: string[];
directories?: string[];
includeHistory?: boolean;
compareWithBaseline?: boolean;
maxIssues?: number;
excludePatterns?: string[];
customRules?: Array<{
name: string;
pattern: string;
severity: 'error' | 'warning' | 'info';
category: string;
message: string;
}>;
}
interface ReviewCodeInput {
file: string;
compareWith?: string;
includeHistory?: boolean;
focusAreas?: Array<'security' | 'performance' | 'maintainability' | 'style' | 'testing'>;
}
interface TrackCodeMetricsInput {
projectId?: string;
branch?: string;
commit?: string;
tags?: string[];
}
interface GetAnalysisHistoryInput {
limit?: number;
includeBaseline?: boolean;
}
interface GetCodeReviewsInput {
fileFilter?: string;
limit?: number;
}
interface SetMetricsBaselineInput {
historyId?: string;
tags?: string[];
}
interface GetMetricsTrendsInput {
metricPath: string;
days?: number;
}
interface ManageCustomRulesInput {
action: 'list' | 'add' | 'update' | 'delete' | 'toggle';
ruleId?: string;
rule?: {
name: string;
description: string;
pattern: string;
severity: 'error' | 'warning' | 'info';
category: string;
message: string;
};
}
/**
* Analyze code quality with comprehensive metrics and issue detection
*/
const analyzeCodeQualityTool = createTool<AnalyzeCodeQualityInput, any>({
name: 'analyze_code_quality',
description: 'Perform comprehensive code quality analysis with complexity metrics and issue detection',
category: 'code-analysis',
inputSchema: {
type: 'object',
properties: {
files: {
type: 'array',
items: { type: 'string' },
description: 'Specific files to analyze'
},
directories: {
type: 'array',
items: { type: 'string' },
description: 'Directories to recursively analyze'
},
includeHistory: {
type: 'boolean',
description: 'Save results to history for trend analysis',
default: false
},
compareWithBaseline: {
type: 'boolean',
description: 'Compare results with baseline metrics',
default: false
},
maxIssues: {
type: 'number',
description: 'Maximum number of issues to report',
default: 100
},
excludePatterns: {
type: 'array',
items: { type: 'string' },
description: 'Glob patterns to exclude from analysis'
},
customRules: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
pattern: { type: 'string' },
severity: { type: 'string', enum: ['error', 'warning', 'info'] },
category: { type: 'string' },
message: { type: 'string' }
},
required: ['name', 'pattern', 'severity', 'category', 'message']
},
description: 'Custom rules to apply during analysis'
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: AnalyzeCodeQualityInput, context: RequestContext) {
try {
const projectId = context.projectId || 'default';
const analysisId = `analysis-${randomUUID()}`;
const now = Date.now();
// Collect files to analyze
let filesToAnalyze: string[] = [];
if (input.files && input.files.length > 0) {
filesToAnalyze = input.files;
}
if (input.directories && input.directories.length > 0) {
for (const dir of input.directories) {
const dirFiles = await findCodeFiles(dir, input.excludePatterns || []);
filesToAnalyze.push(...dirFiles);
}
}
if (filesToAnalyze.length === 0) {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: 'No files specified for analysis',
category: 'validation'
});
}
// Simulate code analysis (in real implementation, this would use actual analysis tools)
const fileResults = await Promise.all(
filesToAnalyze.map(async (file) => await analyzeFile(file))
);
// Calculate aggregate metrics
const totalFiles = fileResults.length;
const averageComplexity = fileResults.reduce((sum, f) => sum + f.complexity, 0) / totalFiles;
const averageMaintainability = fileResults.reduce((sum, f) => sum + f.maintainability, 0) / totalFiles;
const totalIssues = fileResults.reduce((sum, f) => sum + f.issues.length, 0);
const criticalIssues = fileResults.reduce((sum, f) => sum + f.issues.filter(i => i.severity === 'critical').length, 0);
const duplicationPercentage = fileResults.reduce((sum, f) => sum + f.duplication, 0) / totalFiles;
// Calculate overall score and grade
const overallScore = Math.max(0, Math.round(100 - (averageComplexity * 5) - (totalIssues * 2) - duplicationPercentage));
const overallGrade = overallScore >= 90 ? 'A' : overallScore >= 80 ? 'B' : overallScore >= 70 ? 'C' : overallScore >= 60 ? 'D' : 'F';
// Save analysis result
const analysisResult = await context.db.run(
`INSERT INTO code_analysis_results
(id, project_id, timestamp, files_analyzed, average_complexity, average_maintainability,
total_issues, critical_issues, duplication_percentage, overall_score, overall_grade,
options, recommendations, baseline_comparison, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
analysisId,
projectId,
now,
totalFiles,
averageComplexity,
averageMaintainability,
totalIssues,
criticalIssues,
duplicationPercentage,
overallScore,
overallGrade,
JSON.stringify(input),
JSON.stringify(generateRecommendations(fileResults)),
JSON.stringify({}), // baseline comparison - would be populated if requested
now
]
);
if (!analysisResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to save analysis result',
details: { error: analysisResult.error },
category: 'system'
});
}
// Save file results and issues
for (const fileResult of fileResults) {
const fileResultId = `file-${randomUUID()}`;
await context.db.run(
`INSERT INTO code_analysis_file_results
(id, analysis_id, project_id, file_path, file_size, language, last_modified,
cyclomatic_complexity, cognitive_complexity, halstead_difficulty, halstead_volume, halstead_effort,
max_complexity, total_complexity, function_complexities,
maintainability_index, maintainability_rating, line_count, comment_ratio, test_coverage,
duplication_percentage, duplicate_blocks, duplicated_lines, total_lines,
total_issues, high_issues, medium_issues, low_issues, code_smells, technical_debt,
quality_score, quality_grade, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
fileResultId,
analysisId,
projectId,
fileResult.path,
fileResult.size,
fileResult.language,
fileResult.lastModified,
fileResult.complexity,
fileResult.complexity, // cognitive = cyclomatic for simplicity
0, 0, 0, // halstead metrics
fileResult.complexity,
fileResult.complexity,
JSON.stringify([]), // function complexities
fileResult.maintainability,
fileResult.maintainability >= 80 ? 'A' : fileResult.maintainability >= 60 ? 'B' : 'C',
fileResult.lineCount,
0, 0, // comment ratio, test coverage
fileResult.duplication,
JSON.stringify([]), // duplicate blocks
0, fileResult.lineCount, // duplicated lines, total lines
fileResult.issues.length,
fileResult.issues.filter(i => i.severity === 'high').length,
fileResult.issues.filter(i => i.severity === 'medium').length,
fileResult.issues.filter(i => i.severity === 'low').length,
0, 0, // code smells, technical debt
overallScore,
overallGrade,
now
]
);
// Save issues
for (const issue of fileResult.issues) {
const issueId = `issue-${randomUUID()}`;
await context.db.run(
`INSERT INTO code_analysis_issues
(id, analysis_id, file_result_id, project_id, issue_type, severity, rule, message,
file_path, line_number, column_number, end_line_number, end_column_number,
is_fixable, category, suggestion, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
issueId,
analysisId,
fileResultId,
projectId,
issue.type,
issue.severity,
issue.rule,
issue.message,
fileResult.path,
issue.line,
issue.column,
issue.endLine || null,
issue.endColumn || null,
issue.fixable || false,
issue.category,
issue.suggestion || null,
now
]
);
}
}
// Get baseline comparison if requested
let baselineComparison = null;
if (input.compareWithBaseline) {
const baseline = await context.db.get(
'SELECT * FROM code_metrics_baselines WHERE project_id = ? ORDER BY created_at DESC LIMIT 1',
[projectId]
);
if (baseline.success && baseline.data) {
const baselineHistory = await context.db.get(
'SELECT * FROM code_metrics_history WHERE id = ?',
[baseline.data.metrics_history_id]
);
if (baselineHistory.success && baselineHistory.data) {
baselineComparison = {
complexityChange: averageComplexity - baselineHistory.data.average_complexity,
maintainabilityChange: averageMaintainability - baselineHistory.data.average_maintainability,
issuesChange: totalIssues - baselineHistory.data.total_issues,
duplicationChange: duplicationPercentage - baselineHistory.data.duplication_percentage
};
}
}
}
return createSuccessResult({
analysis: {
id: analysisId,
projectId,
timestamp: new Date(now).toISOString(),
filesAnalyzed: totalFiles,
summary: {
averageComplexity,
averageMaintainability,
totalIssues,
criticalIssues,
duplicationPercentage,
overallScore,
overallGrade
}
},
fileResults: fileResults.slice(0, 10), // Return first 10 files for brevity
baselineComparison,
recommendations: generateRecommendations(fileResults),
message: overallScore >= 80
? `✅ Code quality is good (${overallScore}/100)`
: `⚠️ Code quality needs improvement (${overallScore}/100)`,
insights: [
`Analyzed ${totalFiles} files`,
`Average complexity: ${averageComplexity.toFixed(1)}`,
`Total issues found: ${totalIssues}`,
criticalIssues > 0 ? `⚠️ ${criticalIssues} critical issues found` : 'No critical issues'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Code analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Perform AI-powered code review with detailed findings
*/
const reviewCodeTool = createTool<ReviewCodeInput, any>({
name: 'review_code',
description: 'AI-powered code review with detailed findings and improvement suggestions',
category: 'code-analysis',
inputSchema: {
type: 'object',
properties: {
file: {
type: 'string',
description: 'File to review'
},
compareWith: {
type: 'string',
description: 'Compare with another file (e.g., previous version)'
},
includeHistory: {
type: 'boolean',
description: 'Save review to history',
default: false
},
focusAreas: {
type: 'array',
items: {
type: 'string',
enum: ['security', 'performance', 'maintainability', 'style', 'testing']
},
description: 'Specific areas to focus on'
}
},
required: ['file'],
additionalProperties: false
} as JSONSchema7,
async execute(input: ReviewCodeInput, context: RequestContext) {
try {
const projectId = context.projectId || 'default';
const reviewId = `review-${randomUUID()}`;
const now = Date.now();
// Check if file exists
try {
await fs.access(input.file);
} catch {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: `File not found: ${input.file}`,
category: 'validation'
});
}
// Analyze the file
const fileAnalysis = await analyzeFile(input.file);
// Generate findings
const findings = fileAnalysis.issues.map((issue, index) => ({
id: `finding-${index}`,
type: issue.type === 'error' ? 'bug' : issue.type === 'warning' ? 'improvement' : 'style',
severity: issue.severity,
title: issue.rule,
description: issue.message,
line: issue.line,
column: issue.column,
suggestedFix: issue.suggestion
}));
// Generate suggestions
const suggestions = generateCodeSuggestions(fileAnalysis, input.focusAreas);
// Calculate review score
const reviewScore = Math.max(0, Math.round(100 - (fileAnalysis.complexity * 5) - (findings.length * 3)));
// Save review
const reviewResult = await context.db.run(
`INSERT INTO code_reviews
(id, project_id, file_path, file_hash, file_name, timestamp, overall_score,
summary, approved, comparison_data, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
reviewId,
projectId,
input.file,
null, // file hash - would be calculated in real implementation
path.basename(input.file),
now,
reviewScore,
generateReviewSummary(findings, suggestions),
false, // not approved by default
JSON.stringify({}), // comparison data
now
]
);
if (!reviewResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to save review',
details: { error: reviewResult.error },
category: 'system'
});
}
// Save findings
for (const finding of findings) {
await context.db.run(
`INSERT INTO code_review_findings
(id, review_id, project_id, finding_type, severity, title, description,
file_path, line_number, column_number, end_line_number, end_column_number,
suggested_fix, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
randomUUID(),
reviewId,
projectId,
finding.type,
finding.severity,
finding.title,
finding.description,
input.file,
finding.line,
finding.column,
null, null, // end line/column
finding.suggestedFix,
now
]
);
}
// Save suggestions
for (const suggestion of suggestions) {
await context.db.run(
`INSERT INTO code_review_suggestions
(id, review_id, project_id, title, description, priority, suggestion_type,
file_path, line_number, column_number, example, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
randomUUID(),
reviewId,
projectId,
suggestion.title,
suggestion.description,
suggestion.priority,
suggestion.type,
input.file,
suggestion.line || null,
suggestion.column || null,
suggestion.example || null,
now
]
);
}
return createSuccessResult({
review: {
id: reviewId,
file: input.file,
score: reviewScore,
timestamp: new Date(now).toISOString(),
summary: generateReviewSummary(findings, suggestions)
},
findings: findings.slice(0, 10), // Show first 10 findings
suggestions: suggestions.slice(0, 5), // Show first 5 suggestions
message: reviewScore >= 80
? `✅ Code review passed (${reviewScore}/100)`
: `⚠️ Code review needs attention (${reviewScore}/100)`,
insights: [
`Found ${findings.length} issues`,
`Generated ${suggestions.length} improvement suggestions`,
`File complexity: ${fileAnalysis.complexity.toFixed(1)}`,
`Maintainability: ${fileAnalysis.maintainability.toFixed(1)}`
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Code review failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Track code quality metrics over time
*/
const trackCodeMetricsTool = createTool<TrackCodeMetricsInput, any>({
name: 'track_code_metrics',
description: 'Track code quality metrics over time with trend analysis',
category: 'code-analysis',
inputSchema: {
type: 'object',
properties: {
projectId: {
type: 'string',
description: 'Project identifier (defaults to directory name)'
},
branch: {
type: 'string',
description: 'Git branch name'
},
commit: {
type: 'string',
description: 'Git commit hash'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for this metrics snapshot (e.g., "release", "baseline")'
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: TrackCodeMetricsInput, context: RequestContext) {
try {
const projectId = input.projectId || context.projectId || 'default';
const historyId = `history-${randomUUID()}`;
const now = Date.now();
// Analyze entire project
const projectFiles = await findCodeFiles('.', [
'node_modules/**',
'dist/**',
'build/**',
'.git/**',
'*.test.*',
'*.spec.*'
]);
const fileAnalyses = await Promise.all(
projectFiles.map(async (file) => await analyzeFile(file))
);
// Calculate project metrics
const totalFiles = fileAnalyses.length;
const averageComplexity = fileAnalyses.reduce((sum, f) => sum + f.complexity, 0) / totalFiles;
const maxComplexity = Math.max(...fileAnalyses.map(f => f.complexity));
const totalComplexity = fileAnalyses.reduce((sum, f) => sum + f.complexity, 0);
const averageMaintainability = fileAnalyses.reduce((sum, f) => sum + f.maintainability, 0) / totalFiles;
const minMaintainability = Math.min(...fileAnalyses.map(f => f.maintainability));
const duplicationPercentage = fileAnalyses.reduce((sum, f) => sum + f.duplication, 0) / totalFiles;
const totalIssues = fileAnalyses.reduce((sum, f) => sum + f.issues.length, 0);
const highIssues = fileAnalyses.reduce((sum, f) => sum + f.issues.filter(i => i.severity === 'high').length, 0);
const mediumIssues = fileAnalyses.reduce((sum, f) => sum + f.issues.filter(i => i.severity === 'medium').length, 0);
const lowIssues = fileAnalyses.reduce((sum, f) => sum + f.issues.filter(i => i.severity === 'low').length, 0);
const qualityScore = Math.max(0, Math.round(100 - (averageComplexity * 5) - (totalIssues * 2) - duplicationPercentage));
const qualityGrade = qualityScore >= 90 ? 'A' : qualityScore >= 80 ? 'B' : qualityScore >= 70 ? 'C' : qualityScore >= 60 ? 'D' : 'F';
// Save metrics history
const historyResult = await context.db.run(
`INSERT INTO code_metrics_history
(id, project_id, timestamp, branch, commit_hash, tags,
average_complexity, max_complexity, total_complexity,
average_maintainability, min_maintainability, duplication_percentage,
total_issues, high_issues, medium_issues, low_issues,
quality_score, quality_grade, raw_metrics, summary_data, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
historyId,
projectId,
now,
input.branch || null,
input.commit || null,
JSON.stringify(input.tags || []),
averageComplexity,
maxComplexity,
totalComplexity,
averageMaintainability,
minMaintainability,
duplicationPercentage,
totalIssues,
highIssues,
mediumIssues,
lowIssues,
qualityScore,
qualityGrade,
JSON.stringify({ fileAnalyses }),
JSON.stringify({
filesAnalyzed: totalFiles,
timestamp: now
}),
now
]
);
if (!historyResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to save metrics history',
details: { error: historyResult.error },
category: 'system'
});
}
// Update baseline if tagged as baseline
if (input.tags && input.tags.includes('baseline')) {
// Delete existing baseline
await context.db.run(
'DELETE FROM code_metrics_baselines WHERE project_id = ?',
[projectId]
);
// Create new baseline
await context.db.run(
`INSERT INTO code_metrics_baselines
(id, project_id, metrics_history_id, name, commit_hash, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[
`baseline-${randomUUID()}`,
projectId,
historyId,
'baseline',
input.commit || null,
now
]
);
}
// Calculate trends
const trends = await calculateTrends(context, projectId);
return createSuccessResult({
metrics: {
id: historyId,
projectId,
timestamp: new Date(now).toISOString(),
branch: input.branch,
commit: input.commit,
tags: input.tags || [],
summary: {
filesAnalyzed: totalFiles,
averageComplexity,
maxComplexity,
averageMaintainability,
duplicationPercentage,
totalIssues,
highIssues,
qualityScore,
qualityGrade
}
},
trends,
message: `📊 Metrics tracked for ${totalFiles} files`,
insights: [
`Quality score: ${qualityScore}/100 (${qualityGrade})`,
`Average complexity: ${averageComplexity.toFixed(1)}`,
`Total issues: ${totalIssues} (${highIssues} high priority)`,
trends.length > 0 ? `Trends available for ${trends.length} metrics` : 'No trend data yet'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Metrics tracking failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get analysis history
*/
const getAnalysisHistoryTool = createTool<GetAnalysisHistoryInput, any>({
name: 'get_analysis_history',
description: 'Get code analysis history with optional baseline comparison',
category: 'code-analysis',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of results to return',
default: 20
},
includeBaseline: {
type: 'boolean',
description: 'Include baseline comparison data',
default: false
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: GetAnalysisHistoryInput, context: RequestContext) {
try {
const projectId = context.projectId || 'default';
const limit = input.limit || 20;
const results = await context.db.all(
`SELECT * FROM code_analysis_results
WHERE project_id = ?
ORDER BY timestamp DESC
LIMIT ?`,
[projectId, limit]
);
if (!results.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to fetch analysis history',
details: { error: results.error },
category: 'system'
});
}
const formattedResults = results.data.map((r: any) => ({
id: r.id,
timestamp: new Date(r.timestamp).toISOString(),
filesAnalyzed: r.files_analyzed,
averageComplexity: r.average_complexity,
averageMaintainability: r.average_maintainability,
totalIssues: r.total_issues,
criticalIssues: r.critical_issues,
duplicationPercentage: r.duplication_percentage,
overallScore: r.overall_score,
overallGrade: r.overall_grade,
recommendations: JSON.parse(r.recommendations || '[]')
}));
return createSuccessResult({
history: formattedResults,
totalCount: formattedResults.length,
message: `Found ${formattedResults.length} analysis result(s)`,
insights: formattedResults.length > 0 ? [
`Latest score: ${formattedResults[0].overallScore}/100 (${formattedResults[0].overallGrade})`,
`Average complexity: ${formattedResults[0].averageComplexity.toFixed(1)}`,
`Total issues: ${formattedResults[0].totalIssues}`
] : ['No analysis history available']
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get analysis history: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get code reviews
*/
const getCodeReviewsTool = createTool<GetCodeReviewsInput, any>({
name: 'get_code_reviews',
description: 'Get code review history with optional file filtering',
category: 'code-analysis',
inputSchema: {
type: 'object',
properties: {
fileFilter: {
type: 'string',
description: 'Filter reviews by file path pattern'
},
limit: {
type: 'number',
description: 'Maximum number of results to return',
default: 20
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: GetCodeReviewsInput, context: RequestContext) {
try {
const projectId = context.projectId || 'default';
const limit = input.limit || 20;
let query = `
SELECT r.*,
(SELECT COUNT(*) FROM code_review_findings WHERE review_id = r.id) as findings_count,
(SELECT COUNT(*) FROM code_review_suggestions WHERE review_id = r.id) as suggestions_count
FROM code_reviews r
WHERE r.project_id = ?
`;
const params: any[] = [projectId];
if (input.fileFilter) {
query += ' AND r.file_path LIKE ?';
params.push(`%${input.fileFilter}%`);
}
query += ' ORDER BY r.timestamp DESC LIMIT ?';
params.push(limit);
const results = await context.db.all(query, params);
if (!results.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to fetch code reviews',
details: { error: results.error },
category: 'system'
});
}
const formattedResults = results.data.map((r: any) => ({
id: r.id,
filePath: r.file_path,
fileName: r.file_name,
timestamp: new Date(r.timestamp).toISOString(),
overallScore: r.overall_score,
summary: r.summary,
approved: r.approved,
findingsCount: r.findings_count,
suggestionsCount: r.suggestions_count
}));
return createSuccessResult({
reviews: formattedResults,
totalCount: formattedResults.length,
message: `Found ${formattedResults.length} code review(s)`,
insights: formattedResults.length > 0 ? [
`Latest review score: ${formattedResults[0].overallScore}/100`,
`Total findings: ${formattedResults.reduce((sum: number, r: any) => sum + r.findingsCount, 0)}`,
`Total suggestions: ${formattedResults.reduce((sum: number, r: any) => sum + r.suggestionsCount, 0)}`
] : ['No code reviews available']
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get code reviews: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Set metrics baseline
*/
const setMetricsBaselineTool = createTool<SetMetricsBaselineInput, any>({
name: 'set_metrics_baseline',
description: 'Set a metrics history entry as the baseline for comparisons',
category: 'code-analysis',
inputSchema: {
type: 'object',
properties: {
historyId: {
type: 'string',
description: 'Metrics history ID to set as baseline (defaults to latest)'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Additional tags for the baseline'
}
},
additionalProperties: false
} as JSONSchema7,
async execute(input: SetMetricsBaselineInput, context: RequestContext) {
try {
const projectId = context.projectId || 'default';
let targetId = input.historyId;
// Use latest history if no ID specified
if (!targetId) {
const latest = await context.db.get(
'SELECT id FROM code_metrics_history WHERE project_id = ? ORDER BY timestamp DESC LIMIT 1',
[projectId]
);
if (!latest.success || !latest.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'No metrics history available to set as baseline',
category: 'validation'
});
}
targetId = latest.data.id;
}
// Verify history exists
const historyEntry = await context.db.get(
'SELECT * FROM code_metrics_history WHERE id = ? AND project_id = ?',
[targetId, projectId]
);
if (!historyEntry.success || !historyEntry.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Metrics history ${targetId} not found`,
category: 'validation'
});
}
// Delete existing baseline
await context.db.run(
'DELETE FROM code_metrics_baselines WHERE project_id = ?',
[projectId]
);
// Create new baseline
const baselineId = `baseline-${randomUUID()}`;
const result = await context.db.run(
`INSERT INTO code_metrics_baselines
(id, project_id, metrics_history_id, name, commit_hash, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[
baselineId,
projectId,
targetId,
'baseline',
historyEntry.data.commit_hash,
Date.now()
]
);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to set baseline',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
baseline: {
id: baselineId,
historyId: targetId,
timestamp: new Date(historyEntry.data.timestamp).toISOString(),
qualityScore: historyEntry.data.quality_score,
averageComplexity: historyEntry.data.average_complexity,
totalIssues: historyEntry.data.total_issues
},
message: '✅ Metrics baseline set',
details: [
`Quality Score: ${historyEntry.data.quality_score}/100`,
`Average Complexity: ${historyEntry.data.average_complexity.toFixed(1)}`,
`Total Issues: ${historyEntry.data.total_issues}`,
'Future metrics will be compared against this baseline'
]
});
} catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to set baseline: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Setup all code analysis tools
*/
export async function setupCodeAnalysisTools(): Promise<ToolRegistration> {
return {
module: 'code-analysis',
tools: [
analyzeCodeQualityTool,
reviewCodeTool,
trackCodeMetricsTool,
getAnalysisHistoryTool,
getCodeReviewsTool,
setMetricsBaselineTool
]
};
}
// Helper functions
async function findCodeFiles(dir: string, excludePatterns: string[]): Promise<string[]> {
const files: string[] = [];
const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.cs', '.php', '.rb', '.go'];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip if matches exclude patterns
if (excludePatterns.some(pattern => fullPath.includes(pattern.replace('**', '')))) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await findCodeFiles(fullPath, excludePatterns);
files.push(...subFiles);
} else if (entry.isFile() && codeExtensions.some(ext => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
} catch (error) {
// Directory doesn't exist or can't be read
}
return files;
}
async function analyzeFile(filePath: string): Promise<any> {
try {
const stats = await fs.stat(filePath);
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n');
// Simulate analysis (in real implementation, use actual analysis tools)
const complexity = Math.random() * 20 + 1; // 1-21
const maintainability = Math.random() * 40 + 60; // 60-100
const duplication = Math.random() * 15; // 0-15%
// Generate some mock issues
const issues = [];
const numIssues = Math.floor(Math.random() * 5);
for (let i = 0; i < numIssues; i++) {
issues.push({
type: ['error', 'warning', 'info'][Math.floor(Math.random() * 3)],
severity: ['critical', 'high', 'medium', 'low'][Math.floor(Math.random() * 4)],
rule: `rule-${i + 1}`,
message: `Mock issue ${i + 1} in ${path.basename(filePath)}`,
line: Math.floor(Math.random() * lines.length) + 1,
column: Math.floor(Math.random() * 80) + 1,
category: ['security', 'performance', 'maintainability', 'style'][Math.floor(Math.random() * 4)],
fixable: Math.random() > 0.5
});
}
return {
path: filePath,
size: stats.size,
language: getLanguageFromExtension(filePath),
lastModified: stats.mtime.getTime(),
lineCount: lines.length,
complexity,
maintainability,
duplication,
issues
};
} catch (error) {
throw new Error(`Failed to analyze file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
function getLanguageFromExtension(filePath: string): string {
const ext = path.extname(filePath);
const languageMap: Record<string, string> = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp',
'.php': 'php',
'.rb': 'ruby',
'.go': 'go'
};
return languageMap[ext] || 'unknown';
}
function generateRecommendations(fileResults: any[]): string[] {
const recommendations = [];
const avgComplexity = fileResults.reduce((sum, f) => sum + f.complexity, 0) / fileResults.length;
if (avgComplexity > 10) {
recommendations.push('Consider refactoring complex functions to improve maintainability');
}
const totalIssues = fileResults.reduce((sum, f) => sum + f.issues.length, 0);
if (totalIssues > 50) {
recommendations.push('Address code quality issues to improve overall health');
}
const avgDuplication = fileResults.reduce((sum, f) => sum + f.duplication, 0) / fileResults.length;
if (avgDuplication > 10) {
recommendations.push('Reduce code duplication by extracting common functionality');
}
return recommendations;
}
function generateCodeSuggestions(fileAnalysis: any, focusAreas?: string[]): any[] {
const suggestions = [];
if (!focusAreas || focusAreas.includes('performance')) {
if (fileAnalysis.complexity > 15) {
suggestions.push({
title: 'Optimize Complex Functions',
description: 'Consider breaking down complex functions into smaller, more manageable pieces',
priority: 'high',
type: 'refactoring',
line: Math.floor(Math.random() * fileAnalysis.lineCount) + 1,
column: 1
});
}
}
if (!focusAreas || focusAreas.includes('maintainability')) {
if (fileAnalysis.maintainability < 70) {
suggestions.push({
title: 'Improve Code Documentation',
description: 'Add comments and documentation to improve code maintainability',
priority: 'medium',
type: 'documentation'
});
}
}
return suggestions;
}
function generateReviewSummary(findings: any[], suggestions: any[]): string {
const highPriorityFindings = findings.filter(f => f.severity === 'high').length;
const mediumPriorityFindings = findings.filter(f => f.severity === 'medium').length;
let summary = '';
if (highPriorityFindings > 0) {
summary += `Found ${highPriorityFindings} high-priority issues that should be addressed immediately. `;
}
if (mediumPriorityFindings > 0) {
summary += `Found ${mediumPriorityFindings} medium-priority issues that should be addressed soon. `;
}
if (suggestions.length > 0) {
summary += `Generated ${suggestions.length} improvement suggestions. `;
}
if (findings.length === 0 && suggestions.length === 0) {
summary = 'Code looks good! No significant issues found.';
}
return summary;
}
async function calculateTrends(context: RequestContext, projectId: string): Promise<any[]> {
const trends = [];
// Get recent history entries
const recent = await context.db.all(
'SELECT * FROM code_metrics_history WHERE project_id = ? ORDER BY timestamp DESC LIMIT 10',
[projectId]
);
if (recent.success && recent.data.length >= 2) {
const latestEntry = recent.data[0];
const previousEntry = recent.data[1];
const complexityChange = latestEntry.average_complexity - previousEntry.average_complexity;
const maintainabilityChange = latestEntry.average_maintainability - previousEntry.average_maintainability;
const issuesChange = latestEntry.total_issues - previousEntry.total_issues;
trends.push({
metric: 'complexity',
direction: complexityChange > 0.1 ? 'increasing' : complexityChange < -0.1 ? 'decreasing' : 'stable',
change: complexityChange,
percentage: (complexityChange / previousEntry.average_complexity) * 100
});
trends.push({
metric: 'maintainability',
direction: maintainabilityChange > 1 ? 'improving' : maintainabilityChange < -1 ? 'degrading' : 'stable',
change: maintainabilityChange,
percentage: (maintainabilityChange / previousEntry.average_maintainability) * 100
});
trends.push({
metric: 'issues',
direction: issuesChange > 0 ? 'increasing' : issuesChange < 0 ? 'decreasing' : 'stable',
change: issuesChange,
percentage: previousEntry.total_issues > 0 ? (issuesChange / previousEntry.total_issues) * 100 : 0
});
}
return trends;
}