kickload-watcher-mcp 0.1.4 → 0.1.6

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.
@@ -16,7 +16,7 @@ import fs from "fs";
16
16
  import path from "path";
17
17
 
18
18
  const POLL_INTERVAL_MS = 5000; // 5s between polls — matches backend
19
- const POLL_TIMEOUT_MS = 1800000; // 30 min
19
+ const POLL_TIMEOUT_MS = 600000; // 10 minutes max
20
20
 
21
21
  export class KickloadClient {
22
22
  /**
@@ -59,16 +59,20 @@ export class KickloadClient {
59
59
  }
60
60
 
61
61
  if (!response.ok) {
62
- const err = new Error(
63
- `Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}\n` +
64
- `URL: ${url}\n` +
65
- `Response: ${text.substring(0, 500)}`
66
- );
67
- err.status = response.status;
68
- err.body = text;
69
- err.parsed = parsed;
70
- throw err;
71
- }
62
+ if (response.status === 401 || response.status === 403) {
63
+ throw new Error(
64
+ "Kickload authentication failed — check your API token or subscription"
65
+ );
66
+ }
67
+
68
+ if (response.status === 504) {
69
+ throw new Error("Kickload server timeout (504)");
70
+ }
71
+
72
+ throw new Error(
73
+ `Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}`
74
+ );
75
+ }
72
76
 
73
77
  return parsed;
74
78
  }
@@ -82,19 +86,34 @@ export class KickloadClient {
82
86
  // Returns: { status: "success", jmx_filename: "test_plan_xxx.jmx" }
83
87
  // ══════════════════════════════════════════════════════════════
84
88
  async generateTestPlan({ prompt, jmxFilePath } = {}) {
85
- if (!prompt && !jmxFilePath) {
86
- throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
87
- }
89
+ if (!prompt && !jmxFilePath) {
90
+ throw new Error("generateTestPlan: provide at least a prompt or jmxFilePath");
91
+ }
92
+
93
+ const form = new FormData();
94
+
95
+ if (prompt) {
96
+ form.append("prompt", prompt);
97
+ }
98
+
99
+ if (jmxFilePath) {
100
+ const resolved = path.resolve(jmxFilePath);
101
+ if (!fs.existsSync(resolved)) {
102
+ throw new Error(`JMX file not found: ${resolved}`);
103
+ }
104
+ form.append("file", fs.createReadStream(resolved), {
105
+ filename: path.basename(resolved),
106
+ contentType: "application/xml",
107
+ });
108
+ }
88
109
 
89
- console.log("\n📋 STEP 1: Generating test plan...");
90
- if (prompt) console.log(` Prompt : ${prompt.substring(0, 100)}`);
91
- if (jmxFilePath) console.log(` JMX file: ${jmxFilePath}`);
110
+ console.log("\n📋 STEP 1: Generating test plan...");
111
+ if (prompt) console.log(` Prompt : ${prompt.substring(0, 100)}`);
112
+ if (jmxFilePath) console.log(` JMX file: ${jmxFilePath}`);
92
113
 
93
- // ✅ FIX: send JSON when only prompt is provided
94
- if (prompt && !jmxFilePath) {
95
114
  const result = await this._fetch("POST", "/generate-test-plan", {
96
- body: JSON.stringify({ prompt }),
97
- isJson: true,
115
+ body: form,
116
+ headers: form.getHeaders(),
98
117
  });
99
118
 
100
119
  if (result.status !== "success") {
@@ -107,38 +126,6 @@ export class KickloadClient {
107
126
  return result;
108
127
  }
109
128
 
110
- // ✅ fallback: use FormData only if file is present
111
- const form = new FormData();
112
-
113
- if (prompt) {
114
- form.append("prompt", prompt);
115
- }
116
-
117
- if (jmxFilePath) {
118
- const resolved = path.resolve(jmxFilePath);
119
- if (!fs.existsSync(resolved)) {
120
- throw new Error(`JMX file not found: ${resolved}`);
121
- }
122
- form.append("file", fs.createReadStream(resolved), {
123
- filename: path.basename(resolved),
124
- contentType: "application/xml",
125
- });
126
- }
127
-
128
- const result = await this._fetch("POST", "/generate-test-plan", {
129
- body: form,
130
- headers: form.getHeaders(),
131
- });
132
-
133
- if (result.status !== "success") {
134
- throw new Error(
135
- `Test plan generation failed: ${result.message || JSON.stringify(result)}`
136
- );
137
- }
138
-
139
- console.log(`✅ STEP 1 done — JMX: ${result.jmx_filename}`);
140
- return result;
141
- }
142
129
  // ══════════════════════════════════════════════════════════════
143
130
  // STEP 2 — Run Test
144
131
  // POST /run-test/{jmx_filename}
@@ -214,13 +201,7 @@ export class KickloadClient {
214
201
  throw new Error(`Task ${taskId} unknown status: ${result.status}`);
215
202
  }
216
203
 
217
- throw new Error(
218
- `Task ${taskId} timed out after ${POLL_TIMEOUT_MS / 60000} minutes.\n` +
219
- `Possible reasons:\n` +
220
- `- Large load test\n` +
221
- `- Slow backend\n` +
222
- `- KickLoad server delay`
223
- );
204
+ throw new Error(`Task ${taskId} timed out after ${POLL_TIMEOUT_MS / 60000} minutes`);
224
205
  }
225
206
 
226
207
  // ══════════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kickload-watcher-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Automated API performance testing for Claude Code teams via KickLoad",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/pipeline.js CHANGED
@@ -226,85 +226,11 @@ export async function runPhase1Pipeline(event) {
226
226
  const client = new KickloadClient(config.kickload.baseUrl, config.kickload.apiToken);
227
227
 
228
228
  logger.step("Generating test plan");
229
+ const { jmx_filename: jmxFilename } = await withRetry(
230
+ () => client.generateTestPlan({ prompt: testParams.testPrompt, jmxFilePath: jmxFilePath || null }),
231
+ "generate-test-plan"
232
+ );
229
233
 
230
- let jmxFilename = null;
231
- let kickloadFailed = false;
232
- let kickloadErrorMsg = "";
233
-
234
- for (let attempt = 1; attempt <= 3; attempt++) {
235
- try {
236
- const res = await client.generateTestPlan({
237
- prompt: testParams.testPrompt,
238
- jmxFilePath: jmxFilePath || null
239
- });
240
-
241
- jmxFilename = res.jmx_filename;
242
- break;
243
-
244
- } catch (err) {
245
- kickloadErrorMsg = err.message || "unknown error";
246
-
247
- const status =
248
- err.status ||
249
- err.response?.status ||
250
- (err.message?.includes("500") ? 500 : 0);
251
-
252
- // retry only for 503 / 5xx
253
- if (status >= 500 && attempt < 3) {
254
- logger.warn(`Kickload attempt ${attempt}/3 failed — retrying in 3s (${kickloadErrorMsg})`);
255
- await sleep(3000);
256
- } else {
257
- kickloadFailed = true;
258
- logger.error(`Kickload failed after retries: ${kickloadErrorMsg}`);
259
- break;
260
- }
261
- }
262
- }
263
- if (kickloadFailed || !jmxFilename) {
264
- logger.warn("⚠️ Kickload failed — continuing with fallback result");
265
- logger.info(`Fallback reason: ${kickloadErrorMsg}`);
266
-
267
- // fake result so pipeline completes fully
268
- const totalMs = Date.now() - start;
269
-
270
- const summary = {
271
- passed: false,
272
- averageLatency: null,
273
- errorPercentage: 100,
274
- throughput: null,
275
- note: "Test plan generation failed — fallback result generated"
276
- };
277
-
278
- logPipelineSummary({
279
- endpoint: endpoints.join(", "),
280
- passed: summary.passed,
281
- latency: summary.averageLatency,
282
- errorRate: summary.errorPercentage,
283
- throughput: summary.throughput,
284
- durationSec: Math.round(totalMs / 1000),
285
- emailSent: !!devEmail,
286
- devEmail,
287
- });
288
-
289
- // send email
290
- if (devEmail) {
291
- await safeResultEmail({
292
- toEmail: devEmail,
293
- endpoint: endpoints.join(", "),
294
- passed: false,
295
- summary,
296
- downloadUrl: null,
297
- pdfFilename: null,
298
- durationMs: totalMs,
299
- testPrompt: testParams.testPrompt,
300
- });
301
- }
302
-
303
- return {
304
- success: true, // 👈 IMPORTANT: pipeline completed
305
- fallback: true
306
- };
307
- }
308
234
  logger.step("Running test");
309
235
  const { task_id: taskId } = await withRetry(
310
236
  () => client.runTest(jmxFilename, { numThreads: testParams.numThreads, loopCount: testParams.loopCount, rampTime: testParams.rampTime }),
@@ -352,7 +278,19 @@ return {
352
278
  return { success: true, backendUrl, jmxFilename, taskId, jtlFilename: taskResult.result_file, pdfFilename, downloadUrl, summary };
353
279
 
354
280
  } catch (err) {
355
- logger.error(`Pipeline failed: ${err.message}`);
281
+ if (err.message.includes("504") || err.message.includes("Gateway")) {
282
+ logger.error("❌ Kickload server is currently unavailable (504 timeout)");
283
+ logger.error(" This is a server-side issue, not your API");
284
+ logger.error(" Try again after some time\n");
285
+ } else if (err.message.includes("401") || err.message.includes("403")) {
286
+ logger.error("❌ Kickload authentication failed");
287
+ logger.error(" Possible reasons:");
288
+ logger.error(" • Invalid API token");
289
+ logger.error(" • Token expired");
290
+ logger.error(" • Subscription required\n");
291
+ } else {
292
+ logger.error(`Pipeline failed: ${err.message}`);
293
+ }
356
294
 
357
295
  if (devEmail) {
358
296
  await safeResultEmail({
@@ -488,10 +426,7 @@ async function withRetry(fn, label) {
488
426
  return await fn();
489
427
  } catch (err) {
490
428
  lastErr = err;
491
- const status =
492
- err.status ||
493
- err.response?.status ||
494
- (err.message?.includes("500") ? 500 : 0);
429
+ const status = err.status || 0;
495
430
  if (status >= 400 && status < 500 && status !== 429) throw err;
496
431
  if (attempt <= MAX_RETRIES) {
497
432
  logger.warn(`[${label}] attempt ${attempt} failed — retrying in ${RETRY_DELAY_MS / 1000}s (${err.message})`);