Skip to main content
Glama
giri-jeedigunta

Test Analyzer MCP Server

get_test_summary

Analyze JavaScript/TypeScript test setups to generate comprehensive summaries of test coverage, framework detection, and actionable improvement recommendations.

Instructions

Get a comprehensive summary of the test setup, coverage, and recommendations

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
repoPathYesPath to the repository

Implementation Reference

  • The primary handler function that executes the 'get_test_summary' tool. Validates repoPath input, orchestrates test setup analysis and coverage checking, then generates and returns a comprehensive summary report.
    private async getTestSummary(args: any) {
      if (!args.repoPath || typeof args.repoPath !== 'string') {
        throw new McpError(ErrorCode.InvalidParams, 'repoPath is required');
      }
    
      try {
        const repoPath = path.resolve(args.repoPath);
        
        // Get test setup analysis
        const setupResult = await this.analyzeTestSetup({ repoPath });
        const setup = JSON.parse(setupResult.content[0].text);
        
        // Get coverage data
        const coverageResult = await this.checkCoverage({ repoPath, runTests: false });
        const coverage = coverageResult.content[0].text.includes('No coverage data') 
          ? null 
          : JSON.parse(coverageResult.content[0].text);
        
        // Generate comprehensive summary
        const summary = this.generateComprehensiveSummary(setup, coverage);
        
        return {
          content: [
            {
              type: 'text',
              text: summary,
            },
          ],
        };
      } catch (error) {
        if (error instanceof McpError) throw error;
        
        return {
          content: [
            {
              type: 'text',
              text: `Error generating summary: ${error instanceof Error ? error.message : String(error)}`,
            },
          ],
          isError: true,
        };
      }
    }
  • src/index.ts:155-170 (registration)
    The CallToolRequestSchema handler with switch statement that dispatches 'get_test_summary' calls to the getTestSummary method.
      this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
        switch (request.params.name) {
          case 'analyze_test_setup':
            return await this.analyzeTestSetup(request.params.arguments);
          case 'check_coverage':
            return await this.checkCoverage(request.params.arguments);
          case 'get_test_summary':
            return await this.getTestSummary(request.params.arguments);
          default:
            throw new McpError(
              ErrorCode.MethodNotFound,
              `Unknown tool: ${request.params.name}`
            );
        }
      });
    }
  • src/index.ts:138-151 (registration)
    Registers the 'get_test_summary' tool in the ListToolsRequestSchema response, providing name, description, and input schema.
    {
      name: 'get_test_summary',
      description: 'Get a comprehensive summary of the test setup, coverage, and recommendations',
      inputSchema: {
        type: 'object',
        properties: {
          repoPath: {
            type: 'string',
            description: 'Path to the repository',
          },
        },
        required: ['repoPath'],
      },
    },
  • Input schema definition for the 'get_test_summary' tool, specifying a required 'repoPath' string parameter.
    inputSchema: {
      type: 'object',
      properties: {
        repoPath: {
          type: 'string',
          description: 'Path to the repository',
        },
      },
      required: ['repoPath'],
    },
  • Key helper function called by the handler to generate the final comprehensive markdown summary report with sections for setup, coverage, dependencies, and recommendations.
      }
    
      private async detectTestFramework(repoPath: string): Promise<TestFramework | null> {
        for (const framework of this.frameworks) {
          for (const configFile of framework.configFiles) {
            const configPath = path.join(repoPath, configFile);
            try {
              await fs.access(configPath);
              
              // Special check for Jest in package.json
              if (configFile === 'package.json' && framework.name === 'jest') {
                const packageJson = JSON.parse(await fs.readFile(configPath, 'utf-8'));
                if (packageJson.jest || (packageJson.scripts && Object.values(packageJson.scripts).some((script: any) => script.includes('jest')))) {
                  return framework;
                }
              } else {
                return framework;
              }
            } catch {
              // Config file doesn't exist, continue checking
            }
          }
        }
        
        // Check package.json for test dependencies
        try {
          const packageJsonPath = path.join(repoPath, 'package.json');
          const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
          const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
          
          for (const framework of this.frameworks) {
            if (allDeps[framework.name]) {
              return framework;
            }
          }
        } catch {
          // No package.json or error reading it
        }
        
        return null;
      }
    
      private async findTestFiles(repoPath: string, framework: TestFramework | null): Promise<string[]> {
        const patterns = framework?.testFilePatterns || [
          '**/*.test.{js,jsx,ts,tsx}',
          '**/*.spec.{js,jsx,ts,tsx}',
          '**/__tests__/**/*.{js,jsx,ts,tsx}',
        ];
        
        const testFiles: string[] = [];
        
        for (const pattern of patterns) {
          const files = await glob(pattern, {
            cwd: repoPath,
            absolute: true,
            ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**'],
          });
          testFiles.push(...files);
        }
        
        // Remove duplicates
        return [...new Set(testFiles)];
      }
    
      private async analyzeTestStructure(testFiles: string[]): Promise<any> {
        let suites = 0;
        let tests = 0;
        const hooks = new Set<string>();
        
        for (const file of testFiles) {
          try {
            const content = await fs.readFile(file, 'utf-8');
            const ast = parser.parse(content, {
              sourceType: 'module',
              plugins: ['jsx', 'typescript'],
            });
            
            traverse.default(ast, {
              CallExpression(path: any) {
                const callee = path.node.callee;
                if (callee.type === 'Identifier') {
                  const name = callee.name;
                  
                  // Count test suites
                  if (['describe', 'suite', 'context'].includes(name)) {
                    suites++;
                  }
                  
                  // Count individual tests
                  if (['it', 'test', 'specify'].includes(name)) {
                    tests++;
                  }
                  
                  // Track hooks
                  if (['beforeEach', 'afterEach', 'beforeAll', 'afterAll', 'before', 'after'].includes(name)) {
                    hooks.add(name);
                  }
                }
              },
            });
          } catch {
            // Error parsing file, skip it
          }
        }
        
        return {
          suites,
          tests,
          hooks: Array.from(hooks),
        };
      }
    
      private async getCoverageConfig(repoPath: string, framework: TestFramework | null): Promise<any> {
        const configs: any = {};
        
        // Check for NYC/Istanbul config
        const nycConfigFiles = ['.nycrc', '.nycrc.json', '.nycrc.yml', '.nycrc.yaml'];
        for (const configFile of nycConfigFiles) {
          try {
            const configPath = path.join(repoPath, configFile);
            const content = await fs.readFile(configPath, 'utf-8');
            configs.nyc = JSON.parse(content);
            break;
          } catch {
            // Config doesn't exist or can't be parsed
          }
        }
        
        // Check package.json for coverage config
        try {
          const packageJsonPath = path.join(repoPath, 'package.json');
          const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
          
          if (packageJson.nyc) {
            configs.nyc = packageJson.nyc;
          }
          
          if (packageJson.jest?.collectCoverage !== undefined || packageJson.jest?.coverageDirectory) {
            configs.jest = packageJson.jest;
          }
        } catch {
          // No package.json or error reading it
        }
        
        // Check for framework-specific coverage config
        if (framework?.name === 'jest') {
          try {
            const jestConfigPath = path.join(repoPath, 'jest.config.js');
            await fs.access(jestConfigPath);
            configs.jestConfigFile = true;
          } catch {
            // No jest.config.js
          }
        }
        
        return configs;
      }
    
      private async getTestDependencies(repoPath: string): Promise<string[]> {
        const testDeps: string[] = [];
        
        try {
          const packageJsonPath = path.join(repoPath, 'package.json');
          const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
          const devDeps = packageJson.devDependencies || {};
          
          const testRelatedPackages = [
            'jest', 'vitest', 'mocha', 'chai', 'sinon', 'cypress', 'playwright',
            '@testing-library/react', '@testing-library/jest-dom', '@testing-library/user-event',
            'enzyme', 'react-test-renderer', 'karma', 'jasmine', 'qunit',
            'nyc', 'c8', 'istanbul', '@vitest/coverage-c8', '@vitest/coverage-istanbul',
          ];
          
          for (const pkg of testRelatedPackages) {
            if (devDeps[pkg]) {
              testDeps.push(`${pkg}@${devDeps[pkg]}`);
            }
          }
        } catch {
          // Error reading package.json
        }
        
        return testDeps;
      }
    
      private async runTestsWithCoverage(repoPath: string): Promise<CoverageResult | null> {
        try {
          // Try common test coverage commands
          const commands = [
            'npm run test:coverage',
            'npm run coverage',
            'npm test -- --coverage',
            'yarn test:coverage',
            'yarn coverage',
            'yarn test --coverage',
            'npx jest --coverage',
            'npx vitest run --coverage',
          ];
          
          for (const cmd of commands) {
            try {
              const { stdout } = await execAsync(cmd, { cwd: repoPath });
              
              // Try to parse coverage from output
              const coverage = this.parseCoverageFromOutput(stdout);
              if (coverage) {
                return coverage;
              }
            } catch {
              // Command failed, try next one
            }
          }
          
          return null;
        } catch {
          return null;
        }
      }
    
      private async readExistingCoverage(repoPath: string): Promise<CoverageResult | null> {
        // Common coverage output locations
        const coverageFiles = [
          'coverage/coverage-summary.json',
          'coverage/lcov-report/index.html',
          'coverage-final.json',
          '.nyc_output/processinfo/index.json',
        ];
        
        for (const file of coverageFiles) {
          try {
            const coveragePath = path.join(repoPath, file);
            const content = await fs.readFile(coveragePath, 'utf-8');
            
            if (file.endsWith('.json')) {
              const data = JSON.parse(content);
              
              // Parse coverage-summary.json format
              if (data.total) {
                return {
                  lines: data.total.lines,
                  statements: data.total.statements,
                  functions: data.total.functions,
                  branches: data.total.branches,
                  summary: this.generateCoverageSummary(data.total),
                };
              }
            }
          } catch {
            // File doesn't exist or can't be parsed
          }
        }
        
        return null;
      }
    
      private parseCoverageFromOutput(output: string): CoverageResult | null {
        // Try to parse coverage table from console output
        const lines = output.split('\n');
        
        for (let i = 0; i < lines.length; i++) {
          const line = lines[i];
          
          // Look for coverage summary patterns
          if (line.includes('Statements') && line.includes('%')) {
            try {
              // Parse Jest/Vitest style output
              const stmtMatch = line.match(/Statements\s*:\s*([\d.]+)%/);
              const branchMatch = lines[i + 1]?.match(/Branches\s*:\s*([\d.]+)%/);
              const funcMatch = lines[i + 2]?.match(/Functions\s*:\s*([\d.]+)%/);
              const lineMatch = lines[i + 3]?.match(/Lines\s*:\s*([\d.]+)%/);
              
              if (stmtMatch) {
                const result: CoverageResult = {
                  statements: { percentage: parseFloat(stmtMatch[1]), covered: 0, total: 0 },
                  branches: { percentage: parseFloat(branchMatch?.[1] || '0'), covered: 0, total: 0 },
                  functions: { percentage: parseFloat(funcMatch?.[1] || '0'), covered: 0, total: 0 },
                  lines: { percentage: parseFloat(lineMatch?.[1] || '0'), covered: 0, total: 0 },
                  summary: '',
                };
                
                result.summary = this.generateCoverageSummary(result);
                return result;
              }
            } catch {
              // Continue searching
            }
          }
        }
        
        return null;
      }
    
      private generateCoverageSummary(coverage: any): string {
        const getStatus = (percentage: number) => {
          if (percentage >= 80) return '✅ Good';
          if (percentage >= 60) return '⚠️ Fair';
          return '❌ Poor';
        };
        
        const lines = coverage.lines?.percentage || coverage.lines?.pct || 0;
        const statements = coverage.statements?.percentage || coverage.statements?.pct || 0;
        const functions = coverage.functions?.percentage || coverage.functions?.pct || 0;
        const branches = coverage.branches?.percentage || coverage.branches?.pct || 0;
        
        return `Coverage Summary:
    - Lines: ${lines.toFixed(1)}% ${getStatus(lines)}
    - Statements: ${statements.toFixed(1)}% ${getStatus(statements)}
    - Functions: ${functions.toFixed(1)}% ${getStatus(functions)}
    - Branches: ${branches.toFixed(1)}% ${getStatus(branches)}
    
    Overall: ${getStatus((lines + statements + functions + branches) / 4)}`;
      }
    
      private generateTestSetupSummary(data: any): string {
        const { framework, testFiles, testStructure, coverageConfig, dependencies } = data;
        
        let summary = `Test Setup Analysis:\n\n`;
        
        summary += `📦 Framework: ${framework || 'None detected'}\n`;
        summary += `📁 Test Files: ${testFiles.length} files found\n`;
        summary += `🧪 Tests: ${testStructure.tests} tests in ${testStructure.suites} suites\n`;
        
        if (testStructure.hooks.length > 0) {
          summary += `🔧 Hooks: ${testStructure.hooks.join(', ')}\n`;
        }
        
        if (Object.keys(coverageConfig).length > 0) {
          summary += `\n📊 Coverage Configuration:\n`;
          Object.keys(coverageConfig).forEach((key: string) => {
            summary += `  - ${key}: Configured\n`;
          });
        }
        
        if (dependencies.length > 0) {
          summary += `\n📚 Test Dependencies:\n`;
          dependencies.slice(0, 10).forEach((dep: string) => {
            summary += `  - ${dep}\n`;
          });
          if (dependencies.length > 10) {
            summary += `  ... and ${dependencies.length - 10} more\n`;
          }
        }
        
        return summary;
      }
    
      private generateComprehensiveSummary(setup: TestAnalysisResult, coverage: CoverageResult | null): string {

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/giri-jeedigunta/hello-mcp'

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