/** * Code Analyzer MCP Server - CONSOLIDATED * * Comprehensive code analysis combining: * - Original code-analyzer (metrics, complexity, smells) * - explore (file search, patterns, dependencies) * - analyzer (pattern detection) * * This is the SINGLE source for code-level analysis. * Use project-analysis for project-level (structure, health). */ import { XrayKnowledgeSkillBase } from "../shared/knowledge-skill-base.js"; import * as fs from "fs"; import * as path from "path"; import { frameworkLogger } from "../../core/framework-logger.js"; import { createGracefulShutdown } from "../../utils/shutdown-handler.js"; interface Tool { name: string; description: string; inputSchema: object; } interface CodeMetrics { totalLines: number; nonEmptyLines: number; functions: number; classes: number; interfaces: number; imports: number; exports: number; cyclomaticComplexity: number; error?: string; } interface CodeSmell { file: string; type: string; line?: number; } interface PatternMatch { file: string; line: number; content: string; } interface FunctionDefinition { file: string; line: number; context: string; } interface FileTreeNode { type: "directory" | "file" | "error"; name: string; children?: FileTreeNode[]; size?: number; } class CodeAnalyzerServer extends XrayKnowledgeSkillBase { constructor() { super("code-analyzer", "3.1.0"); this.tools = [ // ===== CODE ANALYSIS ===== { name: "analyze_code", description: "Perform comprehensive static analysis on source code files", inputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" }, description: "Files or directories to analyze", }, language: { type: "string", description: "Programming language (typescript, javascript, python, etc.)", }, }, required: ["files"], }, }, { name: "calculate_complexity", description: "Calculate cyclomatic complexity and other code metrics", inputSchema: { type: "object", properties: { code: { type: "string", description: "Source code to analyze", }, language: { type: "string", description: "Programming language", }, }, required: ["code"], }, }, { name: "detect_code_smells", description: "Detect code smells, anti-patterns, and potential issues", inputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" }, description: "Files to analyze", }, thresholds: { type: "object", description: "Detection thresholds", properties: { maxFunctionLength: { type: "number" }, maxNestingDepth: { type: "number" }, maxParameters: { type: "number" }, }, }, }, required: ["files"], }, }, { name: "extract_metrics", description: "Extract detailed code metrics (lines, functions, classes, etc.)", inputSchema: { type: "object", properties: { filePath: { type: "string", description: "File or directory to analyze", }, includeHistory: { type: "boolean", default: false, description: "Include historical metrics if available", }, }, required: ["filePath"], }, }, // ===== EXPLORE FEATURES ===== { name: "explore_codebase", description: "Explore codebase structure and find files matching patterns. Fast file search.", inputSchema: { type: "object", properties: { scope: { type: "string", enum: ["full", "directory", "file"], description: "Scope of exploration", }, patterns: { type: "array", items: { type: "string" }, description: "Glob patterns to match (e.g., '**/*.ts', 'src/**/*.js')", }, basePath: { type: "string", description: "Base directory to search from (defaults to current directory)", }, maxResults: { type: "number", default: 50, description: "Maximum number of files to return", }, }, required: ["scope", "patterns"], }, }, { name: "find_patterns", description: "Find code patterns, functions, classes, or specific text across the codebase", inputSchema: { type: "object", properties: { pattern: { type: "string", description: "Regex or text pattern to search for", }, fileTypes: { type: "array", items: { type: "string" }, description: "File extensions to search (e.g., ['.ts', '.js'])", }, basePath: { type: "string", description: "Base directory to search from", }, maxMatches: { type: "number", default: 100, description: "Maximum number of matches to return", }, }, required: ["pattern"], }, }, { name: "find_function", description: "Locate specific function definitions across the codebase", inputSchema: { type: "object", properties: { functionName: { type: "string", description: "Name of the function to find", }, fileTypes: { type: "array", items: { type: "string" }, description: "File types to search", }, }, required: ["functionName"], }, }, { name: "get_file_structure", description: "Get directory structure and file tree for a given path", inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path to explore", }, maxDepth: { type: "number", default: 3, description: "Maximum depth to traverse", }, includeFiles: { type: "boolean", default: true, description: "Include files in the output", }, }, required: ["path"], }, }, // ===== DEPENDENCY ANALYSIS ===== { name: "analyze_dependencies", description: "Analyze import/export dependencies and generate dependency graph", inputSchema: { type: "object", properties: { rootDir: { type: "string", description: "Root directory to analyze", }, maxDepth: { type: "number", default: 3, description: "Maximum depth for dependency traversal", }, }, required: ["rootDir"], }, }, // ===== DUPLICATE DETECTION ===== { name: "find_duplicates", description: "Find duplicate or similar code patterns in the codebase", inputSchema: { type: "object", properties: { directory: { type: "string", description: "Directory to search", }, minLength: { type: "number", default: 10, description: "Minimum line count for duplicate detection", }, }, required: ["directory"], }, }, ]; this.handlers = { "analyze_code": async (args) => this.handleAnalyzeCode(args), "calculate_complexity": async (args) => this.handleCalculateComplexity(args), "detect_code_smells": async (args) => this.handleDetectCodeSmells(args), "extract_metrics": async (args) => this.handleExtractMetrics(args), "explore_codebase": async (args) => this.handleExploreCodebase(args), "find_patterns": async (args) => this.handleFindPatterns(args), "find_function": async (args) => this.handleFindFunction(args), "get_file_structure": async (args) => this.handleGetFileStructure(args), "analyze_dependencies": async (args) => this.handleAnalyzeDependencies(args), "find_duplicates": async (args) => this.handleFindDuplicates(args), }; this.setupToolHandlers(); } private handleAnalyzeCode(args: unknown) { const { files = [], language = "typescript" } = args as { files?: string[]; language?: string }; const results: Array<{ file: string; metrics: CodeMetrics }> = []; for (const file of files) { try { const content = fs.readFileSync(file, "utf-8"); results.push({ file, metrics: this.calculateCodeMetrics(content, language) }); } catch (e) { results.push({ file, metrics: { totalLines: 0, nonEmptyLines: 0, functions: 0, classes: 0, interfaces: 0, imports: 0, exports: 0, cyclomaticComplexity: 0, error: String(e) } }); } } return { content: [{ type: "text", text: JSON.stringify({ totalFiles: results.length, results }, null, 2) }] }; } private calculateCodeMetrics(content: string, language: string): CodeMetrics { const lines = content.split("\n"); return { totalLines: lines.length, nonEmptyLines: lines.filter(l => l.trim()).length, functions: (content.match(/function\s+\w+|const\s+\w+\s*=\s*\([^)]*\)\s*=>/g) || []).length, classes: (content.match(/class\s+\w+/g) || []).length, interfaces: (content.match(/interface\s+\w+/g) || []).length, imports: (content.match(/import\s+.*?from\s+['"]/g) || []).length, exports: (content.match(/export\s+/g) || []).length, cyclomaticComplexity: this.calculateCyclomaticComplexity(content), }; } private calculateCyclomaticComplexity(content: string): number { const patterns = [/\bif\s*\(/g, /\belse\s+if\s*\(/g, /\bfor\s*\(/g, /\bwhile\s*\(/g, /\bcase\s+/g, /\bcatch\s*\(/g, /\&\&/g, /\|\|/g]; let c = 1; for (const p of patterns) { const m = content.match(p); if (m) c += m.length; } return c; } private handleCalculateComplexity(args: unknown) { const { code, language = "typescript" } = args as { code: string; language?: string }; const complexity = this.calculateCyclomaticComplexity(code); return { content: [{ type: "text", text: JSON.stringify({ cyclomaticComplexity: complexity, complexityLevel: complexity < 10 ? "low" : complexity < 20 ? "moderate" : "high", metrics: this.calculateCodeMetrics(code, language) }, null, 2) }] }; } private handleDetectCodeSmells(args: unknown) { const { files = [], thresholds = {} } = args as { files?: string[]; thresholds?: { maxFunctionLength?: number; maxNestingDepth?: number; maxParameters?: number } }; const { maxFunctionLength = 30, maxNestingDepth = 4, maxParameters = 5 } = thresholds; const smells: CodeSmell[] = []; for (const file of files) { try { const content = fs.readFileSync(file, "utf-8"); const lines = content.split("\n"); let inFunc = false, funcStart = 0, braceCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i] || ""; if (line.match(/function\s+\w+|const\s+\w+\s*=\s*\([^)]*\)\s*=>/)) { inFunc = true; funcStart = i; braceCount = 0; } if (inFunc) { braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; if (i - funcStart > maxFunctionLength && braceCount === 0) { smells.push({ file, type: "long-function", line: i + 1 }); inFunc = false; } } if ((line.match(/{/g) || []).length > maxNestingDepth) smells.push({ file, type: "deep-nesting", line: i + 1 }); } const paramMatches = content.match(/function\s+\w+\s*\(([^)]*)\)/g); if (paramMatches) for (const m of paramMatches) { const params = m.replace(/function\s+\w+\s*\(/, "").replace(/\)/, ""); if (params.split(",").filter(p => p.trim()).length > maxParameters) smells.push({ file, type: "too-many-params" }); } } catch { /* skip */ } } return { content: [{ type: "text", text: JSON.stringify({ totalSmells: smells.length, smells }, null, 2) }] }; } private handleExtractMetrics(args: unknown) { const { filePath } = args as { filePath: string }; const metrics: { file: string; analyzedAt: string; size?: number; codeMetrics?: CodeMetrics; error?: string } = { file: filePath, analyzedAt: new Date().toISOString() }; try { const stat = fs.statSync(filePath); metrics.size = stat.size; if (stat.isFile()) metrics.codeMetrics = this.calculateCodeMetrics(fs.readFileSync(filePath, "utf-8"), "typescript"); } catch (e) { metrics.error = String(e); } return { content: [{ type: "text", text: JSON.stringify(metrics, null, 2) }] }; } private handleExploreCodebase(args: unknown) { const { patterns, basePath = ".", maxResults = 50 } = args as { patterns: string[]; basePath?: string; maxResults?: number }; const results: string[] = []; const root = path.resolve(basePath); const walk = (dir: string, depth = 0) => { if (results.length >= maxResults || depth > 10) return; try { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (results.length >= maxResults) break; const full = path.join(dir, entry.name); if (entry.isDirectory() && !["node_modules", ".git", "dist"].includes(entry.name)) walk(full, depth + 1); else if (entry.isFile() && patterns.some((p: string) => this.matchesPattern(entry.name, p))) results.push(path.relative(root, full)); } } catch { /* skip */ } }; walk(root); return { content: [{ type: "text", text: JSON.stringify({ totalFound: results.length, files: results.slice(0, maxResults) }, null, 2) }] }; } private matchesPattern(name: string, pattern: string): boolean { if (pattern.startsWith("**/")) pattern = pattern.slice(3); if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1)); if (pattern.includes("*")) return new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(name); return name.includes(pattern); } private handleFindPatterns(args: unknown) { const { pattern, fileTypes = [], basePath = ".", maxMatches = 100 } = args as { pattern: string; fileTypes?: string[]; basePath?: string; maxMatches?: number }; const results: PatternMatch[] = []; const root = path.resolve(basePath); const search = (file: string) => { try { const lines = fs.readFileSync(file, "utf-8").split("\n"); const regex = new RegExp(pattern, "gi"); for (let i = 0; i < lines.length && results.length < maxMatches; i++) { const line = lines[i]; if (line && regex.test(line)) results.push({ file: path.relative(root, file), line: i + 1, content: line.trim().substring(0, 100) }); } } catch { /* skip */ } }; const walk = (dir: string, depth = 0) => { if (results.length >= maxMatches || depth > 10) return; try { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (results.length >= maxMatches) break; const full = path.join(dir, entry.name); if (entry.isDirectory() && !["node_modules", ".git", "dist"].includes(entry.name)) walk(full, depth + 1); else if (entry.isFile()) { const ext = path.extname(entry.name); if (!fileTypes.length || fileTypes.includes(ext)) search(full); } } } catch { /* skip */ } }; walk(root); return { content: [{ type: "text", text: JSON.stringify({ totalMatches: results.length, matches: results }, null, 2) }] }; } private handleFindFunction(args: unknown) { const { functionName, fileTypes = [".ts", ".js"] } = args as { functionName: string; fileTypes?: string[] }; const results: FunctionDefinition[] = []; const search = (file: string) => { try { const lines = fs.readFileSync(file, "utf-8").split("\n"); const patterns = [new RegExp(`function\\s+${functionName}\\s*\\(`), new RegExp(`const\\s+${functionName}\\s*=`), new RegExp(`async\\s+function\\s+${functionName}\\s*\\(`)]; for (let i = 0; i < lines.length; i++) { const line = lines[i] || ""; for (const p of patterns) { if (p.test(line)) { results.push({ file, line: i + 1, context: lines[Math.max(0, i - 1)]?.trim().substring(0, 80) || "" }); break; } } } } catch { /* skip */ } }; const walk = (dir: string, depth = 0) => { if (results.length >= 50 || depth > 10) return; try { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (results.length >= 50) break; const full = path.join(dir, entry.name); if (entry.isDirectory() && !["node_modules", ".git", "dist"].includes(entry.name)) walk(full, depth + 1); else if (entry.isFile() && fileTypes.includes(path.extname(entry.name))) search(full); } } catch { /* skip */ } }; walk("."); return { content: [{ type: "text", text: JSON.stringify({ functionName, totalFound: results.length, definitions: results }, null, 2) }] }; } private handleGetFileStructure(args: unknown) { const { path: dirPath, maxDepth = 3, includeFiles = true } = args as { path: string; maxDepth?: number; includeFiles?: boolean }; const root = path.resolve(dirPath); const build = (dir: string, depth: number): FileTreeNode | null => { if (depth > maxDepth) return null; try { const node: FileTreeNode = { type: "directory", name: path.basename(dir), children: [] }; for (const entry of fs.readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory() ? !["node_modules", ".git", "dist"].includes(e.name) : includeFiles).sort((a, b) => (a.isDirectory() ? -1 : 1) - (b.isDirectory() ? -1 : 1))) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { const child = build(full, depth + 1); if (child) node.children!.push(child); } else try { node.children!.push({ type: "file", name: entry.name, size: fs.statSync(full).size }); } catch { /* skip */ } } return node; } catch { return { type: "error", name: path.basename(dir) }; } }; return { content: [{ type: "text", text: JSON.stringify({ structure: build(root, 0) }, null, 2) }] }; } private handleAnalyzeDependencies(args: unknown) { const { rootDir, maxDepth = 3 } = args as { rootDir: string; maxDepth?: number }; const deps: Record = {}; const visited = new Set(); const process = (file: string, depth: number) => { if (depth > maxDepth || visited.has(file)) return; visited.add(file); try { const content = fs.readFileSync(file, "utf-8"); const imports: string[] = []; let m; const ir = /import\s+.*?\s+from\s+['"](.+?)['"]/g; while ((m = ir.exec(content)) !== null) { if (m[1]) imports.push(m[1]); } const rr = /require\s*\(\s*['"](.+?)['"]\s*\)/g; while ((m = rr.exec(content)) !== null) { if (m[1]) imports.push(m[1]); } deps[file] = imports.filter(i => !i.startsWith(".") && !i.startsWith("/")); } catch { /* skip */ } }; const walk = (dir: string, depth: number) => { if (depth > maxDepth) return; try { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory() && !["node_modules", ".git", "dist"].includes(entry.name)) walk(full, depth + 1); else if (entry.isFile() && /\.(ts|js|tsx|jsx)$/.test(entry.name)) process(full, depth); } } catch { /* skip */ } }; walk(path.resolve(rootDir), 0); return { content: [{ type: "text", text: JSON.stringify({ totalFiles: Object.keys(deps).length, externalDependencies: [...new Set(Object.values(deps).flat())], dependencies: deps }, null, 2) }] }; } private handleFindDuplicates(args: unknown) { const { directory, minLength = 10 } = args as { directory: string; minLength?: number }; const blocks = new Map(); const extract = (content: string): string[] => { const b: string[] = [], lines = content.split("\n"); let current: string[] = []; for (const line of lines) { if (line.trim()) current.push(line); else if (current.length >= minLength) { b.push(current.join("\n")); current = []; } else { current = []; } } if (current.length >= minLength) b.push(current.join("\n")); return b; }; const walk = (dir: string) => { try { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory() && !["node_modules", ".git", "dist"].includes(entry.name)) walk(full); else if (entry.isFile() && /\.(ts|js)$/.test(entry.name)) { for (const block of extract(fs.readFileSync(full, "utf-8"))) { const norm = block.trim(); const existing = blocks.get(norm) || []; existing.push(full); blocks.set(norm, existing); } } } } catch { /* skip */ } }; walk(path.resolve(directory)); const duplicates = [...blocks.entries()].filter(([_, f]) => f.length > 1).map(([block, files]) => ({ block: block.substring(0, 100) + "...", files, length: block.split("\n").length })); return { content: [{ type: "text", text: JSON.stringify({ totalDuplicates: duplicates.length, duplicates }, null, 2) }] }; } } if (import.meta.url === `file://${fs.realpathSync(process.argv[1]!)}`) { new CodeAnalyzerServer().run().catch(() => {}); } export default CodeAnalyzerServer;