Skip to main content
Glama
sbarron

Ambiance MCP Server

by sbarron

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,
      };
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions 'embedding-enhanced analysis' and 'semantic embeddings', hinting at computational intensity, but does not disclose performance implications, rate limits, or error handling. For a tool with 10 parameters and no annotations, this leaves significant behavioral gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded, starting with a clear purpose and key features. It uses two sentences efficiently, though the second sentence could be slightly more streamlined. There is no wasted text, earning a high score for conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (10 parameters, no annotations, no output schema), the description is moderately complete. It covers the purpose and high-level functionality but lacks details on output format, error cases, and behavioral constraints. Without an output schema, the agent must infer return values, making this adequate but with clear gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds minimal value beyond the schema by implying the tool uses embeddings for analysis, but it does not explain parameter interactions or provide additional context. Baseline 3 is appropriate as the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with specific verbs ('map', 'analyzes') and resources ('routes, components, data flow, design system, and risks', 'Next.js/React projects'). It distinguishes itself from sibling tools by focusing on embedding-enhanced analysis for architecture insights, component similarities, and potential issues, unlike tools like ast_grep_search or local_file_summary which suggest different scopes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no explicit guidance on when to use this tool versus alternatives. It mentions analyzing Next.js/React projects but does not specify scenarios, prerequisites, or exclusions. Without context on how this differs from sibling tools like local_project_hints, the agent lacks clear usage direction.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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