Skip to main content
Glama

mcp-adr-analysis-server

by tosin2013
adr-discovery.ts12.1 kB
/** * ADR Discovery Utilities * * Utilities for discovering and analyzing existing ADRs in the project */ import { McpAdrError } from '../types/index.js'; /** * Represents a discovered Architectural Decision Record */ export interface DiscoveredAdr { /** Filename of the ADR file */ filename: string; /** Title extracted from the ADR */ title: string; /** Status of the decision (proposed, accepted, deprecated, etc.) */ status: string; /** Date when the decision was made */ date: string | undefined; /** Full file path to the ADR */ path: string; /** Full content of the ADR file (optional) */ content?: string; /** Context section of the ADR */ context?: string; /** Decision section of the ADR */ decision?: string; /** Consequences section of the ADR */ consequences?: string; /** Additional metadata about the ADR */ metadata?: { /** ADR number or identifier */ number?: string; /** Category or domain of the decision */ category?: string; /** Tags for categorization */ tags?: string[]; }; } /** * Result of ADR discovery operation */ export interface AdrDiscoveryResult { /** Directory where ADRs were discovered */ directory: string; /** Total number of ADRs found */ totalAdrs: number; /** Array of discovered ADRs */ adrs: DiscoveredAdr[]; /** Summary statistics of discovered ADRs */ summary: { /** Count of ADRs by status */ byStatus: Record<string, number>; /** Count of ADRs by category */ byCategory: Record<string, number>; }; /** Recommendations for improving ADR management */ recommendations: string[]; } /** * Discover ADRs in a directory using file system operations * * @param adrDirectory - Relative path to ADR directory * @param includeContent - Whether to include full content of ADR files * @param projectPath - Root path of the project * @returns Promise resolving to ADR discovery results * @throws McpAdrError if directory access fails or parsing errors occur */ export async function discoverAdrsInDirectory( adrDirectory: string, includeContent: boolean = false, projectPath: string ): Promise<AdrDiscoveryResult> { try { const fs = await import('fs/promises'); const path = await import('path'); // Try multiple strategies to resolve the correct ADR directory path // This handles cases where: // 1. projectPath is already in a subdirectory (e.g., /project/docs) // 2. adrDirectory is relative (e.g., "docs/adrs") // 3. adrDirectory is already absolute let fullAdrPath: string; let dirExists = false; // Strategy 1: Try resolving as-is (standard behavior) fullAdrPath = path.resolve(projectPath, adrDirectory); try { const stat = await fs.stat(fullAdrPath); dirExists = stat.isDirectory(); } catch { dirExists = false; } // Strategy 2: If that fails and adrDirectory contains the same path segment as projectPath, // the projectPath might be pointing to a subdirectory. Try stripping the common suffix. if (!dirExists && adrDirectory.includes(path.sep)) { const projectPathParts = projectPath.split(path.sep); const adrDirParts = adrDirectory.split(path.sep); // Check if projectPath ends with the first part of adrDirectory // e.g., projectPath ends with "docs" and adrDirectory starts with "docs/adrs" const lastProjectPart = projectPathParts[projectPathParts.length - 1]; if (lastProjectPart && lastProjectPart === adrDirParts[0]) { // Remove the redundant first part from adrDirectory const adjustedAdrDir = adrDirParts.slice(1).join(path.sep); fullAdrPath = path.resolve(projectPath, adjustedAdrDir); try { const stat = await fs.stat(fullAdrPath); dirExists = stat.isDirectory(); } catch { dirExists = false; } } } // Strategy 3: If still not found, try checking one level up from projectPath if (!dirExists) { const parentPath = path.dirname(projectPath); fullAdrPath = path.resolve(parentPath, adrDirectory); try { const stat = await fs.stat(fullAdrPath); dirExists = stat.isDirectory(); } catch { dirExists = false; } } if (!dirExists) { return { directory: adrDirectory, totalAdrs: 0, adrs: [], summary: { byStatus: {}, byCategory: {} }, recommendations: [ `ADR directory '${adrDirectory}' does not exist (tried multiple path resolutions)`, `Attempted paths: ${path.resolve(projectPath, adrDirectory)}, ${path.resolve(path.dirname(projectPath), adrDirectory)}`, '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: DiscoveredAdr[] = []; 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: string, filename: string): boolean { 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: string, filename: string, fullPath: string): DiscoveredAdr { // 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: string | undefined; 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: string | undefined; const numberMatch = filename.match(/(?:adr[-_]?)?(\d+)/i) || content.match(/adr[-_]?(\d+)/i); if (numberMatch && numberMatch[1]) { number = numberMatch[1]; } // Extract context let context: string | undefined; 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: string | undefined; 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: string | undefined; 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: string[] = []; const tagMatch = content.match(/(?:tags?|categories?):\s*(.+?)(?:\n|$)/i); if (tagMatch && tagMatch[1]) { tags.push(...tagMatch[1].split(',').map(tag => tag.trim())); } const metadata: DiscoveredAdr['metadata'] = { tags }; if (number) metadata.number = number; if (tags[0]) metadata.category = tags[0]; const result: DiscoveredAdr = { 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: DiscoveredAdr[]) { const byStatus: Record<string, number> = {}; const byCategory: Record<string, number> = {}; 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: DiscoveredAdr[], adrDirectory: string): string[] { const recommendations: string[] = []; 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 is string => 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; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/mcp-adr-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server