@testkase/mcp-server 2.0.13 → 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 -229
- package/build/index.js +1348 -719
- 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
|
@@ -3,854 +3,1483 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
|
-
// Get configuration from environment variables
|
|
7
6
|
const config = {
|
|
8
7
|
apiBaseUrl: process.env.TESTKASE_API_BASE_URL || "https://apiqa.testkase.com",
|
|
9
8
|
patToken: process.env.TESTKASE_PAT_TOKEN,
|
|
10
9
|
};
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|
|
16
|
+
// ─── Auth helpers (reused from v1) ───────────────────────────────────────────
|
|
17
|
+
function validateAuth() {
|
|
18
|
+
if (!config.patToken) {
|
|
19
|
+
throw new Error("Authentication required. Please set TESTKASE_PAT_TOKEN environment variable. " +
|
|
20
|
+
"Generate a Personal Access Token (PAT) from your TestKase account settings.");
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (!config.patToken.startsWith("xyz_")) {
|
|
23
|
+
throw new Error("Invalid PAT token format. TestKase PAT tokens must start with 'xyz_' prefix. " +
|
|
24
|
+
"Please generate a valid PAT from your TestKase account settings.");
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
}
|
|
27
|
+
function getApiHeaders() {
|
|
28
|
+
return {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${config.patToken}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ─── Cached org ID (auto-resolved from PAT, never a tool param) ─────────────
|
|
34
|
+
let cachedOrganizationId = null;
|
|
35
|
+
async function getOrganizationId() {
|
|
36
|
+
if (cachedOrganizationId !== null)
|
|
37
|
+
return cachedOrganizationId;
|
|
26
38
|
validateAuth();
|
|
27
39
|
try {
|
|
28
|
-
const response = await axios.get(`${config.apiBaseUrl}/api/v1/organizations/my-organization`, {
|
|
29
|
-
headers: {
|
|
30
|
-
"Content-Type": "application/json",
|
|
31
|
-
"Authorization": `Bearer ${config.patToken}`,
|
|
32
|
-
},
|
|
33
|
-
});
|
|
40
|
+
const response = await axios.get(`${config.apiBaseUrl}/api/v1/organizations/my-organization`, { headers: getApiHeaders() });
|
|
34
41
|
const orgData = response.data?.data || response.data;
|
|
35
42
|
if (orgData?.organization_id) {
|
|
36
|
-
|
|
37
|
-
cachedOrganizationId
|
|
38
|
-
return orgId;
|
|
43
|
+
cachedOrganizationId = orgData.organization_id;
|
|
44
|
+
return cachedOrganizationId;
|
|
39
45
|
}
|
|
40
46
|
throw new Error("Could not determine organization from PAT token");
|
|
41
47
|
}
|
|
42
48
|
catch (error) {
|
|
43
|
-
|
|
49
|
+
if (error instanceof Error && error.message.includes("Could not determine"))
|
|
50
|
+
throw error;
|
|
51
|
+
throw new Error("Failed to fetch organization. Check your PAT token.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ─── API call helper ─────────────────────────────────────────────────────────
|
|
55
|
+
async function callApi(method, path, options = {}) {
|
|
56
|
+
validateAuth();
|
|
57
|
+
const orgId = options.skipOrgId ? undefined : await getOrganizationId();
|
|
58
|
+
const queryParams = new URLSearchParams();
|
|
59
|
+
if (orgId)
|
|
60
|
+
queryParams.append("organizationId", orgId.toString());
|
|
61
|
+
if (options.query) {
|
|
62
|
+
for (const [key, val] of Object.entries(options.query)) {
|
|
63
|
+
if (val !== undefined && val !== null && val !== "") {
|
|
64
|
+
queryParams.append(key, String(val));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
44
67
|
}
|
|
68
|
+
const qs = queryParams.toString();
|
|
69
|
+
const url = `${config.apiBaseUrl}/api/v1/${path}${qs ? `?${qs}` : ""}`;
|
|
70
|
+
const response = await axios({
|
|
71
|
+
method,
|
|
72
|
+
url,
|
|
73
|
+
headers: getApiHeaders(),
|
|
74
|
+
...(options.body !== undefined && { data: options.body }),
|
|
75
|
+
});
|
|
76
|
+
return response.data;
|
|
45
77
|
}
|
|
46
|
-
//
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
// ─── Error handler (reused) ──────────────────────────────────────────────────
|
|
79
|
+
function handleApiError(error) {
|
|
80
|
+
if (axios.isAxiosError(error)) {
|
|
81
|
+
const e = error;
|
|
82
|
+
let msg = `API request failed: ${e.message}`;
|
|
83
|
+
if (e.response) {
|
|
84
|
+
msg += `\nStatus: ${e.response.status}`;
|
|
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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return new Error(msg);
|
|
52
96
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
return error;
|
|
98
|
+
}
|
|
99
|
+
// ─── JSON response helper ────────────────────────────────────────────────────
|
|
100
|
+
function jsonResponse(data) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
103
|
+
};
|
|
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.`);
|
|
56
113
|
}
|
|
57
|
-
throw new Error("No active project set. You MUST first: 1) Call get_my_projects to list projects, " +
|
|
58
|
-
"2) ASK the user which project they want to work with, " +
|
|
59
|
-
"3) Call set_active_project with the user's chosen project ID. " +
|
|
60
|
-
"Do NOT select a project automatically.");
|
|
61
114
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
65
120
|
}
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
187
|
+
// ─── Param normalizer (handles camelCase / snake_case) ───────────────────────
|
|
188
|
+
function p(args, ...names) {
|
|
189
|
+
for (const n of names) {
|
|
190
|
+
if (args[n] !== undefined)
|
|
191
|
+
return args[n];
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
196
|
+
// TOOL DEFINITIONS (11 tools)
|
|
197
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
198
|
+
const LIST_PROJECTS_TOOL = {
|
|
199
|
+
name: "list_projects",
|
|
200
|
+
description: "List all projects the authenticated user has access to. " +
|
|
201
|
+
"Use this first to find a projectId, then pass it to other tools.",
|
|
71
202
|
inputSchema: {
|
|
72
203
|
type: "object",
|
|
73
204
|
properties: {
|
|
74
|
-
search: { type: "string", description: "Search
|
|
205
|
+
search: { type: "string", description: "Search by project name or ID" },
|
|
75
206
|
page: { type: "number", description: "Page number (default: 1)" },
|
|
76
207
|
limit: { type: "number", description: "Items per page (default: 20)" },
|
|
77
|
-
filters: { type: "string", description: "JSON filters. Example: {\"status\":[\"active\"],\"priority\":[\"high\"]}" },
|
|
78
208
|
},
|
|
79
209
|
required: [],
|
|
80
210
|
},
|
|
81
211
|
};
|
|
82
|
-
const
|
|
83
|
-
name: "
|
|
84
|
-
description: "Get
|
|
212
|
+
const GET_PROJECT_STRUCTURE_TOOL = {
|
|
213
|
+
name: "get_project_structure",
|
|
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.",
|
|
85
216
|
inputSchema: {
|
|
86
217
|
type: "object",
|
|
87
218
|
properties: {
|
|
88
|
-
|
|
219
|
+
projectId: { type: "string", description: "Project ID (e.g. PRJ-1001)" },
|
|
220
|
+
section: {
|
|
221
|
+
type: "string",
|
|
222
|
+
enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
|
|
223
|
+
description: "Folder section (default: TESTCASE)",
|
|
224
|
+
},
|
|
225
|
+
include: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Comma-separated list of what to include: folders,labels,members,field_options (default: all)",
|
|
228
|
+
},
|
|
89
229
|
},
|
|
90
|
-
required: ["
|
|
230
|
+
required: ["projectId"],
|
|
91
231
|
},
|
|
92
232
|
};
|
|
93
|
-
const
|
|
94
|
-
name: "
|
|
95
|
-
description: "
|
|
233
|
+
const SEARCH_TESTCASES_TOOL = {
|
|
234
|
+
name: "search_testcases",
|
|
235
|
+
description: "Search and list test cases in a project. Supports text search, filters, sorting, and pagination.",
|
|
96
236
|
inputSchema: {
|
|
97
237
|
type: "object",
|
|
98
238
|
properties: {
|
|
99
|
-
|
|
239
|
+
projectId: { type: "string", description: "Project ID" },
|
|
240
|
+
search: { type: "string", description: "Search text (title or summary)" },
|
|
241
|
+
filters: {
|
|
242
|
+
type: "string",
|
|
243
|
+
description: 'JSON string of filters. Example: {"status":["active"],"priority":["high","critical"]}',
|
|
244
|
+
},
|
|
245
|
+
page: { type: "number", description: "Page number (default: 1)" },
|
|
246
|
+
limit: { type: "number", description: "Items per page (default: 20)" },
|
|
247
|
+
sortBy: { type: "string", description: "Field to sort by (e.g. created_at, title)" },
|
|
248
|
+
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
100
249
|
},
|
|
101
|
-
required: [],
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
const GET_LABELS_TOOL = {
|
|
105
|
-
name: "get_labels",
|
|
106
|
-
description: "Get all labels available in the active project for categorizing testcases.",
|
|
107
|
-
inputSchema: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {},
|
|
110
|
-
required: [],
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
const GET_MY_PROJECTS_TOOL = {
|
|
114
|
-
name: "get_my_projects",
|
|
115
|
-
description: "IMPORTANT: Call this FIRST. Lists all projects. " +
|
|
116
|
-
"ALWAYS ASK the user which project to use, then call set_active_project with their choice.",
|
|
117
|
-
inputSchema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {},
|
|
120
|
-
required: [],
|
|
250
|
+
required: ["projectId"],
|
|
121
251
|
},
|
|
122
252
|
};
|
|
123
|
-
const
|
|
124
|
-
name: "
|
|
125
|
-
description: "
|
|
253
|
+
const GET_TESTCASE_TOOL = {
|
|
254
|
+
name: "get_testcase",
|
|
255
|
+
description: "Get full details of a single test case including test steps and attachments.",
|
|
126
256
|
inputSchema: {
|
|
127
257
|
type: "object",
|
|
128
258
|
properties: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
priority: { type: "string", description: "low, medium, high, or critical" },
|
|
132
|
-
folder_id: { type: "string", description: "Folder ID from get_folders" },
|
|
133
|
-
test_steps: { type: "string", description: "JSON array: [{\"step\":\"...\",\"expected_result\":\"...\"}]" },
|
|
259
|
+
projectId: { type: "string", description: "Project ID" },
|
|
260
|
+
testcaseId: { type: "string", description: "Test case ID (e.g. TEST-123)" },
|
|
134
261
|
},
|
|
135
|
-
required: ["
|
|
262
|
+
required: ["projectId", "testcaseId"],
|
|
136
263
|
},
|
|
137
264
|
};
|
|
138
|
-
const
|
|
139
|
-
name: "
|
|
140
|
-
description: "
|
|
265
|
+
const MANAGE_TESTCASE_TOOL = {
|
|
266
|
+
name: "manage_testcase",
|
|
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' }",
|
|
141
279
|
inputSchema: {
|
|
142
280
|
type: "object",
|
|
143
281
|
properties: {
|
|
144
|
-
|
|
282
|
+
projectId: { type: "string", description: "Project ID" },
|
|
283
|
+
action: {
|
|
284
|
+
type: "string",
|
|
285
|
+
enum: ["create", "create_bulk", "update", "delete"],
|
|
286
|
+
description: "Action to perform",
|
|
287
|
+
},
|
|
288
|
+
// create params
|
|
289
|
+
title: { type: "string", description: "[create] Test case title" },
|
|
290
|
+
summary: { type: "string", description: "[create] Description/summary" },
|
|
291
|
+
priority: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "[create] Priority. Values are project-specific — call get_project_structure(projectId, include='field_options') first to discover allowed values.",
|
|
294
|
+
},
|
|
295
|
+
folder_id: { type: "string", description: "[create] Folder ID to place the test case in" },
|
|
296
|
+
labels: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "[create] Comma-separated label names",
|
|
299
|
+
},
|
|
300
|
+
test_steps: {
|
|
301
|
+
type: "string",
|
|
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"]})',
|
|
325
|
+
},
|
|
326
|
+
// create_bulk params
|
|
327
|
+
testcases: {
|
|
328
|
+
type: "string",
|
|
329
|
+
description: "[create_bulk] JSON array of test cases, each with title, summary, priority, folder_id, test_steps",
|
|
330
|
+
},
|
|
331
|
+
// update/delete params
|
|
332
|
+
ids: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "[update/delete] Test case IDs, comma-separated (e.g. 'TEST-1' or 'TEST-1,TEST-2')",
|
|
335
|
+
},
|
|
336
|
+
field: {
|
|
337
|
+
type: "string",
|
|
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.",
|
|
339
|
+
},
|
|
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\"})" },
|
|
145
341
|
},
|
|
146
|
-
required: ["
|
|
342
|
+
required: ["projectId", "action"],
|
|
147
343
|
},
|
|
148
344
|
};
|
|
149
|
-
const
|
|
150
|
-
name: "
|
|
151
|
-
description: "
|
|
345
|
+
const MANAGE_FOLDER_TOOL = {
|
|
346
|
+
name: "manage_folder",
|
|
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)",
|
|
152
353
|
inputSchema: {
|
|
153
354
|
type: "object",
|
|
154
355
|
properties: {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
356
|
+
projectId: { type: "string", description: "Project ID" },
|
|
357
|
+
action: {
|
|
358
|
+
type: "string",
|
|
359
|
+
enum: ["create", "rename", "move", "delete"],
|
|
360
|
+
description: "Action to perform",
|
|
361
|
+
},
|
|
362
|
+
section: {
|
|
363
|
+
type: "string",
|
|
364
|
+
enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
|
|
365
|
+
description: "Folder section (default: TESTCASE)",
|
|
366
|
+
},
|
|
367
|
+
name: { type: "string", description: "[create/rename] Folder name (max 50 chars)" },
|
|
368
|
+
folderId: {
|
|
369
|
+
type: "number",
|
|
370
|
+
description: "[rename/move/delete] ID of the folder to modify",
|
|
371
|
+
},
|
|
372
|
+
parentId: {
|
|
373
|
+
type: "number",
|
|
374
|
+
description: "[create] Parent folder ID for subfolder. [move] New parent ID (null for root).",
|
|
375
|
+
},
|
|
158
376
|
},
|
|
159
|
-
required: ["
|
|
377
|
+
required: ["projectId", "action"],
|
|
160
378
|
},
|
|
161
379
|
};
|
|
162
|
-
const
|
|
163
|
-
name: "
|
|
164
|
-
description: "
|
|
380
|
+
const SEARCH_TEST_CYCLES_TOOL = {
|
|
381
|
+
name: "search_test_cycles",
|
|
382
|
+
description: "List and search test cycles in a project with execution progress.",
|
|
165
383
|
inputSchema: {
|
|
166
384
|
type: "object",
|
|
167
385
|
properties: {
|
|
168
|
-
|
|
386
|
+
projectId: { type: "string", description: "Project ID" },
|
|
387
|
+
search: { type: "string", description: "Search by title" },
|
|
388
|
+
folderId: { type: "number", description: "Filter by folder ID" },
|
|
389
|
+
status: { type: "string", description: "Filter by status (open, closed, etc.)" },
|
|
390
|
+
page: { type: "number", description: "Page number (default: 1)" },
|
|
391
|
+
limit: { type: "number", description: "Items per page (default: 20)" },
|
|
169
392
|
},
|
|
170
|
-
required: ["
|
|
393
|
+
required: ["projectId"],
|
|
171
394
|
},
|
|
172
395
|
};
|
|
173
|
-
const
|
|
174
|
-
name: "
|
|
175
|
-
description: "
|
|
396
|
+
const MANAGE_TEST_CYCLE_TOOL = {
|
|
397
|
+
name: "manage_test_cycle",
|
|
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",
|
|
176
409
|
inputSchema: {
|
|
177
410
|
type: "object",
|
|
178
411
|
properties: {
|
|
179
|
-
|
|
180
|
-
|
|
412
|
+
projectId: { type: "string", description: "Project ID" },
|
|
413
|
+
action: {
|
|
414
|
+
type: "string",
|
|
415
|
+
enum: [
|
|
416
|
+
"create",
|
|
417
|
+
"update",
|
|
418
|
+
"delete",
|
|
419
|
+
"get_details",
|
|
420
|
+
"get_testcases",
|
|
421
|
+
"link_testcases",
|
|
422
|
+
"unlink_testcases",
|
|
423
|
+
"assign_testcases",
|
|
424
|
+
],
|
|
425
|
+
description: "Action to perform",
|
|
426
|
+
},
|
|
427
|
+
// Identifiers
|
|
428
|
+
cycleId: {
|
|
429
|
+
type: "string",
|
|
430
|
+
description: "Test cycle ID (e.g. TCYCLE-1). Required for update/delete/get_details/get_testcases/link/unlink/assign.",
|
|
431
|
+
},
|
|
432
|
+
// create/update fields
|
|
433
|
+
title: { type: "string", description: "[create/update] Cycle title" },
|
|
434
|
+
summary: { type: "string", description: "[create/update] Description" },
|
|
435
|
+
status: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "[create/update] Status: open, in_progress, completed",
|
|
438
|
+
},
|
|
439
|
+
planned_start_date: {
|
|
440
|
+
type: "string",
|
|
441
|
+
description: "[create/update] Start date (YYYY-MM-DD)",
|
|
442
|
+
},
|
|
443
|
+
planned_end_date: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "[create/update] End date (YYYY-MM-DD)",
|
|
446
|
+
},
|
|
447
|
+
folder_id: {
|
|
448
|
+
type: "number",
|
|
449
|
+
description: "[create/update] Folder ID",
|
|
450
|
+
},
|
|
451
|
+
// delete
|
|
452
|
+
ids: {
|
|
453
|
+
type: "string",
|
|
454
|
+
description: "[delete] Comma-separated cycle IDs to delete",
|
|
455
|
+
},
|
|
456
|
+
// link/unlink/assign
|
|
457
|
+
testcase_ids: {
|
|
458
|
+
type: "string",
|
|
459
|
+
description: "[link_testcases/unlink_testcases/assign_testcases] Comma-separated test case IDs",
|
|
460
|
+
},
|
|
461
|
+
assignee_id: {
|
|
462
|
+
type: "number",
|
|
463
|
+
description: "[assign_testcases] User ID to assign",
|
|
464
|
+
},
|
|
465
|
+
// get_testcases params
|
|
466
|
+
search: { type: "string", description: "[get_testcases] Search text" },
|
|
467
|
+
page: { type: "number", description: "[get_testcases] Page number" },
|
|
468
|
+
limit: { type: "number", description: "[get_testcases] Items per page" },
|
|
181
469
|
},
|
|
182
|
-
required: ["
|
|
470
|
+
required: ["projectId", "action"],
|
|
183
471
|
},
|
|
184
472
|
};
|
|
185
|
-
const
|
|
186
|
-
name: "
|
|
187
|
-
description: "
|
|
473
|
+
const EXECUTE_TESTS_TOOL = {
|
|
474
|
+
name: "execute_tests",
|
|
475
|
+
description: "Record test execution results for test cases in a test cycle. " +
|
|
476
|
+
"Pass a single result or an array of results. " +
|
|
477
|
+
"Valid execution statuses: pass, fail, blocked, not_executed.",
|
|
188
478
|
inputSchema: {
|
|
189
479
|
type: "object",
|
|
190
480
|
properties: {
|
|
191
|
-
|
|
481
|
+
projectId: { type: "string", description: "Project ID" },
|
|
482
|
+
cycleId: { type: "string", description: "Test cycle ID (e.g. TCYCLE-1)" },
|
|
483
|
+
results: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: 'JSON array of results: [{"testcase_id":"TEST-1","execution_status":"pass","actual_result":"Worked","environment":"staging"}]. ' +
|
|
486
|
+
"Each object must have testcase_id and execution_status. actual_result and environment are optional.",
|
|
487
|
+
},
|
|
192
488
|
},
|
|
193
|
-
required: ["
|
|
489
|
+
required: ["projectId", "cycleId", "results"],
|
|
194
490
|
},
|
|
195
491
|
};
|
|
196
|
-
const
|
|
197
|
-
name: "
|
|
198
|
-
description: "
|
|
492
|
+
const MANAGE_TEST_PLAN_TOOL = {
|
|
493
|
+
name: "manage_test_plan",
|
|
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",
|
|
199
505
|
inputSchema: {
|
|
200
506
|
type: "object",
|
|
201
|
-
properties: {
|
|
202
|
-
|
|
507
|
+
properties: {
|
|
508
|
+
projectId: { type: "string", description: "Project ID" },
|
|
509
|
+
action: {
|
|
510
|
+
type: "string",
|
|
511
|
+
enum: [
|
|
512
|
+
"create",
|
|
513
|
+
"update",
|
|
514
|
+
"delete",
|
|
515
|
+
"list",
|
|
516
|
+
"get_details",
|
|
517
|
+
"link_cycles",
|
|
518
|
+
"unlink_cycles",
|
|
519
|
+
"get_cycles",
|
|
520
|
+
"get_testcases",
|
|
521
|
+
],
|
|
522
|
+
description: "Action to perform",
|
|
523
|
+
},
|
|
524
|
+
// Identifier
|
|
525
|
+
planId: {
|
|
526
|
+
type: "string",
|
|
527
|
+
description: "Test plan ID (e.g. TPLAN-1). Required for update/delete/get_details/link_cycles/unlink_cycles/get_cycles/get_testcases.",
|
|
528
|
+
},
|
|
529
|
+
// create/update fields
|
|
530
|
+
title: { type: "string", description: "[create/update] Plan title" },
|
|
531
|
+
summary: { type: "string", description: "[create/update] Description" },
|
|
532
|
+
status: { type: "string", description: "[create/update] Status" },
|
|
533
|
+
planned_start_date: {
|
|
534
|
+
type: "string",
|
|
535
|
+
description: "[create/update] Start date (YYYY-MM-DD)",
|
|
536
|
+
},
|
|
537
|
+
planned_end_date: {
|
|
538
|
+
type: "string",
|
|
539
|
+
description: "[create/update] End date (YYYY-MM-DD)",
|
|
540
|
+
},
|
|
541
|
+
folder_id: { type: "number", description: "[create/update] Folder ID" },
|
|
542
|
+
// delete
|
|
543
|
+
ids: {
|
|
544
|
+
type: "string",
|
|
545
|
+
description: "[delete] Comma-separated plan IDs to delete",
|
|
546
|
+
},
|
|
547
|
+
// link/unlink cycles
|
|
548
|
+
cycle_ids: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "[link_cycles/unlink_cycles] Comma-separated test cycle IDs",
|
|
551
|
+
},
|
|
552
|
+
// list/get_cycles/get_testcases pagination
|
|
553
|
+
search: { type: "string", description: "[list/get_cycles/get_testcases] Search text" },
|
|
554
|
+
page: { type: "number", description: "[list/get_cycles/get_testcases] Page number" },
|
|
555
|
+
limit: { type: "number", description: "[list/get_cycles/get_testcases] Items per page" },
|
|
556
|
+
},
|
|
557
|
+
required: ["projectId", "action"],
|
|
203
558
|
},
|
|
204
559
|
};
|
|
205
|
-
const
|
|
206
|
-
name: "
|
|
207
|
-
description: "
|
|
560
|
+
const GET_REPORT_TOOL = {
|
|
561
|
+
name: "get_report",
|
|
562
|
+
description: "Get reports and analytics for a project. Covers execution, coverage, trends, team, defects, and AI insights. " +
|
|
563
|
+
"Available report types: execution_summary, execution_by_cycle, execution_by_tester, execution_by_priority, " +
|
|
564
|
+
"execution_by_environment, execution_by_folder, requirement_coverage, traceability_matrix, " +
|
|
565
|
+
"failed_requirements, uncovered_requirements, testcase_coverage, unlinked_testcases, " +
|
|
566
|
+
"defects_by_folder, tester_workload, testcase_distribution, " +
|
|
567
|
+
"execution_trend, execution_burnup, execution_burndown, test_creation, cycle_comparison, " +
|
|
568
|
+
"scorecard_by_folder, scorecard_by_tester, created_vs_executed, execution_by_automation, " +
|
|
569
|
+
"defects_by_cycle, defects_by_tester, requirement_coverage_trend, " +
|
|
570
|
+
"flaky_tests, release_readiness, risk_heatmap_folder, risk_heatmap_feature, " +
|
|
571
|
+
"testcase_quality, predictive_failure, smart_prioritization, tester_effectiveness, " +
|
|
572
|
+
"stale_tests, defect_hotspots, cycle_health, suite_optimization, " +
|
|
573
|
+
"requirement_risk_matrix, execution_velocity, project_health.",
|
|
208
574
|
inputSchema: {
|
|
209
575
|
type: "object",
|
|
210
576
|
properties: {
|
|
211
|
-
projectId: { type: "string", description: "Project ID
|
|
577
|
+
projectId: { type: "string", description: "Project ID" },
|
|
578
|
+
report_type: {
|
|
579
|
+
type: "string",
|
|
580
|
+
description: "Report type (see description for full list)",
|
|
581
|
+
},
|
|
582
|
+
test_cycle_ids: {
|
|
583
|
+
type: "string",
|
|
584
|
+
description: "Comma-separated test cycle IDs to filter by",
|
|
585
|
+
},
|
|
586
|
+
test_plan_ids: {
|
|
587
|
+
type: "string",
|
|
588
|
+
description: "Comma-separated test plan IDs to filter by",
|
|
589
|
+
},
|
|
590
|
+
startDate: { type: "string", description: "Start date filter (YYYY-MM-DD)" },
|
|
591
|
+
endDate: { type: "string", description: "End date filter (YYYY-MM-DD)" },
|
|
592
|
+
granularity: {
|
|
593
|
+
type: "string",
|
|
594
|
+
enum: ["day", "week", "month"],
|
|
595
|
+
description: "Granularity for trend reports",
|
|
596
|
+
},
|
|
597
|
+
// cycle_comparison specific
|
|
598
|
+
cycle_ids: {
|
|
599
|
+
type: "string",
|
|
600
|
+
description: "[cycle_comparison] Comma-separated cycle IDs to compare",
|
|
601
|
+
},
|
|
212
602
|
},
|
|
213
|
-
required: ["projectId"],
|
|
603
|
+
required: ["projectId", "report_type"],
|
|
214
604
|
},
|
|
215
605
|
};
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
606
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
607
|
+
// REPORT TYPE → ENDPOINT MAPPING
|
|
608
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
609
|
+
const REPORT_TYPE_MAP = {
|
|
610
|
+
// Execution
|
|
611
|
+
execution_summary: "execution/summary",
|
|
612
|
+
execution_by_cycle: "execution/by-cycle",
|
|
613
|
+
execution_by_tester: "execution/by-tester",
|
|
614
|
+
execution_by_priority: "execution/by-priority",
|
|
615
|
+
execution_by_environment: "execution/by-environment",
|
|
616
|
+
execution_by_folder: "execution/by-folder",
|
|
617
|
+
// Coverage
|
|
618
|
+
requirement_coverage: "coverage/requirement-coverage",
|
|
619
|
+
traceability_matrix: "coverage/traceability-matrix",
|
|
620
|
+
failed_requirements: "coverage/failed-requirements",
|
|
621
|
+
uncovered_requirements: "coverage/uncovered-requirements",
|
|
622
|
+
testcase_coverage: "coverage/testcase-coverage",
|
|
623
|
+
unlinked_testcases: "coverage/unlinked-testcases",
|
|
624
|
+
// Defects
|
|
625
|
+
defects_by_folder: "defects/by-folder",
|
|
626
|
+
// Team
|
|
627
|
+
tester_workload: "team/tester-workload",
|
|
628
|
+
testcase_distribution: "team/testcase-distribution",
|
|
629
|
+
// Trends
|
|
630
|
+
execution_trend: "trends/execution-trend",
|
|
631
|
+
execution_burnup: "trends/execution-burnup",
|
|
632
|
+
execution_burndown: "trends/execution-burndown",
|
|
633
|
+
test_creation: "trends/test-creation",
|
|
634
|
+
cycle_comparison: "trends/cycle-comparison",
|
|
635
|
+
scorecard_by_folder: "trends/scorecard-by-folder",
|
|
636
|
+
scorecard_by_tester: "trends/scorecard-by-tester",
|
|
637
|
+
created_vs_executed: "trends/created-vs-executed",
|
|
638
|
+
execution_by_automation: "trends/execution-by-automation",
|
|
639
|
+
defects_by_cycle: "trends/defects-by-cycle",
|
|
640
|
+
defects_by_tester: "trends/defects-by-tester",
|
|
641
|
+
requirement_coverage_trend: "trends/requirement-coverage-trend",
|
|
642
|
+
// AI Insights
|
|
643
|
+
flaky_tests: "ai/flaky-tests",
|
|
644
|
+
release_readiness: "ai/release-readiness",
|
|
645
|
+
risk_heatmap_folder: "ai/risk-heatmap-folder",
|
|
646
|
+
risk_heatmap_feature: "ai/risk-heatmap-feature",
|
|
647
|
+
testcase_quality: "ai/testcase-quality",
|
|
648
|
+
predictive_failure: "ai/predictive-failure",
|
|
649
|
+
smart_prioritization: "ai/smart-prioritization",
|
|
650
|
+
tester_effectiveness: "ai/tester-effectiveness",
|
|
651
|
+
stale_tests: "ai/stale-tests",
|
|
652
|
+
defect_hotspots: "ai/defect-hotspots",
|
|
653
|
+
cycle_health: "ai/cycle-health",
|
|
654
|
+
suite_optimization: "ai/suite-optimization",
|
|
655
|
+
requirement_risk_matrix: "ai/requirement-risk-matrix",
|
|
656
|
+
execution_velocity: "ai/execution-velocity",
|
|
657
|
+
project_health: "ai/project-health",
|
|
224
658
|
};
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
GET_ACTIVE_PROJECT_TOOL,
|
|
241
|
-
GET_TESTCASES_TOOL,
|
|
242
|
-
GET_TESTCASE_DETAIL_TOOL,
|
|
243
|
-
CREATE_TESTCASE_TOOL,
|
|
244
|
-
CREATE_BULK_TESTCASE_TOOL,
|
|
245
|
-
UPDATE_TESTCASE_FIELD_TOOL,
|
|
246
|
-
DELETE_TESTCASE_TOOL,
|
|
247
|
-
GET_FOLDERS_TOOL,
|
|
248
|
-
CREATE_FOLDER_TOOL,
|
|
249
|
-
GET_LABELS_TOOL,
|
|
250
|
-
CREATE_LABEL_TOOL,
|
|
251
|
-
GET_USER_PROFILE_TOOL,
|
|
252
|
-
],
|
|
253
|
-
};
|
|
254
|
-
});
|
|
255
|
-
// Handle tool calls
|
|
256
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
257
|
-
const { name, arguments: args } = request.params;
|
|
258
|
-
try {
|
|
259
|
-
if (name === "get_my_projects") {
|
|
260
|
-
return await handleGetMyProjects(args);
|
|
261
|
-
}
|
|
262
|
-
else if (name === "set_active_project") {
|
|
263
|
-
return await handleSetActiveProject(args);
|
|
264
|
-
}
|
|
265
|
-
else if (name === "get_active_project") {
|
|
266
|
-
return await handleGetActiveProject();
|
|
267
|
-
}
|
|
268
|
-
else if (name === "get_testcases") {
|
|
269
|
-
return await handleGetTestcases(args);
|
|
270
|
-
}
|
|
271
|
-
else if (name === "get_testcase_detail") {
|
|
272
|
-
return await handleGetTestcaseDetail(args);
|
|
273
|
-
}
|
|
274
|
-
else if (name === "create_testcase") {
|
|
275
|
-
return await handleCreateTestcase(args);
|
|
276
|
-
}
|
|
277
|
-
else if (name === "create_bulk_testcase") {
|
|
278
|
-
return await handleCreateBulkTestcase(args);
|
|
279
|
-
}
|
|
280
|
-
else if (name === "update_testcase_field") {
|
|
281
|
-
return await handleUpdateTestcaseField(args);
|
|
282
|
-
}
|
|
283
|
-
else if (name === "delete_testcase") {
|
|
284
|
-
return await handleDeleteTestcase(args);
|
|
285
|
-
}
|
|
286
|
-
else if (name === "get_folders") {
|
|
287
|
-
return await handleGetFolders(args);
|
|
288
|
-
}
|
|
289
|
-
else if (name === "create_folder") {
|
|
290
|
-
return await handleCreateFolder(args);
|
|
291
|
-
}
|
|
292
|
-
else if (name === "get_labels") {
|
|
293
|
-
return await handleGetLabels(args);
|
|
294
|
-
}
|
|
295
|
-
else if (name === "create_label") {
|
|
296
|
-
return await handleCreateLabel(args);
|
|
297
|
-
}
|
|
298
|
-
else if (name === "get_user_profile") {
|
|
299
|
-
return await handleGetUserProfile(args);
|
|
300
|
-
}
|
|
301
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
302
|
-
}
|
|
303
|
-
catch (error) {
|
|
304
|
-
if (error instanceof Error) {
|
|
305
|
-
return {
|
|
306
|
-
content: [
|
|
307
|
-
{
|
|
308
|
-
type: "text",
|
|
309
|
-
text: `Error: ${error.message}`,
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
isError: true,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
// Handler for get_testcases tool
|
|
319
|
-
async function handleGetTestcases(args) {
|
|
320
|
-
validateAuth();
|
|
321
|
-
// Get project and organization IDs (from args or cached)
|
|
322
|
-
const projectId = getProjectId(args.projectId);
|
|
323
|
-
const organizationId = await getOrganizationId(args.organizationId);
|
|
324
|
-
// Build query parameters
|
|
325
|
-
const queryParams = new URLSearchParams();
|
|
326
|
-
queryParams.append("organizationId", organizationId.toString());
|
|
327
|
-
if (args.page !== undefined)
|
|
328
|
-
queryParams.append("page", args.page.toString());
|
|
329
|
-
if (args.limit !== undefined)
|
|
330
|
-
queryParams.append("limit", args.limit.toString());
|
|
331
|
-
if (args.selectAll !== undefined)
|
|
332
|
-
queryParams.append("selectAll", args.selectAll.toString());
|
|
333
|
-
if (args.columns)
|
|
334
|
-
queryParams.append("columns", args.columns);
|
|
335
|
-
if (args.filters)
|
|
336
|
-
queryParams.append("filters", args.filters);
|
|
337
|
-
if (args.search)
|
|
338
|
-
queryParams.append("search", args.search);
|
|
339
|
-
if (args.searchType)
|
|
340
|
-
queryParams.append("searchType", args.searchType);
|
|
341
|
-
if (args.sortBy)
|
|
342
|
-
queryParams.append("sortBy", args.sortBy);
|
|
343
|
-
if (args.sortOrder)
|
|
344
|
-
queryParams.append("sortOrder", args.sortOrder);
|
|
345
|
-
if (args.create_start_date)
|
|
346
|
-
queryParams.append("create_start_date", args.create_start_date);
|
|
347
|
-
if (args.create_end_date)
|
|
348
|
-
queryParams.append("create_end_date", args.create_end_date);
|
|
349
|
-
if (args.updated_start_date)
|
|
350
|
-
queryParams.append("updated_start_date", args.updated_start_date);
|
|
351
|
-
if (args.updated_end_date)
|
|
352
|
-
queryParams.append("updated_end_date", args.updated_end_date);
|
|
353
|
-
// Construct API URL
|
|
354
|
-
const url = `${config.apiBaseUrl}/api/v1/projects/testcases/get-testcase/${projectId}?${queryParams.toString()}`;
|
|
355
|
-
// Prepare headers with PAT token
|
|
356
|
-
// The PAT token is sent as Bearer token and will be validated by TestKase's JwtAuthGuard
|
|
357
|
-
// which checks for 'xyz_' prefix and validates against the PAT database
|
|
358
|
-
const headers = {
|
|
359
|
-
"Content-Type": "application/json",
|
|
360
|
-
"Authorization": `Bearer ${config.patToken}`,
|
|
361
|
-
};
|
|
362
|
-
try {
|
|
363
|
-
// Make API request
|
|
364
|
-
const response = await axios.get(url, { headers });
|
|
365
|
-
// Format the response
|
|
366
|
-
const data = response.data;
|
|
367
|
-
let resultText = `# Testcases for Project ${args.projectId}\n\n`;
|
|
368
|
-
if (data.data && Array.isArray(data.data)) {
|
|
369
|
-
resultText += `**Total Results:** ${data.total || data.data.length}\n`;
|
|
370
|
-
if (data.page)
|
|
371
|
-
resultText += `**Page:** ${data.page}\n`;
|
|
372
|
-
if (data.limit)
|
|
373
|
-
resultText += `**Limit:** ${data.limit}\n`;
|
|
374
|
-
resultText += `\n## Testcases (${data.data.length} items)\n\n`;
|
|
375
|
-
data.data.forEach((testcase, index) => {
|
|
376
|
-
resultText += `### ${index + 1}. ${testcase.title || "Untitled"}\n`;
|
|
377
|
-
resultText += `- **ID:** ${testcase.id}\n`;
|
|
378
|
-
if (testcase.status)
|
|
379
|
-
resultText += `- **Status:** ${testcase.status}\n`;
|
|
380
|
-
if (testcase.priority)
|
|
381
|
-
resultText += `- **Priority:** ${testcase.priority}\n`;
|
|
382
|
-
if (testcase.automated !== undefined)
|
|
383
|
-
resultText += `- **Automated:** ${testcase.automated}\n`;
|
|
384
|
-
if (testcase.summary)
|
|
385
|
-
resultText += `- **Summary:** ${testcase.summary}\n`;
|
|
386
|
-
if (testcase.owner_id)
|
|
387
|
-
resultText += `- **Owner ID:** ${testcase.owner_id}\n`;
|
|
388
|
-
if (testcase.folder_id)
|
|
389
|
-
resultText += `- **Folder ID:** ${testcase.folder_id}\n`;
|
|
390
|
-
if (testcase.created_at)
|
|
391
|
-
resultText += `- **Created:** ${testcase.created_at}\n`;
|
|
392
|
-
if (testcase.updated_at)
|
|
393
|
-
resultText += `- **Updated:** ${testcase.updated_at}\n`;
|
|
394
|
-
if (testcase.labels && Array.isArray(testcase.labels) && testcase.labels.length > 0) {
|
|
395
|
-
resultText += `- **Labels:** ${testcase.labels.join(", ")}\n`;
|
|
396
|
-
}
|
|
397
|
-
if (testcase.features && Array.isArray(testcase.features) && testcase.features.length > 0) {
|
|
398
|
-
resultText += `- **Features:** ${testcase.features.join(", ")}\n`;
|
|
399
|
-
}
|
|
400
|
-
resultText += `\n`;
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
resultText += "No testcases found or unexpected response format.\n\n";
|
|
405
|
-
resultText += `**Raw Response:**\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`\n`;
|
|
406
|
-
}
|
|
407
|
-
return {
|
|
408
|
-
content: [
|
|
409
|
-
{
|
|
410
|
-
type: "text",
|
|
411
|
-
text: resultText,
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
if (axios.isAxiosError(error)) {
|
|
418
|
-
const axiosError = error;
|
|
419
|
-
let errorMessage = `API request failed: ${axiosError.message}`;
|
|
420
|
-
if (axiosError.response) {
|
|
421
|
-
errorMessage += `\nStatus: ${axiosError.response.status}`;
|
|
422
|
-
errorMessage += `\nResponse: ${JSON.stringify(axiosError.response.data)}`;
|
|
423
|
-
}
|
|
424
|
-
throw new Error(errorMessage);
|
|
425
|
-
}
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
659
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
660
|
+
// TOOL HANDLERS
|
|
661
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
662
|
+
// 1. list_projects
|
|
663
|
+
async function handleListProjects(args) {
|
|
664
|
+
const data = await callApi("get", "projects/user/my-projects", {
|
|
665
|
+
query: {
|
|
666
|
+
search: p(args, "search"),
|
|
667
|
+
page: p(args, "page"),
|
|
668
|
+
limit: p(args, "limit"),
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
return jsonResponseWithHints(data, [
|
|
672
|
+
"Next: call get_project_structure(projectId) to see folders, labels, and allowed field values.",
|
|
673
|
+
]);
|
|
428
674
|
}
|
|
429
|
-
//
|
|
430
|
-
async function
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
675
|
+
// 2. get_project_structure
|
|
676
|
+
async function handleGetProjectStructure(args) {
|
|
677
|
+
const projectId = p(args, "projectId", "project_id");
|
|
678
|
+
if (!projectId)
|
|
679
|
+
throw new Error("projectId is required");
|
|
680
|
+
validateId(projectId, "projectId");
|
|
681
|
+
const section = p(args, "section") || "TESTCASE";
|
|
682
|
+
const includeRaw = p(args, "include") || "folders,labels,members,field_options";
|
|
683
|
+
const includes = includeRaw.split(",").map((s) => s.trim().toLowerCase());
|
|
684
|
+
const orgId = await getOrganizationId();
|
|
685
|
+
const result = {};
|
|
686
|
+
const promises = [];
|
|
687
|
+
if (includes.includes("folders")) {
|
|
688
|
+
promises.push(callApi("get", "folders/get-folders", {
|
|
689
|
+
query: { projectId, section },
|
|
690
|
+
}).then((data) => {
|
|
691
|
+
result.folders = data;
|
|
692
|
+
}));
|
|
438
693
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const response = await axios.get(url, {
|
|
444
|
-
headers: {
|
|
445
|
-
"Content-Type": "application/json",
|
|
446
|
-
"Authorization": `Bearer ${config.patToken}`,
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
|
-
const data = response.data;
|
|
450
|
-
return formatResponse(data, `Testcase Detail: ${testcaseId}`);
|
|
694
|
+
if (includes.includes("labels")) {
|
|
695
|
+
promises.push(callApi("get", `projects/labels/get-label/${projectId}`).then((data) => {
|
|
696
|
+
result.labels = data;
|
|
697
|
+
}));
|
|
451
698
|
}
|
|
452
|
-
|
|
453
|
-
|
|
699
|
+
if (includes.includes("members")) {
|
|
700
|
+
promises.push(callApi("get", `projects/${projectId}/team-members`).then((data) => {
|
|
701
|
+
result.members = data;
|
|
702
|
+
}));
|
|
454
703
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const projectId = getProjectId(args.projectId);
|
|
460
|
-
const organizationId = await getOrganizationId(args.organizationId);
|
|
461
|
-
const queryParams = new URLSearchParams();
|
|
462
|
-
queryParams.append("organizationId", organizationId.toString());
|
|
463
|
-
queryParams.append("projectId", projectId);
|
|
464
|
-
queryParams.append("section", "TESTCASE");
|
|
465
|
-
if (args.search) {
|
|
466
|
-
queryParams.append("search", args.search);
|
|
467
|
-
}
|
|
468
|
-
const url = `${config.apiBaseUrl}/api/v1/folders/get-folders?${queryParams.toString()}`;
|
|
469
|
-
try {
|
|
470
|
-
const response = await axios.get(url, {
|
|
471
|
-
headers: {
|
|
472
|
-
"Content-Type": "application/json",
|
|
473
|
-
"Authorization": `Bearer ${config.patToken}`,
|
|
474
|
-
},
|
|
475
|
-
});
|
|
476
|
-
const data = response.data;
|
|
477
|
-
return formatResponse(data, `Folders for Project: ${args.projectId}`);
|
|
478
|
-
}
|
|
479
|
-
catch (error) {
|
|
480
|
-
throw handleApiError(error);
|
|
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
|
+
}));
|
|
481
708
|
}
|
|
709
|
+
await Promise.all(promises);
|
|
710
|
+
return jsonResponseWithHints(result, [
|
|
711
|
+
"Use the field_options values above when creating/updating test cases.",
|
|
712
|
+
]);
|
|
482
713
|
}
|
|
483
|
-
//
|
|
484
|
-
async function
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
714
|
+
// 3. search_testcases
|
|
715
|
+
async function handleSearchTestcases(args) {
|
|
716
|
+
const projectId = p(args, "projectId", "project_id");
|
|
717
|
+
if (!projectId)
|
|
718
|
+
throw new Error("projectId is required");
|
|
719
|
+
validateId(projectId, "projectId");
|
|
720
|
+
const data = await callApi("get", `projects/testcases/get-testcase/${projectId}`, {
|
|
721
|
+
query: {
|
|
722
|
+
search: p(args, "search"),
|
|
723
|
+
filters: p(args, "filters"),
|
|
724
|
+
page: p(args, "page"),
|
|
725
|
+
limit: p(args, "limit"),
|
|
726
|
+
sortBy: p(args, "sortBy", "sort_by"),
|
|
727
|
+
sortOrder: p(args, "sortOrder", "sort_order"),
|
|
728
|
+
},
|
|
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}`;
|
|
497
735
|
});
|
|
498
|
-
const data = response.data;
|
|
499
|
-
return formatResponse(data, `Labels for Project: ${args.projectId}`);
|
|
500
|
-
}
|
|
501
|
-
catch (error) {
|
|
502
|
-
throw handleApiError(error);
|
|
503
736
|
}
|
|
737
|
+
return jsonResponse(data);
|
|
504
738
|
}
|
|
505
|
-
//
|
|
506
|
-
async function
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"Authorization": `Bearer ${config.patToken}`,
|
|
519
|
-
},
|
|
520
|
-
});
|
|
521
|
-
const data = response.data;
|
|
522
|
-
return formatResponse(data, "My Projects");
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
throw handleApiError(error);
|
|
739
|
+
// 4. get_testcase
|
|
740
|
+
async function handleGetTestcase(args) {
|
|
741
|
+
const projectId = p(args, "projectId", "project_id");
|
|
742
|
+
const testcaseId = p(args, "testcaseId", "testcase_id", "id");
|
|
743
|
+
if (!projectId)
|
|
744
|
+
throw new Error("projectId is required");
|
|
745
|
+
if (!testcaseId)
|
|
746
|
+
throw new Error("testcaseId is required");
|
|
747
|
+
validateId(projectId, "projectId");
|
|
748
|
+
validateId(testcaseId, "testcaseId");
|
|
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}`;
|
|
526
752
|
}
|
|
753
|
+
return jsonResponse(data);
|
|
527
754
|
}
|
|
528
|
-
//
|
|
529
|
-
async function
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
755
|
+
// 5. manage_testcase
|
|
756
|
+
async function handleManageTestcase(args) {
|
|
757
|
+
const projectId = p(args, "projectId", "project_id");
|
|
758
|
+
const action = p(args, "action");
|
|
759
|
+
if (!projectId)
|
|
760
|
+
throw new Error("projectId is required");
|
|
761
|
+
if (!action)
|
|
762
|
+
throw new Error("action is required");
|
|
763
|
+
validateId(projectId, "projectId");
|
|
764
|
+
const orgId = await getOrganizationId();
|
|
765
|
+
switch (action) {
|
|
766
|
+
case "create": {
|
|
767
|
+
const title = p(args, "title", "name");
|
|
768
|
+
if (!title)
|
|
769
|
+
throw new Error("title is required for create action");
|
|
770
|
+
const testcaseData = { title };
|
|
771
|
+
if (p(args, "summary", "description"))
|
|
772
|
+
testcaseData.summary = p(args, "summary", "description");
|
|
773
|
+
if (p(args, "priority"))
|
|
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
|
+
}
|
|
780
|
+
const labelsRaw = p(args, "labels");
|
|
781
|
+
if (labelsRaw) {
|
|
782
|
+
testcaseData.labels =
|
|
783
|
+
typeof labelsRaw === "string"
|
|
784
|
+
? labelsRaw.split(",").map((l) => l.trim()).filter(Boolean)
|
|
785
|
+
: labelsRaw;
|
|
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
|
+
}
|
|
826
|
+
// Parse test_steps
|
|
827
|
+
const stepsRaw = p(args, "test_steps", "testSteps", "steps");
|
|
828
|
+
const testSteps = stepsRaw ? safeJsonParse(stepsRaw, "test_steps") : null;
|
|
829
|
+
// Step 1: Create testcase
|
|
830
|
+
const createResult = await callApi("post", `projects/testcases/create-testcase/${projectId}`, { body: testcaseData });
|
|
831
|
+
// Step 2: Add test steps if provided
|
|
832
|
+
const created = createResult?.data || createResult;
|
|
833
|
+
const tcId = created?.id || created?.testcaseId;
|
|
834
|
+
if (testSteps && Array.isArray(testSteps) && testSteps.length > 0 && tcId) {
|
|
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
|
+
}
|
|
846
|
+
}
|
|
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
|
+
]);
|
|
577
856
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
857
|
+
case "create_bulk": {
|
|
858
|
+
const rawTestcases = p(args, "testcases", "test_cases", "items");
|
|
859
|
+
if (!rawTestcases)
|
|
860
|
+
throw new Error("testcases is required for create_bulk action");
|
|
861
|
+
const testcases = safeJsonParse(rawTestcases, "testcases");
|
|
862
|
+
// Separate test_steps for post-creation
|
|
863
|
+
const withSteps = testcases.map((tc) => {
|
|
864
|
+
const normalized = { title: tc.title || tc.name };
|
|
865
|
+
if (tc.summary || tc.description)
|
|
866
|
+
normalized.summary = tc.summary || tc.description;
|
|
867
|
+
if (tc.priority)
|
|
868
|
+
normalized.priority = tc.priority;
|
|
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;
|
|
885
|
+
return {
|
|
886
|
+
testcase: normalized,
|
|
887
|
+
test_steps: tc.test_steps || tc.testSteps || tc.steps,
|
|
888
|
+
};
|
|
889
|
+
});
|
|
890
|
+
const cleanTestcases = withSteps.map((t) => t.testcase);
|
|
891
|
+
const bulkResult = await callApi("post", `projects/testcases/create-bulk-testcase/${projectId}`, { body: { testcases: cleanTestcases } });
|
|
892
|
+
// Add test steps for each created testcase
|
|
893
|
+
const results = bulkResult?.results || [];
|
|
894
|
+
let stepsCreated = 0;
|
|
895
|
+
for (let i = 0; i < results.length; i++) {
|
|
896
|
+
const r = results[i];
|
|
897
|
+
const steps = withSteps[i]?.test_steps;
|
|
898
|
+
if (r.status === "success" && steps?.length) {
|
|
899
|
+
const tcId = r.testcaseId || r.id;
|
|
900
|
+
if (tcId) {
|
|
901
|
+
try {
|
|
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
|
+
}
|
|
913
|
+
stepsCreated++;
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
// Non-fatal: steps failed but testcase was created
|
|
917
|
+
}
|
|
635
918
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return jsonResponse({ ...bulkResult, steps_created_for: stepsCreated });
|
|
922
|
+
}
|
|
923
|
+
case "update": {
|
|
924
|
+
const ids = p(args, "ids", "testcase_ids", "testcaseIds");
|
|
925
|
+
const field = p(args, "field", "field_name", "fieldName");
|
|
926
|
+
const value = p(args, "value", "field_value", "fieldValue", "new_value");
|
|
927
|
+
if (!ids)
|
|
928
|
+
throw new Error("ids is required for update action");
|
|
929
|
+
if (!field)
|
|
930
|
+
throw new Error("field is required for update action");
|
|
931
|
+
if (value === undefined)
|
|
932
|
+
throw new Error("value is required for update action");
|
|
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]];
|
|
639
954
|
}
|
|
640
955
|
}
|
|
956
|
+
parsedValue = parsed;
|
|
641
957
|
}
|
|
958
|
+
const data = await callApi("patch", `projects/testcases/update-field/${projectId}`, {
|
|
959
|
+
body: { ids: idsStr, [field]: parsedValue },
|
|
960
|
+
});
|
|
961
|
+
return jsonResponse(data);
|
|
642
962
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
963
|
+
case "delete": {
|
|
964
|
+
const ids = p(args, "ids", "testcase_ids", "testcaseIds");
|
|
965
|
+
if (!ids)
|
|
966
|
+
throw new Error("ids is required for delete action");
|
|
967
|
+
const idsStr = Array.isArray(ids) ? ids.join(",") : String(ids);
|
|
968
|
+
const data = await callApi("delete", `projects/testcases/delete-testcase/${projectId}`, {
|
|
969
|
+
query: { ids: idsStr },
|
|
970
|
+
});
|
|
971
|
+
return jsonResponse(data);
|
|
972
|
+
}
|
|
973
|
+
default:
|
|
974
|
+
throw new Error(`Unknown action: ${action}. Use: create, create_bulk, update, delete`);
|
|
647
975
|
}
|
|
648
976
|
}
|
|
649
|
-
//
|
|
650
|
-
async function
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
977
|
+
// 6. manage_folder
|
|
978
|
+
async function handleManageFolder(args) {
|
|
979
|
+
const projectId = p(args, "projectId", "project_id");
|
|
980
|
+
const action = p(args, "action");
|
|
981
|
+
if (!projectId)
|
|
982
|
+
throw new Error("projectId is required");
|
|
983
|
+
if (!action)
|
|
984
|
+
throw new Error("action is required");
|
|
985
|
+
validateId(projectId, "projectId");
|
|
986
|
+
const section = p(args, "section") || "TESTCASE";
|
|
987
|
+
const orgId = await getOrganizationId();
|
|
988
|
+
switch (action) {
|
|
989
|
+
case "create": {
|
|
990
|
+
const name = p(args, "name", "folder_name");
|
|
991
|
+
if (!name)
|
|
992
|
+
throw new Error("name is required for create action");
|
|
993
|
+
const body = {
|
|
994
|
+
organizationId: orgId,
|
|
995
|
+
projectId,
|
|
996
|
+
section,
|
|
997
|
+
name,
|
|
998
|
+
};
|
|
999
|
+
const parentId = p(args, "parentId", "parent_id");
|
|
1000
|
+
if (parentId)
|
|
1001
|
+
body.parentId = parentId;
|
|
1002
|
+
const data = await callApi("post", "folders/create-folders", { body, skipOrgId: true });
|
|
1003
|
+
return jsonResponse(data);
|
|
1004
|
+
}
|
|
1005
|
+
case "rename": {
|
|
1006
|
+
const folderId = p(args, "folderId", "folder_id", "id");
|
|
1007
|
+
const name = p(args, "name", "folder_name");
|
|
1008
|
+
if (!folderId)
|
|
1009
|
+
throw new Error("folderId is required for rename action");
|
|
1010
|
+
if (!name)
|
|
1011
|
+
throw new Error("name is required for rename action");
|
|
1012
|
+
validateId(folderId, "folderId");
|
|
1013
|
+
const data = await callApi("patch", `folders/${folderId}`, {
|
|
1014
|
+
query: { projectId, section },
|
|
1015
|
+
body: { name },
|
|
1016
|
+
});
|
|
1017
|
+
return jsonResponse(data);
|
|
1018
|
+
}
|
|
1019
|
+
case "move": {
|
|
1020
|
+
const folderId = p(args, "folderId", "folder_id", "id");
|
|
1021
|
+
if (!folderId)
|
|
1022
|
+
throw new Error("folderId is required for move action");
|
|
1023
|
+
validateId(folderId, "folderId");
|
|
1024
|
+
const parentId = p(args, "parentId", "parent_id");
|
|
1025
|
+
const data = await callApi("patch", `folders/${folderId}/move`, {
|
|
1026
|
+
query: { projectId, section },
|
|
1027
|
+
body: { parentId: parentId ?? null },
|
|
1028
|
+
});
|
|
1029
|
+
return jsonResponse(data);
|
|
1030
|
+
}
|
|
1031
|
+
case "delete": {
|
|
1032
|
+
const folderId = p(args, "folderId", "folder_id", "id");
|
|
1033
|
+
if (!folderId)
|
|
1034
|
+
throw new Error("folderId is required for delete action");
|
|
1035
|
+
validateId(folderId, "folderId");
|
|
1036
|
+
const data = await callApi("delete", `folders/delete/${folderId}`, {
|
|
1037
|
+
query: { projectId, section },
|
|
1038
|
+
});
|
|
1039
|
+
return jsonResponse(data);
|
|
1040
|
+
}
|
|
1041
|
+
default:
|
|
1042
|
+
throw new Error(`Unknown action: ${action}. Use: create, rename, move, delete`);
|
|
679
1043
|
}
|
|
680
1044
|
}
|
|
681
|
-
//
|
|
682
|
-
async function
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1045
|
+
// 7. search_test_cycles
|
|
1046
|
+
async function handleSearchTestCycles(args) {
|
|
1047
|
+
const projectId = p(args, "projectId", "project_id");
|
|
1048
|
+
if (!projectId)
|
|
1049
|
+
throw new Error("projectId is required");
|
|
1050
|
+
validateId(projectId, "projectId");
|
|
1051
|
+
const data = await callApi("get", `projects/test-cycles/get-test-cycles/${projectId}`, {
|
|
1052
|
+
query: {
|
|
1053
|
+
search: p(args, "search"),
|
|
1054
|
+
folderId: p(args, "folderId", "folder_id"),
|
|
1055
|
+
status: p(args, "status"),
|
|
1056
|
+
page: p(args, "page"),
|
|
1057
|
+
limit: p(args, "limit"),
|
|
1058
|
+
},
|
|
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}`;
|
|
697
1065
|
});
|
|
698
|
-
return formatResponse(response.data, "Testcase Deleted");
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
throw handleApiError(error);
|
|
702
1066
|
}
|
|
1067
|
+
return jsonResponse(data);
|
|
703
1068
|
}
|
|
704
|
-
//
|
|
705
|
-
async function
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1069
|
+
// 8. manage_test_cycle
|
|
1070
|
+
async function handleManageTestCycle(args) {
|
|
1071
|
+
const projectId = p(args, "projectId", "project_id");
|
|
1072
|
+
const action = p(args, "action");
|
|
1073
|
+
if (!projectId)
|
|
1074
|
+
throw new Error("projectId is required");
|
|
1075
|
+
if (!action)
|
|
1076
|
+
throw new Error("action is required");
|
|
1077
|
+
validateId(projectId, "projectId");
|
|
1078
|
+
switch (action) {
|
|
1079
|
+
case "create": {
|
|
1080
|
+
const title = p(args, "title");
|
|
1081
|
+
if (!title)
|
|
1082
|
+
throw new Error("title is required for create action");
|
|
1083
|
+
const body = { title };
|
|
1084
|
+
if (p(args, "summary"))
|
|
1085
|
+
body.summary = p(args, "summary");
|
|
1086
|
+
if (p(args, "status"))
|
|
1087
|
+
body.status = p(args, "status");
|
|
1088
|
+
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1089
|
+
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1090
|
+
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
1091
|
+
body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
|
|
1092
|
+
if (p(args, "folder_id", "folderId"))
|
|
1093
|
+
body.folder_id = p(args, "folder_id", "folderId");
|
|
1094
|
+
const data = await callApi("post", `projects/test-cycles/create-test-cycle/${projectId}`, { body });
|
|
1095
|
+
return jsonResponse(data);
|
|
1096
|
+
}
|
|
1097
|
+
case "update": {
|
|
1098
|
+
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
1099
|
+
if (!cycleId)
|
|
1100
|
+
throw new Error("cycleId is required for update action");
|
|
1101
|
+
const body = { id: cycleId };
|
|
1102
|
+
if (p(args, "title"))
|
|
1103
|
+
body.title = p(args, "title");
|
|
1104
|
+
if (p(args, "summary"))
|
|
1105
|
+
body.summary = p(args, "summary");
|
|
1106
|
+
if (p(args, "status"))
|
|
1107
|
+
body.status = p(args, "status");
|
|
1108
|
+
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1109
|
+
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1110
|
+
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
1111
|
+
body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
|
|
1112
|
+
if (p(args, "folder_id", "folderId"))
|
|
1113
|
+
body.folder_id = p(args, "folder_id", "folderId");
|
|
1114
|
+
const data = await callApi("post", `projects/test-cycles/create-test-cycle/${projectId}`, { body });
|
|
1115
|
+
return jsonResponse(data);
|
|
1116
|
+
}
|
|
1117
|
+
case "delete": {
|
|
1118
|
+
const ids = p(args, "ids", "cycleId", "cycle_id");
|
|
1119
|
+
if (!ids)
|
|
1120
|
+
throw new Error("ids (or cycleId) is required for delete action");
|
|
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 } });
|
|
1123
|
+
return jsonResponse(data);
|
|
1124
|
+
}
|
|
1125
|
+
case "get_details": {
|
|
1126
|
+
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
1127
|
+
if (!cycleId)
|
|
1128
|
+
throw new Error("cycleId is required for get_details action");
|
|
1129
|
+
validateId(cycleId, "cycleId");
|
|
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
|
+
}
|
|
1134
|
+
return jsonResponse(data);
|
|
1135
|
+
}
|
|
1136
|
+
case "get_testcases": {
|
|
1137
|
+
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
1138
|
+
if (!cycleId)
|
|
1139
|
+
throw new Error("cycleId is required for get_testcases action");
|
|
1140
|
+
validateId(cycleId, "cycleId");
|
|
1141
|
+
const data = await callApi("get", `projects/test-cycles/testcases/${projectId}/${cycleId}`, {
|
|
1142
|
+
query: {
|
|
1143
|
+
search: p(args, "search"),
|
|
1144
|
+
page: p(args, "page"),
|
|
1145
|
+
limit: p(args, "limit"),
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
return jsonResponse(data);
|
|
1149
|
+
}
|
|
1150
|
+
case "link_testcases": {
|
|
1151
|
+
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
1152
|
+
const tcIds = p(args, "testcase_ids", "testcaseIds");
|
|
1153
|
+
if (!cycleId)
|
|
1154
|
+
throw new Error("cycleId is required for link_testcases action");
|
|
1155
|
+
if (!tcIds)
|
|
1156
|
+
throw new Error("testcase_ids is required for link_testcases action");
|
|
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 } });
|
|
1160
|
+
return jsonResponse(data);
|
|
1161
|
+
}
|
|
1162
|
+
case "unlink_testcases": {
|
|
1163
|
+
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
1164
|
+
const tcIds = p(args, "testcase_ids", "testcaseIds");
|
|
1165
|
+
if (!cycleId)
|
|
1166
|
+
throw new Error("cycleId is required for unlink_testcases action");
|
|
1167
|
+
if (!tcIds)
|
|
1168
|
+
throw new Error("testcase_ids is required for unlink_testcases action");
|
|
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 } });
|
|
1172
|
+
return jsonResponse(data);
|
|
1173
|
+
}
|
|
1174
|
+
case "assign_testcases": {
|
|
1175
|
+
const cycleId = p(args, "cycleId", "cycle_id", "id");
|
|
1176
|
+
const tcIds = p(args, "testcase_ids", "testcaseIds");
|
|
1177
|
+
const assigneeId = p(args, "assignee_id", "assigneeId");
|
|
1178
|
+
if (!cycleId)
|
|
1179
|
+
throw new Error("cycleId is required for assign_testcases action");
|
|
1180
|
+
if (!tcIds)
|
|
1181
|
+
throw new Error("testcase_ids is required for assign_testcases action");
|
|
1182
|
+
if (!assigneeId)
|
|
1183
|
+
throw new Error("assignee_id is required for assign_testcases action");
|
|
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) } });
|
|
1187
|
+
return jsonResponse(data);
|
|
1188
|
+
}
|
|
1189
|
+
default:
|
|
1190
|
+
throw new Error(`Unknown action: ${action}. Use: create, update, delete, get_details, get_testcases, link_testcases, unlink_testcases, assign_testcases`);
|
|
731
1191
|
}
|
|
732
1192
|
}
|
|
733
|
-
//
|
|
734
|
-
async function
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
|
|
750
|
-
});
|
|
751
|
-
return formatResponse(response.data, "Label Created");
|
|
752
|
-
}
|
|
753
|
-
catch (error) {
|
|
754
|
-
throw handleApiError(error);
|
|
1193
|
+
// 9. execute_tests
|
|
1194
|
+
async function handleExecuteTests(args) {
|
|
1195
|
+
const projectId = p(args, "projectId", "project_id");
|
|
1196
|
+
const cycleId = p(args, "cycleId", "cycle_id");
|
|
1197
|
+
const resultsRaw = p(args, "results");
|
|
1198
|
+
if (!projectId)
|
|
1199
|
+
throw new Error("projectId is required");
|
|
1200
|
+
if (!cycleId)
|
|
1201
|
+
throw new Error("cycleId is required");
|
|
1202
|
+
if (!resultsRaw)
|
|
1203
|
+
throw new Error("results is required");
|
|
1204
|
+
validateId(projectId, "projectId");
|
|
1205
|
+
validateId(cycleId, "cycleId");
|
|
1206
|
+
const results = safeJsonParse(resultsRaw, "results");
|
|
1207
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
1208
|
+
throw new Error("results must be a non-empty array");
|
|
755
1209
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
});
|
|
765
|
-
return formatResponse(response.data, "User Profile");
|
|
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
|
+
}
|
|
766
1218
|
}
|
|
767
|
-
|
|
768
|
-
|
|
1219
|
+
// Single result → use PATCH update-execution endpoint
|
|
1220
|
+
if (results.length === 1) {
|
|
1221
|
+
const r = results[0];
|
|
1222
|
+
const testcaseId = r.testcase_id || r.testcaseId;
|
|
1223
|
+
const body = {
|
|
1224
|
+
execution_status: r.execution_status || r.executionStatus,
|
|
1225
|
+
};
|
|
1226
|
+
if (r.actual_result || r.actualResult)
|
|
1227
|
+
body.actual_result = r.actual_result || r.actualResult;
|
|
1228
|
+
if (r.environment)
|
|
1229
|
+
body.environment = r.environment;
|
|
1230
|
+
const data = await callApi("patch", `projects/test-cycles/testcases/update-execution/${projectId}/${cycleId}/${testcaseId}`, { body });
|
|
1231
|
+
return jsonResponse(data);
|
|
769
1232
|
}
|
|
1233
|
+
// Multiple results → use POST execute-bulk endpoint
|
|
1234
|
+
const bulkResults = results.map((r) => ({
|
|
1235
|
+
testcase_id: r.testcase_id || r.testcaseId,
|
|
1236
|
+
execution_status: r.execution_status || r.executionStatus,
|
|
1237
|
+
...(r.actual_result || r.actualResult
|
|
1238
|
+
? { actual_result: r.actual_result || r.actualResult }
|
|
1239
|
+
: {}),
|
|
1240
|
+
...(r.environment ? { environment: r.environment } : {}),
|
|
1241
|
+
}));
|
|
1242
|
+
const data = await callApi("post", `projects/test-cycles/execute-bulk/${projectId}/${cycleId}`, { body: { results: bulkResults } });
|
|
1243
|
+
return jsonResponse(data);
|
|
770
1244
|
}
|
|
771
|
-
//
|
|
772
|
-
async function
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
if (!projectId)
|
|
1245
|
+
// 10. manage_test_plan
|
|
1246
|
+
async function handleManageTestPlan(args) {
|
|
1247
|
+
const projectId = p(args, "projectId", "project_id");
|
|
1248
|
+
const action = p(args, "action");
|
|
1249
|
+
if (!projectId)
|
|
776
1250
|
throw new Error("projectId is required");
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1251
|
+
if (!action)
|
|
1252
|
+
throw new Error("action is required");
|
|
1253
|
+
validateId(projectId, "projectId");
|
|
1254
|
+
switch (action) {
|
|
1255
|
+
case "create": {
|
|
1256
|
+
const title = p(args, "title");
|
|
1257
|
+
if (!title)
|
|
1258
|
+
throw new Error("title is required for create action");
|
|
1259
|
+
const body = { title };
|
|
1260
|
+
if (p(args, "summary"))
|
|
1261
|
+
body.summary = p(args, "summary");
|
|
1262
|
+
if (p(args, "status"))
|
|
1263
|
+
body.status = p(args, "status");
|
|
1264
|
+
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1265
|
+
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1266
|
+
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
1267
|
+
body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
|
|
1268
|
+
if (p(args, "folder_id", "folderId"))
|
|
1269
|
+
body.folder_id = p(args, "folder_id", "folderId");
|
|
1270
|
+
const data = await callApi("post", `projects/test-plans/create-test-plan/${projectId}`, { body });
|
|
1271
|
+
return jsonResponse(data);
|
|
1272
|
+
}
|
|
1273
|
+
case "update": {
|
|
1274
|
+
const planId = p(args, "planId", "plan_id", "id");
|
|
1275
|
+
if (!planId)
|
|
1276
|
+
throw new Error("planId is required for update action");
|
|
1277
|
+
const body = { id: planId };
|
|
1278
|
+
if (p(args, "title"))
|
|
1279
|
+
body.title = p(args, "title");
|
|
1280
|
+
if (p(args, "summary"))
|
|
1281
|
+
body.summary = p(args, "summary");
|
|
1282
|
+
if (p(args, "status"))
|
|
1283
|
+
body.status = p(args, "status");
|
|
1284
|
+
if (p(args, "planned_start_date", "plannedStartDate"))
|
|
1285
|
+
body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
|
|
1286
|
+
if (p(args, "planned_end_date", "plannedEndDate"))
|
|
1287
|
+
body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
|
|
1288
|
+
if (p(args, "folder_id", "folderId"))
|
|
1289
|
+
body.folder_id = p(args, "folder_id", "folderId");
|
|
1290
|
+
const data = await callApi("post", `projects/test-plans/create-test-plan/${projectId}`, { body });
|
|
1291
|
+
return jsonResponse(data);
|
|
1292
|
+
}
|
|
1293
|
+
case "delete": {
|
|
1294
|
+
const ids = p(args, "ids", "planId", "plan_id");
|
|
1295
|
+
if (!ids)
|
|
1296
|
+
throw new Error("ids (or planId) is required for delete action");
|
|
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 } });
|
|
1299
|
+
return jsonResponse(data);
|
|
1300
|
+
}
|
|
1301
|
+
case "list": {
|
|
1302
|
+
const data = await callApi("get", `projects/test-plans/get-test-plans/${projectId}`, {
|
|
1303
|
+
query: {
|
|
1304
|
+
search: p(args, "search"),
|
|
1305
|
+
page: p(args, "page"),
|
|
1306
|
+
limit: p(args, "limit"),
|
|
797
1307
|
},
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
}
|
|
1316
|
+
return jsonResponse(data);
|
|
1317
|
+
}
|
|
1318
|
+
case "get_details": {
|
|
1319
|
+
const planId = p(args, "planId", "plan_id", "id");
|
|
1320
|
+
if (!planId)
|
|
1321
|
+
throw new Error("planId is required for get_details action");
|
|
1322
|
+
validateId(planId, "planId");
|
|
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
|
+
}
|
|
1327
|
+
return jsonResponse(data);
|
|
1328
|
+
}
|
|
1329
|
+
case "link_cycles": {
|
|
1330
|
+
const planId = p(args, "planId", "plan_id", "id");
|
|
1331
|
+
const cycleIds = p(args, "cycle_ids", "cycleIds");
|
|
1332
|
+
if (!planId)
|
|
1333
|
+
throw new Error("planId is required for link_cycles action");
|
|
1334
|
+
if (!cycleIds)
|
|
1335
|
+
throw new Error("cycle_ids is required for link_cycles action");
|
|
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 } });
|
|
1339
|
+
return jsonResponse(data);
|
|
1340
|
+
}
|
|
1341
|
+
case "unlink_cycles": {
|
|
1342
|
+
const planId = p(args, "planId", "plan_id", "id");
|
|
1343
|
+
const cycleIds = p(args, "cycle_ids", "cycleIds");
|
|
1344
|
+
if (!planId)
|
|
1345
|
+
throw new Error("planId is required for unlink_cycles action");
|
|
1346
|
+
if (!cycleIds)
|
|
1347
|
+
throw new Error("cycle_ids is required for unlink_cycles action");
|
|
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 } });
|
|
1351
|
+
return jsonResponse(data);
|
|
1352
|
+
}
|
|
1353
|
+
case "get_cycles": {
|
|
1354
|
+
const planId = p(args, "planId", "plan_id", "id");
|
|
1355
|
+
if (!planId)
|
|
1356
|
+
throw new Error("planId is required for get_cycles action");
|
|
1357
|
+
validateId(planId, "planId");
|
|
1358
|
+
const data = await callApi("get", `projects/test-plans/testcycles/list/${projectId}/${planId}`, {
|
|
1359
|
+
query: {
|
|
1360
|
+
search: p(args, "search"),
|
|
1361
|
+
page: p(args, "page"),
|
|
1362
|
+
limit: p(args, "limit"),
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
return jsonResponse(data);
|
|
1366
|
+
}
|
|
1367
|
+
case "get_testcases": {
|
|
1368
|
+
const planId = p(args, "planId", "plan_id", "id");
|
|
1369
|
+
if (!planId)
|
|
1370
|
+
throw new Error("planId is required for get_testcases action");
|
|
1371
|
+
validateId(planId, "planId");
|
|
1372
|
+
const data = await callApi("get", `projects/test-plans/testcases/list/${projectId}/${planId}`, {
|
|
1373
|
+
query: {
|
|
1374
|
+
search: p(args, "search"),
|
|
1375
|
+
page: p(args, "page"),
|
|
1376
|
+
limit: p(args, "limit"),
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
return jsonResponse(data);
|
|
1380
|
+
}
|
|
1381
|
+
default:
|
|
1382
|
+
throw new Error(`Unknown action: ${action}. Use: create, update, delete, list, get_details, link_cycles, unlink_cycles, get_cycles, get_testcases`);
|
|
800
1383
|
}
|
|
801
|
-
return {
|
|
802
|
-
content: [
|
|
803
|
-
{
|
|
804
|
-
type: "text",
|
|
805
|
-
text: `Current active project: ${projectId}`,
|
|
806
|
-
},
|
|
807
|
-
],
|
|
808
|
-
};
|
|
809
1384
|
}
|
|
810
|
-
//
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1385
|
+
// 11. get_report
|
|
1386
|
+
async function handleGetReport(args) {
|
|
1387
|
+
const projectId = p(args, "projectId", "project_id");
|
|
1388
|
+
const reportType = p(args, "report_type", "reportType");
|
|
1389
|
+
if (!projectId)
|
|
1390
|
+
throw new Error("projectId is required");
|
|
1391
|
+
validateId(projectId, "projectId");
|
|
1392
|
+
if (!reportType)
|
|
1393
|
+
throw new Error("report_type is required");
|
|
1394
|
+
const endpointPath = REPORT_TYPE_MAP[reportType];
|
|
1395
|
+
if (!endpointPath) {
|
|
1396
|
+
throw new Error(`Unknown report_type: ${reportType}. Valid types: ${Object.keys(REPORT_TYPE_MAP).join(", ")}`);
|
|
819
1397
|
}
|
|
1398
|
+
const query = {};
|
|
1399
|
+
// Common filters
|
|
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");
|
|
1404
|
+
if (p(args, "startDate", "start_date"))
|
|
1405
|
+
query.startDate = p(args, "startDate", "start_date");
|
|
1406
|
+
if (p(args, "endDate", "end_date"))
|
|
1407
|
+
query.endDate = p(args, "endDate", "end_date");
|
|
1408
|
+
if (p(args, "granularity"))
|
|
1409
|
+
query.granularity = p(args, "granularity");
|
|
1410
|
+
// cycle_comparison specific
|
|
1411
|
+
if (p(args, "cycle_ids", "cycleIds"))
|
|
1412
|
+
query.cycleIds = p(args, "cycle_ids", "cycleIds");
|
|
1413
|
+
const data = await callApi("get", `projects/reports/${endpointPath}/${projectId}`, {
|
|
1414
|
+
query,
|
|
1415
|
+
});
|
|
1416
|
+
return jsonResponse(data);
|
|
820
1417
|
}
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1418
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1419
|
+
// SERVER SETUP
|
|
1420
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1421
|
+
const server = new Server({ name: "testkase-mcp-server", version: "2.2.0" }, { capabilities: { tools: {} } });
|
|
1422
|
+
// List all 11 tools
|
|
1423
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1424
|
+
tools: [
|
|
1425
|
+
LIST_PROJECTS_TOOL,
|
|
1426
|
+
GET_PROJECT_STRUCTURE_TOOL,
|
|
1427
|
+
SEARCH_TESTCASES_TOOL,
|
|
1428
|
+
GET_TESTCASE_TOOL,
|
|
1429
|
+
MANAGE_TESTCASE_TOOL,
|
|
1430
|
+
MANAGE_FOLDER_TOOL,
|
|
1431
|
+
SEARCH_TEST_CYCLES_TOOL,
|
|
1432
|
+
MANAGE_TEST_CYCLE_TOOL,
|
|
1433
|
+
EXECUTE_TESTS_TOOL,
|
|
1434
|
+
MANAGE_TEST_PLAN_TOOL,
|
|
1435
|
+
GET_REPORT_TOOL,
|
|
1436
|
+
],
|
|
1437
|
+
}));
|
|
1438
|
+
// Route tool calls
|
|
1439
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1440
|
+
const { name, arguments: args } = request.params;
|
|
1441
|
+
try {
|
|
1442
|
+
switch (name) {
|
|
1443
|
+
case "list_projects":
|
|
1444
|
+
return await handleListProjects(args);
|
|
1445
|
+
case "get_project_structure":
|
|
1446
|
+
return await handleGetProjectStructure(args);
|
|
1447
|
+
case "search_testcases":
|
|
1448
|
+
return await handleSearchTestcases(args);
|
|
1449
|
+
case "get_testcase":
|
|
1450
|
+
return await handleGetTestcase(args);
|
|
1451
|
+
case "manage_testcase":
|
|
1452
|
+
return await handleManageTestcase(args);
|
|
1453
|
+
case "manage_folder":
|
|
1454
|
+
return await handleManageFolder(args);
|
|
1455
|
+
case "search_test_cycles":
|
|
1456
|
+
return await handleSearchTestCycles(args);
|
|
1457
|
+
case "manage_test_cycle":
|
|
1458
|
+
return await handleManageTestCycle(args);
|
|
1459
|
+
case "execute_tests":
|
|
1460
|
+
return await handleExecuteTests(args);
|
|
1461
|
+
case "manage_test_plan":
|
|
1462
|
+
return await handleManageTestPlan(args);
|
|
1463
|
+
case "get_report":
|
|
1464
|
+
return await handleGetReport(args);
|
|
1465
|
+
default:
|
|
1466
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
844
1467
|
}
|
|
845
|
-
return new Error(errorMessage);
|
|
846
1468
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1469
|
+
catch (error) {
|
|
1470
|
+
const err = axios.isAxiosError(error) ? handleApiError(error) : (error instanceof Error ? error : new Error(String(error)));
|
|
1471
|
+
const enrichedMessage = enrichErrorMessage(err.message);
|
|
1472
|
+
return {
|
|
1473
|
+
content: [{ type: "text", text: `Error: ${enrichedMessage}` }],
|
|
1474
|
+
isError: true,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
// Start
|
|
850
1479
|
async function main() {
|
|
851
1480
|
const transport = new StdioServerTransport();
|
|
852
1481
|
await server.connect(transport);
|
|
853
|
-
console.error("TestKase MCP Server running on stdio");
|
|
1482
|
+
console.error("TestKase MCP Server v2.2.0 running on stdio (11 tools)");
|
|
854
1483
|
}
|
|
855
1484
|
main().catch((error) => {
|
|
856
1485
|
console.error("Fatal error in main():", error);
|