@skyramp/mcp
Version:
Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution
222 lines (208 loc) • 8.94 kB
JavaScript
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, {});
}
});
}