@skyramp/mcp 0.0.54 → 0.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -30,7 +30,11 @@ import { registerExecuteBatchTestsTool } from "./tools/test-maintenance/executeB
30
30
  import { registerCalculateHealthScoresTool } from "./tools/test-maintenance/calculateHealthScoresTool.js";
31
31
  import { registerActionsTool } from "./tools/test-maintenance/actionsTool.js";
32
32
  import { registerStateCleanupTool } from "./tools/test-maintenance/stateCleanupTool.js";
33
+ import { registerTestbotPrompt } from "./prompts/testbot/testbot-prompts.js";
34
+ import { registerInitTestbotTool } from "./tools/initTestbotTool.js";
35
+ import { registerSubmitReportTool } from "./tools/submitReportTool.js";
33
36
  import { AnalyticsService } from "./services/AnalyticsService.js";
37
+ import { initCheck } from "./utils/initAgent.js";
34
38
  const server = new McpServer({
35
39
  name: "Skyramp MCP Server",
36
40
  version: "1.0.0",
@@ -44,6 +48,24 @@ const server = new McpServer({
44
48
  },
45
49
  },
46
50
  });
51
+ // Check for first-time invocation after version update (runs in background, doesn't block)
52
+ let initCheckCalled = false;
53
+ const originalRegisterTool = server.registerTool.bind(server);
54
+ server.registerTool = function (name, definition, handler) {
55
+ const wrappedHandler = async (...args) => {
56
+ if (!initCheckCalled) {
57
+ initCheck()
58
+ .then(() => {
59
+ initCheckCalled = true; // Only set to true if initCheck succeeds, allowing retry on failure
60
+ })
61
+ .catch((err) => {
62
+ logger.error("Background initialization check failed", { error: err });
63
+ });
64
+ }
65
+ return handler(...args);
66
+ };
67
+ return originalRegisterTool(name, definition, wrappedHandler);
68
+ };
47
69
  // Register prompts
48
70
  logger.info("Starting prompt registration process");
49
71
  const prompts = [
@@ -51,6 +73,10 @@ const prompts = [
51
73
  registerStartTraceCollectionPrompt,
52
74
  registerTestHealthPrompt,
53
75
  ];
76
+ if (process.env.SKYRAMP_FEATURE_TESTBOT === "1") {
77
+ prompts.push(registerTestbotPrompt);
78
+ logger.info("TestBot prompt enabled via SKYRAMP_FEATURE_TESTBOT");
79
+ }
54
80
  prompts.forEach((registerPrompt) => registerPrompt(server));
55
81
  logger.info("All prompts registered successfully");
56
82
  // Register test generation tools
@@ -91,6 +117,11 @@ const infrastructureTools = [
91
117
  registerTraceTool,
92
118
  registerTraceStopTool,
93
119
  ];
120
+ if (process.env.SKYRAMP_FEATURE_TESTBOT === "1") {
121
+ infrastructureTools.push(registerInitTestbotTool);
122
+ infrastructureTools.push(registerSubmitReportTool);
123
+ logger.info("TestBot tools enabled via SKYRAMP_FEATURE_TESTBOT");
124
+ }
94
125
  infrastructureTools.forEach((registerTool) => registerTool(server));
95
126
  // Global error handlers for crash telemetry
96
127
  process.on("uncaughtException", async (error) => {
@@ -22,13 +22,21 @@ export function registerStartTraceCollectionPrompt(mcpServer) {
22
22
  **Playwright Configuration Options:**
23
23
  When playwright is enabled for trace collection, you can optionally configure:
24
24
 
25
- 1. **Playwright Storage Path** (playwrightStoragePath):
25
+ 1. **Browser** (browser):
26
+ - Choose which browser to use for trace collection
27
+ - Supported browsers:
28
+ * 'chromium' - Chrome/Chromium browser (default)
29
+ * 'firefox' - Mozilla Firefox browser
30
+ * 'webkit' - Safari/WebKit browser
31
+ - Use firefox or webkit when you need to test cross-browser compatibility or specific browser behaviors
32
+
33
+ 2. **Playwright Storage Path** (playwrightStoragePath):
26
34
  - Path to a playwright session storage file containing authentication data (cookies, localStorage, sessionStorage, etc.)
27
35
  - MUST be an absolute path like /path/to/storage.json
28
36
  - Use this when you have manually created a session from the login flow and want to reuse it for future trace collections to avoid manual login every time
29
37
  - The session file should be created beforehand using Playwright's storageState feature during the login flow
30
38
 
31
- 2. **Playwright Viewport Size** (playwrightViewportSize):
39
+ 3. **Playwright Viewport Size** (playwrightViewportSize):
32
40
  - Defines the browser window size for trace collection
33
41
  - Supported formats:
34
42
  * 'hd' - 1280x720
@@ -47,6 +55,11 @@ When playwright is enabled for trace collection, you can optionally configure:
47
55
  **Example usage prompt for trace collection with playwright storage and viewport:**
48
56
  * Start playwright trace collection with storage path /Users/dev/session-storage.json and viewport size full-hd
49
57
 
58
+ **Example usage prompt for trace collection with specific browser:**
59
+ * Start trace collection with Firefox browser
60
+ * Collect UI traces using webkit browser
61
+ * Start playwright trace collection with chromium browser (default)
62
+
50
63
  **CRITICAL: NEVER SHOW THE CLI COMMANDS.**
51
64
  `,
52
65
  },
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { logger } from "../../utils/logger.js";
3
+ function getTestbotPrompt(prTitle, prDescription, diffFile, testDirectory, summaryOutputFile) {
4
+ return `<TITLE>${prTitle}</TITLE>
5
+ <DESCRIPTION>${prDescription}</DESCRIPTION>
6
+ <CODE CHANGES>${diffFile}</CODE CHANGES>
7
+ <TEST DIRECTORY>${testDirectory}</TEST DIRECTORY>
8
+
9
+ For all the following work, use the tools offered by Skyramp MCP server.
10
+
11
+ First analyze the pull request title, description, and code changes to determine a business case
12
+ justification for this code change.
13
+
14
+ Then perform the following tasks based on your analysis:
15
+
16
+ ## Task 1: New Test Recommendations
17
+
18
+ 1. If, and only if, there are new endpoints in the code changes, get recommendations
19
+ for new contract, e2e, smoke, load tests specific to the code changes
20
+ and business case analysis. DO NOT use the whole repository for generating recommendations.
21
+ 2. If, and only if, there are recommendations for new tests, then use Skyramp MCP generate
22
+ tools to generate contract, e2e, smoke and load tests.
23
+ 3. Use Skyramp MCP to execute the generated tests and validate the results.
24
+
25
+ ## Task 2: Existing Test Maintenance
26
+
27
+ 1. Check if there are any relevant Skyramp tests in the '${testDirectory}' directory that correspond to the code changes.
28
+ 2. If there are relevant tests, use Skyramp MCP tools to analyze and modify them to ensure they pass with the new changes.
29
+ 3. Use Skyramp MCP to execute the updated tests and validate the results.
30
+
31
+ ## Task 3: Submit Report
32
+
33
+ After completing Tasks 1 and 2, you MUST call the Skyramp MCP tool "skyramp_submit_report" to submit your report.
34
+ Pass '${summaryOutputFile}' as the summaryOutputFile parameter.
35
+
36
+ Do NOT write the report to a file yourself. Do NOT skip this step. The skyramp_submit_report tool is the ONLY way to submit the report.
37
+
38
+ Reminder: Use the Skyramp MCP tools available to you for test analysis, generation, and execution.`;
39
+ }
40
+ export function registerTestbotPrompt(server) {
41
+ logger.info("Registering testbot prompt");
42
+ server.registerPrompt("skyramp_testbot", {
43
+ description: "Run Skyramp TestBot to generate test recommendations and perform test maintenance for a pull request.",
44
+ argsSchema: {
45
+ prTitle: z.string().describe("Pull request title"),
46
+ prDescription: z
47
+ .string()
48
+ .describe("Pull request description/body"),
49
+ diffFile: z.string().describe("Path to the git diff file"),
50
+ testDirectory: z
51
+ .string()
52
+ .default("tests")
53
+ .describe("Directory containing Skyramp tests"),
54
+ summaryOutputFile: z
55
+ .string()
56
+ .describe("File path where the agent should write the testbot summary report"),
57
+ },
58
+ }, (args) => {
59
+ const prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.diffFile, args.testDirectory, args.summaryOutputFile);
60
+ return {
61
+ messages: [
62
+ {
63
+ role: "user",
64
+ content: {
65
+ type: "text",
66
+ text: prompt,
67
+ },
68
+ },
69
+ ],
70
+ };
71
+ });
72
+ }
@@ -6,7 +6,7 @@ import { stripVTControlCharacters } from "util";
6
6
  import { logger } from "../utils/logger.js";
7
7
  const DEFAULT_TIMEOUT = 300000; // 5 minutes
8
8
  const MAX_CONCURRENT_EXECUTIONS = 5;
9
- const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.6";
9
+ const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.9";
10
10
  const DOCKER_PLATFORM = "linux/amd64";
11
11
  const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
12
12
  // Files and directories to exclude when mounting workspace to Docker container
@@ -0,0 +1,187 @@
1
+ import { z } from "zod";
2
+ import { simpleGit } from "simple-git";
3
+ import * as fs from "fs/promises";
4
+ import * as path from "path";
5
+ import { logger } from "../utils/logger.js";
6
+ import { AnalyticsService } from "../services/AnalyticsService.js";
7
+ const TOOL_NAME = "skyramp_init_testbot";
8
+ const WORKFLOW_FILENAME = "skyramp-test-bot.yml";
9
+ function generateWorkflow(targetBranches, agentType) {
10
+ const apiKeyLine = agentType === "copilot"
11
+ ? " copilot_api_key: ${{ secrets.COPILOT_API_KEY }}"
12
+ : " cursor_api_key: ${{ secrets.CURSOR_API_KEY }}";
13
+ const branchesBlock = targetBranches && targetBranches.length > 0
14
+ ? `\n branches:\n${targetBranches.map((b) => ` - '${b}'`).join("\n")}`
15
+ : "";
16
+ return `name: Skyramp Test Automation
17
+ on:
18
+ pull_request:${branchesBlock}
19
+
20
+ jobs:
21
+ test-maintenance:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: write
25
+ pull-requests: write
26
+
27
+ steps:
28
+ - name: Checkout code
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 0
32
+
33
+ - name: Run Skyramp Test Bot
34
+ uses: skyramp/test-bot@v1
35
+ with:
36
+ skyramp_license_file: \${{ secrets.SKYRAMP_LICENSE }}
37
+ ${apiKeyLine}
38
+ `;
39
+ }
40
+ export function registerInitTestbotTool(server) {
41
+ server.registerTool(TOOL_NAME, {
42
+ description: `Initialize Skyramp Test Bot for a GitHub repository
43
+
44
+ Sets up a GitHub Actions workflow that automatically generates and maintains tests on pull requests using Skyramp Test Bot. This tool validates the repository, creates the workflow file, and provides setup instructions for required secrets.`,
45
+ inputSchema: {
46
+ repository_path: z
47
+ .string()
48
+ .describe("Absolute path to the user's GitHub repository"),
49
+ agent_type: z
50
+ .enum(["cursor", "copilot"])
51
+ .default("cursor")
52
+ .describe("Which AI agent the user uses (cursor or copilot). Defaults to cursor."),
53
+ target_branches: z
54
+ .array(z.string())
55
+ .optional()
56
+ .describe("Branches to trigger the test bot on pull requests. If not specified, the workflow triggers on all pull requests."),
57
+ },
58
+ _meta: {
59
+ keywords: [
60
+ "init",
61
+ "testbot",
62
+ "test-bot",
63
+ "github actions",
64
+ "workflow",
65
+ "setup",
66
+ "ci",
67
+ ],
68
+ },
69
+ }, async (params) => {
70
+ let errorResult;
71
+ const repoPath = params.repository_path;
72
+ const agentType = params.agent_type;
73
+ const targetBranches = params.target_branches;
74
+ logger.info("Initializing Skyramp Test Bot", {
75
+ repository_path: repoPath,
76
+ agent_type: agentType,
77
+ target_branches: targetBranches,
78
+ });
79
+ try {
80
+ // Validate it's a git repo
81
+ const git = simpleGit(repoPath);
82
+ const isRepo = await git.checkIsRepo();
83
+ if (!isRepo) {
84
+ errorResult = {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: `Error: "${repoPath}" is not a Git repository. Please run this tool from a valid Git repository.`,
89
+ },
90
+ ],
91
+ isError: true,
92
+ };
93
+ return errorResult;
94
+ }
95
+ // Validate it has a GitHub remote
96
+ const remotes = await git.getRemotes(true);
97
+ const hasGitHubRemote = remotes.some((remote) => remote.refs.fetch?.includes("github.com") ||
98
+ remote.refs.push?.includes("github.com"));
99
+ if (!hasGitHubRemote) {
100
+ errorResult = {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: `Error: No GitHub remote found in "${repoPath}". Skyramp Test Bot requires a GitHub repository. Please add a GitHub remote and try again.`,
105
+ },
106
+ ],
107
+ isError: true,
108
+ };
109
+ return errorResult;
110
+ }
111
+ // Create the workflow directory if needed and write the workflow file atomically
112
+ const workflowDir = path.join(repoPath, ".github", "workflows");
113
+ await fs.mkdir(workflowDir, { recursive: true });
114
+ const workflowPath = path.join(workflowDir, WORKFLOW_FILENAME);
115
+ const workflowContent = generateWorkflow(targetBranches, agentType);
116
+ try {
117
+ await fs.writeFile(workflowPath, workflowContent, {
118
+ encoding: "utf-8",
119
+ flag: "wx",
120
+ });
121
+ }
122
+ catch (err) {
123
+ if (err.code === "EEXIST") {
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: `Skyramp Test Bot workflow already exists at ".github/workflows/${WORKFLOW_FILENAME}". No changes were made. If you want to reconfigure it, delete the existing file and run this tool again.`,
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ throw err;
134
+ }
135
+ // Build success message with setup instructions
136
+ const secretName = agentType === "copilot" ? "COPILOT_API_KEY" : "CURSOR_API_KEY";
137
+ const agentLabel = agentType === "copilot" ? "Copilot" : "Cursor";
138
+ const successMessage = `Skyramp Test Bot workflow created successfully at ".github/workflows/${WORKFLOW_FILENAME}".
139
+
140
+ To complete the setup, configure the following GitHub Secrets in your repository (Settings > Secrets and variables > Actions):
141
+
142
+ 1. **SKYRAMP_LICENSE** - The contents of your Skyramp license file (paste the full license file text)
143
+ 2. **${secretName}** - Your ${agentLabel} API key
144
+
145
+ The workflow is configured to run on ${targetBranches && targetBranches.length > 0 ? `pull requests targeting: ${targetBranches.join(", ")}` : "all pull requests"}
146
+
147
+ Required repository permissions (already configured in the workflow):
148
+ - \`contents: write\` - To commit generated tests
149
+ - \`pull-requests: write\` - To post comments on PRs
150
+
151
+ Next steps:
152
+ 1. Add the required secrets to your GitHub repository
153
+ 2. Commit and push the new workflow file
154
+ 3. Open a pull request to see Skyramp Test Bot in action`;
155
+ return {
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: successMessage,
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ catch (error) {
165
+ const errorMessage = `Failed to initialize Skyramp Test Bot: ${error.message}`;
166
+ logger.error(errorMessage, { error });
167
+ errorResult = {
168
+ content: [
169
+ {
170
+ type: "text",
171
+ text: errorMessage,
172
+ },
173
+ ],
174
+ isError: true,
175
+ };
176
+ return errorResult;
177
+ }
178
+ finally {
179
+ const recordParams = {
180
+ repository_path: repoPath,
181
+ agent_type: agentType,
182
+ target_branches: targetBranches ? targetBranches.join(",") : "",
183
+ };
184
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
185
+ }
186
+ });
187
+ }
@@ -0,0 +1,194 @@
1
+ // @ts-ignore
2
+ import { registerInitTestbotTool } from "./initTestbotTool.js";
3
+ import { simpleGit } from "simple-git";
4
+ import * as fs from "fs/promises";
5
+ import * as fsSync from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ // Mock logger and AnalyticsService to avoid side effects
9
+ jest.mock("../utils/logger.js", () => ({
10
+ logger: { info: jest.fn(), error: jest.fn() },
11
+ }));
12
+ jest.mock("../services/AnalyticsService.js", () => ({
13
+ AnalyticsService: { pushMCPToolEvent: jest.fn() },
14
+ }));
15
+ /**
16
+ * Captures the tool handler callback from registerInitTestbotTool
17
+ * by providing a fake McpServer with a mock registerTool method.
18
+ */
19
+ function captureToolHandler() {
20
+ let handler;
21
+ const fakeServer = {
22
+ registerTool: (_name, _opts, fn) => {
23
+ handler = fn;
24
+ },
25
+ };
26
+ registerInitTestbotTool(fakeServer);
27
+ return handler;
28
+ }
29
+ /**
30
+ * Creates a temp directory, initializes a git repo, and optionally
31
+ * adds a GitHub remote.
32
+ */
33
+ async function createTempRepo(options) {
34
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "init-testbot-test-"));
35
+ const git = simpleGit(tmpDir);
36
+ await git.init();
37
+ if (options?.addGithubRemote !== false) {
38
+ await git.addRemote("origin", "https://github.com/test-org/test-repo.git");
39
+ }
40
+ return tmpDir;
41
+ }
42
+ describe("registerInitTestbotTool", () => {
43
+ let handler;
44
+ let tmpDirs = [];
45
+ beforeAll(() => {
46
+ handler = captureToolHandler();
47
+ });
48
+ afterEach(async () => {
49
+ for (const dir of tmpDirs) {
50
+ await fs.rm(dir, { recursive: true, force: true });
51
+ }
52
+ tmpDirs = [];
53
+ });
54
+ it("should register the tool on the server", () => {
55
+ const registerToolMock = jest.fn();
56
+ const fakeServer = { registerTool: registerToolMock };
57
+ registerInitTestbotTool(fakeServer);
58
+ expect(registerToolMock).toHaveBeenCalledWith("skyramp_init_testbot", expect.objectContaining({
59
+ description: expect.any(String),
60
+ inputSchema: expect.any(Object),
61
+ }), expect.any(Function));
62
+ });
63
+ it("should return error for non-git directory", async () => {
64
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "init-testbot-test-"));
65
+ tmpDirs.push(tmpDir);
66
+ const result = await handler({
67
+ repository_path: tmpDir,
68
+ agent_type: "cursor",
69
+ });
70
+ expect(result.isError).toBe(true);
71
+ expect(result.content[0].text).toContain("is not a Git repository");
72
+ });
73
+ it("should return error for git repo without GitHub remote", async () => {
74
+ const tmpDir = await createTempRepo({ addGithubRemote: false });
75
+ tmpDirs.push(tmpDir);
76
+ const result = await handler({
77
+ repository_path: tmpDir,
78
+ agent_type: "cursor",
79
+ });
80
+ expect(result.isError).toBe(true);
81
+ expect(result.content[0].text).toContain("No GitHub remote found");
82
+ });
83
+ it("should create workflow file on success", async () => {
84
+ const tmpDir = await createTempRepo();
85
+ tmpDirs.push(tmpDir);
86
+ const result = await handler({
87
+ repository_path: tmpDir,
88
+ agent_type: "cursor",
89
+ });
90
+ expect(result.isError).toBeUndefined();
91
+ expect(result.content[0].text).toContain("workflow created successfully");
92
+ const workflowPath = path.join(tmpDir, ".github", "workflows", "skyramp-test-bot.yml");
93
+ expect(fsSync.existsSync(workflowPath)).toBe(true);
94
+ });
95
+ it("should return already-exists message when workflow file exists", async () => {
96
+ const tmpDir = await createTempRepo();
97
+ tmpDirs.push(tmpDir);
98
+ // Create the workflow file first
99
+ const workflowDir = path.join(tmpDir, ".github", "workflows");
100
+ await fs.mkdir(workflowDir, { recursive: true });
101
+ await fs.writeFile(path.join(workflowDir, "skyramp-test-bot.yml"), "existing");
102
+ const result = await handler({
103
+ repository_path: tmpDir,
104
+ agent_type: "cursor",
105
+ });
106
+ expect(result.isError).toBeUndefined();
107
+ expect(result.content[0].text).toContain("already exists");
108
+ // Verify original file was not overwritten
109
+ const content = await fs.readFile(path.join(workflowDir, "skyramp-test-bot.yml"), "utf-8");
110
+ expect(content).toBe("existing");
111
+ });
112
+ it("should generate workflow with cursor API key by default", async () => {
113
+ const tmpDir = await createTempRepo();
114
+ tmpDirs.push(tmpDir);
115
+ await handler({
116
+ repository_path: tmpDir,
117
+ agent_type: "cursor",
118
+ });
119
+ const workflowContent = await fs.readFile(path.join(tmpDir, ".github", "workflows", "skyramp-test-bot.yml"), "utf-8");
120
+ expect(workflowContent).toContain("cursor_api_key");
121
+ expect(workflowContent).not.toContain("copilot_api_key");
122
+ });
123
+ it("should generate workflow with copilot API key when agent_type is copilot", async () => {
124
+ const tmpDir = await createTempRepo();
125
+ tmpDirs.push(tmpDir);
126
+ await handler({
127
+ repository_path: tmpDir,
128
+ agent_type: "copilot",
129
+ });
130
+ const workflowContent = await fs.readFile(path.join(tmpDir, ".github", "workflows", "skyramp-test-bot.yml"), "utf-8");
131
+ expect(workflowContent).toContain("copilot_api_key");
132
+ expect(workflowContent).not.toContain("cursor_api_key");
133
+ });
134
+ it("should generate workflow without branches key when target_branches is not provided", async () => {
135
+ const tmpDir = await createTempRepo();
136
+ tmpDirs.push(tmpDir);
137
+ await handler({
138
+ repository_path: tmpDir,
139
+ agent_type: "cursor",
140
+ });
141
+ const workflowContent = await fs.readFile(path.join(tmpDir, ".github", "workflows", "skyramp-test-bot.yml"), "utf-8");
142
+ expect(workflowContent).toContain("pull_request:");
143
+ expect(workflowContent).not.toContain("branches:");
144
+ });
145
+ it("should generate workflow with branches key when target_branches is provided", async () => {
146
+ const tmpDir = await createTempRepo();
147
+ tmpDirs.push(tmpDir);
148
+ await handler({
149
+ repository_path: tmpDir,
150
+ agent_type: "cursor",
151
+ target_branches: ["main", "develop"],
152
+ });
153
+ const workflowContent = await fs.readFile(path.join(tmpDir, ".github", "workflows", "skyramp-test-bot.yml"), "utf-8");
154
+ expect(workflowContent).toContain("branches:");
155
+ expect(workflowContent).toContain("- 'main'");
156
+ expect(workflowContent).toContain("- 'develop'");
157
+ });
158
+ it("should include setup instructions with correct secret names for cursor", async () => {
159
+ const tmpDir = await createTempRepo();
160
+ tmpDirs.push(tmpDir);
161
+ const result = await handler({
162
+ repository_path: tmpDir,
163
+ agent_type: "cursor",
164
+ });
165
+ expect(result.content[0].text).toContain("CURSOR_API_KEY");
166
+ expect(result.content[0].text).toContain("SKYRAMP_LICENSE");
167
+ expect(result.content[0].text).toContain("all pull requests");
168
+ });
169
+ it("should include setup instructions with correct secret names for copilot", async () => {
170
+ const tmpDir = await createTempRepo();
171
+ tmpDirs.push(tmpDir);
172
+ const result = await handler({
173
+ repository_path: tmpDir,
174
+ agent_type: "copilot",
175
+ target_branches: ["main"],
176
+ });
177
+ expect(result.content[0].text).toContain("COPILOT_API_KEY");
178
+ expect(result.content[0].text).toContain("pull requests targeting: main");
179
+ });
180
+ it("should generate valid YAML workflow structure", async () => {
181
+ const tmpDir = await createTempRepo();
182
+ tmpDirs.push(tmpDir);
183
+ await handler({
184
+ repository_path: tmpDir,
185
+ agent_type: "cursor",
186
+ });
187
+ const workflowContent = await fs.readFile(path.join(tmpDir, ".github", "workflows", "skyramp-test-bot.yml"), "utf-8");
188
+ expect(workflowContent).toContain("name: Skyramp Test Automation");
189
+ expect(workflowContent).toContain("runs-on: ubuntu-latest");
190
+ expect(workflowContent).toContain("uses: actions/checkout@v4");
191
+ expect(workflowContent).toContain("uses: skyramp/test-bot@v1");
192
+ expect(workflowContent).toContain("skyramp_license_file:");
193
+ });
194
+ });
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ import { logger } from "../utils/logger.js";
3
+ import * as fs from "fs/promises";
4
+ import * as path from "path";
5
+ import { AnalyticsService } from "../services/AnalyticsService.js";
6
+ const TOOL_NAME = "skyramp_submit_report";
7
+ const testResultSchema = z.object({
8
+ testType: z.string().describe("Type of test: Smoke, Contract, Integration, Fuzz, E2E, Load, etc."),
9
+ endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
10
+ status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
11
+ details: z.string().describe("Execution time and test file name, e.g. '10.8s, products_smoke_test.py'"),
12
+ });
13
+ const newTestSchema = z.object({
14
+ testType: z.string().describe("Type of test created: Smoke, Contract, Integration, etc."),
15
+ endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
16
+ fileName: z.string().describe("Name of the generated test file"),
17
+ });
18
+ const descriptionSchema = z.object({
19
+ description: z.string().describe("One-line description"),
20
+ });
21
+ export function registerSubmitReportTool(server) {
22
+ server.registerTool(TOOL_NAME, {
23
+ description: "Submit the final testbot report. Call this tool once after completing all test analysis, generation, and execution. " +
24
+ "This is the ONLY way to submit the report — do NOT write the report to a file manually.",
25
+ inputSchema: {
26
+ summaryOutputFile: z
27
+ .string()
28
+ .describe("The file path where the report should be written (provided in the task instructions)"),
29
+ businessCaseAnalysis: z
30
+ .string()
31
+ .describe("2-3 sentence business justification for this PR"),
32
+ newTestsCreated: z
33
+ .array(newTestSchema)
34
+ .describe("List of new tests created. Use empty array [] if none."),
35
+ testMaintenance: z
36
+ .array(descriptionSchema)
37
+ .describe("List of existing test modifications. Use empty array [] if none."),
38
+ testResults: z
39
+ .array(testResultSchema)
40
+ .describe("List of ALL test execution results. One entry per test executed."),
41
+ issuesFound: z
42
+ .array(descriptionSchema)
43
+ .describe("List of issues, failures, or bugs found. Use empty array [] if none."),
44
+ },
45
+ _meta: {
46
+ keywords: ["report", "summary", "testbot", "submit"],
47
+ },
48
+ }, async (params) => {
49
+ const startTime = Date.now();
50
+ let errorResult;
51
+ const reportJson = JSON.stringify({
52
+ businessCaseAnalysis: params.businessCaseAnalysis,
53
+ newTestsCreated: params.newTestsCreated,
54
+ testMaintenance: params.testMaintenance,
55
+ testResults: params.testResults,
56
+ issuesFound: params.issuesFound,
57
+ }, null, 2);
58
+ logger.info("Submitting testbot report", {
59
+ outputFile: params.summaryOutputFile,
60
+ payloadBytes: reportJson.length,
61
+ testResultCount: params.testResults.length,
62
+ });
63
+ try {
64
+ await fs.mkdir(path.dirname(params.summaryOutputFile), { recursive: true });
65
+ await fs.writeFile(params.summaryOutputFile, reportJson, "utf-8");
66
+ const elapsed = Date.now() - startTime;
67
+ logger.info("Testbot report written successfully", {
68
+ outputFile: params.summaryOutputFile,
69
+ elapsedMs: elapsed,
70
+ });
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `Report submitted successfully to ${params.summaryOutputFile}`,
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ catch (error) {
81
+ const elapsed = Date.now() - startTime;
82
+ const errorMessage = `Failed to write report: ${error.message}`;
83
+ logger.error(errorMessage, { error, elapsedMs: elapsed });
84
+ errorResult = {
85
+ content: [{ type: "text", text: errorMessage }],
86
+ isError: true,
87
+ };
88
+ return errorResult;
89
+ }
90
+ finally {
91
+ const recordParams = {
92
+ summary_output_file: params.summaryOutputFile,
93
+ };
94
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,118 @@
1
+ // @ts-ignore
2
+ import { registerSubmitReportTool } from "./submitReportTool.js";
3
+ import * as fs from "fs/promises";
4
+ import * as path from "path";
5
+ import * as os from "os";
6
+ jest.mock("../utils/logger.js", () => ({
7
+ logger: { info: jest.fn(), error: jest.fn() },
8
+ }));
9
+ jest.mock("../services/AnalyticsService.js", () => ({
10
+ AnalyticsService: { pushMCPToolEvent: jest.fn() },
11
+ }));
12
+ function captureToolHandler() {
13
+ let handler;
14
+ const fakeServer = {
15
+ registerTool: (_name, _opts, fn) => {
16
+ handler = fn;
17
+ },
18
+ };
19
+ registerSubmitReportTool(fakeServer);
20
+ return handler;
21
+ }
22
+ function sampleReportParams(outputFile) {
23
+ return {
24
+ summaryOutputFile: outputFile,
25
+ businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
26
+ newTestsCreated: [
27
+ { testType: "Smoke", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py" },
28
+ ],
29
+ testMaintenance: [{ description: "Updated auth flow in existing tests" }],
30
+ testResults: [
31
+ { testType: "Smoke", endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
32
+ { testType: "Smoke", endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
33
+ ],
34
+ issuesFound: [{ description: "Search endpoint returns 500 with category filter" }],
35
+ };
36
+ }
37
+ describe("registerSubmitReportTool", () => {
38
+ let handler;
39
+ let tmpDirs = [];
40
+ beforeAll(() => {
41
+ handler = captureToolHandler();
42
+ });
43
+ afterEach(async () => {
44
+ for (const dir of tmpDirs) {
45
+ await fs.rm(dir, { recursive: true, force: true });
46
+ }
47
+ tmpDirs = [];
48
+ });
49
+ it("should register the tool on the server", () => {
50
+ const registerToolMock = jest.fn();
51
+ const fakeServer = { registerTool: registerToolMock };
52
+ registerSubmitReportTool(fakeServer);
53
+ expect(registerToolMock).toHaveBeenCalledWith("skyramp_submit_report", expect.objectContaining({
54
+ description: expect.any(String),
55
+ inputSchema: expect.any(Object),
56
+ }), expect.any(Function));
57
+ });
58
+ it("should write JSON report to the specified file", async () => {
59
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
60
+ tmpDirs.push(tmpDir);
61
+ const outputFile = path.join(tmpDir, "report.json");
62
+ const result = await handler(sampleReportParams(outputFile));
63
+ expect(result.isError).toBeUndefined();
64
+ expect(result.content[0].text).toContain("Report submitted successfully");
65
+ const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
66
+ expect(written.businessCaseAnalysis).toBe("This PR adds product search. Tests needed for filtering.");
67
+ expect(written.newTestsCreated).toHaveLength(1);
68
+ expect(written.testResults).toHaveLength(2);
69
+ expect(written.issuesFound).toHaveLength(1);
70
+ expect(written.testMaintenance).toHaveLength(1);
71
+ });
72
+ it("should create parent directories if they don't exist", async () => {
73
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
74
+ tmpDirs.push(tmpDir);
75
+ const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
76
+ const result = await handler(sampleReportParams(outputFile));
77
+ expect(result.isError).toBeUndefined();
78
+ const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
79
+ expect(written.businessCaseAnalysis).toBeDefined();
80
+ });
81
+ it("should handle empty arrays for optional sections", async () => {
82
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
83
+ tmpDirs.push(tmpDir);
84
+ const outputFile = path.join(tmpDir, "report.json");
85
+ const result = await handler({
86
+ summaryOutputFile: outputFile,
87
+ businessCaseAnalysis: "Simple config change.",
88
+ newTestsCreated: [],
89
+ testMaintenance: [],
90
+ testResults: [
91
+ { testType: "Smoke", endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
92
+ ],
93
+ issuesFound: [],
94
+ });
95
+ expect(result.isError).toBeUndefined();
96
+ const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
97
+ expect(written.newTestsCreated).toEqual([]);
98
+ expect(written.testMaintenance).toEqual([]);
99
+ expect(written.issuesFound).toEqual([]);
100
+ expect(written.testResults).toHaveLength(1);
101
+ });
102
+ it("should return error for invalid file path", async () => {
103
+ const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json"));
104
+ expect(result.isError).toBe(true);
105
+ expect(result.content[0].text).toContain("Failed to write report");
106
+ });
107
+ it("should produce valid JSON with pretty formatting", async () => {
108
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
109
+ tmpDirs.push(tmpDir);
110
+ const outputFile = path.join(tmpDir, "report.json");
111
+ await handler(sampleReportParams(outputFile));
112
+ const raw = await fs.readFile(outputFile, "utf-8");
113
+ // Pretty-printed JSON should have indentation
114
+ expect(raw).toContain(" ");
115
+ // Should not contain summaryOutputFile in the output
116
+ expect(raw).not.toContain("summaryOutputFile");
117
+ });
118
+ });
@@ -30,6 +30,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
30
30
  .boolean()
31
31
  .describe("Whether to enable Playwright for trace collection. Set to true for UI interactions, false for API-only tracing")
32
32
  .default(true),
33
+ browser: basePlaywrightSchema.shape.browser,
33
34
  playwrightStoragePath: basePlaywrightSchema.shape.playwrightStoragePath,
34
35
  playwrightSaveStoragePath: basePlaywrightSchema.shape.playwrightSaveStoragePath,
35
36
  playwrightViewportSize: basePlaywrightSchema.shape.playwrightViewportSize,
@@ -85,6 +86,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
85
86
  };
86
87
  logger.info("Starting trace collection", {
87
88
  playwright: params.playwright,
89
+ browser: params.browser,
88
90
  runtime: params.runtime,
89
91
  include: params.include,
90
92
  exclude: params.exclude,
@@ -116,6 +118,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
116
118
  generateNoProxy: params.noProxy,
117
119
  unblock: true,
118
120
  playwright: params.playwright,
121
+ browser: params.browser,
119
122
  playwrightStoragePath: params.playwrightStoragePath,
120
123
  playwrightViewportSize: params.playwrightViewportSize,
121
124
  entrypoint: TELEMETRY_entrypoint_FIELD_NAME,
@@ -96,6 +96,10 @@ export const basePlaywrightSchema = z.object({
96
96
  .string()
97
97
  .default("")
98
98
  .describe("Viewport size for playwright browser. THE VALUE MUST BE IN THE FORMAT ['', 'hd', 'full-hd', '2k', 'x,y' e.g. '1920,1080' for 1920x1080 resolution]. DEFAULT VALUE IS ''. If set to '', the browser will use its default viewport size."),
99
+ browser: z
100
+ .enum(["chromium", "firefox", "webkit"])
101
+ .default("chromium")
102
+ .describe("Browser to use for Playwright trace collection. Supported browsers: 'chromium' (default), 'firefox', 'webkit'. Choose based on your testing requirements and target browser compatibility."),
99
103
  });
100
104
  export const baseTraceSchema = z.object({
101
105
  trace: z
@@ -0,0 +1,34 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { SkyrampClient } from "@skyramp/skyramp";
5
+ import { logger } from "./logger.js";
6
+ /**
7
+ * Get the current MCP package version from package.json
8
+ */
9
+ export function getPackageVersion() {
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const packageJson = fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8");
13
+ const packageJsonData = JSON.parse(packageJson);
14
+ return packageJsonData.version;
15
+ }
16
+ /**
17
+ * Checks if this is the first time after extension install and initializes the agent if needed.
18
+ */
19
+ export async function initCheck() {
20
+ const currentVersion = getPackageVersion();
21
+ try {
22
+ const client = new SkyrampClient();
23
+ // @ts-ignore - Backend will be updated to add initAgent method that accepts version and entryPoint parameters
24
+ const initOutput = await client.initAgent({
25
+ version: currentVersion,
26
+ entryPoint: "mcp",
27
+ });
28
+ logger.info("Skyramp MCP agent initialized", { initOutput });
29
+ }
30
+ catch (error) {
31
+ logger.error("Error during first-time initialization", { error });
32
+ throw error;
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.24.3",
48
- "@skyramp/skyramp": "1.3.6",
49
48
  "@playwright/test": "^1.55.0",
49
+ "@skyramp/skyramp": "1.3.9",
50
50
  "dockerode": "^4.0.6",
51
51
  "fast-glob": "^3.3.3",
52
52
  "simple-git": "^3.30.0",