Skip to main content
Glama

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
NameRequiredDescriptionDefault
projectPathYesAbsolute or relative path to the Next.js project directory
formatNoOutput format for the analysis resultsstructured
includeContentNoInclude detailed file content analysis
subtreeNoFrontend directory path to analyze (default: web/app)web/app
maxFilesNoMaximum number of files to analyze
useEmbeddingsNoEnable embedding-based similarity analysis for enhanced insights
embeddingSimilarityThresholdNoSimilarity threshold for embedding-based matches (lower = more results, higher = more precise)
maxSimilarComponentsNoMaximum number of similar components to analyze per component
analyzePatternsNoEnable pattern detection for code smells, anti-patterns, and security issues
generateEmbeddingsIfMissingNoGenerate 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, };
  • 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, }; }

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/sbarron/AmbianceMCP'

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