frontend_insights
Analyze Next.js/React projects to map architecture, detect component similarities, identify code patterns, and assess risks using semantic embedding analysis.
Instructions
🔍 Map routes, components, data flow, design system, and risks in the web layer with embedding-enhanced analysis. Analyzes Next.js/React projects for architecture insights, component similarities, and potential issues using semantic embeddings.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectPath | Yes | Absolute or relative path to the Next.js project directory | |
| format | No | Output format for the analysis results | structured |
| includeContent | No | Include detailed file content analysis | |
| subtree | No | Frontend directory path to analyze (default: web/app) | web/app |
| maxFiles | No | Maximum number of files to analyze | |
| useEmbeddings | No | Enable embedding-based similarity analysis for enhanced insights | |
| embeddingSimilarityThreshold | No | Similarity threshold for embedding-based matches (lower = more results, higher = more precise) | |
| maxSimilarComponents | No | Maximum number of similar components to analyze per component | |
| analyzePatterns | No | Enable pattern detection for code smells, anti-patterns, and security issues | |
| generateEmbeddingsIfMissing | No | Generate embeddings for project files if they don't exist (may take time for large projects) |
Implementation Reference
- Main handler function that orchestrates frontend analysis: discovers files, analyzes routes, components, data flow, generates insights, and formats output.async function handleFrontendInsights(args: any): Promise<any> { const { projectPath, format = 'structured', subtree = 'web/app', maxFiles = 2000 } = args; // Validate that projectPath is provided if (!projectPath) { throw new Error( '❌ projectPath is required. Please provide an absolute path to the Next.js project directory.' ); } const resolvedProjectPath = validateAndResolvePath(projectPath); logger.info('🔍 Starting frontend insights analysis', { originalPath: projectPath, resolvedPath: resolvedProjectPath, format, subtree, maxFiles, }); try { // Discover files in the project const fileDiscovery = new FileDiscovery(resolvedProjectPath); const allFiles = await fileDiscovery.discoverFiles(); // Filter files for frontend types (including HTML, CSS, config files, and other relevant files) const frontendFiles = allFiles.filter(file => { const isFrontendCode = /\.(ts|tsx|js|jsx|vue|svelte|html|css|scss|sass|less|astro|mdx)$/.test( file.relPath ); const isConfigFile = file.relPath.includes('.config.') || file.relPath.includes('config.'); const isExcluded = file.relPath.includes('node_modules') || file.relPath.includes('dist') || file.relPath.includes('.next') || file.relPath.includes('build'); return (isFrontendCode || isConfigFile) && !isExcluded; }); logger.info(`📁 Found ${frontendFiles.length} frontend files (${allFiles.length} total)`); // Analyze file composition const fileComposition = analyzeFileComposition(allFiles, frontendFiles); // Auto-detect the correct app directory if subtree is default or doesn't exist let effectiveSubtree = subtree; if (subtree === 'web/app' || subtree === 'app') { // Check for common app directory patterns const appPatterns = ['app', 'src/app', 'web/app', 'pages', 'src/pages']; let detectedAppDir = null; for (const pattern of appPatterns) { const testPath = path.join(resolvedProjectPath, pattern); const filesInPattern = frontendFiles.filter(file => file.absPath.startsWith(testPath)); if (filesInPattern.length > 0) { // Check if there are actual page files in this directory // For App Router: look for /page. files // For Pages Router: look for any JS/TS/JSX/TSX files (not special App Router files) const pageFiles = filesInPattern.filter(file => { const relPath = file.relPath; if (pattern.includes('pages')) { // Pages Router: any JS/TS file that's not an API route or special file return ( /\.(js|jsx|ts|tsx)$/.test(relPath) && !relPath.includes('/api/') && !relPath.includes('/_') && !relPath.includes('/page.') && !relPath.includes('/layout.') && !relPath.includes('/route.') ); } else { // App Router: look for /page. files return relPath.includes('/page.'); } }); if (pageFiles.length > 0) { detectedAppDir = pattern; logger.info( `🔍 Auto-detected ${pattern.includes('pages') ? 'Pages Router' : 'App Router'} directory: ${pattern} (${pageFiles.length} page files found)` ); break; } } } if (detectedAppDir) { effectiveSubtree = detectedAppDir; } else { // If no specific app directory found, use the entire project but prioritize app-like structures effectiveSubtree = '.'; logger.info(`🔍 No specific app directory detected, analyzing entire project`); } } // Filter files for the effective subtree const targetPath = path.join(resolvedProjectPath, effectiveSubtree); const subtreeFiles = frontendFiles.filter( file => effectiveSubtree === '.' || file.absPath.startsWith(targetPath) ); const filesToAnalyze = subtreeFiles.length > 0 ? subtreeFiles.slice(0, maxFiles) : frontendFiles.slice(0, maxFiles); logger.info(`📁 Analyzing ${filesToAnalyze.length} files in frontend`); // Initialize basic analysis results const insights: FrontendInsights = { generatedAt: new Date().toISOString(), summary: { pages: 0, clientComponents: 0, serverComponents: 0, stateStores: [], dataLibraries: [], designSystem: [], fileComposition, }, routes: { pages: [], handlers: [], }, boundaries: [], components: [], dataFlow: { endpoints: [], externalBases: [], endpointCalls: [], duplicateEndpoints: [], }, env: { nextPublic: [], clientLeaks: [], leaks: [], }, performance: { heavyClientImports: [], noDynamicCandidates: [], }, accessibility: [], risks: { score: 0, trustedScore: 0, rules: [], }, recommendedNextSteps: [], }; // Basic analysis try { // Analyze routes - pass the detected app directory logger.info('🛣️ Analyzing routes'); const appDir = effectiveSubtree === '.' ? 'app' : effectiveSubtree; // Use detected directory or default to 'app' const routeAnalysis = await analyzeRoutes(filesToAnalyze, appDir); let totalPages = 0; // Handle the case where routeAnalysis might be an array or have a different structure if (Array.isArray(routeAnalysis)) { insights.routes = { pages: routeAnalysis as any, handlers: [] }; // Count only routes that have page files (App Router) const appRouterPages = routeAnalysis.filter((route: any) => route.files?.page).length; totalPages += appRouterPages; } else if (routeAnalysis && typeof routeAnalysis === 'object') { insights.routes = routeAnalysis as any; totalPages += (routeAnalysis as any).pages?.length || 0; } else { insights.routes = { pages: [], handlers: [] }; } // Count Pages Router pages and HTML files const pagesRouterPages = filesToAnalyze.filter(file => { const relPath = file.relPath.replace(/\\/g, '/'); // Pages Router: files in pages/ or src/pages/ that are JS/TS/JSX/TSX (but not API routes or special App Router files) const isInPagesDir = relPath.includes('/pages/') || relPath.startsWith('pages/') || relPath.startsWith('src/pages/'); if (isInPagesDir) { return ( /\.(js|jsx|ts|tsx)$/.test(relPath) && !relPath.includes('/api/') && !relPath.includes('/_') && !relPath.includes('/page.') && !relPath.includes('/layout.') && !relPath.includes('/route.') ); } return false; }).length; // Count HTML pages const htmlPages = filesToAnalyze.filter( file => file.relPath.endsWith('.html') && !file.relPath.includes('node_modules') && !file.relPath.includes('dist') && !file.relPath.includes('.next') ).length; totalPages += pagesRouterPages + htmlPages; if (pagesRouterPages > 0) { logger.info(`📄 Found ${pagesRouterPages} Pages Router pages`); } if (htmlPages > 0) { logger.info(`📄 Found ${htmlPages} HTML pages`); } insights.summary.pages = totalPages; } catch (error) { logger.warn('Route analysis failed:', { error: error instanceof Error ? error.message : String(error), }); insights.routes = { pages: [], handlers: [] }; } try { // Analyze components logger.info('⚛️ Analyzing components'); const componentAnalysis = await analyzeComponents(filesToAnalyze); insights.components = Array.isArray(componentAnalysis) ? componentAnalysis : []; insights.summary.clientComponents = insights.components.filter( (c: any) => c.kind === 'client' ).length; insights.summary.serverComponents = insights.components.filter( (c: any) => c.kind === 'server' ).length; } catch (error) { logger.warn('Component analysis failed:', { error: error instanceof Error ? error.message : String(error), }); insights.components = []; } try { // Analyze data flow logger.info('🔄 Analyzing data flow'); const dataFlowAnalysis = await analyzeDataFlow(filesToAnalyze, insights.components); insights.dataFlow.endpoints = (dataFlowAnalysis.endpoints || []).map((e: any) => ({ method: e.method || 'GET', path: e.path, usedBy: e.usedBy, })); insights.dataFlow.endpointCalls = dataFlowAnalysis.endpointCalls || []; insights.dataFlow.duplicateEndpoints = dataFlowAnalysis.duplicateEndpoints || []; } catch (error) { logger.warn('Data flow analysis failed:', { error: error instanceof Error ? error.message : String(error), }); insights.dataFlow = { endpoints: [], externalBases: [], endpointCalls: [], duplicateEndpoints: [], }; } // Generate simple recommended next steps const nextSteps = []; if (insights.dataFlow.duplicateEndpoints.length > 3) { nextSteps.push({ title: `Consolidate ${insights.dataFlow.duplicateEndpoints.length} duplicate API calls`, }); } if (insights.components.length > 50) { nextSteps.push({ title: `Review component architecture (${insights.components.length} components found)`, }); } if (insights.dataFlow.endpoints.length > 20) { nextSteps.push({ title: `Consider API consolidation (${insights.dataFlow.endpoints.length} endpoints found)`, }); } insights.recommendedNextSteps = nextSteps; logger.info('✅ Frontend insights analysis complete', { pages: insights.summary.pages, components: insights.components.length, endpoints: insights.dataFlow.endpoints.length, }); // Format and return results const formattedResult = formatFrontendInsights(insights, format); return { content: [ { type: 'text', text: formattedResult, }, ], }; } catch (error) { logger.error('Failed to analyze frontend insights:', { error: error instanceof Error ? error.message : String(error), }); throw new Error( `Frontend insights analysis failed: ${error instanceof Error ? error.message : String(error)}` ); } }
- Tool definition including name, description, and detailed inputSchema for MCP protocol validation.const frontendInsightsTool = { name: 'frontend_insights', description: '🔍 Map routes, components, data flow, design system, and risks in the web layer with embedding-enhanced analysis. Analyzes Next.js/React projects for architecture insights, component similarities, and potential issues using semantic embeddings.', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Absolute or relative path to the Next.js project directory', }, format: { type: 'string', enum: ['structured', 'json', 'compact', 'markdown'], default: 'structured', description: 'Output format for the analysis results', }, includeContent: { type: 'boolean', default: true, description: 'Include detailed file content analysis', }, subtree: { type: 'string', default: 'web/app', description: 'Frontend directory path to analyze (default: web/app)', }, maxFiles: { type: 'number', default: 2000, minimum: 1, maximum: 10000, description: 'Maximum number of files to analyze', }, useEmbeddings: { type: 'boolean', default: true, description: 'Enable embedding-based similarity analysis for enhanced insights', }, embeddingSimilarityThreshold: { type: 'number', default: 0.3, minimum: 0.0, maximum: 1.0, description: 'Similarity threshold for embedding-based matches (lower = more results, higher = more precise)', }, maxSimilarComponents: { type: 'number', default: 5, minimum: 1, maximum: 20, description: 'Maximum number of similar components to analyze per component', }, analyzePatterns: { type: 'boolean', default: true, description: 'Enable pattern detection for code smells, anti-patterns, and security issues', }, generateEmbeddingsIfMissing: { type: 'boolean', default: false, description: "Generate embeddings for project files if they don't exist (may take time for large projects)", }, }, required: ['projectPath'], }, };
- src/index.ts:126-142 (registration)Registration of the frontendInsightsTool in the MCP server's tools array and mapping of 'frontend_insights' to its handler in the handlers object.this.tools = [ ...(allowLocalContext ? [localSemanticCompactTool] : []), localProjectHintsTool, localFileSummaryTool, frontendInsightsTool, localDebugContextTool, astGrepTool, ]; this.handlers = { ...(allowLocalContext ? { local_context: handleSemanticCompact } : {}), local_project_hints: handleProjectHints, local_file_summary: handleFileSummary, frontend_insights: handleFrontendInsights, local_debug_context: handleLocalDebugContext, ast_grep_search: handleAstGrep, };
- src/tools/localTools/index.ts:119-137 (registration)Export and inclusion of frontendInsightsTool in localTools array and 'frontend_insights' handler mapping in localHandlers, imported by main server.export const localTools = [ ...(allowLocalContext ? [localSemanticCompactTool] : []), localProjectHintsTool, localFileSummaryTool, frontendInsightsTool, localDebugContextTool, manageEmbeddingsTool, astGrepTool, ]; export const localHandlers = { ...(allowLocalContext ? { local_context: handleSemanticCompact } : {}), local_project_hints: handleProjectHints, local_file_summary: handleFileSummary, frontend_insights: handleFrontendInsights, local_debug_context: handleLocalDebugContext, manage_embeddings: handleManageEmbeddings, ast_grep_search: handleAstGrep, };
- Helper function to analyze file composition by extension, used in summary generation.function analyzeFileComposition( allFiles: FileInfo[], frontendFiles: FileInfo[] ): FrontendInsights['summary']['fileComposition'] { const byType: Record<string, number> = {}; const filteredOut: Record<string, number> = {}; // Count all files by extension for (const file of allFiles) { const ext = file.ext || path.extname(file.relPath).toLowerCase() || 'no-extension'; byType[ext] = (byType[ext] || 0) + 1; } // Count filtered out files (not in frontendFiles) const frontendFileSet = new Set(frontendFiles.map(f => f.absPath)); for (const file of allFiles) { if (!frontendFileSet.has(file.absPath)) { const ext = file.ext || path.extname(file.relPath).toLowerCase() || 'no-extension'; filteredOut[ext] = (filteredOut[ext] || 0) + 1; } } return { totalFiles: allFiles.length, byType, analyzedFiles: frontendFiles.length, filteredOut, }; }