@skyramp/mcp 0.0.1
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/README.md +163 -0
- package/build/index.js +55 -0
- package/build/prompts/startTraceCollectionPrompts.js +54 -0
- package/build/prompts/stopTraceCollectionPrompts.js +113 -0
- package/build/prompts/testGenerationPrompt.js +374 -0
- package/build/services/TestGenerationService.js +138 -0
- package/build/tools/executeSkyrampTestTool.js +107 -0
- package/build/tools/generateContractRestTool.js +34 -0
- package/build/tools/generateE2ERestTool.js +34 -0
- package/build/tools/generateFuzzRestTool.js +32 -0
- package/build/tools/generateIntegrationRestTool.js +29 -0
- package/build/tools/generateLoadRestTool.js +61 -0
- package/build/tools/generateSmokeRestTool.js +23 -0
- package/build/tools/generateUIRestTool.js +37 -0
- package/build/tools/startTraceCollectionTool.js +79 -0
- package/build/tools/stopTraceCollectionTool.js +86 -0
- package/build/types/TestTypes.js +101 -0
- package/build/utils/analyze-openapi.js +25 -0
- package/build/utils/logger.js +38 -0
- package/build/utils/proxy-terminal.js +439 -0
- package/build/utils/utils.js +118 -0
- package/build/utils/utils.test.js +32 -0
- package/package.json +61 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { spawn, exec } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
// Mapping of environment variables
|
|
9
|
+
const shellEnvVariables = {
|
|
10
|
+
http_proxy: "http://localhost:8888",
|
|
11
|
+
HTTPS_PROXY: "http://localhost:8888",
|
|
12
|
+
HTTP_PROXY: "http://localhost:8888",
|
|
13
|
+
https_proxy: "http://localhost:8888",
|
|
14
|
+
no_proxy: "",
|
|
15
|
+
NO_PROXY: "",
|
|
16
|
+
};
|
|
17
|
+
let terminalProcess;
|
|
18
|
+
let terminalPID;
|
|
19
|
+
function getTerminalConfig() {
|
|
20
|
+
const platform = os.platform();
|
|
21
|
+
switch (platform) {
|
|
22
|
+
case "win32":
|
|
23
|
+
return {
|
|
24
|
+
command: "cmd",
|
|
25
|
+
args: ["/c", "start", "cmd", "/k"],
|
|
26
|
+
scriptExtension: ".bat",
|
|
27
|
+
scriptHeader: "@echo off",
|
|
28
|
+
pidCommand: "echo %$",
|
|
29
|
+
};
|
|
30
|
+
case "darwin":
|
|
31
|
+
return {
|
|
32
|
+
command: "open",
|
|
33
|
+
args: ["-a", "Terminal"],
|
|
34
|
+
scriptExtension: ".sh",
|
|
35
|
+
scriptHeader: "#!/bin/bash",
|
|
36
|
+
pidCommand: "echo $$",
|
|
37
|
+
};
|
|
38
|
+
case "linux":
|
|
39
|
+
default:
|
|
40
|
+
// Try common Linux terminals in order of preference
|
|
41
|
+
const linuxTerminals = [
|
|
42
|
+
"gnome-terminal",
|
|
43
|
+
"konsole",
|
|
44
|
+
"xfce4-terminal",
|
|
45
|
+
"xterm",
|
|
46
|
+
"terminator",
|
|
47
|
+
"alacritty",
|
|
48
|
+
];
|
|
49
|
+
// For now, default to gnome-terminal, but this could be enhanced
|
|
50
|
+
// to detect which terminal is available
|
|
51
|
+
return {
|
|
52
|
+
command: "gnome-terminal",
|
|
53
|
+
args: ["--"],
|
|
54
|
+
scriptExtension: ".sh",
|
|
55
|
+
scriptHeader: "#!/bin/bash",
|
|
56
|
+
pidCommand: "echo $$",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function createEnvironmentScript(config) {
|
|
61
|
+
const tempDir = os.tmpdir();
|
|
62
|
+
const scriptPath = path.join(tempDir, `skyramp-launch${config.scriptExtension}`);
|
|
63
|
+
const pidFile = path.join(tempDir, "skyramp-terminal.pid");
|
|
64
|
+
const infoMessage = os.platform() === "win32"
|
|
65
|
+
? "A new terminal has been spawned here by Skyramp to enable trace collection.\\nAsk skyramp agent to stop trace to terminate trace collection and return to where you were.\\nFor more information, visit skyramp.dev/docs."
|
|
66
|
+
: "\\033[33mA new terminal has been spawned here by Skyramp to enable trace collection.\\nAsk skyramp agent to stop trace to terminate trace collection and return to where you were.\\nFor more information, visit skyramp.dev/docs.\\033[0m";
|
|
67
|
+
let scriptContent;
|
|
68
|
+
if (os.platform() === "win32") {
|
|
69
|
+
// Windows batch script
|
|
70
|
+
const setStatements = Object.entries(shellEnvVariables)
|
|
71
|
+
.map(([key, value]) => `set ${key}=${value}`)
|
|
72
|
+
.join("\n");
|
|
73
|
+
scriptContent = `${config.scriptHeader}
|
|
74
|
+
${setStatements}
|
|
75
|
+
echo %^%^% > "${pidFile}"
|
|
76
|
+
echo ${infoMessage}
|
|
77
|
+
cmd /k`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Unix-like script (macOS/Linux)
|
|
81
|
+
const exportStatements = Object.entries(shellEnvVariables)
|
|
82
|
+
.map(([key, value]) => `export ${key}="${value}"`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
scriptContent = `${config.scriptHeader}
|
|
85
|
+
${exportStatements}
|
|
86
|
+
${config.pidCommand} > "${pidFile}"
|
|
87
|
+
echo -e "${infoMessage}"
|
|
88
|
+
exec $SHELL`;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// Remove existing script if it exists
|
|
92
|
+
if (fs.existsSync(scriptPath)) {
|
|
93
|
+
try {
|
|
94
|
+
fs.unlinkSync(scriptPath);
|
|
95
|
+
logger.debug("Removed existing script file", { scriptPath });
|
|
96
|
+
}
|
|
97
|
+
catch (removeError) {
|
|
98
|
+
logger.warning("Could not remove existing script file", {
|
|
99
|
+
scriptPath,
|
|
100
|
+
error: removeError instanceof Error
|
|
101
|
+
? removeError.message
|
|
102
|
+
: String(removeError),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Create the script
|
|
107
|
+
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
108
|
+
// Verify the script was created successfully
|
|
109
|
+
if (!fs.existsSync(scriptPath)) {
|
|
110
|
+
throw new Error("Script file was not created successfully");
|
|
111
|
+
}
|
|
112
|
+
// Check if the script is readable and executable
|
|
113
|
+
try {
|
|
114
|
+
fs.accessSync(scriptPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
115
|
+
}
|
|
116
|
+
catch (accessError) {
|
|
117
|
+
logger.warning("Script may not have correct permissions", {
|
|
118
|
+
scriptPath,
|
|
119
|
+
error: accessError instanceof Error
|
|
120
|
+
? accessError.message
|
|
121
|
+
: String(accessError),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
logger.debug("Cross-platform script created and verified", {
|
|
125
|
+
platform: os.platform(),
|
|
126
|
+
scriptPath,
|
|
127
|
+
scriptSize: fs.statSync(scriptPath).size,
|
|
128
|
+
});
|
|
129
|
+
return scriptPath;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
logger.error("Failed to create terminal script", {
|
|
133
|
+
platform: os.platform(),
|
|
134
|
+
scriptPath,
|
|
135
|
+
tempDir,
|
|
136
|
+
error: error instanceof Error ? error.message : String(error),
|
|
137
|
+
});
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function findAvailableTerminal() {
|
|
142
|
+
if (os.platform() !== "linux") {
|
|
143
|
+
return null; // Use default for non-Linux platforms
|
|
144
|
+
}
|
|
145
|
+
const terminals = [
|
|
146
|
+
"gnome-terminal",
|
|
147
|
+
"konsole",
|
|
148
|
+
"xfce4-terminal",
|
|
149
|
+
"xterm",
|
|
150
|
+
"terminator",
|
|
151
|
+
"alacritty",
|
|
152
|
+
"kitty",
|
|
153
|
+
];
|
|
154
|
+
for (const terminal of terminals) {
|
|
155
|
+
try {
|
|
156
|
+
await execAsync(`which ${terminal}`);
|
|
157
|
+
logger.debug("Found available terminal", { terminal });
|
|
158
|
+
return terminal;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Terminal not found, try next
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
logger.warning("No common Linux terminal found, falling back to xterm");
|
|
165
|
+
return "xterm";
|
|
166
|
+
}
|
|
167
|
+
export default async function openProxyTerminal() {
|
|
168
|
+
logger.info("Starting cross-platform proxy terminal creation", {
|
|
169
|
+
platform: os.platform(),
|
|
170
|
+
arch: os.arch(),
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
const config = getTerminalConfig();
|
|
174
|
+
// For Linux, try to find an available terminal
|
|
175
|
+
if (os.platform() === "linux") {
|
|
176
|
+
const availableTerminal = await findAvailableTerminal();
|
|
177
|
+
if (availableTerminal && availableTerminal !== "gnome-terminal") {
|
|
178
|
+
config.command = availableTerminal;
|
|
179
|
+
// Adjust args based on terminal
|
|
180
|
+
if (availableTerminal === "xterm") {
|
|
181
|
+
config.args = ["-e"];
|
|
182
|
+
}
|
|
183
|
+
else if (availableTerminal === "konsole") {
|
|
184
|
+
config.args = ["-e"];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const scriptPath = createEnvironmentScript(config);
|
|
189
|
+
const pidFile = path.join(os.tmpdir(), "skyramp-terminal.pid");
|
|
190
|
+
// Verify script exists before launching terminal
|
|
191
|
+
if (!fs.existsSync(scriptPath)) {
|
|
192
|
+
throw new Error(`Script file does not exist: ${scriptPath}`);
|
|
193
|
+
}
|
|
194
|
+
// Verify script is accessible
|
|
195
|
+
try {
|
|
196
|
+
fs.accessSync(scriptPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
197
|
+
}
|
|
198
|
+
catch (accessError) {
|
|
199
|
+
throw new Error(`Script file is not accessible: ${scriptPath} - ${accessError instanceof Error
|
|
200
|
+
? accessError.message
|
|
201
|
+
: String(accessError)}`);
|
|
202
|
+
}
|
|
203
|
+
logger.debug("Script verified before terminal launch", {
|
|
204
|
+
scriptPath,
|
|
205
|
+
scriptSize: fs.statSync(scriptPath).size,
|
|
206
|
+
});
|
|
207
|
+
// Launch terminal with script
|
|
208
|
+
const spawnArgs = [...config.args, scriptPath];
|
|
209
|
+
terminalProcess = spawn(config.command, spawnArgs, {
|
|
210
|
+
env: {
|
|
211
|
+
...process.env,
|
|
212
|
+
...shellEnvVariables,
|
|
213
|
+
},
|
|
214
|
+
detached: true,
|
|
215
|
+
stdio: "ignore",
|
|
216
|
+
});
|
|
217
|
+
logger.debug("Spawned cross-platform terminal", {
|
|
218
|
+
platform: os.platform(),
|
|
219
|
+
command: config.command,
|
|
220
|
+
args: spawnArgs,
|
|
221
|
+
pid: terminalProcess.pid,
|
|
222
|
+
});
|
|
223
|
+
// Handle process events
|
|
224
|
+
terminalProcess.on("exit", (code) => {
|
|
225
|
+
logger.info("Terminal launch command completed", {
|
|
226
|
+
platform: os.platform(),
|
|
227
|
+
exitCode: code,
|
|
228
|
+
});
|
|
229
|
+
// Wait for terminal to write PID and execute script
|
|
230
|
+
// On macOS, Terminal.app takes longer to start and execute the script
|
|
231
|
+
const cleanupDelay = os.platform() === "darwin" ? 10000 : 3000;
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
try {
|
|
234
|
+
if (fs.existsSync(pidFile)) {
|
|
235
|
+
const pidContent = fs.readFileSync(pidFile, "utf8").trim();
|
|
236
|
+
const pid = parseInt(pidContent);
|
|
237
|
+
if (!isNaN(pid)) {
|
|
238
|
+
terminalPID = pid;
|
|
239
|
+
logger.info("Terminal PID captured", { terminalPID: pid });
|
|
240
|
+
}
|
|
241
|
+
// Cleanup PID file
|
|
242
|
+
try {
|
|
243
|
+
fs.unlinkSync(pidFile);
|
|
244
|
+
}
|
|
245
|
+
catch (cleanupError) {
|
|
246
|
+
logger.debug("PID file cleanup completed with minor issues", {
|
|
247
|
+
error: cleanupError instanceof Error
|
|
248
|
+
? cleanupError.message
|
|
249
|
+
: String(cleanupError),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Cleanup script file with additional delay and verification
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
try {
|
|
256
|
+
if (fs.existsSync(scriptPath)) {
|
|
257
|
+
fs.unlinkSync(scriptPath);
|
|
258
|
+
logger.debug("Script file cleaned up", { scriptPath });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (scriptCleanupError) {
|
|
262
|
+
logger.debug("Script cleanup completed with minor issues", {
|
|
263
|
+
error: scriptCleanupError instanceof Error
|
|
264
|
+
? scriptCleanupError.message
|
|
265
|
+
: String(scriptCleanupError),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}, 2000);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
logger.warning("Could not read terminal PID", {
|
|
272
|
+
error: err instanceof Error ? err.message : String(err),
|
|
273
|
+
});
|
|
274
|
+
// Even if PID reading fails, still cleanup script after delay
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(scriptPath)) {
|
|
278
|
+
fs.unlinkSync(scriptPath);
|
|
279
|
+
logger.debug("Script file cleaned up (fallback)", {
|
|
280
|
+
scriptPath,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (scriptCleanupError) {
|
|
285
|
+
logger.debug("Fallback script cleanup completed with minor issues", {
|
|
286
|
+
error: scriptCleanupError instanceof Error
|
|
287
|
+
? scriptCleanupError.message
|
|
288
|
+
: String(scriptCleanupError),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}, 2000);
|
|
292
|
+
}
|
|
293
|
+
}, cleanupDelay);
|
|
294
|
+
});
|
|
295
|
+
terminalProcess.on("error", (error) => {
|
|
296
|
+
logger.error("Terminal spawn error", {
|
|
297
|
+
platform: os.platform(),
|
|
298
|
+
command: config.command,
|
|
299
|
+
error: error.message,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
logger.info("Cross-platform proxy terminal opened successfully", {
|
|
303
|
+
platform: os.platform(),
|
|
304
|
+
arch: os.arch(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
logger.error("Failed to open proxy terminal", {
|
|
309
|
+
platform: os.platform(),
|
|
310
|
+
error: error instanceof Error ? error.message : String(error),
|
|
311
|
+
});
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export async function closeProxyTerminal() {
|
|
316
|
+
logger.info("Starting cross-platform proxy terminal closure", {
|
|
317
|
+
platform: os.platform(),
|
|
318
|
+
});
|
|
319
|
+
// Kill the spawn process if it exists
|
|
320
|
+
if (terminalProcess) {
|
|
321
|
+
const killResult = terminalProcess.kill();
|
|
322
|
+
logger.debug("Terminal spawn process kill result", { killResult });
|
|
323
|
+
}
|
|
324
|
+
// Handle platform-specific terminal closing
|
|
325
|
+
if (terminalPID) {
|
|
326
|
+
try {
|
|
327
|
+
const platform = os.platform();
|
|
328
|
+
if (platform === "darwin") {
|
|
329
|
+
// macOS: First try to kill the specific PID and its process group
|
|
330
|
+
try {
|
|
331
|
+
// Kill the process group to ensure child processes are also terminated
|
|
332
|
+
process.kill(-terminalPID, "SIGTERM");
|
|
333
|
+
logger.info("macOS terminal process group killed via SIGTERM", {
|
|
334
|
+
terminalPID,
|
|
335
|
+
});
|
|
336
|
+
// Give processes time to terminate gracefully
|
|
337
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
338
|
+
// Check if process still exists and force kill if needed
|
|
339
|
+
try {
|
|
340
|
+
process.kill(terminalPID, 0); // Check if process exists
|
|
341
|
+
process.kill(-terminalPID, "SIGKILL");
|
|
342
|
+
logger.info("macOS terminal process group force killed", {
|
|
343
|
+
terminalPID,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch (killErr) {
|
|
347
|
+
// Process is already dead, which is what we want
|
|
348
|
+
logger.debug("Terminal process already terminated", {
|
|
349
|
+
terminalPID,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (pidErr) {
|
|
354
|
+
logger.debug("PID-based kill failed, trying AppleScript fallback", {
|
|
355
|
+
error: pidErr instanceof Error ? pidErr.message : String(pidErr),
|
|
356
|
+
});
|
|
357
|
+
// Fallback: Use AppleScript to close terminal windows with our proxy environment
|
|
358
|
+
const closeScript = `tell application "Terminal"
|
|
359
|
+
repeat with w in windows
|
|
360
|
+
repeat with t in tabs of w
|
|
361
|
+
try
|
|
362
|
+
-- Send exit command to close the shell in this tab
|
|
363
|
+
do script "exit" in t
|
|
364
|
+
end try
|
|
365
|
+
end repeat
|
|
366
|
+
end repeat
|
|
367
|
+
end tell`;
|
|
368
|
+
await execAsync(`osascript -e '${closeScript.replace(/'/g, "'\\''")}'`);
|
|
369
|
+
logger.info("macOS terminal closed via AppleScript fallback");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (platform === "win32") {
|
|
373
|
+
// Windows: Use taskkill with /T flag to kill process tree
|
|
374
|
+
await execAsync(`taskkill /PID ${terminalPID} /T /F`);
|
|
375
|
+
logger.info("Windows terminal and children closed via taskkill", {
|
|
376
|
+
terminalPID,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Linux: Send SIGTERM to process group
|
|
381
|
+
try {
|
|
382
|
+
process.kill(-terminalPID, "SIGTERM");
|
|
383
|
+
logger.info("Linux terminal process group closed via SIGTERM", {
|
|
384
|
+
terminalPID,
|
|
385
|
+
});
|
|
386
|
+
// Give processes time to terminate gracefully
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
388
|
+
// Check if process still exists and force kill if needed
|
|
389
|
+
try {
|
|
390
|
+
process.kill(terminalPID, 0); // Check if process exists
|
|
391
|
+
process.kill(-terminalPID, "SIGKILL");
|
|
392
|
+
logger.info("Linux terminal process group force killed", {
|
|
393
|
+
terminalPID,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch (killErr) {
|
|
397
|
+
// Process is already dead, which is what we want
|
|
398
|
+
logger.debug("Terminal process already terminated", {
|
|
399
|
+
terminalPID,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (pidErr) {
|
|
404
|
+
// Fallback to regular process kill
|
|
405
|
+
try {
|
|
406
|
+
process.kill(terminalPID, "SIGTERM");
|
|
407
|
+
logger.info("Linux terminal closed via fallback SIGTERM", {
|
|
408
|
+
terminalPID,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (fallbackErr) {
|
|
412
|
+
logger.warning("Could not kill terminal process", {
|
|
413
|
+
terminalPID,
|
|
414
|
+
error: fallbackErr instanceof Error
|
|
415
|
+
? fallbackErr.message
|
|
416
|
+
: String(fallbackErr),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
logger.warning("Error during platform-specific terminal closure", {
|
|
424
|
+
platform: os.platform(),
|
|
425
|
+
terminalPID,
|
|
426
|
+
error: err instanceof Error ? err.message : String(err),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
logger.warning("No terminal PID available for cleanup - terminal may not have been properly tracked");
|
|
432
|
+
}
|
|
433
|
+
// Reset state
|
|
434
|
+
terminalProcess = undefined;
|
|
435
|
+
terminalPID = undefined;
|
|
436
|
+
logger.info("Cross-platform proxy terminal closure completed", {
|
|
437
|
+
platform: os.platform(),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
export const OUTPUT_DIR_FIELD_NAME = "outputDir";
|
|
4
|
+
export const TRACE_OUTPUT_FILE_FIELD_NAME = "traceOutputFile";
|
|
5
|
+
export const PLAYWRIGHT_OUTPUT_FILE_FIELD_NAME = "playwrightOutput";
|
|
6
|
+
export const PATH_PARAMS_FIELD_NAME = "pathParams";
|
|
7
|
+
export const QUERY_PARAMS_FIELD_NAME = "queryParams";
|
|
8
|
+
export const FORM_PARAMS_FIELD_NAME = "formParams";
|
|
9
|
+
export const TELEMETRY_entrypoint_FIELD_NAME = "mcp";
|
|
10
|
+
export function IsValidKeyValueList(input) {
|
|
11
|
+
// Regular expression to match a comma-separated list of key:value pairs
|
|
12
|
+
const regex = /^([a-zA-Z0-9_-]+)(,([a-zA-Z0-9_-]+))*$/;
|
|
13
|
+
return regex.test(input);
|
|
14
|
+
}
|
|
15
|
+
export function getPathParameterValidationError(requiredPathParams) {
|
|
16
|
+
if (requiredPathParams.split(",").length > 1) {
|
|
17
|
+
return (`Please specify values for the following required path parameters: \n` +
|
|
18
|
+
`${requiredPathParams
|
|
19
|
+
.split(",")
|
|
20
|
+
.map((param, index) => `${index + 1}. \`${param.trim()}\``)
|
|
21
|
+
.join("\n")}\n\n`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
return (`Please specify value for the following required path parameter:\n` +
|
|
25
|
+
`${requiredPathParams
|
|
26
|
+
.split(",")
|
|
27
|
+
.map((param, index) => `- \`${param.trim()}\``)
|
|
28
|
+
.join("\n")}\n\n`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function validatePath(dir, fieldName) {
|
|
32
|
+
if (dir !== "" && !path.isAbsolute(dir)) {
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: `Error: Relative path provided for ${fieldName}: "${dir}"
|
|
38
|
+
|
|
39
|
+
**Root Cause:** This tool requires absolute paths, but a relative path was provided.
|
|
40
|
+
|
|
41
|
+
**Solution:** Use the appropriate Skyramp prompt first to resolve relative paths to absolute paths:
|
|
42
|
+
- For trace collection: Use "skyramp_stop_trace_collection_prompt"
|
|
43
|
+
- For test generation: Provide absolute paths directly
|
|
44
|
+
|
|
45
|
+
**How prompts work:**
|
|
46
|
+
1. Prompts take your relative paths + current working directory from your IDE
|
|
47
|
+
2. They resolve relative paths to absolute paths
|
|
48
|
+
3. They provide the exact absolute paths to use with tools
|
|
49
|
+
|
|
50
|
+
**Alternative:** Provide absolute paths directly (e.g., "/Users/username/project/output.json")
|
|
51
|
+
|
|
52
|
+
Current working directory context is required when using relative paths.`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function validateParams(params, fieldName) {
|
|
61
|
+
const trimmedParams = params.trim();
|
|
62
|
+
// Reject JSON-like input
|
|
63
|
+
if (trimmedParams.startsWith("{") && trimmedParams.endsWith("}")) {
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `Error: ${fieldName} must be a comma-separated string in the format key=value or key1=value1,key2=value2 — not a JSON object.`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Allow empty input
|
|
75
|
+
if (!trimmedParams)
|
|
76
|
+
return null;
|
|
77
|
+
// Validate comma-separated key=value pairs
|
|
78
|
+
const pairs = trimmedParams.split(",");
|
|
79
|
+
for (const pair of pairs) {
|
|
80
|
+
const [key, value] = pair.split("=");
|
|
81
|
+
if (!key ||
|
|
82
|
+
value === undefined ||
|
|
83
|
+
key.trim() === "" ||
|
|
84
|
+
value.trim() === "") {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Error: Each entry in ${fieldName} must be in the format key=value. Invalid entry: "${pair}".`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// check if the value is a path and if it a relative throw an error else return path appended with `@`
|
|
99
|
+
export function validateRequestData(value) {
|
|
100
|
+
if (value.includes("@")) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
//if value is valid json string return value
|
|
104
|
+
try {
|
|
105
|
+
JSON.parse(value);
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
logger.warning("Request data is not a valid JSON string", { value });
|
|
110
|
+
}
|
|
111
|
+
const isAbsolute = path.isAbsolute(value);
|
|
112
|
+
logger.debug("Path validation check", { value, isAbsolute });
|
|
113
|
+
//if value is path
|
|
114
|
+
if (isAbsolute) {
|
|
115
|
+
return "@" + value;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import { validateParams } from "./utils.js";
|
|
3
|
+
describe("validateParams", () => {
|
|
4
|
+
it("should return null for valid comma-separated key=value pairs", () => {
|
|
5
|
+
const result = validateParams("foo=bar,baz=qux", "testField");
|
|
6
|
+
expect(result).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
it("should return null for empty string", () => {
|
|
9
|
+
const result = validateParams("", "testField");
|
|
10
|
+
expect(result).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
it("should return error for JSON-like input", () => {
|
|
13
|
+
const result = validateParams('{"foo":"bar"}', "testField");
|
|
14
|
+
expect(result?.isError).toBe(true);
|
|
15
|
+
expect(result?.content?.[0].text).toMatch(/not a JSON object/);
|
|
16
|
+
});
|
|
17
|
+
it("should return error for missing value", () => {
|
|
18
|
+
const result = validateParams("foo=", "testField");
|
|
19
|
+
expect(result?.isError).toBe(true);
|
|
20
|
+
expect(result?.content?.[0].text).toMatch(/key=value/);
|
|
21
|
+
});
|
|
22
|
+
it("should return error for missing key", () => {
|
|
23
|
+
const result = validateParams("=bar", "testField");
|
|
24
|
+
expect(result?.isError).toBe(true);
|
|
25
|
+
expect(result?.content?.[0].text).toMatch(/key=value/);
|
|
26
|
+
});
|
|
27
|
+
it("should return error for missing key and value", () => {
|
|
28
|
+
const result = validateParams("=", "testField");
|
|
29
|
+
expect(result?.isError).toBe(true);
|
|
30
|
+
expect(result?.content?.[0].text).toMatch(/key=value/);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skyramp/mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "build/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp": "./build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
11
|
+
"build:prod": "tsc --sourceMap false && chmod 755 build/index.js",
|
|
12
|
+
"pack": "npm run build:prod && npm pack",
|
|
13
|
+
"start": "node build/index.js",
|
|
14
|
+
"test:startup": "time node build/index.js --help",
|
|
15
|
+
"pretty": "npx prettier --write .",
|
|
16
|
+
"test": "npx jest"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"build/**/*.js",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"author": "Skyramp Team",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"description": "Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"skyramp",
|
|
29
|
+
"testing",
|
|
30
|
+
"test-generation",
|
|
31
|
+
"ai-testing",
|
|
32
|
+
"cursor",
|
|
33
|
+
"claude"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/skyramp/mcp.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/skyramp/mcp#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/skyramp/mcp/issues"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
45
|
+
"@skyramp/skyramp": "^1.2.7",
|
|
46
|
+
"dockerode": "^4.0.6",
|
|
47
|
+
"zod": "^3.25.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/dockerode": "^3.3.39",
|
|
51
|
+
"@types/jest": "^29.5.14",
|
|
52
|
+
"@types/mocha": "^10.0.10",
|
|
53
|
+
"@types/node": "^22.15.19",
|
|
54
|
+
"jest": "^29.7.0",
|
|
55
|
+
"ts-jest": "^29.3.4",
|
|
56
|
+
"typescript": "^5.8.3"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
}
|
|
61
|
+
}
|