UNPKG

@skyramp/mcp

Version:

Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution

237 lines (231 loc) 11.1 kB
import { z } from "zod"; import { TestHealthService } from "../../services/TestHealthService.js"; import { logger } from "../../utils/logger.js"; import { StateManager, } from "../../utils/AnalysisStateManager.js"; import { AnalyticsService } from "../../services/AnalyticsService.js"; const TOOL_NAME = "skyramp_calculate_health_scores"; /** * Tool that ONLY calculates health scores from pre-gathered data. * This tool does NOT do discovery, drift analysis, or execution. * It expects those results to be provided as input. * * For explicit tool chaining workflow: * 1. Call skyramp_analyze_test_drift (with test list) * 2. Call skyramp_execute_tests_batch (optional) * 3. Call THIS tool with drift and execution results * * Note: Test discovery is no longer needed as drift results contain all test metadata. */ export function registerCalculateHealthScoresTool(server) { server.registerTool(TOOL_NAME, { description: `Calculate test health scores and generate recommendations. This tool is part of an AUTOMATED workflow. **PREREQUISITE:** Must call \`skyramp_analyze_test_drift\` (and optionally \`skyramp_execute_tests_batch\`) **WHAT IT CALCULATES:** - Health scores (0-100) combining drift + execution - Health status (Healthy/At Risk/Broken/Unknown) - Specific issues (breaking changes, test failures) - Prioritized recommendations (REGENERATE/UPDATE/VERIFY) - Coverage analysis - Work estimates (SMALL/MEDIUM/LARGE) **STATE FILE MODE:** - Provide stateFile from skyramp_analyze_test_drift or skyramp_execute_tests_batch - Enriches state file with health scores, issues, and recommendations - Returns summary and updated stateFile path **🤖 AUTOMATION REQUIREMENT:** This tool is part of an automated workflow. After this tool completes: 1. You MUST immediately call \`skyramp_actions\` with the returned stateFile 2. Do NOT wait for user confirmation 3. Do NOT ask the user if they want to proceed 4. The output will include the exact parameters to use **OUTPUT:** Includes summary, recommendations, stateFile path, and automated workflow instructions`, inputSchema: { stateFile: z .string() .describe("Path to state file from skyramp_analyze_test_drift or skyramp_execute_tests_batch (required)"), }, }, async (args) => { let errorResult; try { logger.info(`Calculating test health scores`); // Load tests from state file const stateManager = StateManager.fromStatePath(args.stateFile); const stateData = await stateManager.readData(); const testAnalysisResults = stateData?.tests || []; const fullState = await stateManager.readFullState(); const repositoryPath = fullState?.metadata.repositoryPath || ""; if (!testAnalysisResults || testAnalysisResults.length === 0) { errorResult = { content: [ { type: "text", text: JSON.stringify({ error: "State file is empty or invalid", stateFile: args.stateFile, }, null, 2), }, ], isError: true, }; return errorResult; } logger.info(`Loaded ${testAnalysisResults.length} tests from state file: ${args.stateFile}`); // Validate repositoryPath if (!repositoryPath || typeof repositoryPath !== "string") { errorResult = { content: [ { type: "text", text: JSON.stringify({ error: "repositoryPath not found in state file metadata", }, null, 2), }, ], isError: true, }; return errorResult; } if (testAnalysisResults.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ message: "No tests found in test results", summary: { totalTests: 0, healthy: 0, atRisk: 0, broken: 0, unknown: 0, averageHealthScore: 0, }, recommendations: [], }, null, 2), }, ], }; } // Prepare tests for health service (convert unified format to expected format) const tests = testAnalysisResults.map((test) => ({ testFile: test.testFile, testType: test.testType, language: test.language, apiSchema: test.apiSchema, execution: test.execution ? { testFile: test.testFile, passed: test.execution.passed, duration: test.execution.duration, errors: test.execution.errors, warnings: test.execution.warnings, crashed: test.execution.crashed, executedAt: test.execution.executionTimestamp, } : undefined, })); // Prepare drift data for health service const driftData = testAnalysisResults .filter((test) => test.drift) .map((test) => ({ testFile: test.testFile, lastCommit: test.drift.lastCommit, currentCommit: test.drift.currentCommit, driftScore: test.drift.driftScore, changes: test.drift.changes, affectedFiles: test.drift.affectedFiles, apiSchemaChanges: test.drift.apiSchemaChanges, uiComponentChanges: test.drift.uiComponentChanges, analysisTimestamp: test.drift.analysisTimestamp, recommendations: test.drift.recommendations, })); // Calculate health scores and generate recommendations logger.info("Generating comprehensive health report..."); const healthService = new TestHealthService(); const healthReport = await healthService.generateHealthReport(tests, driftData || undefined); logger.info(`Health report generated: ${healthReport.summary.healthy} healthy, ` + `${healthReport.summary.atRisk} at risk, ${healthReport.summary.broken} broken`); // Enrich testAnalysisResults with health data const enrichedTestResults = testAnalysisResults.map((test) => { const healthAnalysis = healthReport.tests.find((h) => h.testFile === test.testFile); if (healthAnalysis) { return { ...test, healthScore: healthAnalysis.healthScore, issues: healthAnalysis.issues, recommendation: healthAnalysis.recommendation, }; } return test; }); // Store enriched test results with health data in state file await stateManager.writeData({ tests: enrichedTestResults }, { repositoryPath: repositoryPath, step: "health", }); const stateSize = await stateManager.getSizeFormatted(); logger.info(`Saved health report to state file: ${stateManager.getStatePath()} (${stateSize})`); const responseData = { summary: healthReport.summary, recommendations: healthReport.recommendations.map((rec) => ({ testFile: rec.testFile, action: rec.action, priority: rec.priority, rationale: rec.rationale, estimatedWork: rec.estimatedWork, })), stateFile: stateManager.getStatePath(), sessionId: stateManager.getSessionId(), stateFileSize: stateSize, generatedAt: new Date().toISOString(), }; // Build explicit instruction text let responseText = `# HEALTH ANALYSIS COMPLETE\n\n`; responseText += `## Summary\n`; responseText += `- **Total Tests:** ${healthReport.summary.totalTests}\n`; responseText += `- **Healthy:** ${healthReport.summary.healthy}\n`; responseText += `- **At Risk:** ${healthReport.summary.atRisk}\n`; responseText += `- **Broken:** ${healthReport.summary.broken}\n`; responseText += `- **Average Health Score:** ${healthReport.summary.averageHealthScore.toFixed(1)}\n\n`; responseText += `## Recommendations (${healthReport.recommendations.length} total)\n`; healthReport.recommendations.forEach((rec, idx) => { responseText += `${idx + 1}. **${rec.testFile}** - Action: ${rec.action}, Priority: ${rec.priority}\n`; responseText += ` - ${rec.rationale}\n`; }); responseText += `\n## 💾 Analysis State\n`; responseText += `Results saved to: \`${stateManager.getStatePath()}\` (${stateSize})\n\n`; responseText += `---\n\n`; responseText += `## 🔄 Next Steps\n\n`; responseText += `The analysis is complete. You can now:\n`; responseText += `- Review the health scores above\n`; responseText += `- Call \`skyramp_actions\` to see recommended fixes\n`; responseText += `- Use the state file for further automation\n\n`; return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { logger.error(`Health score calculation failed: ${error.message}`, error); errorResult = { content: [ { type: "text", text: JSON.stringify({ error: error.message, }, null, 2), }, ], isError: true, }; return errorResult; } finally { AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {}); } }); }