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 {
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 'get a comprehensive summary' but doesn't specify if this is a read-only operation, requires permissions, has rate limits, or details the return format. For a tool with zero annotation coverage, this leaves significant gaps in understanding its behavior.

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 a single, efficient sentence that front-loads the purpose without unnecessary details. It earns its place by clearly stating what the tool does, though it could be slightly more structured to include usage hints.

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

Completeness2/5

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

Given the lack of annotations and output schema, the description is incomplete for a tool that likely returns complex data (e.g., test summaries). It doesn't explain what 'comprehensive summary' entails, such as structure or key fields, leaving the agent with insufficient context for effective use.

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?

The input schema has 100% description coverage, with 'repoPath' clearly documented as 'Path to the repository'. The description adds no additional meaning beyond this, such as format examples or constraints. Since the schema does the heavy lifting, the baseline score of 3 is appropriate.

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

Purpose4/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 ('get a comprehensive summary') and resources ('test setup, coverage, and recommendations'), making it easy to understand what it does. However, it doesn't explicitly differentiate from sibling tools like 'analyze_test_setup' or 'check_coverage', which might have overlapping functionality.

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 guidance on when to use this tool versus alternatives like the sibling tools. It lacks explicit instructions on context, prerequisites, or exclusions, leaving the agent to infer usage based on the purpose alone.

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

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