@skyramp/mcp 0.0.45 → 0.0.46

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 (32) hide show
  1. package/build/index.js +48 -18
  2. package/build/services/AnalyticsService.js +86 -0
  3. package/build/services/DriftAnalysisService.js +6 -2
  4. package/build/services/TestExecutionService.js +195 -2
  5. package/build/services/TestHealthService.js +8 -5
  6. package/build/tools/auth/loginTool.js +12 -2
  7. package/build/tools/auth/logoutTool.js +12 -2
  8. package/build/tools/code-refactor/codeReuseTool.js +41 -15
  9. package/build/tools/code-refactor/modularizationTool.js +36 -9
  10. package/build/tools/executeSkyrampTestTool.js +17 -3
  11. package/build/tools/fixErrorTool.js +37 -13
  12. package/build/tools/generate-tests/generateContractRestTool.js +8 -2
  13. package/build/tools/generate-tests/generateE2ERestTool.js +8 -2
  14. package/build/tools/generate-tests/generateFuzzRestTool.js +8 -2
  15. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -2
  16. package/build/tools/generate-tests/generateLoadRestTool.js +8 -2
  17. package/build/tools/generate-tests/generateScenarioRestTool.js +8 -2
  18. package/build/tools/generate-tests/generateSmokeRestTool.js +8 -2
  19. package/build/tools/generate-tests/generateUIRestTool.js +8 -2
  20. package/build/tools/test-maintenance/actionsTool.js +175 -147
  21. package/build/tools/test-maintenance/analyzeTestDriftTool.js +14 -5
  22. package/build/tools/test-maintenance/calculateHealthScoresTool.js +13 -4
  23. package/build/tools/test-maintenance/discoverTestsTool.js +10 -2
  24. package/build/tools/test-maintenance/executeBatchTestsTool.js +14 -4
  25. package/build/tools/test-maintenance/stateCleanupTool.js +11 -3
  26. package/build/tools/test-recommendation/analyzeRepositoryTool.js +11 -2
  27. package/build/tools/test-recommendation/mapTestsTool.js +9 -2
  28. package/build/tools/test-recommendation/recommendTestsTool.js +15 -3
  29. package/build/tools/trace/startTraceCollectionTool.js +17 -4
  30. package/build/tools/trace/stopTraceCollectionTool.js +27 -3
  31. package/build/utils/AnalysisStateManager.js +3 -1
  32. package/package.json +2 -2
package/build/index.js CHANGED
@@ -30,6 +30,7 @@ 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 { AnalyticsService } from "./services/AnalyticsService.js";
33
34
  const server = new McpServer({
34
35
  name: "Skyramp MCP Server",
35
36
  version: "1.0.0",
@@ -71,23 +72,17 @@ const codeQualityTools = [
71
72
  registerCodeReuseTool,
72
73
  ];
73
74
  codeQualityTools.forEach((registerTool) => registerTool(server));
74
- // Register internal/test recommendation tools (controlled by environment variable)
75
- const enableInternalTools = process.env.SKYRAMP_ENABLE_INTERNAL_TOOLS === "true";
76
- if (enableInternalTools) {
77
- logger.info("Internal tools enabled - registering test recommendation tools");
78
- registerAnalyzeRepositoryTool(server);
79
- registerMapTestsTool(server);
80
- registerRecommendTestsTool(server);
81
- registerDiscoverTestsTool(server);
82
- registerAnalyzeTestDriftTool(server);
83
- registerExecuteBatchTestsTool(server);
84
- registerCalculateHealthScoresTool(server);
85
- registerActionsTool(server);
86
- registerStateCleanupTool(server);
87
- }
88
- else {
89
- logger.info("Internal tools disabled. Set SKYRAMP_ENABLE_INTERNAL_TOOLS=true to enable.");
90
- }
75
+ // Register test recommendation tools
76
+ registerAnalyzeRepositoryTool(server);
77
+ registerMapTestsTool(server);
78
+ registerRecommendTestsTool(server);
79
+ // Register test maintenance tools
80
+ registerDiscoverTestsTool(server);
81
+ registerAnalyzeTestDriftTool(server);
82
+ registerExecuteBatchTestsTool(server);
83
+ registerCalculateHealthScoresTool(server);
84
+ registerActionsTool(server);
85
+ registerStateCleanupTool(server);
91
86
  // Register other Skyramp tools
92
87
  const infrastructureTools = [
93
88
  registerLoginTool,
@@ -97,6 +92,35 @@ const infrastructureTools = [
97
92
  registerTraceStopTool,
98
93
  ];
99
94
  infrastructureTools.forEach((registerTool) => registerTool(server));
95
+ // Global error handlers for crash telemetry
96
+ process.on("uncaughtException", async (error) => {
97
+ logger.critical("Uncaught exception - MCP server crashing", {
98
+ error: error.message,
99
+ stack: error.stack,
100
+ });
101
+ try {
102
+ await AnalyticsService.pushServerCrashEvent("uncaughtException", error.message, error.stack);
103
+ }
104
+ catch (telemetryError) {
105
+ logger.error("Failed to send crash telemetry", { telemetryError });
106
+ }
107
+ process.exit(1);
108
+ });
109
+ process.on("unhandledRejection", async (reason, promise) => {
110
+ const errorMessage = reason instanceof Error ? reason.message : String(reason);
111
+ const errorStack = reason instanceof Error ? reason.stack : undefined;
112
+ logger.critical("Unhandled promise rejection - MCP server crashing", {
113
+ error: errorMessage,
114
+ stack: errorStack,
115
+ });
116
+ try {
117
+ await AnalyticsService.pushServerCrashEvent("unhandledRejection", errorMessage, errorStack);
118
+ }
119
+ catch (telemetryError) {
120
+ logger.error("Failed to send crash telemetry", { telemetryError });
121
+ }
122
+ process.exit(1);
123
+ });
100
124
  // Start MCP server
101
125
  async function main() {
102
126
  const transport = new StdioServerTransport();
@@ -117,10 +141,16 @@ async function main() {
117
141
  process.exit(0);
118
142
  });
119
143
  }
120
- main().catch((error) => {
144
+ main().catch(async (error) => {
121
145
  logger.critical("Fatal error in main()", {
122
146
  error: error.message,
123
147
  stack: error.stack,
124
148
  });
149
+ try {
150
+ await AnalyticsService.pushServerCrashEvent("mainFatalError", error.message, error.stack);
151
+ }
152
+ catch (telemetryError) {
153
+ logger.error("Failed to send crash telemetry", { telemetryError });
154
+ }
125
155
  process.exit(1);
126
156
  });
@@ -0,0 +1,86 @@
1
+ import { pushToolEvent } from "@skyramp/skyramp";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ export class AnalyticsService {
6
+ static entryPoint = "mcp";
7
+ static async pushTestGenerationToolEvent(toolName, result, params) {
8
+ const analyticsResult = {};
9
+ analyticsResult["prompt"] = params.prompt;
10
+ analyticsResult["language"] = params.language;
11
+ this.pushMCPToolEvent(toolName, result, analyticsResult);
12
+ }
13
+ static async pushMCPToolEvent(toolName, result, params) {
14
+ // Error message.
15
+ let errorMessage = "";
16
+ if (result && result.isError) {
17
+ for (const content of result?.content ?? []) {
18
+ if ("text" in content && content.text) {
19
+ errorMessage += content.text + ", ";
20
+ }
21
+ }
22
+ if (errorMessage.length > 0) {
23
+ errorMessage = errorMessage.slice(0, -2);
24
+ }
25
+ }
26
+ //add mcp server version from package.json
27
+ params.mcpServerVersion = getMCPPackageVersion();
28
+ console.error("pushToolEvent", {
29
+ entryPoint: this.entryPoint,
30
+ toolName: toolName,
31
+ errorMessage: errorMessage,
32
+ analyticsResult: params,
33
+ });
34
+ // console.error(
35
+ // "tool name ::: ",
36
+ // toolName,
37
+ // "params ::: ",
38
+ // params,
39
+ // "error message ::: ",
40
+ // errorMessage,
41
+ // );
42
+ await pushToolEvent(this.entryPoint, toolName, errorMessage, params);
43
+ }
44
+ /**
45
+ * Track server crash events
46
+ */
47
+ static async pushServerCrashEvent(crashType, errorMessage, errorStack) {
48
+ const params = {
49
+ crashType: crashType,
50
+ errorStack: errorStack || "no stack trace",
51
+ mcpServerVersion: getMCPPackageVersion(),
52
+ };
53
+ console.error("pushServerCrashEvent", {
54
+ entryPoint: this.entryPoint,
55
+ toolName: "mcp_server_crash",
56
+ errorMessage: errorMessage,
57
+ analyticsResult: params,
58
+ });
59
+ await pushToolEvent(this.entryPoint, "mcp_server_crash", errorMessage, params);
60
+ }
61
+ /**
62
+ * Track tool timeout events
63
+ */
64
+ static async pushToolTimeoutEvent(toolName, timeoutMs, params) {
65
+ const errorMessage = `Tool ${toolName} timed out after ${timeoutMs}ms`;
66
+ const timeoutParams = {
67
+ ...params,
68
+ timeoutMs: timeoutMs.toString(),
69
+ mcpServerVersion: getMCPPackageVersion(),
70
+ };
71
+ console.error("pushToolTimeoutEvent", {
72
+ entryPoint: this.entryPoint,
73
+ toolName: toolName,
74
+ errorMessage: errorMessage,
75
+ analyticsResult: timeoutParams,
76
+ });
77
+ await pushToolEvent(this.entryPoint, `${toolName}_timeout`, errorMessage, timeoutParams);
78
+ }
79
+ }
80
+ function getMCPPackageVersion() {
81
+ const __filename = fileURLToPath(import.meta.url);
82
+ const __dirname = path.dirname(__filename);
83
+ const packageJson = fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8");
84
+ const packageJsonData = JSON.parse(packageJson);
85
+ return packageJsonData.version;
86
+ }
@@ -411,8 +411,12 @@ export class EnhancedDriftAnalysisService {
411
411
  }
412
412
  // Check authentication changes
413
413
  // OpenAPI 3.x uses components.securitySchemes, Swagger 2.x uses securityDefinitions
414
- const oldAuth = JSON.stringify(oldParsed.components?.securitySchemes || oldParsed.securityDefinitions || {});
415
- const newAuth = JSON.stringify(newParsed.components?.securitySchemes || newParsed.securityDefinitions || {});
414
+ const oldAuth = JSON.stringify(oldParsed.components?.securitySchemes ||
415
+ oldParsed.securityDefinitions ||
416
+ {});
417
+ const newAuth = JSON.stringify(newParsed.components?.securitySchemes ||
418
+ newParsed.securityDefinitions ||
419
+ {});
416
420
  if (oldAuth !== newAuth) {
417
421
  changes.authenticationChanged = true;
418
422
  changes.authenticationDetails = "Security schemes have been modified";
@@ -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.2.38";
9
+ const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.2.40";
10
10
  const DOCKER_PLATFORM = "linux/amd64";
11
11
  // Files and directories to exclude when mounting workspace to Docker container
12
12
  export const EXCLUDED_MOUNT_ITEMS = [
@@ -14,6 +14,160 @@ export const EXCLUDED_MOUNT_ITEMS = [
14
14
  "package.json",
15
15
  "node_modules",
16
16
  ];
17
+ /**
18
+ * Find the start index of a comment in a line, ignoring comment delimiters inside strings
19
+ * Returns -1 if no comment is found outside of strings
20
+ */
21
+ function findCommentStart(line) {
22
+ let inSingleQuote = false;
23
+ let inDoubleQuote = false;
24
+ let escaped = false;
25
+ for (let i = 0; i < line.length; i++) {
26
+ const char = line[i];
27
+ const nextChar = i + 1 < line.length ? line[i + 1] : '';
28
+ // Handle escape sequences
29
+ if (escaped) {
30
+ escaped = false;
31
+ continue;
32
+ }
33
+ if (char === '\\') {
34
+ escaped = true;
35
+ continue;
36
+ }
37
+ // Track string boundaries
38
+ if (char === "'" && !inDoubleQuote) {
39
+ inSingleQuote = !inSingleQuote;
40
+ continue;
41
+ }
42
+ if (char === '"' && !inSingleQuote) {
43
+ inDoubleQuote = !inDoubleQuote;
44
+ continue;
45
+ }
46
+ // Check for comments only if not in a string
47
+ if (!inSingleQuote && !inDoubleQuote) {
48
+ if (char === '/' && nextChar === '/') {
49
+ return i;
50
+ }
51
+ if (char === '#') {
52
+ return i;
53
+ }
54
+ }
55
+ }
56
+ return -1; // No comment found
57
+ }
58
+ /**
59
+ * Filter out comments from code lines
60
+ * Returns array of non-comment lines with inline comments removed
61
+ */
62
+ function filterComments(lines) {
63
+ const nonCommentLines = [];
64
+ let inMultiLineComment = false;
65
+ let inPythonMultiLineComment = false;
66
+ for (let line of lines) {
67
+ const trimmed = line.trim();
68
+ // Check for Python multi-line string comments (""" or ''')
69
+ // Count occurrences to handle single-line strings like '''comment'''
70
+ const tripleDoubleCount = (trimmed.match(/"""/g) || []).length;
71
+ const tripleSingleCount = (trimmed.match(/'''/g) || []).length;
72
+ if (tripleDoubleCount > 0) {
73
+ // Odd number: opening or closing a multi-line comment (toggle state)
74
+ // Even number: complete string on same line (e.g., """comment""" - don't toggle)
75
+ if (tripleDoubleCount % 2 === 1) {
76
+ inPythonMultiLineComment = !inPythonMultiLineComment;
77
+ }
78
+ continue; // Skip any line with triple quotes
79
+ }
80
+ if (tripleSingleCount > 0) {
81
+ if (tripleSingleCount % 2 === 1) {
82
+ inPythonMultiLineComment = !inPythonMultiLineComment;
83
+ }
84
+ continue;
85
+ }
86
+ if (inPythonMultiLineComment) {
87
+ continue;
88
+ }
89
+ // Check for multi-line comment start/end (/* */)
90
+ if (trimmed.includes('/*')) {
91
+ if (trimmed.includes('*/')) {
92
+ // Single-line multi-line comment (e.g., /* comment */)
93
+ // Don't change state, just skip the line
94
+ continue;
95
+ }
96
+ else {
97
+ // Opening a multi-line comment
98
+ inMultiLineComment = true;
99
+ continue;
100
+ }
101
+ }
102
+ if (inMultiLineComment) {
103
+ if (trimmed.includes('*/')) {
104
+ inMultiLineComment = false;
105
+ }
106
+ continue;
107
+ }
108
+ // Skip single-line comments
109
+ if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
110
+ continue;
111
+ }
112
+ // Remove inline comments from the line before processing
113
+ // Use helper function to avoid removing comment delimiters inside strings
114
+ const commentIndex = findCommentStart(line);
115
+ if (commentIndex >= 0) {
116
+ line = line.substring(0, commentIndex);
117
+ }
118
+ nonCommentLines.push(line);
119
+ }
120
+ return nonCommentLines;
121
+ }
122
+ /**
123
+ * Detect session file paths referenced in test files
124
+ * Looks for storageState patterns in TypeScript/JavaScript/Python/Java/C# test files
125
+ * Excludes matches found in comments
126
+ */
127
+ function detectSessionFiles(testFilePath) {
128
+ try {
129
+ const content = fs.readFileSync(testFilePath, "utf-8");
130
+ const lines = content.split('\n');
131
+ const sessionFiles = [];
132
+ // Pattern for TypeScript/JavaScript: storageState: '/path/to/file' or storageState: "/path/to/file"
133
+ const tsJsPattern = /storageState:\s*['"]([^'"]+)['"]/g;
134
+ // Pattern for Python: storage_state='/path/to/file' or storage_state="/path/to/file"
135
+ const pythonPattern = /storage_state\s*=\s*['"]([^'"]+)['"]/g;
136
+ // Pattern for Java: setStorageState(Paths.get("path")) or setStorageState("path")
137
+ // Enforces proper parenthesis matching: first ) is required, second ) is required when using Paths.get
138
+ const javaPattern = /setStorageState(?:Path)?\s*\(\s*(?:Paths\.get\s*\(\s*)?['"]([^'"]+)['"]\s*\)(?:\s*\))?/g;
139
+ // Pattern for C#: StorageStatePath = "path" or StorageState = "path"
140
+ const csharpPattern = /StorageState(?:Path)?\s*=\s*['"]([^'"]+)['"]/g;
141
+ // Filter out comments
142
+ const codeLines = filterComments(lines);
143
+ // Process each non-comment line
144
+ for (const line of codeLines) {
145
+ // Try all patterns on this line
146
+ let match;
147
+ tsJsPattern.lastIndex = 0;
148
+ while ((match = tsJsPattern.exec(line)) !== null) {
149
+ sessionFiles.push(match[1]);
150
+ }
151
+ pythonPattern.lastIndex = 0;
152
+ while ((match = pythonPattern.exec(line)) !== null) {
153
+ sessionFiles.push(match[1]);
154
+ }
155
+ javaPattern.lastIndex = 0;
156
+ while ((match = javaPattern.exec(line)) !== null) {
157
+ sessionFiles.push(match[1]);
158
+ }
159
+ csharpPattern.lastIndex = 0;
160
+ while ((match = csharpPattern.exec(line)) !== null) {
161
+ sessionFiles.push(match[1]);
162
+ }
163
+ }
164
+ return sessionFiles;
165
+ }
166
+ catch (error) {
167
+ logger.error(`Failed to detect session files in ${testFilePath}`, { error });
168
+ return [];
169
+ }
170
+ }
17
171
  export class TestExecutionService {
18
172
  docker;
19
173
  imageReady;
@@ -93,7 +247,7 @@ export class TestExecutionService {
93
247
  testFilePath = path.resolve(containerMountPath, testFilePath);
94
248
  // Prepare Docker command
95
249
  const command = [
96
- "./root/runner.sh",
250
+ "/root/runner.sh",
97
251
  options.language,
98
252
  testFilePath,
99
253
  options.testType,
@@ -117,6 +271,44 @@ export class TestExecutionService {
117
271
  Target: path.join(containerMountPath, file),
118
272
  Source: path.join(workspacePath, file),
119
273
  })));
274
+ // Detect and mount session files
275
+ const sessionFiles = detectSessionFiles(options.testFile);
276
+ const mountedPaths = new Set(); // Track mounted file paths to prevent duplicates
277
+ for (const sessionFile of sessionFiles) {
278
+ let sessionFileSource;
279
+ let sessionFileTarget;
280
+ if (path.isAbsolute(sessionFile)) {
281
+ // Absolute path: mount at the same absolute path in container
282
+ sessionFileSource = sessionFile;
283
+ sessionFileTarget = sessionFile;
284
+ }
285
+ else {
286
+ // Relative path: resolve from test file directory on host
287
+ const testFileDir = path.dirname(options.testFile);
288
+ sessionFileSource = path.resolve(testFileDir, sessionFile);
289
+ // Mount at the corresponding relative path in the container
290
+ const testFileDirInContainer = path.dirname(testFilePath);
291
+ sessionFileTarget = path.resolve(testFileDirInContainer, sessionFile);
292
+ }
293
+ // Check if session file exists on host
294
+ if (fs.existsSync(sessionFileSource)) {
295
+ // Only mount if we haven't already mounted this path
296
+ if (!mountedPaths.has(sessionFileTarget)) {
297
+ logger.info(` docker run -v ${sessionFileSource}:${sessionFileTarget}:ro ...`);
298
+ hostConfig.Mounts?.push({
299
+ Type: "bind",
300
+ Target: sessionFileTarget,
301
+ Source: sessionFileSource,
302
+ ReadOnly: true,
303
+ });
304
+ mountedPaths.add(sessionFileTarget);
305
+ }
306
+ }
307
+ else {
308
+ logger.error(`✗ Session file not found: ${sessionFileSource}`);
309
+ logger.error(` Referenced in test as: ${sessionFile}`);
310
+ }
311
+ }
120
312
  // Prepare environment variables
121
313
  const env = [
122
314
  `SKYRAMP_TEST_TOKEN=${options.token || ""}`,
@@ -145,6 +337,7 @@ export class TestExecutionService {
145
337
  .run(EXECUTOR_DOCKER_IMAGE, command, stream, {
146
338
  Env: env,
147
339
  HostConfig: hostConfig,
340
+ WorkingDir: containerMountPath, // Explicitly set working directory
148
341
  })
149
342
  .then(function (data) {
150
343
  const result = data[0];
@@ -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
  }