Skip to main content
Glama

documcp

by tosin2013
optimize-readme.ts13.7 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 OptimizeReadmeInputSchema = z.object({ readme_path: z.string().min(1, 'README path is required'), strategy: z .enum(['community_focused', 'enterprise_focused', 'developer_focused', 'general']) .optional() .default('community_focused'), max_length: z.number().min(50).max(1000).optional().default(300), include_tldr: z.boolean().optional().default(true), preserve_existing: z.boolean().optional().default(false), output_path: z.string().optional(), create_docs_directory: z.boolean().optional().default(true), }); export type OptimizeReadmeInput = z.infer<typeof OptimizeReadmeInputSchema>; interface OptimizationResult { originalLength: number; optimizedLength: number; reductionPercentage: number; optimizedContent: string; extractedSections: ExtractedSection[]; tldrGenerated: string | null; restructuringChanges: RestructuringChange[]; recommendations: string[]; } interface ExtractedSection { title: string; content: string; suggestedLocation: string; reason: string; } interface RestructuringChange { type: 'moved' | 'condensed' | 'split' | 'added' | 'removed'; section: string; description: string; impact: string; } export async function optimizeReadme( input: Partial<OptimizeReadmeInput>, ): Promise<MCPToolResponse<{ optimization: OptimizationResult; nextSteps: string[] }>> { const startTime = Date.now(); try { // Validate input const validatedInput = OptimizeReadmeInputSchema.parse(input); const { readme_path, strategy, max_length, include_tldr, output_path, create_docs_directory } = validatedInput; // Read original README const originalContent = await fs.readFile(readme_path, 'utf-8'); const originalLength = originalContent.split('\n').length; // Parse README structure const sections = parseReadmeStructure(originalContent); // Generate TL;DR if requested const tldrGenerated = include_tldr ? generateTldr(originalContent, sections) : null; // Identify sections to extract const extractedSections = identifySectionsToExtract(sections, strategy, max_length); // Create basic optimization result const optimizedContent = originalContent + '\n\n## TL;DR\n\n' + (tldrGenerated || 'Quick overview of the project.'); const restructuringChanges = [ { type: 'added' as const, section: 'TL;DR', description: 'Added concise project overview', impact: 'Helps users quickly understand project value', }, ]; const optimizedLength = optimizedContent.split('\n').length; const reductionPercentage = Math.round( ((originalLength - optimizedLength) / originalLength) * 100, ); // Create docs directory and extract detailed content if requested if (create_docs_directory && extractedSections.length > 0) { await createDocsStructure(path.dirname(readme_path), extractedSections); } // Write optimized README if output path specified if (output_path) { await fs.writeFile(output_path, optimizedContent, 'utf-8'); } const recommendations = generateOptimizationRecommendations( originalLength, optimizedLength, extractedSections, strategy, ); const optimization: OptimizationResult = { originalLength, optimizedLength, reductionPercentage, optimizedContent, extractedSections, tldrGenerated, restructuringChanges, recommendations, }; const nextSteps = generateOptimizationNextSteps(optimization, validatedInput); return { success: true, data: { optimization, nextSteps, }, metadata: { toolVersion: '1.0.0', executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }; } catch (error) { return { success: false, error: { code: 'OPTIMIZATION_FAILED', message: 'Failed to optimize README', details: error instanceof Error ? error.message : 'Unknown error', resolution: 'Check README file path and permissions', }, metadata: { toolVersion: '1.0.0', executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }; } } interface ReadmeSection { title: string; content: string; level: number; startLine: number; endLine: number; wordCount: number; isEssential: boolean; } function parseReadmeStructure(content: string): ReadmeSection[] { const lines = content.split('\n'); const sections: ReadmeSection[] = []; let currentTitle = ''; let currentLevel = 0; let currentStartLine = 0; lines.forEach((line, index) => { const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { // Save previous section if (currentTitle) { const endLine = index - 1; const sectionContent = lines.slice(currentStartLine, endLine + 1).join('\n'); const wordCount = sectionContent.split(/\s+/).length; const isEssential = isEssentialSection(currentTitle); sections.push({ title: currentTitle, content: sectionContent, level: currentLevel, startLine: currentStartLine, endLine: endLine, wordCount: wordCount, isEssential: isEssential, }); } // Start new section currentTitle = headingMatch[2].trim(); currentLevel = headingMatch[1].length; currentStartLine = index; } }); // Add final section if (currentTitle) { const endLine = lines.length - 1; const sectionContent = lines.slice(currentStartLine, endLine + 1).join('\n'); const wordCount = sectionContent.split(/\s+/).length; const isEssential = isEssentialSection(currentTitle); sections.push({ title: currentTitle, content: sectionContent, level: currentLevel, startLine: currentStartLine, endLine: endLine, wordCount: wordCount, isEssential: isEssential, }); } return sections; } function isEssentialSection(title: string): boolean { const essentialKeywords = [ 'installation', 'install', 'setup', 'getting started', 'quick start', 'usage', 'example', 'api', 'license', 'contributing', ]; return essentialKeywords.some((keyword) => title.toLowerCase().includes(keyword)); } function generateTldr(content: string, sections: ReadmeSection[]): string { // Extract project name from first heading const projectNameMatch = content.match(/^#\s+(.+)$/m); const projectName = projectNameMatch ? projectNameMatch[1] : 'This project'; // Extract description (usually after title or in blockquote) const descriptionMatch = content.match(/>\s*(.+)/); let description = descriptionMatch ? descriptionMatch[1] : ''; // If no description found, try to extract from first paragraph if (!description) { const firstParagraphMatch = content.match(/^[^#\n].{20,200}/m); description = firstParagraphMatch ? firstParagraphMatch[0].substring(0, 100) + '...' : ''; } // Identify key features or use cases const features: string[] = []; sections.forEach((section) => { if ( section.title.toLowerCase().includes('feature') || section.title.toLowerCase().includes('what') || section.title.toLowerCase().includes('why') ) { const bullets = section.content.match(/^\s*[-*+]\s+(.+)$/gm); if (bullets && bullets.length > 0) { features.push(...bullets.slice(0, 3).map((b) => b.replace(/^\s*[-*+]\s+/, '').trim())); } } }); let tldr = `## TL;DR\n\n${projectName} ${description}\n\n`; if (features.length > 0) { tldr += `**Key features:**\n`; features.slice(0, 3).forEach((feature) => { tldr += `- ${feature}\n`; }); tldr += '\n'; } // Add quick start reference const hasInstallSection = sections.some( (s) => s.title.toLowerCase().includes('install') || s.title.toLowerCase().includes('setup'), ); if (hasInstallSection) { tldr += `**Quick start:** See [Installation](#installation) → [Usage](#usage)\n\n`; } return tldr; } function identifySectionsToExtract( sections: ReadmeSection[], strategy: string, maxLength: number, ): ExtractedSection[] { const extractedSections: ExtractedSection[] = []; const currentLength = sections.reduce((sum, s) => sum + s.content.split('\n').length, 0); if (currentLength <= maxLength) { return extractedSections; // No extraction needed } // Define extraction rules based on strategy const extractionRules = getExtractionRules(strategy); sections.forEach((section) => { for (const rule of extractionRules) { if (rule.matcher(section) && !section.isEssential) { extractedSections.push({ title: section.title, content: section.content, suggestedLocation: rule.suggestedLocation, reason: rule.reason, }); break; } } }); return extractedSections; } function getExtractionRules(strategy: string) { const baseRules = [ { matcher: (section: ReadmeSection) => section.wordCount > 200, suggestedLocation: 'docs/detailed-guide.md', reason: 'Section too long for main README', }, { matcher: (section: ReadmeSection) => /troubleshoot|faq|common issues|problems/i.test(section.title), suggestedLocation: 'docs/troubleshooting.md', reason: 'Troubleshooting content better suited for separate document', }, { matcher: (section: ReadmeSection) => /advanced|configuration|config/i.test(section.title), suggestedLocation: 'docs/configuration.md', reason: 'Advanced configuration details', }, { matcher: (section: ReadmeSection) => /development|developer|build|compile/i.test(section.title), suggestedLocation: 'docs/development.md', reason: 'Development-specific information', }, ]; if (strategy === 'community_focused') { baseRules.push({ matcher: (section: ReadmeSection) => /architecture|design|technical/i.test(section.title), suggestedLocation: 'docs/technical.md', reason: 'Technical details can overwhelm community contributors', }); } return baseRules; } async function createDocsStructure( projectDir: string, extractedSections: ExtractedSection[], ): Promise<void> { const docsDir = path.join(projectDir, 'docs'); try { await fs.mkdir(docsDir, { recursive: true }); } catch { // Directory might already exist } // Create extracted documentation files for (const section of extractedSections) { const filePath = path.join(projectDir, section.suggestedLocation); const fileDir = path.dirname(filePath); try { await fs.mkdir(fileDir, { recursive: true }); await fs.writeFile(filePath, section.content, 'utf-8'); } catch (error) { console.warn(`Failed to create ${filePath}:`, error); } } // Create docs index const indexContent = generateDocsIndex(extractedSections); await fs.writeFile(path.join(docsDir, 'README.md'), indexContent, 'utf-8'); } function generateDocsIndex(extractedSections: ExtractedSection[]): string { let content = '# Documentation\n\n'; content += 'This directory contains detailed documentation extracted from the main README for better organization.\n\n'; content += '## Available Documentation\n\n'; extractedSections.forEach((section) => { const filename = path.basename(section.suggestedLocation); content += `- [${section.title}](${filename}) - ${section.reason}\n`; }); return content; } function generateOptimizationRecommendations( originalLength: number, optimizedLength: number, extractedSections: ExtractedSection[], strategy: string, ): string[] { const recommendations: string[] = []; const reduction = originalLength - optimizedLength; if (reduction > 0) { recommendations.push( `✅ Reduced README length by ${reduction} lines (${Math.round( (reduction / originalLength) * 100, )}%)`, ); } if (extractedSections.length > 0) { recommendations.push( `📁 Moved ${extractedSections.length} detailed sections to docs/ directory`, ); } if (strategy === 'community_focused') { recommendations.push( '👥 Optimized for community contributors - prioritized quick start and contribution info', ); } recommendations.push( '🔗 Added links to detailed documentation for users who need more information', ); recommendations.push('📊 Consider adding a table of contents for sections with 5+ headings'); return recommendations; } function generateOptimizationNextSteps( optimization: OptimizationResult, input: OptimizeReadmeInput, ): string[] { const steps: string[] = []; if (!input.output_path) { steps.push('💾 Review optimized content and save to README.md when ready'); } if (optimization.extractedSections.length > 0) { steps.push('📝 Review extracted documentation files in docs/ directory'); steps.push('🔗 Update any internal links that may have been affected'); } if (optimization.reductionPercentage > 30) { steps.push('👀 Have team members review the condensed content for accuracy'); } steps.push('📈 Run analyze_readme again to verify improvements'); steps.push('🎯 Consider setting up automated README length monitoring'); 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