Skip to main content
Glama

documcp

by tosin2013
analyze-readme.ts17.6 kB
import { z } from "zod"; import { promises as fs } from "fs"; import path from "path"; import { MCPToolResponse } from "../types/api.js"; // Input validation schema const AnalyzeReadmeInputSchema = z.object({ project_path: z.string().min(1, "Project path is required"), target_audience: z .enum([ "community_contributors", "enterprise_users", "developers", "general", ]) .optional() .default("community_contributors"), optimization_level: z .enum(["light", "moderate", "aggressive"]) .optional() .default("moderate"), max_length_target: z.number().min(50).max(1000).optional().default(300), }); export type AnalyzeReadmeInput = z.infer<typeof AnalyzeReadmeInputSchema>; interface ReadmeAnalysis { lengthAnalysis: { currentLines: number; currentWords: number; targetLines: number; exceedsTarget: boolean; reductionNeeded: number; }; structureAnalysis: { scannabilityScore: number; headingHierarchy: HeadingInfo[]; sectionLengths: SectionLength[]; hasProperSpacing: boolean; }; contentAnalysis: { hasTldr: boolean; hasQuickStart: boolean; hasPrerequisites: boolean; hasTroubleshooting: boolean; codeBlockCount: number; linkCount: number; }; communityReadiness: { hasContributing: boolean; hasIssueTemplates: boolean; hasCodeOfConduct: boolean; hasSecurity: boolean; badgeCount: number; }; optimizationOpportunities: OptimizationOpportunity[]; overallScore: number; recommendations: string[]; } interface HeadingInfo { level: number; text: string; line: number; sectionLength: number; } interface SectionLength { heading: string; lines: number; words: number; tooLong: boolean; } interface OptimizationOpportunity { type: | "length_reduction" | "structure_improvement" | "content_enhancement" | "community_health"; priority: "high" | "medium" | "low"; description: string; impact: string; effort: "low" | "medium" | "high"; } /** * Analyzes README files for community health, accessibility, and onboarding effectiveness. * * Performs comprehensive README analysis including length assessment, structure evaluation, * content completeness, and community readiness scoring. Provides actionable recommendations * for improving README effectiveness and developer onboarding experience. * * @param input - The input parameters for README analysis * @param input.project_path - The file system path to the project containing the README * @param input.target_audience - The target audience for the README (default: "community_contributors") * @param input.optimization_level - The level of optimization to apply (default: "moderate") * @param input.max_length_target - Target maximum length in lines (default: 300) * * @returns Promise resolving to comprehensive README analysis results * @returns analysis - Complete analysis including length, structure, content, and community readiness * @returns nextSteps - Array of recommended next actions for README improvement * * @throws {Error} When project path is inaccessible or invalid * @throws {Error} When README file cannot be found or read * @throws {Error} When analysis processing fails * * @example * ```typescript * // Analyze README for community contributors * const result = await analyzeReadme({ * project_path: "/path/to/project", * target_audience: "community_contributors", * optimization_level: "moderate" * }); * * console.log(`README Score: ${result.data.analysis.overallScore}/100`); * console.log(`Recommendations: ${result.data.nextSteps.length} suggestions`); * * // Analyze for enterprise users with aggressive optimization * const enterprise = await analyzeReadme({ * project_path: "/path/to/enterprise/project", * target_audience: "enterprise_users", * optimization_level: "aggressive", * max_length_target: 200 * }); * ``` * * @since 1.0.0 */ export async function analyzeReadme( input: Partial<AnalyzeReadmeInput>, ): Promise<MCPToolResponse<{ analysis: ReadmeAnalysis; nextSteps: string[] }>> { const startTime = Date.now(); try { // Validate input const validatedInput = AnalyzeReadmeInputSchema.parse(input); const { project_path, target_audience, optimization_level, max_length_target, } = validatedInput; // Find README file const readmePath = await findReadmeFile(project_path); if (!readmePath) { return { success: false, error: { code: "README_NOT_FOUND", message: "No README file found in the project directory", details: "Looked for README.md, README.txt, readme.md in project root", resolution: "Create a README.md file in the project root directory", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }; } // Read README content const readmeContent = await fs.readFile(readmePath, "utf-8"); // Get project context const projectContext = await analyzeProjectContext(project_path); // Perform comprehensive analysis const lengthAnalysis = analyzeLengthMetrics( readmeContent, max_length_target, ); const structureAnalysis = analyzeStructure(readmeContent); const contentAnalysis = analyzeContent(readmeContent); const communityReadiness = analyzeCommunityReadiness( readmeContent, projectContext, ); // Generate optimization opportunities const optimizationOpportunities = generateOptimizationOpportunities( lengthAnalysis, structureAnalysis, contentAnalysis, communityReadiness, optimization_level, target_audience, ); // Calculate overall score const overallScore = calculateOverallScore( lengthAnalysis, structureAnalysis, contentAnalysis, communityReadiness, ); // Generate recommendations const recommendations = generateRecommendations( optimizationOpportunities, target_audience, optimization_level, ); const analysis: ReadmeAnalysis = { lengthAnalysis, structureAnalysis, contentAnalysis, communityReadiness, optimizationOpportunities, overallScore, recommendations, }; const nextSteps = generateNextSteps(analysis, optimization_level); return { success: true, data: { analysis, nextSteps, }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), analysisId: `readme-analysis-${Date.now()}`, }, }; } catch (error) { return { success: false, error: { code: "ANALYSIS_FAILED", message: "Failed to analyze README", details: error instanceof Error ? error.message : "Unknown error", resolution: "Check project path and README file accessibility", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }; } } async function findReadmeFile(projectPath: string): Promise<string | null> { const possibleNames = [ "README.md", "README.txt", "readme.md", "Readme.md", "README", ]; for (const name of possibleNames) { const filePath = path.join(projectPath, name); try { await fs.access(filePath); return filePath; } catch { continue; } } return null; } async function analyzeProjectContext(projectPath: string): Promise<any> { try { const files = await fs.readdir(projectPath); return { hasPackageJson: files.includes("package.json"), hasContributing: files.includes("CONTRIBUTING.md"), hasCodeOfConduct: files.includes("CODE_OF_CONDUCT.md"), hasSecurity: files.includes("SECURITY.md"), hasGithubDir: files.includes(".github"), hasDocsDir: files.includes("docs"), projectType: detectProjectType(files), }; } catch { return {}; } } function detectProjectType(files: string[]): string { if (files.includes("package.json")) return "javascript"; if (files.includes("requirements.txt") || files.includes("setup.py")) return "python"; if (files.includes("Cargo.toml")) return "rust"; if (files.includes("go.mod")) return "go"; if (files.includes("pom.xml") || files.includes("build.gradle")) return "java"; return "unknown"; } function analyzeLengthMetrics(content: string, targetLines: number) { const lines = content.split("\n"); const words = content.split(/\s+/).length; const currentLines = lines.length; return { currentLines, currentWords: words, targetLines, exceedsTarget: currentLines > targetLines, reductionNeeded: Math.max(0, currentLines - targetLines), }; } function analyzeStructure(content: string) { const lines = content.split("\n"); const headings = extractHeadings(lines); const sectionLengths = calculateSectionLengths(lines, headings); // Calculate scannability score const hasGoodSpacing = /\n\s*\n/.test(content); const hasLists = /^\s*[-*+]\s+/m.test(content); const hasCodeBlocks = /```/.test(content); const properHeadingHierarchy = checkHeadingHierarchy(headings); const scannabilityScore = Math.round( (hasGoodSpacing ? 25 : 0) + (hasLists ? 25 : 0) + (hasCodeBlocks ? 25 : 0) + (properHeadingHierarchy ? 25 : 0), ); return { scannabilityScore, headingHierarchy: headings, sectionLengths, hasProperSpacing: hasGoodSpacing, }; } function extractHeadings(lines: string[]): HeadingInfo[] { const headings: HeadingInfo[] = []; lines.forEach((line, index) => { const match = line.match(/^(#{1,6})\s+(.+)$/); if (match) { headings.push({ level: match[1].length, text: match[2].trim(), line: index + 1, sectionLength: 0, // Will be calculated later }); } }); return headings; } function calculateSectionLengths( lines: string[], headings: HeadingInfo[], ): SectionLength[] { const sections: SectionLength[] = []; headings.forEach((heading, index) => { const startLine = heading.line - 1; const endLine = index < headings.length - 1 ? headings[index + 1].line - 1 : lines.length; const sectionLines = lines.slice(startLine, endLine); const sectionText = sectionLines.join("\n"); const wordCount = sectionText.split(/\s+/).length; sections.push({ heading: heading.text, lines: sectionLines.length, words: wordCount, tooLong: sectionLines.length > 50 || wordCount > 500, }); }); return sections; } function checkHeadingHierarchy(headings: HeadingInfo[]): boolean { if (headings.length === 0) return false; // Check if starts with H1 if (headings[0].level !== 1) return false; // Check for logical hierarchy for (let i = 1; i < headings.length; i++) { const levelDiff = headings[i].level - headings[i - 1].level; if (levelDiff > 1) return false; // Skipping levels } return true; } function analyzeContent(content: string) { return { hasTldr: content.includes("## TL;DR") || content.includes("# TL;DR"), hasQuickStart: /quick start|getting started|installation/i.test(content), hasPrerequisites: /prerequisite|requirement|dependencies/i.test(content), hasTroubleshooting: /troubleshoot|faq|common issues|problems/i.test( content, ), codeBlockCount: (content.match(/```/g) || []).length / 2, linkCount: (content.match(/\[.*?\]\(.*?\)/g) || []).length, }; } function analyzeCommunityReadiness(content: string, projectContext: any) { return { hasContributing: /contributing|contribute/i.test(content) || projectContext.hasContributing, hasIssueTemplates: /issue template|bug report/i.test(content) || projectContext.hasGithubDir, hasCodeOfConduct: /code of conduct/i.test(content) || projectContext.hasCodeOfConduct, hasSecurity: /security/i.test(content) || projectContext.hasSecurity, badgeCount: (content.match(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g) || []).length, }; } function generateOptimizationOpportunities( lengthAnalysis: any, structureAnalysis: any, contentAnalysis: any, communityReadiness: any, optimizationLevel: string, targetAudience: string, ): OptimizationOpportunity[] { const opportunities: OptimizationOpportunity[] = []; // Length reduction opportunities if (lengthAnalysis.exceedsTarget) { opportunities.push({ type: "length_reduction", priority: "high", description: `README is ${lengthAnalysis.reductionNeeded} lines over target (${lengthAnalysis.currentLines}/${lengthAnalysis.targetLines})`, impact: "Improves scannability and reduces cognitive load for new users", effort: lengthAnalysis.reductionNeeded > 100 ? "high" : "medium", }); } // Structure improvements if (structureAnalysis.scannabilityScore < 75) { opportunities.push({ type: "structure_improvement", priority: "high", description: `Low scannability score (${structureAnalysis.scannabilityScore}/100)`, impact: "Makes README easier to navigate and understand quickly", effort: "medium", }); } // Content enhancements if (!contentAnalysis.hasTldr) { opportunities.push({ type: "content_enhancement", priority: "high", description: "Missing TL;DR section for quick project overview", impact: "Helps users quickly understand project value proposition", effort: "low", }); } if (!contentAnalysis.hasQuickStart) { opportunities.push({ type: "content_enhancement", priority: "medium", description: "Missing quick start section", impact: "Reduces time to first success for new users", effort: "medium", }); } // Community health if ( !communityReadiness.hasContributing && targetAudience === "community_contributors" ) { opportunities.push({ type: "community_health", priority: "medium", description: "Missing contributing guidelines", impact: "Encourages community participation and sets expectations", effort: "medium", }); } return opportunities.sort((a, b) => { const priorityOrder = { high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); } function calculateOverallScore( lengthAnalysis: any, structureAnalysis: any, contentAnalysis: any, communityReadiness: any, ): number { let score = 0; // Length score (25 points) score += lengthAnalysis.exceedsTarget ? Math.max(0, 25 - lengthAnalysis.reductionNeeded / 10) : 25; // Structure score (25 points) score += (structureAnalysis.scannabilityScore / 100) * 25; // Content score (25 points) const contentScore = (contentAnalysis.hasTldr ? 8 : 0) + (contentAnalysis.hasQuickStart ? 8 : 0) + (contentAnalysis.hasPrerequisites ? 5 : 0) + (contentAnalysis.codeBlockCount > 0 ? 4 : 0); score += Math.min(25, contentScore); // Community score (25 points) const communityScore = (communityReadiness.hasContributing ? 8 : 0) + (communityReadiness.hasCodeOfConduct ? 5 : 0) + (communityReadiness.hasSecurity ? 5 : 0) + (communityReadiness.badgeCount > 0 ? 4 : 0) + (communityReadiness.hasIssueTemplates ? 3 : 0); score += Math.min(25, communityScore); return Math.round(score); } function generateRecommendations( opportunities: OptimizationOpportunity[], targetAudience: string, optimizationLevel: string, ): string[] { const recommendations: string[] = []; // High priority opportunities first const highPriority = opportunities.filter((op) => op.priority === "high"); highPriority.forEach((op) => { recommendations.push(`🚨 ${op.description} - ${op.impact}`); }); // Audience-specific recommendations if (targetAudience === "community_contributors") { recommendations.push( "👥 Focus on community onboarding: clear contributing guidelines and issue templates", ); } else if (targetAudience === "enterprise_users") { recommendations.push( "🏢 Emphasize security, compliance, and support channels", ); } // Optimization level specific if (optimizationLevel === "aggressive") { recommendations.push( "⚡ Consider moving detailed documentation to separate files (docs/ directory)", ); recommendations.push( "📝 Use progressive disclosure: expandable sections for advanced topics", ); } return recommendations.slice(0, 8); // Limit to top 8 recommendations } function generateNextSteps( analysis: ReadmeAnalysis, optimizationLevel: string, ): string[] { const steps: string[] = []; if (analysis.overallScore < 60) { steps.push("🎯 Priority: Address critical issues first (score < 60)"); } // Add specific next steps based on opportunities const highPriorityOps = analysis.optimizationOpportunities .filter((op) => op.priority === "high") .slice(0, 3); highPriorityOps.forEach((op) => { steps.push(`• ${op.description}`); }); if (optimizationLevel !== "light") { steps.push( "📊 Run optimize_readme tool to get specific restructuring suggestions", ); } steps.push("🔄 Re-analyze after changes to track improvement"); return steps; }

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/documcp'

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