@skyramp/mcp
Version:
Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution
209 lines (202 loc) • 8.58 kB
JavaScript
import { SkyrampClient } from "@skyramp/skyramp";
import { analyzeOpenAPIWithGivenEndpoint } from "../utils/analyze-openapi.js";
import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, TELEMETRY_entrypoint_FIELD_NAME, } from "../utils/utils.js";
import { getLanguageSteps } from "../utils/language-helper.js";
import { logger } from "../utils/logger.js";
export class TestGenerationService {
client;
constructor() {
this.client = new SkyrampClient();
}
async generateTest(params) {
try {
// Log prompt parameter using reusable utility
logger.info("Generating test", {
prompt: params.prompt,
testType: this.getTestType(),
language: params.language,
outputDir: params.outputDir,
});
const validationResult = this.validateInputs(params);
if (validationResult.isError) {
return validationResult;
}
const generateOptions = this.buildGenerationOptions(params);
const apiAnalysisResult = await this.handleApiAnalysis(params, generateOptions);
if (apiAnalysisResult) {
return apiAnalysisResult;
}
const result = await this.executeGeneration(generateOptions);
const testType = this.getTestType();
const languageSteps = getLanguageSteps({
language: params.language || "",
testType: testType,
framework: params.framework,
});
// Orchestrate code optimization workflow based on flags
let postGenerationMessage = "";
if (params.codeReuse) {
// Only code reuse
postGenerationMessage += `
Test generated successfully!
⏭️**CRITICAL NEXT STEP**: Running code reuse analysis with skyramp_reuse_code tool.
`;
}
else if (params.modularizeCode) {
// Check if test type should be modularized
if (!params.trace && !params.scenarioFile && !params.playwrightInput) {
postGenerationMessage += `
Test generated successfully!
**NOTE**: Modularization is intentionally NOT applied to ${testType.toUpperCase()} tests.
${testType.toUpperCase()} tests are designed to be simple, straightforward, and deterministic.
Modularization can introduce unpredictable results and unnecessary complexity for these test types.
The generated test file remains unchanged and ready to use as-is.
`;
}
else {
// Only modularization for UI, E2E, Integration tests
postGenerationMessage += `
Test generated successfully!
⏭️**CRITICAL NEXT STEP**: Running modularization with skyramp_modularization tool.
`;
}
}
return {
content: [
{
type: "text",
text: `${postGenerationMessage}${result}
${languageSteps}`,
},
],
isError: false,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: errorMessage,
},
],
isError: true,
};
}
}
validateInputs(params) {
const errList = { content: [], isError: true };
const pathError = validatePath(params.outputDir, OUTPUT_DIR_FIELD_NAME);
if (pathError?.content)
errList.content.push(pathError.content[0]);
[
validateParams(params.pathParams ?? "", PATH_PARAMS_FIELD_NAME),
validateParams(params.queryParams ?? "", QUERY_PARAMS_FIELD_NAME),
validateParams(params.formParams ?? "", FORM_PARAMS_FIELD_NAME),
].forEach((error) => {
if (error?.content)
errList.content.push(error.content[0]);
});
if (params.requestData &&
validateRequestData(params.requestData) === null) {
errList.content.push({
type: "text",
text: "Error: requestData must be either a valid JSON string or an absolute path to a file.",
});
}
return errList.content.length === 0
? { content: [], isError: false }
: errList;
}
async handleApiAnalysis(params, generateOptions) {
if (params.apiSchema && params.endpointURL && params.method === "") {
try {
const requiredPathParams = await analyzeOpenAPIWithGivenEndpoint(params.apiSchema, params.endpointURL, params.pathParams ?? "");
if (requiredPathParams && requiredPathParams.trim().length > 0) {
return {
content: [
{
type: "text",
text: `${getPathParameterValidationError(requiredPathParams)}
**IMPORTANT: MAKE SURE TO PROVIDE THE PATH PARAMS THAT DO NOT GIVE 404 ERROR FOR GET/DELETE METHODS. IT WILL BE USED AS PATH PARAMS IN THE TEST GENERATION.**`,
},
],
isError: true,
};
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error analyzing OpenAPI schema: ${errorMessage}`,
},
],
isError: true,
};
}
}
return null;
}
async executeGeneration(generateOptions) {
try {
const result = await this.client.generateRestTest(generateOptions);
// Check if the result indicates failure
if (result && result.length > 0) {
const lowerResult = result.toLowerCase();
if (!lowerResult.includes("success")) {
throw new Error(`Test generation failed: ${result}`);
}
}
return `
**Generated Test Details:**
- Test Type: ${this.getTestType()}
- Output Directory: ${generateOptions.outputDir}
- Language: ${generateOptions.language}
- Framework: ${generateOptions.framework}
${result}`;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to execute test generation: ${errorMessage}`);
}
}
buildBaseGenerationOptions(params) {
return {
testType: this.getTestType(),
uri: params.endpointURL,
method: params.method,
apiSchema: params.apiSchema ? [params.apiSchema] : [],
language: params.language ?? "python",
framework: params.framework ??
(params.language === "typescript" || params.language === "javascript"
? "playwright"
: ""),
output: params.output,
outputDir: params.outputDir,
force: params.force ?? true,
deployDashboard: params.deployDashboard,
runtime: params.runtime,
dockerNetwork: params.dockerNetwork,
dockerWorkerPort: params.dockerWorkerPort?.toString(),
k8sNamespace: params.k8sNamespace,
k8sConfig: params.k8sConfig,
k8sContext: params.k8sContext,
authHeader: params.authHeader,
requestData: params.requestData,
pathParams: params.pathParams,
queryParams: params.queryParams,
formParams: params.formParams,
responseStatusCode: params.responseStatusCode,
traceFilePath: params.trace ?? params.scenarioFile,
generateInclude: params.include,
generateExclude: params.exclude,
generateInsecure: params.insecure,
entrypoint: TELEMETRY_entrypoint_FIELD_NAME,
chainingKey: params.chainingKey,
};
}
}