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
| Name | Required | Description | Default |
|---|---|---|---|
| repoPath | Yes | Path to the repository |
Implementation Reference
- src/index.ts:300-342 (handler)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'], }, },
- src/index.ts:141-150 (schema)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'], },
- src/index.ts:342-688 (helper)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 {