UNPKG

@skyramp/mcp

Version:

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

222 lines (208 loc) 8.94 kB
import { z } from "zod"; import { repositoryAnalysisSchema } from "../../types/RepositoryAnalysis.js"; import { TestType } from "../../types/TestTypes.js"; import { ScoringEngine } from "../../utils/scoring-engine.js"; import { StateManager, } from "../../utils/AnalysisStateManager.js"; import { logger } from "../../utils/logger.js"; import { AnalyticsService } from "../../services/AnalyticsService.js"; /** * Map Tests Tool * MCP tool for calculating test priority scores */ const mapTestsSchema = z.object({ repositoryPath: z .string() .describe("Absolute path to the repository that was analyzed (used for saving results)"), analysisReport: z .union([z.string(), repositoryAnalysisSchema]) .describe("Repository analysis result (JSON string or object from skyramp_analyze_repository)"), customWeights: z .record(z.number()) .optional() .describe("Optional: Custom weight multipliers for specific test types (e.g., {'load': 1.5, 'fuzz': 1.3})"), focusTestTypes: z .array(z.nativeEnum(TestType)) .optional() .describe("Optional: Only evaluate specific test types (e.g., ['integration', 'smoke'])"), }); const TOOL_NAME = "skyramp_map_tests"; export function registerMapTestsTool(server) { server.registerTool(TOOL_NAME, { description: `Calculate priority scores for Skyramp test types based on repository analysis. This tool evaluates all test types (E2E, UI, Integration, Load, Fuzz, Contract, Smoke) and calculates priority scores using: - Base impact scores (E2E: 100, UI: 95, Integration: 85, etc.) - Context multipliers based on repository characteristics - Feasibility assessment based on available artifacts The scoring algorithm considers: - Project type (full-stack, microservices, REST API, etc.) - Infrastructure (Kubernetes, Docker Compose, CI/CD) - Existing test coverage gaps - Available artifacts (OpenAPI specs, Playwright recordings) - Security requirements (authentication, sensitive data) Example usage: \`\`\` { "repositoryPath": "/Users/dev/my-api", "analysisReport": "<RepositoryAnalysis JSON from skyramp_analyze_repository>", "customWeights": { "load": 1.5, "fuzz": 1.3 } } \`\`\` **OUTPUT**: Returns the state file path to pass to skyramp_recommend_tests Output: TestMappingResult with priority scores, and state file path for next step.`, inputSchema: mapTestsSchema.shape, }, async (params) => { let errorResult; try { logger.info("Map tests tool invoked"); // Parse and validate analysis report let analysis = params.analysisReport; if (typeof analysis === "string") { try { analysis = JSON.parse(analysis); } catch (error) { throw new Error("analysisReport must be valid JSON string or RepositoryAnalysis object. JSON parsing failed."); } } // Validate the analysis object against the schema const validationResult = repositoryAnalysisSchema.safeParse(analysis); if (!validationResult.success) { const errors = validationResult.error.errors .map((e) => `${e.path.join(".")}: ${e.message}`) .join("; "); throw new Error(`analysisReport validation failed: ${errors}`); } analysis = validationResult.data; // Determine which test types to evaluate const testTypesToEvaluate = params.focusTestTypes || Object.values(TestType).filter((t) => typeof t === "string"); // Calculate scores for each test type const priorityScores = []; for (const testType of testTypesToEvaluate) { const score = ScoringEngine.calculateTestScore(testType, analysis); // Apply custom weights if provided if (params.customWeights && params.customWeights[testType]) { score._finalScore *= params.customWeights[testType]; score.contextMultiplier *= params.customWeights[testType]; score.reasoning += ` (custom weight: ×${params.customWeights[testType]})`; } priorityScores.push(score); } // Sort by final score (descending) priorityScores.sort((a, b) => b._finalScore - a._finalScore); // Create summary const highPriority = []; const mediumPriority = []; const lowPriority = []; for (const score of priorityScores) { if (score.feasibility === "not-applicable") continue; if (score._finalScore >= 80) { highPriority.push(score.testType); } else if (score._finalScore >= 60) { mediumPriority.push(score.testType); } else { lowPriority.push(score.testType); } } // Extract context factors const contextFactors = { applied: [] }; if (analysis.infrastructure.hasKubernetes) { contextFactors.applied.push({ factor: "hasKubernetes", impact: "Increases load and integration test importance", multiplier: 1.2, }); } if (analysis.infrastructure.hasDockerCompose) { contextFactors.applied.push({ factor: "hasDockerCompose", impact: "Suggests scaled infrastructure", multiplier: 1.1, }); } if (analysis.infrastructure.hasCiCd) { contextFactors.applied.push({ factor: "hasCiCd", impact: "Increases smoke test importance", multiplier: 1.1, }); } const mapping = { priorityScores, contextFactors, summary: { highPriority, mediumPriority, lowPriority }, }; // Save results using StateManager (stores in /tmp) const stateManager = new StateManager("recommendation"); const stateData = { repositoryPath: params.repositoryPath, analysis, mapping, }; await stateManager.writeData(stateData, { repositoryPath: params.repositoryPath, step: "map-tests", }); const stateFilePath = stateManager.getStatePath(); const sessionId = stateManager.getSessionId(); const stateSize = await stateManager.getSizeFormatted(); logger.info(`Saved test mapping to: ${stateFilePath} (${stateSize})`); // Format output const output = `# Test Priority Mapping ## Summary - **High Priority**: ${mapping.summary.highPriority.join(", ") || "None"} - **Medium Priority**: ${mapping.summary.mediumPriority.join(", ") || "None"} - **Low Priority**: ${mapping.summary.lowPriority.join(", ") || "None"} ## Test Type Priorities (Ordered by Score) ${mapping.priorityScores .map((score) => `### ${score.testType.toUpperCase()} - **Feasibility**: ${score.feasibility} - **Available Artifacts**: ${score.requiredArtifacts.available.join(", ") || "None"} - **Missing Artifacts**: ${score.requiredArtifacts.missing.join(", ") || "None"} - **Reasoning**: ${score.reasoning} `) .join("\n")} ## Context Factors Applied ${mapping.contextFactors.applied .map((factor) => `- **${factor.factor}**: ${factor.impact}${factor.multiplier})`) .join("\n")} ## Results Saved **State File**: \`${stateFilePath}\` **Session ID**: \`${sessionId}\` **Next Step**: Call \`skyramp_recommend_tests\` with stateFile: \`${stateFilePath}\``; return { content: [ { type: "text", text: output, }, ], isError: false, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error("Map tests tool failed", { error: errorMessage }); errorResult = { content: [ { type: "text", text: `Error mapping tests: ${errorMessage}`, }, ], isError: true, }; return errorResult; } finally { AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {}); } }); }