UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

237 lines 8.87 kB
/** * ADR Discovery Utilities * * Utilities for discovering and analyzing existing ADRs in the project */ import { McpAdrError } from '../types/index.js'; /** * Actually discover ADRs using file system operations */ export async function discoverAdrsInDirectory(adrDirectory, includeContent = false, projectPath) { try { const fs = await import('fs/promises'); const path = await import('path'); // Resolve the ADR directory path const fullAdrPath = path.resolve(projectPath, adrDirectory); // Check if directory exists let dirExists = false; try { const stat = await fs.stat(fullAdrPath); dirExists = stat.isDirectory(); } catch { // Directory doesn't exist } if (!dirExists) { return { directory: adrDirectory, totalAdrs: 0, adrs: [], summary: { byStatus: {}, byCategory: {} }, recommendations: [ `ADR directory '${adrDirectory}' does not exist`, 'Consider creating the directory and adding your first ADR', 'Use the generate_adr_from_decision tool to create new ADRs' ] }; } // Read directory contents const entries = await fs.readdir(fullAdrPath, { withFileTypes: true }); const markdownFiles = entries .filter(entry => entry.isFile() && entry.name.endsWith('.md')) .map(entry => entry.name); // Process each markdown file to check if it's an ADR const discoveredAdrs = []; for (const filename of markdownFiles) { const filePath = path.join(fullAdrPath, filename); try { // Read the file content to check if it's an ADR const content = await fs.readFile(filePath, 'utf-8'); // Check if this looks like an ADR const isAdr = isLikelyAdr(content, filename); if (isAdr) { const adr = parseAdrMetadata(content, filename, path.join(adrDirectory, filename)); if (includeContent) { adr.content = content; } discoveredAdrs.push(adr); } } catch (error) { console.error(`[WARN] Failed to read file ${filename}:`, error); } } // Generate summary const summary = generateAdrSummary(discoveredAdrs); // Generate recommendations const recommendations = generateRecommendations(discoveredAdrs, adrDirectory); return { directory: adrDirectory, totalAdrs: discoveredAdrs.length, adrs: discoveredAdrs, summary, recommendations }; } catch (error) { throw new McpAdrError(`Failed to discover ADRs: ${error instanceof Error ? error.message : String(error)}`, 'DISCOVERY_ERROR'); } } /** * Check if a file is likely an ADR based on content and filename */ function isLikelyAdr(content, filename) { const contentLower = content.toLowerCase(); const filenameLower = filename.toLowerCase(); // Check filename patterns const filenamePatterns = [ /^adr[-_]?\d+/i, // ADR-001, ADR_001, adr-001 /^\d+[-_]/i, // 001-, 0001_ /architectural[-_]?decision/i, // architectural-decision /decision[-_]?record/i // decision-record ]; const hasAdrFilename = filenamePatterns.some(pattern => pattern.test(filenameLower)); // Check content patterns const contentPatterns = [ /# .*decision/i, /## status/i, /## context/i, /## decision/i, /## consequences/i, /architectural decision record/i, /decision record/i, /adr[-_]?\d+/i ]; const hasAdrContent = contentPatterns.some(pattern => pattern.test(contentLower)); // Must have either ADR filename pattern OR ADR content patterns return hasAdrFilename || hasAdrContent; } /** * Parse ADR metadata from content */ function parseAdrMetadata(content, filename, fullPath) { // Extract title (usually first # heading) let title = filename.replace(/\.md$/, ''); const titleMatch = content.match(/^#\s+(.+)$/m); if (titleMatch && titleMatch[1]) { title = titleMatch[1].trim(); } // Extract status let status = 'unknown'; const statusMatch = content.match(/(?:##?\s*status|status:)\s*(.+?)(?:\n|$)/i); if (statusMatch && statusMatch[1]) { status = statusMatch[1].trim().toLowerCase(); } // Extract date let date; const dateMatch = content.match(/(?:##?\s*date|date:)\s*(.+?)(?:\n|$)/i); if (dateMatch && dateMatch[1]) { date = dateMatch[1].trim(); } // Extract ADR number from filename or content let number; const numberMatch = filename.match(/(?:adr[-_]?)?(\d+)/i) || content.match(/adr[-_]?(\d+)/i); if (numberMatch && numberMatch[1]) { number = numberMatch[1]; } // Extract context let context; const contextMatch = content.match(/##?\s*context\s*\n([\s\S]*?)(?=\n##|\n#|$)/i); if (contextMatch && contextMatch[1]) { context = contextMatch[1].trim(); } // Extract decision let decision; const decisionMatch = content.match(/##?\s*decision\s*\n([\s\S]*?)(?=\n##|\n#|$)/i); if (decisionMatch && decisionMatch[1]) { decision = decisionMatch[1].trim(); } // Extract consequences let consequences; const consequencesMatch = content.match(/##?\s*consequences\s*\n([\s\S]*?)(?=\n##|\n#|$)/i); if (consequencesMatch && consequencesMatch[1]) { consequences = consequencesMatch[1].trim(); } // Extract category/tags (if any) const tags = []; const tagMatch = content.match(/(?:tags?|categories?):\s*(.+?)(?:\n|$)/i); if (tagMatch && tagMatch[1]) { tags.push(...tagMatch[1].split(',').map(tag => tag.trim())); } const metadata = { tags }; if (number) metadata.number = number; if (tags[0]) metadata.category = tags[0]; const result = { filename, title, status, date, path: fullPath, metadata }; if (context) result.context = context; if (decision) result.decision = decision; if (consequences) result.consequences = consequences; return result; } /** * Generate summary statistics */ function generateAdrSummary(adrs) { const byStatus = {}; const byCategory = {}; for (const adr of adrs) { // Count by status byStatus[adr.status] = (byStatus[adr.status] || 0) + 1; // Count by category const category = adr.metadata?.category || 'uncategorized'; byCategory[category] = (byCategory[category] || 0) + 1; } return { byStatus, byCategory }; } /** * Generate recommendations based on discovered ADRs */ function generateRecommendations(adrs, adrDirectory) { const recommendations = []; if (adrs.length === 0) { recommendations.push(`No ADRs found in ${adrDirectory}`, 'Consider creating your first ADR using the generate_adr_from_decision tool', 'Use suggest_adrs tool to identify architectural decisions that need documentation'); } else { recommendations.push(`Found ${adrs.length} ADRs in ${adrDirectory}`); // Check for status distribution const statuses = [...new Set(adrs.map(adr => adr.status))]; if (statuses.includes('proposed') || statuses.includes('draft')) { recommendations.push('Consider reviewing and updating proposed/draft ADRs'); } // Check for numbering gaps const numbers = adrs .map(adr => adr.metadata?.number) .filter((n) => n !== undefined) .map(n => parseInt(n, 10)) .sort((a, b) => a - b); if (numbers.length > 1) { const gaps = []; for (let i = 1; i < numbers.length; i++) { const prev = numbers[i - 1]; const curr = numbers[i]; if (prev !== undefined && curr !== undefined && curr - prev > 1) { gaps.push(`${prev + 1}-${curr - 1}`); } } if (gaps.length > 0) { recommendations.push(`Consider filling ADR numbering gaps: ${gaps.join(', ')}`); } } // Suggest using discovered ADRs for analysis recommendations.push('Use suggest_adrs tool with existingAdrs parameter to find missing decisions', 'Use generate_adr_todo tool to create implementation tasks from these ADRs'); } return recommendations; } //# sourceMappingURL=adr-discovery.js.map