Skip to main content
Glama
mednabouli

MCP Multi-Context Hook Generator

by mednabouli
index.ts10.5 kB
#!/usr/bin/env node import path from 'path'; import fs from 'fs'; import { execSync } from 'child_process'; import { crawlApi, ApiRoute } from './crawler/api'; import { crawlComponents, ComponentInfo } from './crawler/components'; import { generateHooks, GeneratorOptions } from './generator/hooks'; import { analyzePages, PageAnalysis } from './mcp/pageAnalyzer'; import { generateDocs, DocOptions } from './mcp/generateDocs'; import { generateComponent } from './mcp/componentGenerator'; import { generateComponentDocs } from './mcp/generateComponentDocs'; import { suggestHookImplementation, suggestPageProps, loadMCPConfig } from './ai/mcpAgent'; import { enrichComponents } from './ai/enrichComponents'; import { generateComponentTests } from './generator/generateComponentTests'; import { checkHooksDiff, checkComponentsDiff, checkPagesDiff, saveSnapshot, } from './mcp-ai-diff-checker'; import { runEslintOnPaths } from './mcp/lint/runEslint'; // -------------------- // CLI Options // -------------------- interface CLIOptions { tests?: boolean; crawl?: boolean; hooks?: boolean; analyze?: boolean; docs?: boolean; all?: boolean; ci?: boolean; dryRun?: boolean; useCursorAssist?: boolean; aiAgent?: 'none' | 'copilot' | 'openai' | 'custom'; autoInferRender?: boolean; } function parseCLIArgs(): CLIOptions { const args = process.argv.slice(2); return { crawl: args.includes('--crawl'), hooks: args.includes('--hooks'), analyze: args.includes('--analyze'), docs: args.includes('--docs'), all: args.includes('--all') || args.length === 0, ci: args.includes('--ci'), dryRun: args.includes('--dry-run'), useCursorAssist: args.includes('--use-cursor-assist'), aiAgent: args.includes('--ai-agent') ? (args[args.indexOf('--ai-agent') + 1] as any) : 'none', autoInferRender: !args.includes('--no-auto-infer'), tests: args.includes('--tests'), }; } // -------------------- // Detect Package Manager // -------------------- function detectPackageManager() { const lockFiles = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']; for (const file of lockFiles) { if (fs.existsSync(path.join(process.cwd(), file))) { if (file === 'pnpm-lock.yaml') return 'pnpm'; if (file === 'package-lock.json') return 'npm'; if (file === 'yarn.lock') return 'yarn'; } } return 'unknown'; } // -------------------- // Incremental file detection // -------------------- function getChangedFiles(rootDir: string): string[] { try { const output = execSync('git diff --name-only HEAD~1', { cwd: rootDir, }).toString(); return output.split('\n').filter((f) => f.endsWith('.ts') || f.endsWith('.tsx')); } catch (err) { console.warn('⚠️ Unable to detect changed files, fallback to full crawl.'); return []; } } // -------------------- // Safe AI wrapper // -------------------- async function safeAISuggestion( hookName: string, route: ApiRoute, aiAgent: 'none' | 'copilot' | 'openai' | 'custom', ): Promise<string> { try { return await suggestHookImplementation(hookName, route, aiAgent); } catch { console.warn(`⚠️ AI suggestion failed for ${hookName}, using default.`); return `// Default hook for ${hookName}`; } } async function safePageSuggestion( pagePath: string, page: PageAnalysis, aiAgent: 'none' | 'copilot' | 'openai' | 'custom', ): Promise<string> { try { return await suggestPageProps(pagePath, page, aiAgent); } catch { console.warn(`⚠️ AI suggestion failed for ${pagePath}`); return `// Default props for ${pagePath}`; } } // -------------------- // Format generated code // -------------------- function formatGeneratedCode(outputDir: string) { try { execSync(`npx prettier --write ${outputDir}`, { stdio: 'inherit' }); execSync(`npx eslint --fix ${outputDir}`, { stdio: 'inherit' }); console.log('🎨 Generated files formatted and linted'); } catch { console.warn('⚠️ Formatting/Linting failed'); } } // -------------------- // MCP Runner // -------------------- async function runMCP() { const options = parseCLIArgs(); const rootDir = process.cwd(); const hooksOutputDir = path.join(rootDir, 'hooks'); const docsOutputDir = path.join(rootDir, 'mcp-docs'); const componentsOutputDir = path.join(rootDir, 'components'); const config = loadMCPConfig(); console.log(`📦 Detected package manager: ${detectPackageManager()}`); console.log(`⚡ CI Mode: ${options.ci ? 'ON' : 'OFF'}`); console.log(`🌙 Dry Run: ${options.dryRun ? 'ON' : 'OFF'}`); // -------------------- // Crawl API routes // -------------------- const changedFiles = getChangedFiles(rootDir); let apiRoutes: ApiRoute[] = []; if (options.crawl || options.all) { console.log('🔍 Crawling APIs...'); apiRoutes = crawlApi(rootDir).filter( (route) => changedFiles.length === 0 || changedFiles.some((f) => route.path.includes(f)), ); console.log(`✅ Found ${apiRoutes.length} API routes`); } // -------------------- // Generate Hooks // -------------------- if (options.hooks || options.all) { if (!apiRoutes.length) apiRoutes = crawlApi(rootDir); console.log('💻 Generating typed hooks...'); const hookOptions: GeneratorOptions = { outputDir: hooksOutputDir, useSWRInfinite: true, // optional }; await generateHooks(apiRoutes, hookOptions); // AI suggestions for hooks if (config.enableAI && !options.dryRun) { for (const route of apiRoutes) { for (const method of route.methods) { const suggestion = await safeAISuggestion( method, route, options.aiAgent || config.defaultAgent, ); console.log(`🤖 AI Suggestion for ${method}:\n${suggestion}`); } } } } // -------------------- // Generate Components // -------------------- let components: ComponentInfo[] = []; if (options.analyze || options.all || options.docs) { console.log('🧩 Crawling components...'); components = crawlComponents(rootDir, 'components'); // AI enrichment console.log('🤖 Enriching component docs with AI...'); components = await enrichComponents(components, options.aiAgent || 'none'); console.log('📚 Generating component documentation...'); generateComponentDocs(path.join(rootDir, 'mcp-docs/components'), components, { dryRun: options.dryRun, }); console.log(`✅ Found ${components.length} components`); components.forEach((c) => console.log(` - ${c.name} (${c.props.length} props)`)); } if (config.autoSuggest?.components) { for (const route of apiRoutes) { await generateComponent(route); } } // -------------------- // Generate Component Docs // -------------------- if (options.docs || options.all) { if (!components.length) components = crawlComponents(rootDir, 'components'); console.log('📚 Generating component documentation...'); generateComponentDocs(path.join(rootDir, 'mcp-docs/components'), components, { dryRun: options.dryRun, }); } // -------------------- // Component Tests // -------------------- if (options.all || options.tests) { console.log('🧪 Generating component tests...'); const components = crawlComponents(rootDir); const enriched = await enrichComponents(components, 'copilot'); generateComponentTests(enriched, { outputDir: 'tests/components', overwrite: true }); } // -------------------- // Analyze Pages // -------------------- let pageAnalyses: PageAnalysis[] = []; if (options.analyze || options.all || options.docs) { if (!apiRoutes.length) apiRoutes = crawlApi(rootDir); console.log('📄 Analyzing pages...'); pageAnalyses = analyzePages(rootDir, apiRoutes); console.log(`✅ Analyzed ${pageAnalyses.length} pages`); } // -------------------- // Generate Docs // -------------------- if (options.docs || options.all) { if (!pageAnalyses.length) pageAnalyses = analyzePages(rootDir, apiRoutes); console.log('📝 Generating documentation...'); const docOptions: DocOptions = { autoInferRender: options.autoInferRender, }; generateDocs(docsOutputDir, pageAnalyses, docOptions); // AI suggestions for pages if (config.enableAI && !options.dryRun) { for (const page of pageAnalyses) { const suggestion = await safePageSuggestion( page.pagePath, page, options.aiAgent || config.defaultAgent, ); console.log(`🤖 AI Suggestion for ${page.pagePath}:\n${suggestion}`); } } } // -------------------- // Format generated files // -------------------- formatGeneratedCode(hooksOutputDir); formatGeneratedCode(docsOutputDir); // -------------------- // Check diffs with AI // -------------------- if (!options.dryRun && options.aiAgent !== 'none') { console.log('🛡 Checking for potential breakages in developer changes...'); const pagesDir = path.join(rootDir, 'pages'); await checkHooksDiff(apiRoutes, hooksOutputDir, options.aiAgent); await checkComponentsDiff(components, componentsOutputDir, options.aiAgent); await checkPagesDiff(pageAnalyses, pagesDir, options.aiAgent); } // Save snapshots after successful generation const pagesDir = path.join(rootDir, 'pages'); saveSnapshot(path.join(hooksOutputDir, 'hooks.ts')); saveSnapshot(path.join(componentsOutputDir, 'components.ts')); saveSnapshot(path.join(pagesDir, 'pages.ts')); apiRoutes.forEach((route) => saveSnapshot(route.path)); components.forEach((comp) => saveSnapshot(comp.filePath)); pageAnalyses.forEach((page) => saveSnapshot(page.pagePath)); // -------------------- // Run ESLint on generated files // -------------------- if (!options.dryRun) { const lintPaths = [ hooksOutputDir, path.join(rootDir, 'components/**/*.tsx'), path.join(rootDir, 'pages/**/*.{ts,tsx}'), ]; const lintSummary = await runEslintOnPaths(lintPaths, rootDir); console.log(`ESLint: ${lintSummary.errorCount} errors, ${lintSummary.warningCount} warnings`); if (lintSummary.errorCount > 0 && config.ci?.failOnBreakingChange) { process.exit(1); } } console.log('🎉 MCP pipeline completed!'); } // -------------------- // Run MCP // -------------------- runMCP().catch((err) => { console.error('❌ MCP pipeline failed:', err); process.exit(1); });

Latest Blog Posts

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/mednabouli/MCPV2'

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