@skyramp/mcp 0.0.45 → 0.0.47

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.
Files changed (36) hide show
  1. package/build/index.js +66 -29
  2. package/build/prompts/test-recommendation/repository-analysis-prompt.js +7 -1
  3. package/build/prompts/test-recommendation/test-recommendation-prompt.js +7 -1
  4. package/build/prompts/testGenerationPrompt.js +1 -0
  5. package/build/services/AnalyticsService.js +78 -0
  6. package/build/services/DriftAnalysisService.js +6 -2
  7. package/build/services/TestExecutionService.js +322 -11
  8. package/build/services/TestHealthService.js +8 -5
  9. package/build/tools/auth/loginTool.js +12 -2
  10. package/build/tools/auth/logoutTool.js +12 -2
  11. package/build/tools/code-refactor/codeReuseTool.js +41 -15
  12. package/build/tools/code-refactor/modularizationTool.js +36 -9
  13. package/build/tools/executeSkyrampTestTool.js +45 -5
  14. package/build/tools/fixErrorTool.js +37 -13
  15. package/build/tools/generate-tests/generateContractRestTool.js +11 -3
  16. package/build/tools/generate-tests/generateE2ERestTool.js +8 -2
  17. package/build/tools/generate-tests/generateFuzzRestTool.js +11 -3
  18. package/build/tools/generate-tests/generateIntegrationRestTool.js +11 -3
  19. package/build/tools/generate-tests/generateLoadRestTool.js +11 -3
  20. package/build/tools/generate-tests/generateScenarioRestTool.js +10 -2
  21. package/build/tools/generate-tests/generateSmokeRestTool.js +11 -3
  22. package/build/tools/generate-tests/generateUIRestTool.js +10 -3
  23. package/build/tools/test-maintenance/actionsTool.js +175 -147
  24. package/build/tools/test-maintenance/analyzeTestDriftTool.js +14 -5
  25. package/build/tools/test-maintenance/calculateHealthScoresTool.js +13 -4
  26. package/build/tools/test-maintenance/discoverTestsTool.js +10 -2
  27. package/build/tools/test-maintenance/executeBatchTestsTool.js +14 -4
  28. package/build/tools/test-maintenance/stateCleanupTool.js +11 -3
  29. package/build/tools/test-recommendation/analyzeRepositoryTool.js +18 -4
  30. package/build/tools/test-recommendation/mapTestsTool.js +21 -5
  31. package/build/tools/test-recommendation/recommendTestsTool.js +17 -3
  32. package/build/tools/trace/startTraceCollectionTool.js +17 -4
  33. package/build/tools/trace/stopTraceCollectionTool.js +27 -3
  34. package/build/types/TestTypes.js +17 -3
  35. package/build/utils/AnalysisStateManager.js +3 -1
  36. package/package.json +2 -2
@@ -6,20 +6,176 @@ 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.2.38";
9
+ const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.2.41";
10
10
  const DOCKER_PLATFORM = "linux/amd64";
11
+ const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
11
12
  // Files and directories to exclude when mounting workspace to Docker container
12
13
  export const EXCLUDED_MOUNT_ITEMS = [
13
14
  "package-lock.json",
14
15
  "package.json",
15
16
  "node_modules",
16
17
  ];
18
+ /**
19
+ * Find the start index of a comment in a line, ignoring comment delimiters inside strings
20
+ * Returns -1 if no comment is found outside of strings
21
+ */
22
+ function findCommentStart(line) {
23
+ let inSingleQuote = false;
24
+ let inDoubleQuote = false;
25
+ let escaped = false;
26
+ for (let i = 0; i < line.length; i++) {
27
+ const char = line[i];
28
+ const nextChar = i + 1 < line.length ? line[i + 1] : "";
29
+ // Handle escape sequences
30
+ if (escaped) {
31
+ escaped = false;
32
+ continue;
33
+ }
34
+ if (char === "\\") {
35
+ escaped = true;
36
+ continue;
37
+ }
38
+ // Track string boundaries
39
+ if (char === "'" && !inDoubleQuote) {
40
+ inSingleQuote = !inSingleQuote;
41
+ continue;
42
+ }
43
+ if (char === '"' && !inSingleQuote) {
44
+ inDoubleQuote = !inDoubleQuote;
45
+ continue;
46
+ }
47
+ // Check for comments only if not in a string
48
+ if (!inSingleQuote && !inDoubleQuote) {
49
+ if (char === "/" && nextChar === "/") {
50
+ return i;
51
+ }
52
+ if (char === "#") {
53
+ return i;
54
+ }
55
+ }
56
+ }
57
+ return -1; // No comment found
58
+ }
59
+ /**
60
+ * Filter out comments from code lines
61
+ * Returns array of non-comment lines with inline comments removed
62
+ */
63
+ function filterComments(lines) {
64
+ const nonCommentLines = [];
65
+ let inMultiLineComment = false;
66
+ let inPythonMultiLineComment = false;
67
+ for (let line of lines) {
68
+ const trimmed = line.trim();
69
+ // Check for Python multi-line string comments (""" or ''')
70
+ // Count occurrences to handle single-line strings like '''comment'''
71
+ const tripleDoubleCount = (trimmed.match(/"""/g) || []).length;
72
+ const tripleSingleCount = (trimmed.match(/'''/g) || []).length;
73
+ if (tripleDoubleCount > 0) {
74
+ // Odd number: opening or closing a multi-line comment (toggle state)
75
+ // Even number: complete string on same line (e.g., """comment""" - don't toggle)
76
+ if (tripleDoubleCount % 2 === 1) {
77
+ inPythonMultiLineComment = !inPythonMultiLineComment;
78
+ }
79
+ continue; // Skip any line with triple quotes
80
+ }
81
+ if (tripleSingleCount > 0) {
82
+ if (tripleSingleCount % 2 === 1) {
83
+ inPythonMultiLineComment = !inPythonMultiLineComment;
84
+ }
85
+ continue;
86
+ }
87
+ if (inPythonMultiLineComment) {
88
+ continue;
89
+ }
90
+ // Check for multi-line comment start/end (/* */)
91
+ if (trimmed.includes("/*")) {
92
+ if (trimmed.includes("*/")) {
93
+ // Single-line multi-line comment (e.g., /* comment */)
94
+ // Don't change state, just skip the line
95
+ continue;
96
+ }
97
+ else {
98
+ // Opening a multi-line comment
99
+ inMultiLineComment = true;
100
+ continue;
101
+ }
102
+ }
103
+ if (inMultiLineComment) {
104
+ if (trimmed.includes("*/")) {
105
+ inMultiLineComment = false;
106
+ }
107
+ continue;
108
+ }
109
+ // Skip single-line comments
110
+ if (trimmed.startsWith("//") || trimmed.startsWith("#")) {
111
+ continue;
112
+ }
113
+ // Remove inline comments from the line before processing
114
+ // Use helper function to avoid removing comment delimiters inside strings
115
+ const commentIndex = findCommentStart(line);
116
+ if (commentIndex >= 0) {
117
+ line = line.substring(0, commentIndex);
118
+ }
119
+ nonCommentLines.push(line);
120
+ }
121
+ return nonCommentLines;
122
+ }
123
+ /**
124
+ * Detect session file paths referenced in test files
125
+ * Looks for storageState patterns in TypeScript/JavaScript/Python/Java/C# test files
126
+ * Excludes matches found in comments
127
+ */
128
+ function detectSessionFiles(testFilePath) {
129
+ try {
130
+ const content = fs.readFileSync(testFilePath, "utf-8");
131
+ const lines = content.split("\n");
132
+ const sessionFiles = [];
133
+ // Pattern for TypeScript/JavaScript: storageState: '/path/to/file' or storageState: "/path/to/file"
134
+ const tsJsPattern = /storageState:\s*['"]([^'"]+)['"]/g;
135
+ // Pattern for Python: storage_state='/path/to/file' or storage_state="/path/to/file"
136
+ const pythonPattern = /storage_state\s*=\s*['"]([^'"]+)['"]/g;
137
+ // Pattern for Java: setStorageState(Paths.get("path")) or setStorageState("path")
138
+ // Enforces proper parenthesis matching: first ) is required, second ) is required when using Paths.get
139
+ const javaPattern = /setStorageState(?:Path)?\s*\(\s*(?:Paths\.get\s*\(\s*)?['"]([^'"]+)['"]\s*\)(?:\s*\))?/g;
140
+ // Pattern for C#: StorageStatePath = "path" or StorageState = "path"
141
+ const csharpPattern = /StorageState(?:Path)?\s*=\s*['"]([^'"]+)['"]/g;
142
+ // Filter out comments
143
+ const codeLines = filterComments(lines);
144
+ // Process each non-comment line
145
+ for (const line of codeLines) {
146
+ // Try all patterns on this line
147
+ let match;
148
+ tsJsPattern.lastIndex = 0;
149
+ while ((match = tsJsPattern.exec(line)) !== null) {
150
+ sessionFiles.push(match[1]);
151
+ }
152
+ pythonPattern.lastIndex = 0;
153
+ while ((match = pythonPattern.exec(line)) !== null) {
154
+ sessionFiles.push(match[1]);
155
+ }
156
+ javaPattern.lastIndex = 0;
157
+ while ((match = javaPattern.exec(line)) !== null) {
158
+ sessionFiles.push(match[1]);
159
+ }
160
+ csharpPattern.lastIndex = 0;
161
+ while ((match = csharpPattern.exec(line)) !== null) {
162
+ sessionFiles.push(match[1]);
163
+ }
164
+ }
165
+ return sessionFiles;
166
+ }
167
+ catch (error) {
168
+ logger.error(`Failed to detect session files in ${testFilePath}`, {
169
+ error,
170
+ });
171
+ return [];
172
+ }
173
+ }
17
174
  export class TestExecutionService {
18
175
  docker;
19
- imageReady;
176
+ imageReady = null;
20
177
  constructor() {
21
178
  this.docker = new Docker();
22
- this.imageReady = this.ensureDockerImage();
23
179
  }
24
180
  /**
25
181
  * Execute multiple tests in parallel batches
@@ -63,13 +219,42 @@ export class TestExecutionService {
63
219
  }
64
220
  /**
65
221
  * Execute a single test
222
+ * @param options Test execution options
223
+ * @param onProgress Optional callback for progress updates
66
224
  */
67
- async executeTest(options) {
225
+ async executeTest(options, onProgress) {
68
226
  const startTime = Date.now();
69
227
  const executedAt = new Date().toISOString();
70
228
  logger.debug(`Executing test: ${options.testFile}`);
71
- // Wait for Docker image to be ready
72
- await this.imageReady;
229
+ // Check and ensure Docker image is ready with progress reporting
230
+ await onProgress?.({
231
+ phase: "docker-check",
232
+ message: "Checking Docker image availability...",
233
+ percent: 5,
234
+ });
235
+ const imageResult = await this.ensureDockerImage(onProgress);
236
+ if (imageResult.cached) {
237
+ await onProgress?.({
238
+ phase: "docker-check",
239
+ message: "Using cached Docker image",
240
+ percent: 20,
241
+ details: { cached: true },
242
+ });
243
+ }
244
+ else {
245
+ await onProgress?.({
246
+ phase: "docker-pull",
247
+ message: "Docker image pull completed",
248
+ percent: 20,
249
+ details: { cached: false },
250
+ });
251
+ }
252
+ // Report preparing phase
253
+ await onProgress?.({
254
+ phase: "preparing",
255
+ message: "Validating workspace and test file...",
256
+ percent: 25,
257
+ });
73
258
  // Validate workspace path - use accessSync for better validation
74
259
  const workspacePath = path.resolve(options.workspacePath);
75
260
  try {
@@ -86,6 +271,11 @@ export class TestExecutionService {
86
271
  if (!fs.existsSync(options.testFile)) {
87
272
  throw new Error(`Test file does not exist: ${options.testFile}`);
88
273
  }
274
+ await onProgress?.({
275
+ phase: "preparing",
276
+ message: "Preparing Docker container configuration...",
277
+ percent: 30,
278
+ });
89
279
  const containerMountPath = "/home/user";
90
280
  const dockerSocketPath = "/var/run/docker.sock";
91
281
  // Calculate relative test file path
@@ -93,7 +283,7 @@ export class TestExecutionService {
93
283
  testFilePath = path.resolve(containerMountPath, testFilePath);
94
284
  // Prepare Docker command
95
285
  const command = [
96
- "./root/runner.sh",
286
+ "/root/runner.sh",
97
287
  options.language,
98
288
  testFilePath,
99
289
  options.testType,
@@ -117,6 +307,44 @@ export class TestExecutionService {
117
307
  Target: path.join(containerMountPath, file),
118
308
  Source: path.join(workspacePath, file),
119
309
  })));
310
+ // Detect and mount session files
311
+ const sessionFiles = detectSessionFiles(options.testFile);
312
+ const mountedPaths = new Set(); // Track mounted file paths to prevent duplicates
313
+ for (const sessionFile of sessionFiles) {
314
+ let sessionFileSource;
315
+ let sessionFileTarget;
316
+ if (path.isAbsolute(sessionFile)) {
317
+ // Absolute path: mount at the same absolute path in container
318
+ sessionFileSource = sessionFile;
319
+ sessionFileTarget = sessionFile;
320
+ }
321
+ else {
322
+ // Relative path: resolve from test file directory on host
323
+ const testFileDir = path.dirname(options.testFile);
324
+ sessionFileSource = path.resolve(testFileDir, sessionFile);
325
+ // Mount at the corresponding relative path in the container
326
+ const testFileDirInContainer = path.dirname(testFilePath);
327
+ sessionFileTarget = path.resolve(testFileDirInContainer, sessionFile);
328
+ }
329
+ // Check if session file exists on host
330
+ if (fs.existsSync(sessionFileSource)) {
331
+ // Only mount if we haven't already mounted this path
332
+ if (!mountedPaths.has(sessionFileTarget)) {
333
+ logger.info(` docker run -v ${sessionFileSource}:${sessionFileTarget}:ro ...`);
334
+ hostConfig.Mounts?.push({
335
+ Type: "bind",
336
+ Target: sessionFileTarget,
337
+ Source: sessionFileSource,
338
+ ReadOnly: true,
339
+ });
340
+ mountedPaths.add(sessionFileTarget);
341
+ }
342
+ }
343
+ else {
344
+ logger.error(`✗ Session file not found: ${sessionFileSource}`);
345
+ logger.error(` Referenced in test as: ${sessionFile}`);
346
+ }
347
+ }
120
348
  // Prepare environment variables
121
349
  const env = [
122
350
  `SKYRAMP_TEST_TOKEN=${options.token || ""}`,
@@ -138,13 +366,40 @@ export class TestExecutionService {
138
366
  }
139
367
  const stream = new DockerStream();
140
368
  try {
369
+ // Report executing phase
370
+ await onProgress?.({
371
+ phase: "executing",
372
+ message: "Starting test execution in Docker container...",
373
+ percent: 40,
374
+ });
141
375
  let statusCode = 0;
142
376
  let containerRef = null;
377
+ // Start periodic progress reporter during execution
378
+ // Runs in parallel with docker.run() to keep the AI agent informed
379
+ let progressIntervalHandle;
380
+ if (onProgress) {
381
+ let progressTick = 0;
382
+ progressIntervalHandle = setInterval(() => {
383
+ progressTick++;
384
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
385
+ // Progress moves from 40% to 79% during execution phase
386
+ // Slowly increment to show activity without reaching 80% (reserved for processing)
387
+ const percent = Math.min(40 + progressTick * 2, 79);
388
+ onProgress({
389
+ phase: "executing",
390
+ message: `Test running... (${elapsed}s elapsed)`,
391
+ percent,
392
+ }).catch(() => {
393
+ // Ignore progress notification errors
394
+ });
395
+ }, EXECUTION_PROGRESS_INTERVAL);
396
+ }
143
397
  // Run container with timeout
144
398
  const executionPromise = this.docker
145
399
  .run(EXECUTOR_DOCKER_IMAGE, command, stream, {
146
400
  Env: env,
147
401
  HostConfig: hostConfig,
402
+ WorkingDir: containerMountPath, // Explicitly set working directory
148
403
  })
149
404
  .then(function (data) {
150
405
  const result = data[0];
@@ -174,14 +429,18 @@ export class TestExecutionService {
174
429
  });
175
430
  try {
176
431
  statusCode = await Promise.race([executionPromise, timeoutPromise]);
177
- // Clear the timeout timer if execution completes successfully
432
+ // Clear timers on successful completion
178
433
  if (timeoutHandle)
179
434
  clearTimeout(timeoutHandle);
435
+ if (progressIntervalHandle)
436
+ clearInterval(progressIntervalHandle);
180
437
  }
181
438
  catch (error) {
182
- // Clear the timeout timer on any error
439
+ // Clear all timers on error
183
440
  if (timeoutHandle)
184
441
  clearTimeout(timeoutHandle);
442
+ if (progressIntervalHandle)
443
+ clearInterval(progressIntervalHandle);
185
444
  // Cleanup container on timeout or other errors
186
445
  if (containerRef !== null) {
187
446
  const container = containerRef;
@@ -199,6 +458,12 @@ export class TestExecutionService {
199
458
  }
200
459
  throw error; // Re-throw the original error
201
460
  }
461
+ // Report processing phase
462
+ await onProgress?.({
463
+ phase: "processing",
464
+ message: "Test execution completed, processing results...",
465
+ percent: 80,
466
+ });
202
467
  const duration = Date.now() - startTime;
203
468
  const cleanOutput = stripVTControlCharacters(output);
204
469
  logger.info("Test execution completed", {
@@ -222,6 +487,14 @@ export class TestExecutionService {
222
487
  output: cleanOutput,
223
488
  exitCode: statusCode,
224
489
  };
490
+ // Report completion
491
+ await onProgress?.({
492
+ phase: "processing",
493
+ message: result.passed
494
+ ? "Test passed successfully"
495
+ : "Test execution completed with failures",
496
+ percent: 100,
497
+ });
225
498
  logger.debug(`Test ${result.passed ? "passed" : "failed"}: ${options.testFile}`);
226
499
  return result;
227
500
  }
@@ -243,17 +516,25 @@ export class TestExecutionService {
243
516
  }
244
517
  /**
245
518
  * Ensure Docker image is available
519
+ * @param onProgress Optional callback for progress updates during pull
520
+ * @returns Object indicating whether image was cached or pulled
246
521
  */
247
- async ensureDockerImage() {
522
+ async ensureDockerImage(onProgress) {
248
523
  try {
249
524
  const images = await this.docker.listImages();
250
525
  const imageExists = images.some((img) => (img.RepoTags && img.RepoTags.includes(EXECUTOR_DOCKER_IMAGE)) ||
251
526
  (img.RepoDigests && img.RepoDigests.includes(EXECUTOR_DOCKER_IMAGE)));
252
527
  if (imageExists) {
253
528
  logger.debug(`Docker image ${EXECUTOR_DOCKER_IMAGE} already available`);
254
- return;
529
+ return { cached: true };
255
530
  }
256
531
  logger.info(`Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`);
532
+ await onProgress?.({
533
+ phase: "docker-pull",
534
+ message: `Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`,
535
+ percent: 10,
536
+ details: { cached: false },
537
+ });
257
538
  await new Promise((resolve, reject) => {
258
539
  this.docker.pull(EXECUTOR_DOCKER_IMAGE, { platform: DOCKER_PLATFORM }, (err, stream) => {
259
540
  if (err)
@@ -265,9 +546,39 @@ export class TestExecutionService {
265
546
  return reject(err);
266
547
  logger.info(`Docker image ${EXECUTOR_DOCKER_IMAGE} pulled successfully`);
267
548
  resolve(res);
549
+ },
550
+ // Progress callback for each layer
551
+ (event) => {
552
+ if (event.status && onProgress) {
553
+ const progressMsg = event.progress || event.status;
554
+ const layerId = event.id || "";
555
+ // Calculate approximate progress (10-19% range during pull)
556
+ let percent = 10;
557
+ if (event.progressDetail?.current &&
558
+ event.progressDetail?.total) {
559
+ const layerProgress = event.progressDetail.current / event.progressDetail.total;
560
+ percent = 10 + Math.floor(layerProgress * 9); // 10-19%
561
+ }
562
+ onProgress({
563
+ phase: "docker-pull",
564
+ message: `${event.status}${layerId ? ` [${layerId}]` : ""}: ${progressMsg}`,
565
+ percent,
566
+ details: {
567
+ cached: false,
568
+ pullProgress: {
569
+ current: event.progressDetail?.current,
570
+ total: event.progressDetail?.total,
571
+ layer: layerId,
572
+ },
573
+ },
574
+ }).catch(() => {
575
+ // Ignore progress notification errors
576
+ });
577
+ }
268
578
  });
269
579
  });
270
580
  });
581
+ return { cached: false };
271
582
  }
272
583
  catch (error) {
273
584
  logger.error(`Failed to ensure Docker image: ${error.message}`);
@@ -323,7 +323,8 @@ export class TestHealthService {
323
323
  // If execution data shows failure, escalate
324
324
  if (execution && !execution.passed) {
325
325
  priority = "HIGH";
326
- rationale = "Drift analysis unavailable and test is failing - investigate immediately";
326
+ rationale =
327
+ "Drift analysis unavailable and test is failing - investigate immediately";
327
328
  estimatedWork = "MEDIUM";
328
329
  }
329
330
  }
@@ -331,14 +332,16 @@ export class TestHealthService {
331
332
  // No drift data but test is failing
332
333
  action = "UPDATE";
333
334
  priority = "HIGH";
334
- rationale = "Test is failing but drift analysis unavailable - review test logic and dependencies";
335
+ rationale =
336
+ "Test is failing but drift analysis unavailable - review test logic and dependencies";
335
337
  estimatedWork = "MEDIUM";
336
338
  }
337
339
  else if (execution && execution.passed) {
338
340
  // No drift data but test is passing
339
341
  action = "VERIFY";
340
342
  priority = "LOW";
341
- rationale = "Drift analysis unavailable but test is passing - periodic verification recommended";
343
+ rationale =
344
+ "Drift analysis unavailable but test is passing - periodic verification recommended";
342
345
  estimatedWork = "SMALL";
343
346
  }
344
347
  else {
@@ -566,10 +569,10 @@ export class TestHealthService {
566
569
  }
567
570
  else {
568
571
  // This is a literal path part, escape special regex chars
569
- return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
572
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
570
573
  }
571
574
  })
572
- .join('');
575
+ .join("");
573
576
  // pathRegexString is already escaped, so we can use it directly
574
577
  const pathRegex = new RegExp(pathRegexString);
575
578
  const method = endpoint.method.toUpperCase();
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { SkyrampClient } from "@skyramp/skyramp";
3
3
  import { logger } from "../../utils/logger.js";
4
+ import { AnalyticsService } from "../../services/AnalyticsService.js";
5
+ const TOOL_NAME = "skyramp_login";
4
6
  export function registerLoginTool(server) {
5
- server.registerTool("skyramp_login", {
7
+ server.registerTool(TOOL_NAME, {
6
8
  description: `Login to Skyramp platform
7
9
 
8
10
  Logging into Skyramp provides access to additional platform features and services.
@@ -16,6 +18,7 @@ Logging into Skyramp provides access to additional platform features and service
16
18
  keywords: ["login", "authenticate", "skyramp login"],
17
19
  },
18
20
  }, async (params) => {
21
+ let errorResult;
19
22
  logger.info("Logging in to Skyramp", {
20
23
  prompt: params.prompt,
21
24
  });
@@ -34,7 +37,7 @@ Logging into Skyramp provides access to additional platform features and service
34
37
  }
35
38
  catch (error) {
36
39
  const errorMessage = `Login to Skyramp failed: ${error.message}`;
37
- return {
40
+ errorResult = {
38
41
  content: [
39
42
  {
40
43
  type: "text",
@@ -43,6 +46,13 @@ Logging into Skyramp provides access to additional platform features and service
43
46
  ],
44
47
  isError: true,
45
48
  };
49
+ return errorResult;
50
+ }
51
+ finally {
52
+ const recordParams = {
53
+ prompt: params.prompt,
54
+ };
55
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
46
56
  }
47
57
  });
48
58
  }
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { SkyrampClient } from "@skyramp/skyramp";
3
3
  import { logger } from "../../utils/logger.js";
4
+ import { AnalyticsService } from "../../services/AnalyticsService.js";
5
+ const TOOL_NAME = "skyramp_logout";
4
6
  export function registerLogoutTool(server) {
5
- server.registerTool("skyramp_logout", {
7
+ server.registerTool(TOOL_NAME, {
6
8
  description: `Logout from Skyramp platform
7
9
 
8
10
  Logout from Skyramp platform to end your authenticated session.`,
@@ -15,6 +17,7 @@ Logout from Skyramp platform to end your authenticated session.`,
15
17
  keywords: ["logout", "sign out", "skyramp logout"],
16
18
  },
17
19
  }, async (params) => {
20
+ let errorResult;
18
21
  logger.info("Logging out from Skyramp", {
19
22
  prompt: params.prompt,
20
23
  });
@@ -32,7 +35,7 @@ Logout from Skyramp platform to end your authenticated session.`,
32
35
  }
33
36
  catch (error) {
34
37
  const errorMessage = `Logout from Skyramp failed: ${error.message}`;
35
- return {
38
+ errorResult = {
36
39
  content: [
37
40
  {
38
41
  type: "text",
@@ -41,6 +44,13 @@ Logout from Skyramp platform to end your authenticated session.`,
41
44
  ],
42
45
  isError: true,
43
46
  };
47
+ return errorResult;
48
+ }
49
+ finally {
50
+ const recordParams = {
51
+ prompt: params.prompt,
52
+ };
53
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
44
54
  }
45
55
  });
46
56
  }
@@ -3,6 +3,7 @@ import { logger } from "../../utils/logger.js";
3
3
  import { getCodeReusePrompt } from "../../prompts/code-reuse.js";
4
4
  import { codeRefactoringSchema, languageSchema, } from "../../types/TestTypes.js";
5
5
  import { SKYRAMP_UTILS_HEADER } from "../../utils/utils.js";
6
+ import { AnalyticsService } from "../../services/AnalyticsService.js";
6
7
  const codeReuseSchema = z.object({
7
8
  testFile: z
8
9
  .string()
@@ -14,8 +15,9 @@ const codeReuseSchema = z.object({
14
15
  .string()
15
16
  .describe("The code content to analyze and optimize for code reuse"),
16
17
  });
18
+ const TOOL_NAME = "skyramp_reuse_code";
17
19
  export function registerCodeReuseTool(server) {
18
- server.registerTool("skyramp_reuse_code", {
20
+ server.registerTool(TOOL_NAME, {
19
21
  description: `Analyzes code for reuse opportunities and enforces code reuse principles.
20
22
 
21
23
  This tool helps identify and reuse ONLY EXISTING helper functions from other test files (grep based on "${SKYRAMP_UTILS_HEADER}").
@@ -58,19 +60,43 @@ export function registerCodeReuseTool(server) {
58
60
  ],
59
61
  },
60
62
  }, async (params) => {
61
- logger.info("Analyzing code for reuse opportunities", {
62
- testFile: params.testFile,
63
- language: params.language,
64
- framework: params.framework,
65
- });
66
- const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language);
67
- return {
68
- content: [
69
- {
70
- type: "text",
71
- text: codeReusePrompt,
72
- },
73
- ],
74
- };
63
+ let errorResult;
64
+ try {
65
+ logger.info("Analyzing code for reuse opportunities", {
66
+ testFile: params.testFile,
67
+ language: params.language,
68
+ framework: params.framework,
69
+ });
70
+ const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language);
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: codeReusePrompt,
76
+ },
77
+ ],
78
+ };
79
+ }
80
+ catch (error) {
81
+ const errorMessage = `Code reuse analysis failed: ${error.message}`;
82
+ errorResult = {
83
+ content: [
84
+ {
85
+ type: "text",
86
+ text: errorMessage,
87
+ },
88
+ ],
89
+ isError: true,
90
+ };
91
+ return errorResult;
92
+ }
93
+ finally {
94
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
95
+ prompt: params.prompt,
96
+ testFile: params.testFile,
97
+ language: params.language,
98
+ framework: params.framework,
99
+ });
100
+ }
75
101
  });
76
102
  }