MCP File Context Server

import { promises as fs } from 'fs'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import * as parser from '@typescript-eslint/parser'; import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types'; import { FileContent } from '../types.js'; const execAsync = promisify(exec); export interface SecurityIssue { type: string; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; line?: number; column?: number; } export interface StyleViolation { rule: string; message: string; line: number; column: number; } export interface ComplexityMetrics { cyclomaticComplexity: number; maintainabilityIndex: number; linesOfCode: number; numberOfFunctions: number; branchCount: number; returnCount: number; maxNestingDepth: number; averageFunctionComplexity: number; functionMetrics: FunctionMetrics[]; } export interface FunctionMetrics { name: string; startLine: number; endLine: number; complexity: number; parameterCount: number; returnCount: number; localVariables: number; nestingDepth: number; } export interface CodeMetrics { complexity: number; lineCount: { total: number; code: number; comment: number; blank: number; }; quality: { longLines: number; duplicateLines: number; complexFunctions: number; }; dependencies: string[]; imports: string[]; definitions: { classes: string[]; functions: string[]; variables: string[]; }; } export interface CodeAnalysisResult { metrics: CodeMetrics; outline: string; language: string; security_issues: any[]; style_violations: any[]; complexity_metrics: any; } interface LanguageConfig { extensions: string[]; securityTool?: string; styleTool?: string; complexityTool?: string; parser?: (code: string) => TSESTree.Program; } export class CodeAnalysisService { private tempDir: string; private languageConfigs: Record<string, LanguageConfig>; private readonly LONG_LINE_THRESHOLD = 100; private readonly COMPLEX_FUNCTION_THRESHOLD = 10; constructor() { this.tempDir = path.join(process.cwd(), '.temp'); this.languageConfigs = { python: { extensions: ['.py'], securityTool: 'bandit', styleTool: 'pylint', complexityTool: 'radon' }, typescript: { extensions: ['.ts', '.tsx'], securityTool: 'tsc --noEmit', styleTool: 'eslint', parser: (code: string) => parser.parse(code, { sourceType: 'module', ecmaFeatures: { jsx: true } }) }, javascript: { extensions: ['.js', '.jsx'], securityTool: 'eslint', styleTool: 'eslint', parser: (code: string) => parser.parse(code, { sourceType: 'module', ecmaFeatures: { jsx: true } }) }, csharp: { extensions: ['.cs'], securityTool: 'security-code-scan', styleTool: 'dotnet format', complexityTool: 'ndepend' }, go: { extensions: ['.go'], securityTool: 'gosec', styleTool: 'golint', complexityTool: 'gocyclo' }, bash: { extensions: ['.sh', '.bash'], securityTool: 'shellcheck', styleTool: 'shellcheck', complexityTool: 'shellcheck' } }; } public async initialize(): Promise<void> { await fs.mkdir(this.tempDir, { recursive: true }); } public async analyzeCode(content: string, filePath: string): Promise<CodeAnalysisResult> { const ext = path.extname(filePath).toLowerCase(); const language = this.getLanguage(ext); const metrics = await this.calculateMetrics(content, language); const outline = await this.generateOutline(content, language); return { metrics, outline, language, security_issues: [], // TODO: Implement security analysis style_violations: [], // TODO: Implement style analysis complexity_metrics: { cyclomaticComplexity: metrics.complexity, linesOfCode: metrics.lineCount.code, maintainabilityIndex: 100 - (metrics.quality.longLines + metrics.quality.duplicateLines) / metrics.lineCount.total * 100 } }; } private getLanguage(ext: string): string { const map: Record<string, string> = { '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript', '.py': 'python', '.go': 'go', '.java': 'java', '.cs': 'csharp', '.cpp': 'cpp', '.c': 'c', '.rb': 'ruby' }; return map[ext] || 'unknown'; } private async calculateMetrics(content: string, language: string): Promise<CodeMetrics> { const lines = content.split('\n'); const lineCount = this.calculateLineCount(lines, language); const complexity = this.calculateComplexity(content, language); const quality = this.calculateQualityMetrics(lines); const { imports, dependencies } = this.extractDependencies(content, language); const definitions = this.extractDefinitions(content, language); return { complexity, lineCount, quality, dependencies, imports, definitions }; } private calculateLineCount(lines: string[], language: string): CodeMetrics['lineCount'] { let code = 0; let comment = 0; let blank = 0; let inMultilineComment = false; const commentStart = this.getCommentPatterns(language); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) { blank++; continue; } if (inMultilineComment) { comment++; if (commentStart.multiEnd && trimmed.includes(commentStart.multiEnd)) { inMultilineComment = false; } continue; } if (commentStart.multi && trimmed.startsWith(commentStart.multi)) { comment++; inMultilineComment = true; continue; } if (commentStart.single.some(pattern => trimmed.startsWith(pattern))) { comment++; } else { code++; } } return { total: lines.length, code, comment, blank }; } private calculateComplexity(content: string, language: string): number { let complexity = 1; const patterns = [ /\bif\b/g, /\belse\b/g, /\bwhile\b/g, /\bfor\b/g, /\bforeach\b/g, /\bcase\b/g, /\bcatch\b/g, /\b\|\|\b/g, /\b&&\b/g, /\?/g ]; patterns.forEach(pattern => { const matches = content.match(pattern); if (matches) { complexity += matches.length; } }); return complexity; } private calculateQualityMetrics(lines: string[]): CodeMetrics['quality'] { const longLines = lines.filter(line => line.length > this.LONG_LINE_THRESHOLD).length; // Simple duplicate line detection const lineSet = new Set<string>(); let duplicateLines = 0; lines.forEach(line => { const trimmed = line.trim(); if (trimmed && lineSet.has(trimmed)) { duplicateLines++; } else { lineSet.add(trimmed); } }); // Count complex functions based on line count and complexity const complexFunctions = this.countComplexFunctions(lines.join('\n')); return { longLines, duplicateLines, complexFunctions }; } private countComplexFunctions(content: string): number { const functionMatches = content.match(/\bfunction\s+\w+\s*\([^)]*\)\s*{[^}]*}/g) || []; return functionMatches.filter(func => { const complexity = this.calculateComplexity(func, 'unknown'); return complexity > this.COMPLEX_FUNCTION_THRESHOLD; }).length; } private extractDependencies(content: string, language: string): { imports: string[], dependencies: string[] } { const imports: string[] = []; const dependencies: string[] = []; switch (language) { case 'typescript': case 'javascript': const importMatches = content.match(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g) || []; const requireMatches = content.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g) || []; importMatches.forEach(match => { const [, path] = match.match(/from\s+['"]([^'"]+)['"]/) || []; if (path) imports.push(path); }); requireMatches.forEach(match => { const [, path] = match.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/) || []; if (path) dependencies.push(path); }); break; case 'python': const pythonImports = content.match(/(?:from\s+(\S+)\s+)?import\s+(\S+)(?:\s+as\s+\S+)?/g) || []; pythonImports.forEach(match => { const [, from, module] = match.match(/(?:from\s+(\S+)\s+)?import\s+(\S+)/) || []; if (from) imports.push(from); if (module) imports.push(module); }); break; } return { imports, dependencies }; } private extractDefinitions(content: string, language: string): CodeMetrics['definitions'] { const definitions: CodeMetrics['definitions'] = { classes: [], functions: [], variables: [] }; switch (language) { case 'typescript': case 'javascript': // Classes const classMatches = content.match(/class\s+(\w+)/g) || []; definitions.classes = classMatches.map(match => match.split(/\s+/)[1]); // Functions const functionMatches = content.match(/(?:function|const|let|var)\s+(\w+)\s*(?:=\s*(?:function|\([^)]*\)\s*=>)|\([^)]*\))/g) || []; definitions.functions = functionMatches.map(match => { const [, name] = match.match(/(?:function|const|let|var)\s+(\w+)/) || []; return name; }).filter(Boolean); // Variables const varMatches = content.match(/(?:const|let|var)\s+(\w+)\s*=/g) || []; definitions.variables = varMatches.map(match => { const [, name] = match.match(/(?:const|let|var)\s+(\w+)/) || []; return name; }).filter(Boolean); break; case 'python': // Classes const pyClassMatches = content.match(/class\s+(\w+)(?:\([^)]*\))?:/g) || []; definitions.classes = pyClassMatches.map(match => { const [, name] = match.match(/class\s+(\w+)/) || []; return name; }).filter(Boolean); // Functions const pyFuncMatches = content.match(/def\s+(\w+)\s*\([^)]*\):/g) || []; definitions.functions = pyFuncMatches.map(match => { const [, name] = match.match(/def\s+(\w+)/) || []; return name; }).filter(Boolean); // Variables const pyVarMatches = content.match(/(\w+)\s*=(?!=)/g) || []; definitions.variables = pyVarMatches.map(match => { const [, name] = match.match(/(\w+)\s*=/) || []; return name; }).filter(Boolean); break; } return definitions; } private getCommentPatterns(language: string): { single: string[], multi?: string, multiEnd?: string } { switch (language) { case 'typescript': case 'javascript': return { single: ['//'], multi: '/*', multiEnd: '*/' }; case 'python': return { single: ['#'] }; case 'ruby': return { single: ['#'] }; default: return { single: ['//'], multi: '/*', multiEnd: '*/' }; } } private async generateOutline(content: string, language: string): Promise<string> { const metrics = await this.calculateMetrics(content, language); const sections: string[] = []; // Add imports section if (metrics.imports.length > 0) { sections.push('Imports:', ...metrics.imports.map(imp => ` - ${imp}`)); } // Add definitions section if (metrics.definitions.classes.length > 0) { sections.push('\nClasses:', ...metrics.definitions.classes.map(cls => ` - ${cls}`)); } if (metrics.definitions.functions.length > 0) { sections.push('\nFunctions:', ...metrics.definitions.functions.map(func => ` - ${func}`)); } // Add metrics section sections.push('\nMetrics:', ` Lines: ${metrics.lineCount.total} (${metrics.lineCount.code} code, ${metrics.lineCount.comment} comments, ${metrics.lineCount.blank} blank)`, ` Complexity: ${metrics.complexity}`, ` Quality Issues:`, ` - ${metrics.quality.longLines} long lines`, ` - ${metrics.quality.duplicateLines} duplicate lines`, ` - ${metrics.quality.complexFunctions} complex functions` ); return sections.join('\n'); } private analyzeAst(ast: TSESTree.Node): ComplexityMetrics { const functionMetrics: FunctionMetrics[] = []; let totalComplexity = 0; let maxNestingDepth = 0; let branchCount = 0; let returnCount = 0; const visitNode = (node: TSESTree.Node, depth: number = 0): void => { maxNestingDepth = Math.max(maxNestingDepth, depth); switch (node.type) { case AST_NODE_TYPES.FunctionDeclaration: case AST_NODE_TYPES.FunctionExpression: case AST_NODE_TYPES.ArrowFunctionExpression: case AST_NODE_TYPES.MethodDefinition: const metrics = this.analyzeFunctionNode(node, depth); functionMetrics.push(metrics); totalComplexity += metrics.complexity; break; case AST_NODE_TYPES.IfStatement: case AST_NODE_TYPES.SwitchCase: case AST_NODE_TYPES.ConditionalExpression: branchCount++; break; case AST_NODE_TYPES.ReturnStatement: returnCount++; break; } // Recursively visit children for (const key in node) { const child = (node as any)[key]; if (child && typeof child === 'object') { if (Array.isArray(child)) { child.forEach(item => { if (item && typeof item === 'object' && item.type) { visitNode(item as TSESTree.Node, depth + 1); } }); } else if (child.type) { visitNode(child as TSESTree.Node, depth + 1); } } } }; visitNode(ast); const averageFunctionComplexity = functionMetrics.length > 0 ? totalComplexity / functionMetrics.length : 0; return { cyclomaticComplexity: totalComplexity, maintainabilityIndex: this.calculateMaintainabilityIndex(totalComplexity, ast.loc?.end.line || 0), linesOfCode: ast.loc?.end.line || 0, numberOfFunctions: functionMetrics.length, branchCount, returnCount, maxNestingDepth, averageFunctionComplexity, functionMetrics }; } private analyzeFunctionNode(node: TSESTree.Node, depth: number): FunctionMetrics { let complexity = 1; // Base complexity let returnCount = 0; let localVariables = 0; const visitFunctionNode = (node: TSESTree.Node): void => { switch (node.type) { case AST_NODE_TYPES.IfStatement: case AST_NODE_TYPES.SwitchCase: case AST_NODE_TYPES.ConditionalExpression: case AST_NODE_TYPES.LogicalExpression: complexity++; break; case AST_NODE_TYPES.ReturnStatement: returnCount++; break; case AST_NODE_TYPES.VariableDeclaration: localVariables += node.declarations.length; break; } // Recursively visit children for (const key in node) { const child = (node as any)[key]; if (child && typeof child === 'object') { if (Array.isArray(child)) { child.forEach(item => { if (item && typeof item === 'object' && item.type) { visitFunctionNode(item as TSESTree.Node); } }); } else if (child.type) { visitFunctionNode(child as TSESTree.Node); } } } }; visitFunctionNode(node); return { name: this.getFunctionName(node), startLine: node.loc?.start.line || 0, endLine: node.loc?.end.line || 0, complexity, parameterCount: this.getParameterCount(node), returnCount, localVariables, nestingDepth: depth }; } private getFunctionName(node: TSESTree.Node): string { switch (node.type) { case AST_NODE_TYPES.FunctionDeclaration: return node.id?.name || 'anonymous'; case AST_NODE_TYPES.MethodDefinition: return node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : 'computed'; default: return 'anonymous'; } } private getParameterCount(node: TSESTree.Node): number { switch (node.type) { case AST_NODE_TYPES.FunctionDeclaration: case AST_NODE_TYPES.FunctionExpression: case AST_NODE_TYPES.ArrowFunctionExpression: return node.params.length; case AST_NODE_TYPES.MethodDefinition: return node.value.params.length; default: return 0; } } private calculateMaintainabilityIndex(complexity: number, linesOfCode: number): number { // Maintainability Index formula: // 171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code) // We're using a simplified version since we don't calculate Halstead Volume const mi = 171 - (0.23 * complexity) - (16.2 * Math.log(linesOfCode)); return Math.max(0, Math.min(100, mi)); } private async runSecurityAnalysis(filePath: string, config: LanguageConfig): Promise<SecurityIssue[]> { if (!config.securityTool) { return []; } try { const { stdout } = await execAsync(`${config.securityTool} ${filePath}`); return this.parseSecurityOutput(stdout, config.securityTool); } catch (error) { console.error('Security analysis failed:', error); return []; } } private async runStyleAnalysis(filePath: string, config: LanguageConfig): Promise<StyleViolation[]> { if (!config.styleTool) { return []; } try { const { stdout } = await execAsync(`${config.styleTool} ${filePath}`); return this.parseStyleOutput(stdout, config.styleTool); } catch (error) { console.error('Style analysis failed:', error); return []; } } private getDefaultComplexityMetrics(code: string): ComplexityMetrics { const lines = code.split('\n'); const functionMatches = code.match(/function|def|func|method/g); const branchMatches = code.match(/if|else|switch|case|while|for|catch/g); const returnMatches = code.match(/return/g); return { cyclomaticComplexity: (branchMatches?.length || 0) + 1, maintainabilityIndex: 100, linesOfCode: lines.length, numberOfFunctions: functionMatches?.length || 0, branchCount: branchMatches?.length || 0, returnCount: returnMatches?.length || 0, maxNestingDepth: 0, averageFunctionComplexity: 1, functionMetrics: [] }; } private parseSecurityOutput(output: string, tool: string): SecurityIssue[] { switch (tool) { case 'bandit': return this.parseBanditOutput(output); case 'eslint': return this.parseEslintOutput(output); default: return []; } } private parseStyleOutput(output: string, tool: string): StyleViolation[] { switch (tool) { case 'pylint': return this.parsePylintOutput(output); case 'eslint': return this.parseEslintOutput(output).map(issue => ({ rule: issue.type, message: issue.description, line: issue.line || 0, column: issue.column || 0 })); default: return []; } } private parseComplexityOutput(output: string, tool: string): ComplexityMetrics { switch (tool) { case 'radon': try { const results = JSON.parse(output); const totalComplexity = Object.values(results).reduce((sum: number, file: any) => { return sum + file.complexity; }, 0); return { cyclomaticComplexity: totalComplexity, maintainabilityIndex: 100 - (totalComplexity * 5), linesOfCode: 0, numberOfFunctions: Object.keys(results).length, branchCount: 0, returnCount: 0, maxNestingDepth: 0, averageFunctionComplexity: totalComplexity / Object.keys(results).length, functionMetrics: [] }; } catch { return this.getDefaultComplexityMetrics(''); } case 'gocyclo': try { const lines = output.split('\n').filter(Boolean); const metrics = lines.map(line => { const [complexity, path, name] = line.split(' '); return { name, complexity: parseInt(complexity, 10), startLine: 0, endLine: 0, parameterCount: 0, returnCount: 0, localVariables: 0, nestingDepth: 0 }; }); const totalComplexity = metrics.reduce((sum, m) => sum + m.complexity, 0); return { cyclomaticComplexity: totalComplexity, maintainabilityIndex: this.calculateMaintainabilityIndex(totalComplexity, 0), linesOfCode: 0, numberOfFunctions: metrics.length, branchCount: 0, returnCount: 0, maxNestingDepth: 0, averageFunctionComplexity: totalComplexity / metrics.length, functionMetrics: metrics }; } catch { return this.getDefaultComplexityMetrics(''); } default: return this.getDefaultComplexityMetrics(''); } } private parseBanditOutput(output: string): SecurityIssue[] { try { const results = JSON.parse(output); return results.results.map((result: any) => ({ type: result.test_id, severity: result.issue_severity, description: result.issue_text, line: result.line_number })); } catch { return []; } } private parsePylintOutput(output: string): StyleViolation[] { try { const results = JSON.parse(output); return results.map((result: any) => ({ rule: result.symbol, message: result.message, line: result.line, column: result.column })); } catch { return []; } } private parseEslintOutput(output: string): SecurityIssue[] { try { const results = JSON.parse(output); return results.map((result: { ruleId: string; severity: number; message: string; line: number; column: number; }) => ({ type: result.ruleId, severity: result.severity === 2 ? 'high' : result.severity === 1 ? 'medium' : 'low', description: result.message, line: result.line, column: result.column })); } catch { return []; } } }