@testkase/mcp-server 2.1.0 → 2.2.0
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 +187 -220
- package/build/index.js +394 -71
- package/build/index.js.map +1 -1
- package/build/test-config.js +1 -0
- package/build/test-config.js.map +1 -1
- package/package.json +13 -6
- package/PAT_APIS_INVENTORY.csv +0 -27
package/build/index.js
CHANGED
|
@@ -7,6 +7,12 @@ const config = {
|
|
|
7
7
|
apiBaseUrl: process.env.TESTKASE_API_BASE_URL || "https://apiqa.testkase.com",
|
|
8
8
|
patToken: process.env.TESTKASE_PAT_TOKEN,
|
|
9
9
|
};
|
|
10
|
+
// ─── Frontend URL helper ─────────────────────────────────────────────────────
|
|
11
|
+
function getFrontendBaseUrl() {
|
|
12
|
+
const url = new URL(config.apiBaseUrl);
|
|
13
|
+
url.hostname = url.hostname.replace(/^api\.?/, "");
|
|
14
|
+
return url.origin;
|
|
15
|
+
}
|
|
10
16
|
// ─── Auth helpers (reused from v1) ───────────────────────────────────────────
|
|
11
17
|
function validateAuth() {
|
|
12
18
|
if (!config.patToken) {
|
|
@@ -76,7 +82,15 @@ function handleApiError(error) {
|
|
|
76
82
|
let msg = `API request failed: ${e.message}`;
|
|
77
83
|
if (e.response) {
|
|
78
84
|
msg += `\nStatus: ${e.response.status}`;
|
|
79
|
-
|
|
85
|
+
if (e.response.status === 401) {
|
|
86
|
+
cachedOrganizationId = null;
|
|
87
|
+
}
|
|
88
|
+
if (e.response.data) {
|
|
89
|
+
const d = e.response.data;
|
|
90
|
+
const safeMsg = d.message || d.error || (typeof d === "string" ? d : undefined);
|
|
91
|
+
if (safeMsg)
|
|
92
|
+
msg += `\nDetails: ${String(safeMsg).slice(0, 500)}`;
|
|
93
|
+
}
|
|
80
94
|
}
|
|
81
95
|
return new Error(msg);
|
|
82
96
|
}
|
|
@@ -88,6 +102,88 @@ function jsonResponse(data) {
|
|
|
88
102
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
89
103
|
};
|
|
90
104
|
}
|
|
105
|
+
function safeJsonParse(raw, fieldName) {
|
|
106
|
+
if (typeof raw !== "string")
|
|
107
|
+
return raw;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new Error(`Invalid JSON in ${fieldName}. Ensure the value is valid JSON.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function validateId(id, fieldName) {
|
|
116
|
+
if (!/^[a-zA-Z0-9_\-]+$/.test(id)) {
|
|
117
|
+
throw new Error(`Invalid ${fieldName}: contains disallowed characters`);
|
|
118
|
+
}
|
|
119
|
+
return id;
|
|
120
|
+
}
|
|
121
|
+
function jsonResponseWithHints(data, hints) {
|
|
122
|
+
const json = JSON.stringify(data, null, 2);
|
|
123
|
+
const hintsBlock = hints.length > 0
|
|
124
|
+
? `\n\n---\nHINTS:\n${hints.map((h) => `- ${h}`).join("\n")}`
|
|
125
|
+
: "";
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: json + hintsBlock }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ─── Error enrichment ───────────────────────────────────────────────────────
|
|
131
|
+
const ERROR_HINTS = [
|
|
132
|
+
{ pattern: /Invalid value.*for field/i,
|
|
133
|
+
hint: "RECOVERY: Call get_project_structure(projectId, include='field_options') to discover allowed values for this field, then retry." },
|
|
134
|
+
{ pattern: /must be a number/i,
|
|
135
|
+
hint: "RECOVERY: This field requires a numeric ID. Call get_project_structure(projectId) to find valid IDs." },
|
|
136
|
+
{ pattern: /folder_id/i,
|
|
137
|
+
hint: "RECOVERY: folder_id must be a numeric folder ID. Call get_project_structure(projectId, include='folders') to list folders with their IDs." },
|
|
138
|
+
{ pattern: /status.*(401|unauthorized)/i,
|
|
139
|
+
hint: "RECOVERY: PAT token may be expired. User needs to regenerate from TestKase account settings." },
|
|
140
|
+
{ pattern: /priority/i,
|
|
141
|
+
hint: "RECOVERY: Priority values are project-specific. Call get_project_structure(projectId, include='field_options') to discover allowed values." },
|
|
142
|
+
];
|
|
143
|
+
function enrichErrorMessage(errorMsg) {
|
|
144
|
+
for (const { pattern, hint } of ERROR_HINTS) {
|
|
145
|
+
if (pattern.test(errorMsg)) {
|
|
146
|
+
return `${errorMsg}\n\n${hint}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return errorMsg;
|
|
150
|
+
}
|
|
151
|
+
// ─── Field options cache (5-min TTL) ────────────────────────────────────────
|
|
152
|
+
const fieldOptionsCache = new Map();
|
|
153
|
+
const FIELD_OPTIONS_TTL = 5 * 60 * 1000; // 5 minutes
|
|
154
|
+
async function getFieldOptions(projectId) {
|
|
155
|
+
const cached = fieldOptionsCache.get(projectId);
|
|
156
|
+
if (cached && Date.now() - cached.ts < FIELD_OPTIONS_TTL) {
|
|
157
|
+
return cached.data;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const response = await callApi("get", `projects/fields/options/${projectId}`);
|
|
161
|
+
const options = response?.data || response || [];
|
|
162
|
+
fieldOptionsCache.set(projectId, { data: options, ts: Date.now() });
|
|
163
|
+
return options;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return cached?.data || [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function coerceFieldValue(projectId, fieldName, rawValue) {
|
|
170
|
+
if (rawValue === undefined || rawValue === null)
|
|
171
|
+
return rawValue;
|
|
172
|
+
const options = await getFieldOptions(projectId);
|
|
173
|
+
const fieldDef = options.find((f) => f.fieldName?.toLowerCase() === fieldName.toLowerCase());
|
|
174
|
+
if (!fieldDef?.values || !Array.isArray(fieldDef.values))
|
|
175
|
+
return rawValue;
|
|
176
|
+
const strValue = String(rawValue);
|
|
177
|
+
// Exact match
|
|
178
|
+
if (fieldDef.values.includes(strValue))
|
|
179
|
+
return strValue;
|
|
180
|
+
// Case-insensitive match
|
|
181
|
+
const match = fieldDef.values.find((v) => v.toLowerCase() === strValue.toLowerCase());
|
|
182
|
+
if (match)
|
|
183
|
+
return match;
|
|
184
|
+
// No match — return as-is, let API validate
|
|
185
|
+
return rawValue;
|
|
186
|
+
}
|
|
91
187
|
// ─── Param normalizer (handles camelCase / snake_case) ───────────────────────
|
|
92
188
|
function p(args, ...names) {
|
|
93
189
|
for (const n of names) {
|
|
@@ -115,8 +211,8 @@ const LIST_PROJECTS_TOOL = {
|
|
|
115
211
|
};
|
|
116
212
|
const GET_PROJECT_STRUCTURE_TOOL = {
|
|
117
213
|
name: "get_project_structure",
|
|
118
|
-
description: "Get the structure of a project: folders, labels, and
|
|
119
|
-
"
|
|
214
|
+
description: "Get the structure of a project: folders, labels, team members, and field options (allowed values for priority, status, etc.). " +
|
|
215
|
+
"IMPORTANT: Call this before create/update to discover allowed field values, which are project-specific.",
|
|
120
216
|
inputSchema: {
|
|
121
217
|
type: "object",
|
|
122
218
|
properties: {
|
|
@@ -128,7 +224,7 @@ const GET_PROJECT_STRUCTURE_TOOL = {
|
|
|
128
224
|
},
|
|
129
225
|
include: {
|
|
130
226
|
type: "string",
|
|
131
|
-
description: "Comma-separated list of what to include: folders,labels,members (default: all)",
|
|
227
|
+
description: "Comma-separated list of what to include: folders,labels,members,field_options (default: all)",
|
|
132
228
|
},
|
|
133
229
|
},
|
|
134
230
|
required: ["projectId"],
|
|
@@ -168,8 +264,18 @@ const GET_TESTCASE_TOOL = {
|
|
|
168
264
|
};
|
|
169
265
|
const MANAGE_TESTCASE_TOOL = {
|
|
170
266
|
name: "manage_testcase",
|
|
171
|
-
description: "
|
|
172
|
-
"
|
|
267
|
+
description: "IMPORTANT: Before creating/updating, call get_project_structure(projectId) to discover folder IDs and allowed field values.\n\n" +
|
|
268
|
+
"Create, bulk create, update, or delete test cases.\n\n" +
|
|
269
|
+
"ACTIONS:\n" +
|
|
270
|
+
" create - Create a single test case. Params: title (required), summary, priority, folder_id, labels, test_steps, precondition, status, automated, test_data, expected_result, platform, environment, features, owner_id, custom_fields\n" +
|
|
271
|
+
" create_bulk - Create multiple test cases. Params: testcases (JSON array)\n" +
|
|
272
|
+
" update - Update one field for test case(s). Params: ids, field, value (all required)\n" +
|
|
273
|
+
" delete - Delete test case(s). Params: ids (required)\n\n" +
|
|
274
|
+
"VALID UPDATE FIELDS: title, summary, precondition, priority, status, automated, test_data, expected_result, labels, features, platform, environment, custom_fields, owner_id, folder_id\n\n" +
|
|
275
|
+
"EXAMPLES:\n" +
|
|
276
|
+
" Create: { action: 'create', title: 'Verify login' }\n" +
|
|
277
|
+
" Update: { action: 'update', ids: 'TEST-1', field: 'priority', value: '<value from get_project_structure>' }\n" +
|
|
278
|
+
" Delete: { action: 'delete', ids: 'TEST-1,TEST-2' }",
|
|
173
279
|
inputSchema: {
|
|
174
280
|
type: "object",
|
|
175
281
|
properties: {
|
|
@@ -184,7 +290,7 @@ const MANAGE_TESTCASE_TOOL = {
|
|
|
184
290
|
summary: { type: "string", description: "[create] Description/summary" },
|
|
185
291
|
priority: {
|
|
186
292
|
type: "string",
|
|
187
|
-
description: "[create] Priority
|
|
293
|
+
description: "[create] Priority. Values are project-specific — call get_project_structure(projectId, include='field_options') first to discover allowed values.",
|
|
188
294
|
},
|
|
189
295
|
folder_id: { type: "string", description: "[create] Folder ID to place the test case in" },
|
|
190
296
|
labels: {
|
|
@@ -193,30 +299,57 @@ const MANAGE_TESTCASE_TOOL = {
|
|
|
193
299
|
},
|
|
194
300
|
test_steps: {
|
|
195
301
|
type: "string",
|
|
196
|
-
description: '[create] JSON array of test steps: [{"
|
|
302
|
+
description: '[create] JSON array of test steps: [{"description":"...","test_data":"...","expected_result":"..."}]',
|
|
303
|
+
},
|
|
304
|
+
precondition: { type: "string", description: "[create] Preconditions for the test case" },
|
|
305
|
+
status: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "[create] Status. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
308
|
+
},
|
|
309
|
+
automated: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "[create] Automation status. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
312
|
+
},
|
|
313
|
+
test_data: { type: "string", description: "[create] Test data for the test case" },
|
|
314
|
+
expected_result: { type: "string", description: "[create] Expected result for the test case" },
|
|
315
|
+
platform: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "[create] Platform. Values are project-specific — call get_project_structure(projectId, include='field_options') first.",
|
|
318
|
+
},
|
|
319
|
+
environment: { type: "string", description: "[create] Comma-separated environment values" },
|
|
320
|
+
features: { type: "string", description: "[create] Comma-separated feature/component names" },
|
|
321
|
+
owner_id: { type: "string", description: "[create] Owner user ID" },
|
|
322
|
+
custom_fields: {
|
|
323
|
+
type: "string",
|
|
324
|
+
description: '[create] JSON object of custom field values (e.g. {"custom1": "value", "custom2": ["val1","val2"]})',
|
|
197
325
|
},
|
|
198
326
|
// create_bulk params
|
|
199
327
|
testcases: {
|
|
200
328
|
type: "string",
|
|
201
329
|
description: "[create_bulk] JSON array of test cases, each with title, summary, priority, folder_id, test_steps",
|
|
202
330
|
},
|
|
203
|
-
// update params
|
|
331
|
+
// update/delete params
|
|
204
332
|
ids: {
|
|
205
333
|
type: "string",
|
|
206
|
-
description: "[update/delete]
|
|
334
|
+
description: "[update/delete] Test case IDs, comma-separated (e.g. 'TEST-1' or 'TEST-1,TEST-2')",
|
|
207
335
|
},
|
|
208
336
|
field: {
|
|
209
337
|
type: "string",
|
|
210
|
-
description: "[update] Field
|
|
338
|
+
description: "[update] Field to update. Valid: title, summary, precondition, priority, status, automated, test_data, expected_result, labels, features, platform, environment, custom_fields, owner_id, folder_id. For priority/status/automated/platform/environment, call get_project_structure(include='field_options') to get allowed values.",
|
|
211
339
|
},
|
|
212
|
-
value: { type: "string", description: "[update] New value for
|
|
340
|
+
value: { type: "string", description: "[update] New value. String for most fields. For labels/features/environment: comma-separated. For custom_fields: JSON object (e.g. {\"fieldName\": \"value\"})" },
|
|
213
341
|
},
|
|
214
342
|
required: ["projectId", "action"],
|
|
215
343
|
},
|
|
216
344
|
};
|
|
217
345
|
const MANAGE_FOLDER_TOOL = {
|
|
218
346
|
name: "manage_folder",
|
|
219
|
-
description: "Create, rename, move, or delete folders. Works across all sections (TESTCASE, TEST_CYCLE, TEST_PLAN)
|
|
347
|
+
description: "Create, rename, move, or delete folders. Works across all sections (TESTCASE, TEST_CYCLE, TEST_PLAN).\n\n" +
|
|
348
|
+
"ACTIONS:\n" +
|
|
349
|
+
" create - Create a folder. Params: name (required), section, parentId\n" +
|
|
350
|
+
" rename - Rename a folder. Params: folderId (required), name (required)\n" +
|
|
351
|
+
" move - Move a folder. Params: folderId (required), parentId (null for root)\n" +
|
|
352
|
+
" delete - Delete a folder. Params: folderId (required)",
|
|
220
353
|
inputSchema: {
|
|
221
354
|
type: "object",
|
|
222
355
|
properties: {
|
|
@@ -262,8 +395,17 @@ const SEARCH_TEST_CYCLES_TOOL = {
|
|
|
262
395
|
};
|
|
263
396
|
const MANAGE_TEST_CYCLE_TOOL = {
|
|
264
397
|
name: "manage_test_cycle",
|
|
265
|
-
description: "Manage test cycles: create, update, delete, get details, list/link/unlink/assign test cases
|
|
266
|
-
"
|
|
398
|
+
description: "Manage test cycles: create, update, delete, get details, list/link/unlink/assign test cases.\n\n" +
|
|
399
|
+
"ACTIONS:\n" +
|
|
400
|
+
" create - Create a cycle. Params: title (required), summary, status, planned_start_date, planned_end_date, folder_id\n" +
|
|
401
|
+
" update - Update a cycle. Params: cycleId (required), plus fields to change (title, summary, status, dates, folder_id)\n" +
|
|
402
|
+
" delete - Delete cycle(s). Params: ids (required, comma-separated)\n" +
|
|
403
|
+
" get_details - Get cycle details. Params: cycleId (required)\n" +
|
|
404
|
+
" get_testcases - List test cases in cycle. Params: cycleId (required), search, page, limit\n" +
|
|
405
|
+
" link_testcases - Link test cases to cycle. Params: cycleId (required), testcase_ids (required, comma-separated)\n" +
|
|
406
|
+
" unlink_testcases - Unlink test cases from cycle. Params: cycleId (required), testcase_ids (required, comma-separated)\n" +
|
|
407
|
+
" assign_testcases - Assign test cases to user. Params: cycleId (required), testcase_ids (required), assignee_id (required)\n\n" +
|
|
408
|
+
"VALID STATUSES: open, in_progress, completed",
|
|
267
409
|
inputSchema: {
|
|
268
410
|
type: "object",
|
|
269
411
|
properties: {
|
|
@@ -292,7 +434,7 @@ const MANAGE_TEST_CYCLE_TOOL = {
|
|
|
292
434
|
summary: { type: "string", description: "[create/update] Description" },
|
|
293
435
|
status: {
|
|
294
436
|
type: "string",
|
|
295
|
-
description: "[create/update] Status: open,
|
|
437
|
+
description: "[create/update] Status: open, in_progress, completed",
|
|
296
438
|
},
|
|
297
439
|
planned_start_date: {
|
|
298
440
|
type: "string",
|
|
@@ -332,7 +474,7 @@ const EXECUTE_TESTS_TOOL = {
|
|
|
332
474
|
name: "execute_tests",
|
|
333
475
|
description: "Record test execution results for test cases in a test cycle. " +
|
|
334
476
|
"Pass a single result or an array of results. " +
|
|
335
|
-
"Valid execution statuses: pass, fail, blocked, not_executed
|
|
477
|
+
"Valid execution statuses: pass, fail, blocked, not_executed.",
|
|
336
478
|
inputSchema: {
|
|
337
479
|
type: "object",
|
|
338
480
|
properties: {
|
|
@@ -349,8 +491,17 @@ const EXECUTE_TESTS_TOOL = {
|
|
|
349
491
|
};
|
|
350
492
|
const MANAGE_TEST_PLAN_TOOL = {
|
|
351
493
|
name: "manage_test_plan",
|
|
352
|
-
description: "Manage test plans: create, update, delete, list, get details, link/unlink cycles, get cycles, get test cases
|
|
353
|
-
"
|
|
494
|
+
description: "Manage test plans: create, update, delete, list, get details, link/unlink cycles, get cycles, get test cases.\n\n" +
|
|
495
|
+
"ACTIONS:\n" +
|
|
496
|
+
" create - Create a plan. Params: title (required), summary, status, planned_start_date, planned_end_date, folder_id\n" +
|
|
497
|
+
" update - Update a plan. Params: planId (required), plus fields to change (title, summary, status, dates, folder_id)\n" +
|
|
498
|
+
" delete - Delete plan(s). Params: ids (required, comma-separated)\n" +
|
|
499
|
+
" list - List all plans. Params: search, page, limit\n" +
|
|
500
|
+
" get_details - Get plan details. Params: planId (required)\n" +
|
|
501
|
+
" link_cycles - Link cycles to plan. Params: planId (required), cycle_ids (required, comma-separated)\n" +
|
|
502
|
+
" unlink_cycles - Unlink cycles from plan. Params: planId (required), cycle_ids (required, comma-separated)\n" +
|
|
503
|
+
" get_cycles - List cycles in plan. Params: planId (required), search, page, limit\n" +
|
|
504
|
+
" get_testcases - List test cases in plan. Params: planId (required), search, page, limit",
|
|
354
505
|
inputSchema: {
|
|
355
506
|
type: "object",
|
|
356
507
|
properties: {
|
|
@@ -517,15 +668,18 @@ async function handleListProjects(args) {
|
|
|
517
668
|
limit: p(args, "limit"),
|
|
518
669
|
},
|
|
519
670
|
});
|
|
520
|
-
return
|
|
671
|
+
return jsonResponseWithHints(data, [
|
|
672
|
+
"Next: call get_project_structure(projectId) to see folders, labels, and allowed field values.",
|
|
673
|
+
]);
|
|
521
674
|
}
|
|
522
675
|
// 2. get_project_structure
|
|
523
676
|
async function handleGetProjectStructure(args) {
|
|
524
677
|
const projectId = p(args, "projectId", "project_id");
|
|
525
678
|
if (!projectId)
|
|
526
679
|
throw new Error("projectId is required");
|
|
680
|
+
validateId(projectId, "projectId");
|
|
527
681
|
const section = p(args, "section") || "TESTCASE";
|
|
528
|
-
const includeRaw = p(args, "include") || "folders,labels,members";
|
|
682
|
+
const includeRaw = p(args, "include") || "folders,labels,members,field_options";
|
|
529
683
|
const includes = includeRaw.split(",").map((s) => s.trim().toLowerCase());
|
|
530
684
|
const orgId = await getOrganizationId();
|
|
531
685
|
const result = {};
|
|
@@ -547,14 +701,22 @@ async function handleGetProjectStructure(args) {
|
|
|
547
701
|
result.members = data;
|
|
548
702
|
}));
|
|
549
703
|
}
|
|
704
|
+
if (includes.includes("field_options")) {
|
|
705
|
+
promises.push(callApi("get", `projects/fields/options/${projectId}`).then((data) => {
|
|
706
|
+
result.field_options = data?.data || data;
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
550
709
|
await Promise.all(promises);
|
|
551
|
-
return
|
|
710
|
+
return jsonResponseWithHints(result, [
|
|
711
|
+
"Use the field_options values above when creating/updating test cases.",
|
|
712
|
+
]);
|
|
552
713
|
}
|
|
553
714
|
// 3. search_testcases
|
|
554
715
|
async function handleSearchTestcases(args) {
|
|
555
716
|
const projectId = p(args, "projectId", "project_id");
|
|
556
717
|
if (!projectId)
|
|
557
718
|
throw new Error("projectId is required");
|
|
719
|
+
validateId(projectId, "projectId");
|
|
558
720
|
const data = await callApi("get", `projects/testcases/get-testcase/${projectId}`, {
|
|
559
721
|
query: {
|
|
560
722
|
search: p(args, "search"),
|
|
@@ -565,6 +727,13 @@ async function handleSearchTestcases(args) {
|
|
|
565
727
|
sortOrder: p(args, "sortOrder", "sort_order"),
|
|
566
728
|
},
|
|
567
729
|
});
|
|
730
|
+
const baseUrl = getFrontendBaseUrl();
|
|
731
|
+
if (Array.isArray(data?.data)) {
|
|
732
|
+
data.data.forEach((tc) => {
|
|
733
|
+
if (tc.id)
|
|
734
|
+
tc.url = `${baseUrl}/${projectId}/testcases/${tc.id}`;
|
|
735
|
+
});
|
|
736
|
+
}
|
|
568
737
|
return jsonResponse(data);
|
|
569
738
|
}
|
|
570
739
|
// 4. get_testcase
|
|
@@ -575,7 +744,12 @@ async function handleGetTestcase(args) {
|
|
|
575
744
|
throw new Error("projectId is required");
|
|
576
745
|
if (!testcaseId)
|
|
577
746
|
throw new Error("testcaseId is required");
|
|
747
|
+
validateId(projectId, "projectId");
|
|
748
|
+
validateId(testcaseId, "testcaseId");
|
|
578
749
|
const data = await callApi("get", `projects/testcases/get-testcase-detail/${projectId}/${testcaseId}`);
|
|
750
|
+
if (data?.data?.testcase) {
|
|
751
|
+
data.data.testcase.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${testcaseId}`;
|
|
752
|
+
}
|
|
579
753
|
return jsonResponse(data);
|
|
580
754
|
}
|
|
581
755
|
// 5. manage_testcase
|
|
@@ -586,6 +760,7 @@ async function handleManageTestcase(args) {
|
|
|
586
760
|
throw new Error("projectId is required");
|
|
587
761
|
if (!action)
|
|
588
762
|
throw new Error("action is required");
|
|
763
|
+
validateId(projectId, "projectId");
|
|
589
764
|
const orgId = await getOrganizationId();
|
|
590
765
|
switch (action) {
|
|
591
766
|
case "create": {
|
|
@@ -596,40 +771,94 @@ async function handleManageTestcase(args) {
|
|
|
596
771
|
if (p(args, "summary", "description"))
|
|
597
772
|
testcaseData.summary = p(args, "summary", "description");
|
|
598
773
|
if (p(args, "priority"))
|
|
599
|
-
testcaseData.priority = p(args, "priority");
|
|
600
|
-
|
|
601
|
-
|
|
774
|
+
testcaseData.priority = await coerceFieldValue(projectId, "priority", p(args, "priority"));
|
|
775
|
+
const rawFolderId = p(args, "folder_id", "folderId");
|
|
776
|
+
if (rawFolderId) {
|
|
777
|
+
const numFolderId = Number(rawFolderId);
|
|
778
|
+
testcaseData.folder_id = isNaN(numFolderId) ? rawFolderId : numFolderId;
|
|
779
|
+
}
|
|
602
780
|
const labelsRaw = p(args, "labels");
|
|
603
781
|
if (labelsRaw) {
|
|
604
782
|
testcaseData.labels =
|
|
605
783
|
typeof labelsRaw === "string"
|
|
606
|
-
? labelsRaw.split(",").map((l) => l.trim())
|
|
784
|
+
? labelsRaw.split(",").map((l) => l.trim()).filter(Boolean)
|
|
607
785
|
: labelsRaw;
|
|
608
786
|
}
|
|
787
|
+
if (p(args, "precondition"))
|
|
788
|
+
testcaseData.precondition = p(args, "precondition");
|
|
789
|
+
if (p(args, "status"))
|
|
790
|
+
testcaseData.status = await coerceFieldValue(projectId, "status", p(args, "status"));
|
|
791
|
+
if (p(args, "automated"))
|
|
792
|
+
testcaseData.automated = await coerceFieldValue(projectId, "automated", p(args, "automated"));
|
|
793
|
+
if (p(args, "test_data"))
|
|
794
|
+
testcaseData.test_data = p(args, "test_data");
|
|
795
|
+
if (p(args, "expected_result"))
|
|
796
|
+
testcaseData.expected_result = p(args, "expected_result");
|
|
797
|
+
if (p(args, "platform"))
|
|
798
|
+
testcaseData.platform = await coerceFieldValue(projectId, "platform", p(args, "platform"));
|
|
799
|
+
const featuresRaw = p(args, "features");
|
|
800
|
+
if (featuresRaw) {
|
|
801
|
+
testcaseData.features = typeof featuresRaw === "string"
|
|
802
|
+
? featuresRaw.split(",").map((s) => s.trim())
|
|
803
|
+
: featuresRaw;
|
|
804
|
+
}
|
|
805
|
+
const envRaw = p(args, "environment");
|
|
806
|
+
if (envRaw) {
|
|
807
|
+
testcaseData.environment = typeof envRaw === "string"
|
|
808
|
+
? envRaw.split(",").map((s) => s.trim())
|
|
809
|
+
: envRaw;
|
|
810
|
+
}
|
|
811
|
+
const ownerId = p(args, "owner_id", "ownerId");
|
|
812
|
+
if (ownerId) {
|
|
813
|
+
const num = Number(ownerId);
|
|
814
|
+
testcaseData.owner_id = isNaN(num) ? ownerId : num;
|
|
815
|
+
}
|
|
816
|
+
const customFieldsRaw = p(args, "custom_fields", "customFields");
|
|
817
|
+
if (customFieldsRaw) {
|
|
818
|
+
const parsed = safeJsonParse(customFieldsRaw, "custom_fields");
|
|
819
|
+
for (const key of Object.keys(parsed)) {
|
|
820
|
+
if (!Array.isArray(parsed[key])) {
|
|
821
|
+
parsed[key] = [parsed[key]];
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
testcaseData.custom_fields = parsed;
|
|
825
|
+
}
|
|
609
826
|
// Parse test_steps
|
|
610
827
|
const stepsRaw = p(args, "test_steps", "testSteps", "steps");
|
|
611
|
-
const testSteps = stepsRaw
|
|
612
|
-
? typeof stepsRaw === "string"
|
|
613
|
-
? JSON.parse(stepsRaw)
|
|
614
|
-
: stepsRaw
|
|
615
|
-
: null;
|
|
828
|
+
const testSteps = stepsRaw ? safeJsonParse(stepsRaw, "test_steps") : null;
|
|
616
829
|
// Step 1: Create testcase
|
|
617
830
|
const createResult = await callApi("post", `projects/testcases/create-testcase/${projectId}`, { body: testcaseData });
|
|
618
831
|
// Step 2: Add test steps if provided
|
|
619
832
|
const created = createResult?.data || createResult;
|
|
620
833
|
const tcId = created?.id || created?.testcaseId;
|
|
621
834
|
if (testSteps && Array.isArray(testSteps) && testSteps.length > 0 && tcId) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
835
|
+
// Backend endpoint accepts a single step per call, so loop through each step
|
|
836
|
+
for (const s of testSteps) {
|
|
837
|
+
const step = {
|
|
838
|
+
description: s.description || s.step,
|
|
839
|
+
test_data: s.test_data,
|
|
840
|
+
expected_result: s.expected_result,
|
|
841
|
+
};
|
|
842
|
+
await callApi("post", `projects/testcases/teststeps/${projectId}/${tcId}`, {
|
|
843
|
+
body: step,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
625
846
|
}
|
|
626
|
-
|
|
847
|
+
if (tcId) {
|
|
848
|
+
const created2 = createResult?.data || createResult;
|
|
849
|
+
if (created2 && typeof created2 === "object") {
|
|
850
|
+
created2.url = `${getFrontendBaseUrl()}/${projectId}/testcases/${tcId}`;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return jsonResponseWithHints(createResult, [
|
|
854
|
+
"To update fields, use manage_testcase(action='update', ids='<id>', field='<name>', value='<value>')",
|
|
855
|
+
]);
|
|
627
856
|
}
|
|
628
857
|
case "create_bulk": {
|
|
629
858
|
const rawTestcases = p(args, "testcases", "test_cases", "items");
|
|
630
859
|
if (!rawTestcases)
|
|
631
860
|
throw new Error("testcases is required for create_bulk action");
|
|
632
|
-
const testcases =
|
|
861
|
+
const testcases = safeJsonParse(rawTestcases, "testcases");
|
|
633
862
|
// Separate test_steps for post-creation
|
|
634
863
|
const withSteps = testcases.map((tc) => {
|
|
635
864
|
const normalized = { title: tc.title || tc.name };
|
|
@@ -637,8 +866,22 @@ async function handleManageTestcase(args) {
|
|
|
637
866
|
normalized.summary = tc.summary || tc.description;
|
|
638
867
|
if (tc.priority)
|
|
639
868
|
normalized.priority = tc.priority;
|
|
640
|
-
|
|
641
|
-
|
|
869
|
+
const rawFolderId = tc.folder_id || tc.folderId;
|
|
870
|
+
if (rawFolderId) {
|
|
871
|
+
const numFolderId = Number(rawFolderId);
|
|
872
|
+
normalized.folder_id = isNaN(numFolderId) ? rawFolderId : numFolderId;
|
|
873
|
+
}
|
|
874
|
+
if (tc.labels) {
|
|
875
|
+
normalized.labels = typeof tc.labels === "string"
|
|
876
|
+
? tc.labels.split(",").map((l) => l.trim()).filter(Boolean)
|
|
877
|
+
: tc.labels;
|
|
878
|
+
}
|
|
879
|
+
if (tc.precondition)
|
|
880
|
+
normalized.precondition = tc.precondition;
|
|
881
|
+
if (tc.status)
|
|
882
|
+
normalized.status = tc.status;
|
|
883
|
+
if (tc.automated)
|
|
884
|
+
normalized.automated = tc.automated;
|
|
642
885
|
return {
|
|
643
886
|
testcase: normalized,
|
|
644
887
|
test_steps: tc.test_steps || tc.testSteps || tc.steps,
|
|
@@ -656,9 +899,17 @@ async function handleManageTestcase(args) {
|
|
|
656
899
|
const tcId = r.testcaseId || r.id;
|
|
657
900
|
if (tcId) {
|
|
658
901
|
try {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
902
|
+
// Backend endpoint accepts a single step per call, so loop through each step
|
|
903
|
+
for (const s of steps) {
|
|
904
|
+
const step = {
|
|
905
|
+
description: s.description || s.step,
|
|
906
|
+
test_data: s.test_data,
|
|
907
|
+
expected_result: s.expected_result,
|
|
908
|
+
};
|
|
909
|
+
await callApi("post", `projects/testcases/teststeps/${projectId}/${tcId}`, {
|
|
910
|
+
body: step,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
662
913
|
stepsCreated++;
|
|
663
914
|
}
|
|
664
915
|
catch {
|
|
@@ -679,9 +930,33 @@ async function handleManageTestcase(args) {
|
|
|
679
930
|
throw new Error("field is required for update action");
|
|
680
931
|
if (value === undefined)
|
|
681
932
|
throw new Error("value is required for update action");
|
|
682
|
-
const
|
|
933
|
+
const idsStr = Array.isArray(ids) ? ids.join(",") : String(ids);
|
|
934
|
+
// Coerce field values for project-specific fields
|
|
935
|
+
const coercibleFields = ["priority", "status", "automated", "platform"];
|
|
936
|
+
// Parse value for fields that expect non-string types
|
|
937
|
+
let parsedValue = value;
|
|
938
|
+
if (field === "folder_id" || field === "owner_id") {
|
|
939
|
+
const num = Number(value);
|
|
940
|
+
parsedValue = isNaN(num) ? value : num;
|
|
941
|
+
}
|
|
942
|
+
else if (coercibleFields.includes(field)) {
|
|
943
|
+
parsedValue = await coerceFieldValue(projectId, field, value);
|
|
944
|
+
}
|
|
945
|
+
else if (field === "labels" || field === "features" || field === "environment") {
|
|
946
|
+
parsedValue = typeof value === "string" ? value.split(",").map((s) => s.trim()).filter(Boolean) : value;
|
|
947
|
+
}
|
|
948
|
+
else if (field === "custom_fields") {
|
|
949
|
+
const parsed = safeJsonParse(value, "custom_fields");
|
|
950
|
+
// Backend requires all custom field values as arrays
|
|
951
|
+
for (const key of Object.keys(parsed)) {
|
|
952
|
+
if (!Array.isArray(parsed[key])) {
|
|
953
|
+
parsed[key] = [parsed[key]];
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
parsedValue = parsed;
|
|
957
|
+
}
|
|
683
958
|
const data = await callApi("patch", `projects/testcases/update-field/${projectId}`, {
|
|
684
|
-
body: { ids:
|
|
959
|
+
body: { ids: idsStr, [field]: parsedValue },
|
|
685
960
|
});
|
|
686
961
|
return jsonResponse(data);
|
|
687
962
|
}
|
|
@@ -689,9 +964,9 @@ async function handleManageTestcase(args) {
|
|
|
689
964
|
const ids = p(args, "ids", "testcase_ids", "testcaseIds");
|
|
690
965
|
if (!ids)
|
|
691
966
|
throw new Error("ids is required for delete action");
|
|
692
|
-
const
|
|
967
|
+
const idsStr = Array.isArray(ids) ? ids.join(",") : String(ids);
|
|
693
968
|
const data = await callApi("delete", `projects/testcases/delete-testcase/${projectId}`, {
|
|
694
|
-
|
|
969
|
+
query: { ids: idsStr },
|
|
695
970
|
});
|
|
696
971
|
return jsonResponse(data);
|
|
697
972
|
}
|
|
@@ -707,6 +982,7 @@ async function handleManageFolder(args) {
|
|
|
707
982
|
throw new Error("projectId is required");
|
|
708
983
|
if (!action)
|
|
709
984
|
throw new Error("action is required");
|
|
985
|
+
validateId(projectId, "projectId");
|
|
710
986
|
const section = p(args, "section") || "TESTCASE";
|
|
711
987
|
const orgId = await getOrganizationId();
|
|
712
988
|
switch (action) {
|
|
@@ -733,6 +1009,7 @@ async function handleManageFolder(args) {
|
|
|
733
1009
|
throw new Error("folderId is required for rename action");
|
|
734
1010
|
if (!name)
|
|
735
1011
|
throw new Error("name is required for rename action");
|
|
1012
|
+
validateId(folderId, "folderId");
|
|
736
1013
|
const data = await callApi("patch", `folders/${folderId}`, {
|
|
737
1014
|
query: { projectId, section },
|
|
738
1015
|
body: { name },
|
|
@@ -743,6 +1020,7 @@ async function handleManageFolder(args) {
|
|
|
743
1020
|
const folderId = p(args, "folderId", "folder_id", "id");
|
|
744
1021
|
if (!folderId)
|
|
745
1022
|
throw new Error("folderId is required for move action");
|
|
1023
|
+
validateId(folderId, "folderId");
|
|
746
1024
|
const parentId = p(args, "parentId", "parent_id");
|
|
747
1025
|
const data = await callApi("patch", `folders/${folderId}/move`, {
|
|
748
1026
|
query: { projectId, section },
|
|
@@ -754,6 +1032,7 @@ async function handleManageFolder(args) {
|
|
|
754
1032
|
const folderId = p(args, "folderId", "folder_id", "id");
|
|
755
1033
|
if (!folderId)
|
|
756
1034
|
throw new Error("folderId is required for delete action");
|
|
1035
|
+
validateId(folderId, "folderId");
|
|
757
1036
|
const data = await callApi("delete", `folders/delete/${folderId}`, {
|
|
758
1037
|
query: { projectId, section },
|
|
759
1038
|
});
|
|
@@ -768,6 +1047,7 @@ async function handleSearchTestCycles(args) {
|
|
|
768
1047
|
const projectId = p(args, "projectId", "project_id");
|
|
769
1048
|
if (!projectId)
|
|
770
1049
|
throw new Error("projectId is required");
|
|
1050
|
+
validateId(projectId, "projectId");
|
|
771
1051
|
const data = await callApi("get", `projects/test-cycles/get-test-cycles/${projectId}`, {
|
|
772
1052
|
query: {
|
|
773
1053
|
search: p(args, "search"),
|
|
@@ -777,6 +1057,13 @@ async function handleSearchTestCycles(args) {
|
|
|
777
1057
|
limit: p(args, "limit"),
|
|
778
1058
|
},
|
|
779
1059
|
});
|
|
1060
|
+
const baseUrl = getFrontendBaseUrl();
|
|
1061
|
+
if (Array.isArray(data?.data)) {
|
|
1062
|
+
data.data.forEach((cycle) => {
|
|
1063
|
+
if (cycle.id)
|
|
1064
|
+
cycle.url = `${baseUrl}/${projectId}/testcycles/${cycle.id}`;
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
780
1067
|
return jsonResponse(data);
|
|
781
1068
|
}
|
|
782
1069
|
// 8. manage_test_cycle
|
|
@@ -787,6 +1074,7 @@ async function handleManageTestCycle(args) {
|
|
|
787
1074
|
throw new Error("projectId is required");
|
|
788
1075
|
if (!action)
|
|
789
1076
|
throw new Error("action is required");
|
|
1077
|
+
validateId(projectId, "projectId");
|
|
790
1078
|
switch (action) {
|
|
791
1079
|
case "create": {
|
|
792
1080
|
const title = p(args, "title");
|
|
@@ -830,21 +1118,26 @@ async function handleManageTestCycle(args) {
|
|
|
830
1118
|
const ids = p(args, "ids", "cycleId", "cycle_id");
|
|
831
1119
|
if (!ids)
|
|
832
1120
|
throw new Error("ids (or cycleId) is required for delete action");
|
|
833
|
-
const
|
|
834
|
-
const data = await callApi("delete", `projects/test-cycles/delete-test-cycle/${projectId}`, { body: { ids:
|
|
1121
|
+
const idsStr = Array.isArray(ids) ? ids.join(",") : String(ids);
|
|
1122
|
+
const data = await callApi("delete", `projects/test-cycles/delete-test-cycle/${projectId}`, { body: { ids: idsStr } });
|
|
835
1123
|
return jsonResponse(data);
|
|
836
1124
|
}
|
|
837
1125
|
case "get_details": {
|
|
838
1126
|
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
839
1127
|
if (!cycleId)
|
|
840
1128
|
throw new Error("cycleId is required for get_details action");
|
|
1129
|
+
validateId(cycleId, "cycleId");
|
|
841
1130
|
const data = await callApi("get", `projects/test-cycles/get-test-cycle-details/${projectId}/${cycleId}`);
|
|
1131
|
+
if (data?.data) {
|
|
1132
|
+
data.data.url = `${getFrontendBaseUrl()}/${projectId}/testcycles/${cycleId}`;
|
|
1133
|
+
}
|
|
842
1134
|
return jsonResponse(data);
|
|
843
1135
|
}
|
|
844
1136
|
case "get_testcases": {
|
|
845
1137
|
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
846
1138
|
if (!cycleId)
|
|
847
1139
|
throw new Error("cycleId is required for get_testcases action");
|
|
1140
|
+
validateId(cycleId, "cycleId");
|
|
848
1141
|
const data = await callApi("get", `projects/test-cycles/testcases/${projectId}/${cycleId}`, {
|
|
849
1142
|
query: {
|
|
850
1143
|
search: p(args, "search"),
|
|
@@ -861,8 +1154,9 @@ async function handleManageTestCycle(args) {
|
|
|
861
1154
|
throw new Error("cycleId is required for link_testcases action");
|
|
862
1155
|
if (!tcIds)
|
|
863
1156
|
throw new Error("testcase_ids is required for link_testcases action");
|
|
864
|
-
|
|
865
|
-
const
|
|
1157
|
+
validateId(cycleId, "cycleId");
|
|
1158
|
+
const idsStr = Array.isArray(tcIds) ? tcIds.join(",") : String(tcIds);
|
|
1159
|
+
const data = await callApi("post", `projects/test-cycles/testcases/link/${projectId}/${cycleId}`, { body: { ids: idsStr } });
|
|
866
1160
|
return jsonResponse(data);
|
|
867
1161
|
}
|
|
868
1162
|
case "unlink_testcases": {
|
|
@@ -872,8 +1166,9 @@ async function handleManageTestCycle(args) {
|
|
|
872
1166
|
throw new Error("cycleId is required for unlink_testcases action");
|
|
873
1167
|
if (!tcIds)
|
|
874
1168
|
throw new Error("testcase_ids is required for unlink_testcases action");
|
|
875
|
-
|
|
876
|
-
const
|
|
1169
|
+
validateId(cycleId, "cycleId");
|
|
1170
|
+
const idsStr = Array.isArray(tcIds) ? tcIds.join(",") : String(tcIds);
|
|
1171
|
+
const data = await callApi("delete", `projects/test-cycles/testcases/unlink/${projectId}/${cycleId}`, { body: { ids: idsStr } });
|
|
877
1172
|
return jsonResponse(data);
|
|
878
1173
|
}
|
|
879
1174
|
case "assign_testcases": {
|
|
@@ -886,8 +1181,9 @@ async function handleManageTestCycle(args) {
|
|
|
886
1181
|
throw new Error("testcase_ids is required for assign_testcases action");
|
|
887
1182
|
if (!assigneeId)
|
|
888
1183
|
throw new Error("assignee_id is required for assign_testcases action");
|
|
889
|
-
|
|
890
|
-
const
|
|
1184
|
+
validateId(cycleId, "cycleId");
|
|
1185
|
+
const idsStr = Array.isArray(tcIds) ? tcIds.join(",") : String(tcIds);
|
|
1186
|
+
const data = await callApi("patch", `projects/test-cycles/testcases/assign/${projectId}/${cycleId}`, { body: { ids: idsStr, assignee: Number(assigneeId) } });
|
|
891
1187
|
return jsonResponse(data);
|
|
892
1188
|
}
|
|
893
1189
|
default:
|
|
@@ -905,16 +1201,25 @@ async function handleExecuteTests(args) {
|
|
|
905
1201
|
throw new Error("cycleId is required");
|
|
906
1202
|
if (!resultsRaw)
|
|
907
1203
|
throw new Error("results is required");
|
|
908
|
-
|
|
1204
|
+
validateId(projectId, "projectId");
|
|
1205
|
+
validateId(cycleId, "cycleId");
|
|
1206
|
+
const results = safeJsonParse(resultsRaw, "results");
|
|
909
1207
|
if (!Array.isArray(results) || results.length === 0) {
|
|
910
1208
|
throw new Error("results must be a non-empty array");
|
|
911
1209
|
}
|
|
1210
|
+
// Validate all results have required fields
|
|
1211
|
+
for (const r of results) {
|
|
1212
|
+
if (!r.testcase_id && !r.testcaseId) {
|
|
1213
|
+
throw new Error("testcase_id is required in each result");
|
|
1214
|
+
}
|
|
1215
|
+
if (!r.execution_status && !r.executionStatus) {
|
|
1216
|
+
throw new Error("execution_status is required in each result. Valid values: pass, fail, blocked, not_executed");
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
912
1219
|
// Single result → use PATCH update-execution endpoint
|
|
913
1220
|
if (results.length === 1) {
|
|
914
1221
|
const r = results[0];
|
|
915
1222
|
const testcaseId = r.testcase_id || r.testcaseId;
|
|
916
|
-
if (!testcaseId)
|
|
917
|
-
throw new Error("testcase_id is required in each result");
|
|
918
1223
|
const body = {
|
|
919
1224
|
execution_status: r.execution_status || r.executionStatus,
|
|
920
1225
|
};
|
|
@@ -945,6 +1250,7 @@ async function handleManageTestPlan(args) {
|
|
|
945
1250
|
throw new Error("projectId is required");
|
|
946
1251
|
if (!action)
|
|
947
1252
|
throw new Error("action is required");
|
|
1253
|
+
validateId(projectId, "projectId");
|
|
948
1254
|
switch (action) {
|
|
949
1255
|
case "create": {
|
|
950
1256
|
const title = p(args, "title");
|
|
@@ -988,8 +1294,8 @@ async function handleManageTestPlan(args) {
|
|
|
988
1294
|
const ids = p(args, "ids", "planId", "plan_id");
|
|
989
1295
|
if (!ids)
|
|
990
1296
|
throw new Error("ids (or planId) is required for delete action");
|
|
991
|
-
const
|
|
992
|
-
const data = await callApi("delete", `projects/test-plans/delete-test-plan/${projectId}`, { body: { ids:
|
|
1297
|
+
const idsStr = Array.isArray(ids) ? ids.join(",") : String(ids);
|
|
1298
|
+
const data = await callApi("delete", `projects/test-plans/delete-test-plan/${projectId}`, { body: { ids: idsStr } });
|
|
993
1299
|
return jsonResponse(data);
|
|
994
1300
|
}
|
|
995
1301
|
case "list": {
|
|
@@ -1000,13 +1306,24 @@ async function handleManageTestPlan(args) {
|
|
|
1000
1306
|
limit: p(args, "limit"),
|
|
1001
1307
|
},
|
|
1002
1308
|
});
|
|
1309
|
+
const baseUrl = getFrontendBaseUrl();
|
|
1310
|
+
if (Array.isArray(data?.data)) {
|
|
1311
|
+
data.data.forEach((plan) => {
|
|
1312
|
+
if (plan.id)
|
|
1313
|
+
plan.url = `${baseUrl}/${projectId}/testplans/${plan.id}`;
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1003
1316
|
return jsonResponse(data);
|
|
1004
1317
|
}
|
|
1005
1318
|
case "get_details": {
|
|
1006
1319
|
const planId = p(args, "planId", "plan_id", "id");
|
|
1007
1320
|
if (!planId)
|
|
1008
1321
|
throw new Error("planId is required for get_details action");
|
|
1322
|
+
validateId(planId, "planId");
|
|
1009
1323
|
const data = await callApi("get", `projects/test-plans/get-test-plan-details/${projectId}/${planId}`);
|
|
1324
|
+
if (data?.data) {
|
|
1325
|
+
data.data.url = `${getFrontendBaseUrl()}/${projectId}/testplans/${planId}`;
|
|
1326
|
+
}
|
|
1010
1327
|
return jsonResponse(data);
|
|
1011
1328
|
}
|
|
1012
1329
|
case "link_cycles": {
|
|
@@ -1016,8 +1333,9 @@ async function handleManageTestPlan(args) {
|
|
|
1016
1333
|
throw new Error("planId is required for link_cycles action");
|
|
1017
1334
|
if (!cycleIds)
|
|
1018
1335
|
throw new Error("cycle_ids is required for link_cycles action");
|
|
1019
|
-
|
|
1020
|
-
const
|
|
1336
|
+
validateId(planId, "planId");
|
|
1337
|
+
const idsStr = Array.isArray(cycleIds) ? cycleIds.join(",") : String(cycleIds);
|
|
1338
|
+
const data = await callApi("post", `projects/test-plans/testcycles/link/${projectId}/${planId}`, { body: { ids: idsStr } });
|
|
1021
1339
|
return jsonResponse(data);
|
|
1022
1340
|
}
|
|
1023
1341
|
case "unlink_cycles": {
|
|
@@ -1027,14 +1345,16 @@ async function handleManageTestPlan(args) {
|
|
|
1027
1345
|
throw new Error("planId is required for unlink_cycles action");
|
|
1028
1346
|
if (!cycleIds)
|
|
1029
1347
|
throw new Error("cycle_ids is required for unlink_cycles action");
|
|
1030
|
-
|
|
1031
|
-
const
|
|
1348
|
+
validateId(planId, "planId");
|
|
1349
|
+
const idsStr = Array.isArray(cycleIds) ? cycleIds.join(",") : String(cycleIds);
|
|
1350
|
+
const data = await callApi("delete", `projects/test-plans/testcycles/unlink/${projectId}/${planId}`, { body: { ids: idsStr } });
|
|
1032
1351
|
return jsonResponse(data);
|
|
1033
1352
|
}
|
|
1034
1353
|
case "get_cycles": {
|
|
1035
1354
|
const planId = p(args, "planId", "plan_id", "id");
|
|
1036
1355
|
if (!planId)
|
|
1037
1356
|
throw new Error("planId is required for get_cycles action");
|
|
1357
|
+
validateId(planId, "planId");
|
|
1038
1358
|
const data = await callApi("get", `projects/test-plans/testcycles/list/${projectId}/${planId}`, {
|
|
1039
1359
|
query: {
|
|
1040
1360
|
search: p(args, "search"),
|
|
@@ -1048,6 +1368,7 @@ async function handleManageTestPlan(args) {
|
|
|
1048
1368
|
const planId = p(args, "planId", "plan_id", "id");
|
|
1049
1369
|
if (!planId)
|
|
1050
1370
|
throw new Error("planId is required for get_testcases action");
|
|
1371
|
+
validateId(planId, "planId");
|
|
1051
1372
|
const data = await callApi("get", `projects/test-plans/testcases/list/${projectId}/${planId}`, {
|
|
1052
1373
|
query: {
|
|
1053
1374
|
search: p(args, "search"),
|
|
@@ -1067,6 +1388,7 @@ async function handleGetReport(args) {
|
|
|
1067
1388
|
const reportType = p(args, "report_type", "reportType");
|
|
1068
1389
|
if (!projectId)
|
|
1069
1390
|
throw new Error("projectId is required");
|
|
1391
|
+
validateId(projectId, "projectId");
|
|
1070
1392
|
if (!reportType)
|
|
1071
1393
|
throw new Error("report_type is required");
|
|
1072
1394
|
const endpointPath = REPORT_TYPE_MAP[reportType];
|
|
@@ -1075,10 +1397,10 @@ async function handleGetReport(args) {
|
|
|
1075
1397
|
}
|
|
1076
1398
|
const query = {};
|
|
1077
1399
|
// Common filters
|
|
1078
|
-
if (p(args, "test_cycle_ids", "testCycleIds"))
|
|
1079
|
-
query.
|
|
1080
|
-
if (p(args, "test_plan_ids", "testPlanIds"))
|
|
1081
|
-
query.
|
|
1400
|
+
if (p(args, "test_cycle_ids", "testCycleIds", "testCycleId"))
|
|
1401
|
+
query.testCycleId = p(args, "test_cycle_ids", "testCycleIds", "testCycleId");
|
|
1402
|
+
if (p(args, "test_plan_ids", "testPlanIds", "testPlanId"))
|
|
1403
|
+
query.testPlanId = p(args, "test_plan_ids", "testPlanIds", "testPlanId");
|
|
1082
1404
|
if (p(args, "startDate", "start_date"))
|
|
1083
1405
|
query.startDate = p(args, "startDate", "start_date");
|
|
1084
1406
|
if (p(args, "endDate", "end_date"))
|
|
@@ -1087,7 +1409,7 @@ async function handleGetReport(args) {
|
|
|
1087
1409
|
query.granularity = p(args, "granularity");
|
|
1088
1410
|
// cycle_comparison specific
|
|
1089
1411
|
if (p(args, "cycle_ids", "cycleIds"))
|
|
1090
|
-
query.
|
|
1412
|
+
query.cycleIds = p(args, "cycle_ids", "cycleIds");
|
|
1091
1413
|
const data = await callApi("get", `projects/reports/${endpointPath}/${projectId}`, {
|
|
1092
1414
|
query,
|
|
1093
1415
|
});
|
|
@@ -1096,7 +1418,7 @@ async function handleGetReport(args) {
|
|
|
1096
1418
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1097
1419
|
// SERVER SETUP
|
|
1098
1420
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1099
|
-
const server = new Server({ name: "testkase-mcp-server", version: "2.
|
|
1421
|
+
const server = new Server({ name: "testkase-mcp-server", version: "2.2.0" }, { capabilities: { tools: {} } });
|
|
1100
1422
|
// List all 11 tools
|
|
1101
1423
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1102
1424
|
tools: [
|
|
@@ -1145,9 +1467,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1145
1467
|
}
|
|
1146
1468
|
}
|
|
1147
1469
|
catch (error) {
|
|
1148
|
-
const err = error instanceof Error ? error :
|
|
1470
|
+
const err = axios.isAxiosError(error) ? handleApiError(error) : (error instanceof Error ? error : new Error(String(error)));
|
|
1471
|
+
const enrichedMessage = enrichErrorMessage(err.message);
|
|
1149
1472
|
return {
|
|
1150
|
-
content: [{ type: "text", text: `Error: ${
|
|
1473
|
+
content: [{ type: "text", text: `Error: ${enrichedMessage}` }],
|
|
1151
1474
|
isError: true,
|
|
1152
1475
|
};
|
|
1153
1476
|
}
|
|
@@ -1156,7 +1479,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1156
1479
|
async function main() {
|
|
1157
1480
|
const transport = new StdioServerTransport();
|
|
1158
1481
|
await server.connect(transport);
|
|
1159
|
-
console.error("TestKase MCP Server v2.
|
|
1482
|
+
console.error("TestKase MCP Server v2.2.0 running on stdio (11 tools)");
|
|
1160
1483
|
}
|
|
1161
1484
|
main().catch((error) => {
|
|
1162
1485
|
console.error("Fatal error in main():", error);
|