UNPKG

@skyramp/mcp

Version:

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

242 lines (235 loc) 11 kB
import { z } from "zod"; import { logger } from "../../utils/logger.js"; import { StateManager, } from "../../utils/AnalysisStateManager.js"; import * as fs from "fs"; import { AnalyticsService } from "../../services/AnalyticsService.js"; const actionsSchema = { stateFile: z .string() .describe("Path to state file from skyramp_calculate_health_scores"), }; const TOOL_NAME = "skyramp_actions"; export function registerActionsTool(server) { server.registerTool(TOOL_NAME, { description: `**AUTOMATICALLY TRIGGERED** tool that executes test maintenance actions. **PREREQUISITE:** MUST Call \'skyramp_calculate_health_scores\`. **CRITICAL:** This tool MUST be called automatically after skyramp_calculate_health_scores completes. Do NOT wait for user confirmation. **INPUT REQUIREMENTS:** - \`stateFile\`: Path to state file from skyramp_calculate_health_scores **EXECUTING ACTIONS:** - UPDATE : Automatically apply changes to the test file using the write tool - REGENERATE : Provide summary of the test and drift score - VERIFY : Provide summary of the test and drift score **AUTOMATION:** - For UPDATE actions: Read test file, apply fixes, write immediately - Do NOT ask for user confirmation before updating files - Use the 'write' tool to save changes directly **OUTPUT:** Comprehensive report with executed actions, summary, and detailed analysis `, inputSchema: actionsSchema, }, async (args) => { let errorResult; try { logger.info("Performing Actions"); // Load data 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; } // Extract recommendations from test analysis results const recommendations = []; testAnalysisResults.forEach((test) => { if (test.healthScore !== undefined && test.recommendation) { recommendations.push({ testFile: test.testFile, action: test.recommendation.action, priority: test.recommendation.priority, rationale: test.recommendation.rationale, estimatedWork: test.recommendation.estimatedWork, issues: test.issues || [], }); } }); if (recommendations.length === 0) { return { content: [ { type: "text", text: JSON.stringify({ message: "No recommendations found in test results", }, null, 2), }, ], }; } // Filter recommendations where action is UPDATE const updateRecommendations = (recommendations || []).filter((rec) => rec.action === "UPDATE"); if (updateRecommendations.length === 0) { // No UPDATE actions, return summary for other actions return { content: [ { type: "text", text: JSON.stringify({ summary: `No UPDATE actions found. ${recommendations.length} other recommendations available.`, recommendations: recommendations.map((rec) => ({ testFile: rec.testFile, action: rec.action, priority: rec.priority, rationale: rec.rationale, })), message: `Review recommendations for REGENERATE or VERIFY actions.`, }, null, 2), }, ], }; } // Process UPDATE actions const updateInstructions = []; const testFilesToUpdate = []; for (const rec of updateRecommendations) { if (!rec.testFile) { logger.warning("Recommendation missing testFile", rec); continue; } testFilesToUpdate.push(rec.testFile); // Find corresponding test data for drift information const testData = testAnalysisResults.find((t) => t.testFile === rec.testFile); const driftData = testData?.drift; const issues = rec.issues || []; const driftChanges = driftData?.changes || []; // Read test file content let testFileContent = ""; try { testFileContent = fs.readFileSync(rec.testFile, "utf-8"); } catch (error) { logger.error(`Failed to read test file ${rec.testFile}: ${error.message}`); continue; } // Build update instructions let instruction = `\n### ${rec.testFile}\n\n`; instruction += `**Priority:** ${rec.priority} | `; instruction += `**Estimated Effort:** ${rec.estimatedWork || "Small"}\n\n`; instruction += `**Why Update Needed:** ${rec.rationale}\n\n`; if (driftData) { instruction += `**Analysis:**\n`; instruction += `- Drift Score: ${driftData.driftScore ?? "N/A"}\n`; instruction += `- Changes Detected: ${driftData.changes?.length || 0}\n`; instruction += `- Affected Files: ${driftData.affectedFiles.files || 0}\n\n`; } if (driftChanges.length > 0) { instruction += `**Changes Detected:**\n`; driftChanges.forEach((change) => { instruction += `**${change.type}** (Severity: ${change.severity}): ${change.description}\n`; if (change.details) { instruction += ` └─ ${change.details}\n`; } if (change.file) { instruction += ` └─ In: \`${change.file}\`\n`; } }); instruction += `\n`; } if (issues.length > 0) { instruction += `**Issues Found:**\n`; issues.forEach((issue) => { instruction += `**${issue.type}** (Severity: ${issue.severity}): ${issue.description}\n`; if (issue.details) { instruction += ` └─ ${issue.details}\n`; } }); instruction += `\n`; } instruction += `**Test File Content:**\n\`\`\`\n${testFileContent}\n\`\`\`\n\n`; updateInstructions.push(instruction); } // Build final response let responseText = `# 🔧 Test Update Recommendations\n\n`; responseText += `Found **${updateRecommendations.length} test(s)** that need updates based on code changes.\n\n`; responseText += `## 📋 Tests Requiring Updates\n\n`; testFilesToUpdate.forEach((file, idx) => { responseText += `${idx + 1}. \`${file}\`\n`; }); responseText += `\n---\n`; responseText += updateInstructions.join("\n---\n"); responseText += `\n\n## ✅ Next Steps\n\n`; responseText += `The AI assistant will:\n`; responseText += `1. Review the changes and issues for each test\n`; responseText += `2. Update test files to fix compatibility issues\n`; responseText += `3. Preserve original test logic and structure\n`; responseText += `4. Show you the changes made\n\n`; responseText += `5. At the end of the tool execution, MUST display the below message\n`; responseText += `**This tool is currently in Early Preview stage. Please verify the results.**\n\n`; // Build LLM-only instructions (hidden from users) const llmInstructions = `<!-- LLM_INSTRUCTIONS: { "workflow": "test_maintenance", "action": "execute_updates", "auto_proceed": true, "files_to_update": ${JSON.stringify(testFilesToUpdate)}, "update_count": ${updateRecommendations.length} } -->\n`; return { content: [ { type: "text", text: responseText, // Explicitly mark as user-facing (though this is default) annotations: { audience: ["user"], }, }, { type: "text", text: llmInstructions, // Mark as assistant-only (may or may not be respected by Cursor) annotations: { audience: ["assistant"], }, }, ], }; } catch (error) { logger.error(`Actions tool failed: ${error.message}`, error); errorResult = { content: [ { type: "text", text: JSON.stringify({ error: error.message, }, null, 2), }, ], isError: true, }; return errorResult; } finally { const fullState = await StateManager.fromStatePath(args.stateFile).readFullState(); const repositoryPath = fullState?.metadata.repositoryPath || ""; AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, { repositoryPath: repositoryPath, }); } }); }