Skip to main content
Glama

documcp

by tosin2013
validate-readme-checklist.ts18.8 kB
import { z } from 'zod'; import { promises as fs } from 'fs'; // Input schema export const ValidateReadmeChecklistSchema = z.object({ readmePath: z.string().min(1, 'README path is required'), projectPath: z.string().optional(), strict: z.boolean().default(false), outputFormat: z.enum(['json', 'markdown', 'console']).default('console'), }); export type ValidateReadmeChecklistInput = z.infer<typeof ValidateReadmeChecklistSchema>; interface ChecklistItem { id: string; category: string; name: string; description: string; required: boolean; weight: number; } interface ValidationResult { item: ChecklistItem; passed: boolean; details: string; suggestions?: string[]; } interface ChecklistReport { overallScore: number; totalItems: number; passedItems: number; failedItems: number; categories: { [category: string]: { score: number; passed: number; total: number; results: ValidationResult[]; }; }; recommendations: string[]; estimatedReadTime: number; wordCount: number; } export class ReadmeChecklistValidator { private checklist: ChecklistItem[] = []; constructor() { this.initializeChecklist(); } private initializeChecklist(): void { this.checklist = [ // Essential Sections { id: 'title', category: 'Essential Sections', name: 'Project Title', description: 'Clear, descriptive project title as main heading', required: true, weight: 10, }, { id: 'description', category: 'Essential Sections', name: 'Project Description', description: 'Brief one-liner describing what the project does', required: true, weight: 10, }, { id: 'tldr', category: 'Essential Sections', name: 'TL;DR Section', description: '2-3 sentence summary of the project', required: true, weight: 8, }, { id: 'quickstart', category: 'Essential Sections', name: 'Quick Start Guide', description: 'Instructions to get running in under 5 minutes', required: true, weight: 10, }, { id: 'installation', category: 'Essential Sections', name: 'Installation Instructions', description: 'Clear installation steps with code examples', required: true, weight: 9, }, { id: 'usage', category: 'Essential Sections', name: 'Basic Usage Examples', description: 'Simple working code examples', required: true, weight: 9, }, { id: 'license', category: 'Essential Sections', name: 'License Information', description: 'Clear license information', required: true, weight: 7, }, // Community Health { id: 'contributing', category: 'Community Health', name: 'Contributing Guidelines', description: 'Link to CONTRIBUTING.md or inline guidelines', required: false, weight: 6, }, { id: 'code-of-conduct', category: 'Community Health', name: 'Code of Conduct', description: 'Link to CODE_OF_CONDUCT.md', required: false, weight: 4, }, { id: 'security', category: 'Community Health', name: 'Security Policy', description: 'Link to SECURITY.md or security reporting info', required: false, weight: 4, }, // Visual Elements { id: 'badges', category: 'Visual Elements', name: 'Status Badges', description: 'Build status, version, license badges', required: false, weight: 3, }, { id: 'screenshots', category: 'Visual Elements', name: 'Screenshots/Demos', description: 'Visual representation for applications/tools', required: false, weight: 5, }, { id: 'formatting', category: 'Visual Elements', name: 'Consistent Formatting', description: 'Proper markdown formatting and structure', required: true, weight: 6, }, // Content Quality { id: 'working-examples', category: 'Content Quality', name: 'Working Code Examples', description: 'All code examples are functional and tested', required: true, weight: 8, }, { id: 'external-links', category: 'Content Quality', name: 'Functional External Links', description: 'All external links are working', required: true, weight: 5, }, { id: 'appropriate-length', category: 'Content Quality', name: 'Appropriate Length', description: 'README under 300 lines for community projects', required: false, weight: 4, }, { id: 'scannable-structure', category: 'Content Quality', name: 'Scannable Structure', description: 'Good heading hierarchy and organization', required: true, weight: 7, }, ]; } async validateReadme(input: ValidateReadmeChecklistInput): Promise<ChecklistReport> { const readmeContent = await fs.readFile(input.readmePath, 'utf-8'); const projectFiles = input.projectPath ? await this.getProjectFiles(input.projectPath) : []; const results: ValidationResult[] = []; const categories: { [key: string]: ValidationResult[] } = {}; // Run validation for each checklist item for (const item of this.checklist) { const result = await this.validateItem(item, readmeContent, projectFiles, input); results.push(result); if (!categories[item.category]) { categories[item.category] = []; } categories[item.category].push(result); } return this.generateReport(results, readmeContent); } private async validateItem( item: ChecklistItem, content: string, projectFiles: string[], _input: ValidateReadmeChecklistInput, ): Promise<ValidationResult> { let passed = false; let details = ''; const suggestions: string[] = []; switch (item.id) { case 'title': { const titleRegex = /^#\s+.+/m; const hasTitle = titleRegex.test(content); passed = hasTitle; details = passed ? 'Project title found' : 'No main heading (# Title) found'; if (!passed) suggestions.push('Add a clear project title as the first heading: # Your Project Name'); break; } case 'description': { const descRegex = /(^>\s+.+|^[^#\n].{20,})/m; const hasDesc = descRegex.test(content); passed = hasDesc; details = passed ? 'Project description found' : 'Missing project description'; if (!passed) suggestions.push('Add a brief description using > quote syntax or paragraph after title'); break; } case 'tldr': { const tldrRegex = /##?\s*(tl;?dr|quick start|at a glance)/i; const hasTldr = tldrRegex.test(content); passed = hasTldr; details = passed ? 'TL;DR section found' : 'Missing TL;DR or quick overview'; if (!passed) suggestions.push( 'Add a ## TL;DR section with 2-3 sentences explaining what your project does', ); break; } case 'quickstart': passed = /##\s*(Quick\s*Start|Getting\s*Started|Installation)/i.test(content); details = passed ? 'Quick start section found' : 'No quick start section found'; if (!passed) suggestions.push('Add a ## Quick Start section with immediate setup instructions'); break; case 'installation': { const installRegex = /##?\s*(install|installation|setup)/i; const hasInstall = installRegex.test(content); passed = hasInstall; details = passed ? 'Installation instructions found' : 'Missing installation instructions'; if (!passed) suggestions.push('Add installation instructions with code blocks showing exact commands'); break; } case 'usage': { const usageRegex = /##?\s*(usage|example|getting started)/i; const hasUsage = usageRegex.test(content); passed = hasUsage; details = passed ? 'Usage examples found' : 'Missing usage examples'; if (!passed) suggestions.push('Add usage examples with working code snippets'); break; } case 'license': { const licenseRegex = /##?\s*license/i; const hasLicense = licenseRegex.test(content); const hasLicenseFile = projectFiles.includes('LICENSE') || projectFiles.includes('LICENSE.md'); passed = hasLicense || hasLicenseFile; details = passed ? 'License information found' : 'Missing license information'; if (!passed) suggestions.push('Add a ## License section or LICENSE file'); break; } case 'contributing': { const hasContributing = /##\s*Contribut/i.test(content); const hasContributingFile = projectFiles.includes('CONTRIBUTING.md'); passed = hasContributing || hasContributingFile; details = passed ? 'Contributing guidelines found' : 'No contributing guidelines found'; if (!passed) suggestions.push('Add contributing guidelines or link to CONTRIBUTING.md'); break; } case 'code-of-conduct': { const hasCodeOfConduct = /code.of.conduct/i.test(content); const hasCodeFile = projectFiles.includes('CODE_OF_CONDUCT.md'); passed = hasCodeOfConduct || hasCodeFile; details = passed ? 'Code of conduct found' : 'No code of conduct found'; break; } case 'security': { const hasSecurity = /security/i.test(content); const hasSecurityFile = projectFiles.includes('SECURITY.md'); passed = hasSecurity || hasSecurityFile; details = passed ? 'Security information found' : 'No security policy found'; break; } case 'badges': { const badgeRegex = /\[!\[.*?\]\(.*?\)\]\(.*?\)|!\[.*?\]\(.*?badge.*?\)/i; const hasBadges = badgeRegex.test(content); passed = hasBadges; details = passed ? 'Status badges found' : 'Consider adding status badges'; if (!passed) suggestions.push('Consider adding badges for build status, version, license'); break; } case 'screenshots': { const imageRegex = /!\[.*?\]\(.*?\.(png|jpg|jpeg|gif|svg).*?\)/i; const hasImages = imageRegex.test(content); passed = hasImages; details = passed ? 'Screenshots/images found' : 'Consider adding screenshots or images'; if (!passed && content.includes('application')) { suggestions.push('Consider adding screenshots or demo GIFs for visual applications'); } break; } case 'formatting': { const hasHeaders = (content.match(/^##?\s+/gm) || []).length >= 3; const hasProperSpacing = !/#{1,6}\s*\n\s*#{1,6}/.test(content); passed = hasHeaders && hasProperSpacing; details = passed ? 'Good markdown formatting' : 'Improve markdown formatting and structure'; if (!passed) suggestions.push('Improve markdown formatting with proper heading hierarchy and spacing'); break; } case 'working-examples': { const codeRegex = /```[\s\S]*?```/g; const codeBlocks = content.match(codeRegex) || []; passed = codeBlocks.length > 0; details = `${codeBlocks.length} code examples found`; if (!passed) suggestions.push('Add working code examples to demonstrate usage'); break; } case 'external-links': { const links = content.match(/\[.*?\]\((https?:\/\/.*?)\)/g) || []; passed = true; // Assume links work unless we can verify details = `${links.length} external links found`; break; } case 'appropriate-length': { const wordCount = content.split(/\s+/).length; passed = wordCount <= 300; details = `${wordCount} words (target: ≤300)`; if (!passed) suggestions.push( 'Consider shortening README or moving detailed content to separate docs', ); break; } case 'scannable-structure': { const sections = (content.match(/^##?\s+/gm) || []).length; const lists = (content.match(/^\s*[-*+]\s+/gm) || []).length; passed = sections >= 3 && lists >= 2; details = passed ? 'Good scannable structure' : 'Improve structure with more sections and lists'; if (!passed) suggestions.push('Improve heading structure with logical hierarchy (H1 → H2 → H3)'); break; } default: passed = false; details = 'Validation not implemented'; } return { item, passed, details, suggestions: suggestions.length > 0 ? suggestions : undefined, }; } private async getProjectFiles(projectPath: string): Promise<string[]> { try { const files = await fs.readdir(projectPath); return files; } catch { return []; } } private generateReport(results: ValidationResult[], content: string): ChecklistReport { const categories: { [category: string]: { score: number; passed: number; total: number; results: ValidationResult[]; }; } = {}; let totalWeight = 0; let passedWeight = 0; let passedItems = 0; const totalItems = results.length; // Group results by category and calculate scores for (const result of results) { const category = result.item.category; if (!categories[category]) { categories[category] = { score: 0, passed: 0, total: 0, results: [] }; } categories[category].results.push(result); categories[category].total++; totalWeight += result.item.weight; if (result.passed) { categories[category].passed++; passedWeight += result.item.weight; passedItems++; } } // Calculate category scores for (const category in categories) { const cat = categories[category]; cat.score = Math.round((cat.passed / cat.total) * 100); } const overallScore = Math.round((passedWeight / totalWeight) * 100); const wordCount = content.split(/\s+/).length; const estimatedReadTime = Math.ceil(wordCount / 200); // 200 words per minute // Generate recommendations const recommendations: string[] = []; if (overallScore < 70) { recommendations.push('README needs significant improvement to meet community standards'); } if (categories['Essential Sections']?.score < 80) { recommendations.push('Focus on completing essential sections first'); } if (wordCount > 2000) { recommendations.push('Consider breaking up content into separate documentation files'); } if (!results.find((r) => r.item.id === 'badges')?.passed) { recommendations.push('Add status badges to improve project credibility'); } return { overallScore, totalItems, passedItems, failedItems: totalItems - passedItems, categories, recommendations, estimatedReadTime, wordCount, }; } formatReport(report: ChecklistReport, format: 'json' | 'markdown' | 'console'): string { switch (format) { case 'json': return JSON.stringify(report, null, 2); case 'markdown': return this.formatMarkdownReport(report); case 'console': default: return this.formatConsoleReport(report); } } private formatMarkdownReport(report: ChecklistReport): string { let output = '# README Checklist Report\n\n'; output += `## Overall Score: ${report.overallScore}%\n\n`; output += `- **Passed**: ${report.passedItems}/${report.totalItems} items\n`; output += `- **Word Count**: ${report.wordCount} words\n`; output += `- **Estimated Read Time**: ${report.estimatedReadTime} minutes\n\n`; output += '## Category Breakdown\n\n'; for (const [categoryName, category] of Object.entries(report.categories)) { output += `### ${categoryName} (${category.score}%)\n\n`; for (const result of category.results) { const status = result.passed ? '✅' : '❌'; output += `- ${status} **${result.item.name}**: ${result.details}\n`; if (result.suggestions) { for (const suggestion of result.suggestions) { output += ` - 💡 ${suggestion}\n`; } } } output += '\n'; } if (report.recommendations.length > 0) { output += '## Recommendations\n\n'; for (const rec of report.recommendations) { output += `- ${rec}\n`; } } return output; } private formatConsoleReport(report: ChecklistReport): string { let output = '\n📋 README Checklist Report\n'; output += '='.repeat(50) + '\n'; const scoreColor = report.overallScore >= 80 ? '🟢' : report.overallScore >= 60 ? '🟡' : '🔴'; output += `${scoreColor} Overall Score: ${report.overallScore}%\n`; output += `✅ Passed: ${report.passedItems}/${report.totalItems} items\n`; output += `📄 Word Count: ${report.wordCount} words\n`; output += `⏱️ Read Time: ${report.estimatedReadTime} minutes\n\n`; for (const [categoryName, category] of Object.entries(report.categories)) { const catColor = category.score >= 80 ? '🟢' : category.score >= 60 ? '🟡' : '🔴'; output += `${catColor} ${categoryName} (${category.score}%)\n`; output += '-'.repeat(30) + '\n'; for (const result of category.results) { const status = result.passed ? '✅' : '❌'; output += `${status} ${result.item.name}: ${result.details}\n`; if (result.suggestions) { for (const suggestion of result.suggestions) { output += ` 💡 ${suggestion}\n`; } } } output += '\n'; } if (report.recommendations.length > 0) { output += '🎯 Recommendations:\n'; for (const rec of report.recommendations) { output += `• ${rec}\n`; } } return output; } } export async function validateReadmeChecklist( input: ValidateReadmeChecklistInput, ): Promise<ChecklistReport> { const validatedInput = ValidateReadmeChecklistSchema.parse(input); const validator = new ReadmeChecklistValidator(); return await validator.validateReadme(validatedInput); }

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