@testkase/mcp-server 2.0.13 → 2.1.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/build/index.js CHANGED
@@ -3,854 +3,1160 @@ 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
- // Cached organization data (fetched once from PAT token)
12
- let cachedOrganizationId = null;
13
- // Cached project ID (set when user works on a project)
14
- let cachedProjectId = null;
15
- // Helper to get organization ID (fetches and caches on first call)
16
- async function getOrganizationId(providedOrgId) {
17
- // If explicitly provided, use it
18
- if (providedOrgId) {
19
- return providedOrgId;
10
+ // ─── Auth helpers (reused from v1) ───────────────────────────────────────────
11
+ function validateAuth() {
12
+ if (!config.patToken) {
13
+ throw new Error("Authentication required. Please set TESTKASE_PAT_TOKEN environment variable. " +
14
+ "Generate a Personal Access Token (PAT) from your TestKase account settings.");
20
15
  }
21
- // If cached, return cached value
22
- if (cachedOrganizationId !== null) {
23
- return cachedOrganizationId;
16
+ if (!config.patToken.startsWith("xyz_")) {
17
+ throw new Error("Invalid PAT token format. TestKase PAT tokens must start with 'xyz_' prefix. " +
18
+ "Please generate a valid PAT from your TestKase account settings.");
24
19
  }
25
- // Fetch from API using PAT token
20
+ }
21
+ function getApiHeaders() {
22
+ return {
23
+ "Content-Type": "application/json",
24
+ Authorization: `Bearer ${config.patToken}`,
25
+ };
26
+ }
27
+ // ─── Cached org ID (auto-resolved from PAT, never a tool param) ─────────────
28
+ let cachedOrganizationId = null;
29
+ async function getOrganizationId() {
30
+ if (cachedOrganizationId !== null)
31
+ return cachedOrganizationId;
26
32
  validateAuth();
27
33
  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
- });
34
+ const response = await axios.get(`${config.apiBaseUrl}/api/v1/organizations/my-organization`, { headers: getApiHeaders() });
34
35
  const orgData = response.data?.data || response.data;
35
36
  if (orgData?.organization_id) {
36
- const orgId = orgData.organization_id;
37
- cachedOrganizationId = orgId;
38
- return orgId;
37
+ cachedOrganizationId = orgData.organization_id;
38
+ return cachedOrganizationId;
39
39
  }
40
40
  throw new Error("Could not determine organization from PAT token");
41
41
  }
42
42
  catch (error) {
43
- throw new Error("Failed to fetch organization. Please provide organizationId or check your PAT token.");
43
+ if (error instanceof Error && error.message.includes("Could not determine"))
44
+ throw error;
45
+ throw new Error("Failed to fetch organization. Check your PAT token.");
44
46
  }
45
47
  }
46
- // Helper to get project ID (uses cached value if available)
47
- function getProjectId(providedProjectId) {
48
- // If explicitly provided, cache it and use it
49
- if (providedProjectId) {
50
- cachedProjectId = providedProjectId;
51
- return providedProjectId;
48
+ // ─── API call helper ─────────────────────────────────────────────────────────
49
+ async function callApi(method, path, options = {}) {
50
+ validateAuth();
51
+ const orgId = options.skipOrgId ? undefined : await getOrganizationId();
52
+ const queryParams = new URLSearchParams();
53
+ if (orgId)
54
+ queryParams.append("organizationId", orgId.toString());
55
+ if (options.query) {
56
+ for (const [key, val] of Object.entries(options.query)) {
57
+ if (val !== undefined && val !== null && val !== "") {
58
+ queryParams.append(key, String(val));
59
+ }
60
+ }
52
61
  }
53
- // If cached, return cached value
54
- if (cachedProjectId !== null) {
55
- return cachedProjectId;
62
+ const qs = queryParams.toString();
63
+ const url = `${config.apiBaseUrl}/api/v1/${path}${qs ? `?${qs}` : ""}`;
64
+ const response = await axios({
65
+ method,
66
+ url,
67
+ headers: getApiHeaders(),
68
+ ...(options.body !== undefined && { data: options.body }),
69
+ });
70
+ return response.data;
71
+ }
72
+ // ─── Error handler (reused) ──────────────────────────────────────────────────
73
+ function handleApiError(error) {
74
+ if (axios.isAxiosError(error)) {
75
+ const e = error;
76
+ let msg = `API request failed: ${e.message}`;
77
+ if (e.response) {
78
+ msg += `\nStatus: ${e.response.status}`;
79
+ msg += `\nResponse: ${JSON.stringify(e.response.data)}`;
80
+ }
81
+ return new Error(msg);
56
82
  }
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.");
83
+ return error;
61
84
  }
62
- // Helper to set active project (for explicit project switching)
63
- function setActiveProject(projectId) {
64
- cachedProjectId = projectId;
85
+ // ─── JSON response helper ────────────────────────────────────────────────────
86
+ function jsonResponse(data) {
87
+ return {
88
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
89
+ };
65
90
  }
66
- // Tool definitions
67
- const GET_TESTCASES_TOOL = {
68
- name: "get_testcases",
69
- description: "Retrieve testcases from the active project. Returns list with title, status, priority, etc. " +
70
- "Supports search and filters. Call get_my_projects first if no active project is set.",
91
+ // ─── Param normalizer (handles camelCase / snake_case) ───────────────────────
92
+ function p(args, ...names) {
93
+ for (const n of names) {
94
+ if (args[n] !== undefined)
95
+ return args[n];
96
+ }
97
+ return undefined;
98
+ }
99
+ // ═══════════════════════════════════════════════════════════════════════════════
100
+ // TOOL DEFINITIONS (11 tools)
101
+ // ═══════════════════════════════════════════════════════════════════════════════
102
+ const LIST_PROJECTS_TOOL = {
103
+ name: "list_projects",
104
+ description: "List all projects the authenticated user has access to. " +
105
+ "Use this first to find a projectId, then pass it to other tools.",
71
106
  inputSchema: {
72
107
  type: "object",
73
108
  properties: {
74
- search: { type: "string", description: "Search term to filter by title or summary" },
109
+ search: { type: "string", description: "Search by project name or ID" },
75
110
  page: { type: "number", description: "Page number (default: 1)" },
76
111
  limit: { type: "number", description: "Items per page (default: 20)" },
77
- filters: { type: "string", description: "JSON filters. Example: {\"status\":[\"active\"],\"priority\":[\"high\"]}" },
78
112
  },
79
113
  required: [],
80
114
  },
81
115
  };
82
- const GET_TESTCASE_DETAIL_TOOL = {
83
- name: "get_testcase_detail",
84
- description: "Get full details of a testcase including test steps and attachments.",
116
+ const GET_PROJECT_STRUCTURE_TOOL = {
117
+ name: "get_project_structure",
118
+ description: "Get the structure of a project: folders, labels, and/or team members. " +
119
+ "Useful to discover folder IDs, label names, and member IDs before creating or updating items.",
85
120
  inputSchema: {
86
121
  type: "object",
87
122
  properties: {
88
- testcaseId: { type: "string", description: "The testcase ID" },
123
+ projectId: { type: "string", description: "Project ID (e.g. PRJ-1001)" },
124
+ section: {
125
+ type: "string",
126
+ enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
127
+ description: "Folder section (default: TESTCASE)",
128
+ },
129
+ include: {
130
+ type: "string",
131
+ description: "Comma-separated list of what to include: folders,labels,members (default: all)",
132
+ },
89
133
  },
90
- required: ["testcaseId"],
134
+ required: ["projectId"],
91
135
  },
92
136
  };
93
- const GET_FOLDERS_TOOL = {
94
- name: "get_folders",
95
- description: "Get folder structure of the active project. Use folder IDs when creating testcases.",
137
+ const SEARCH_TESTCASES_TOOL = {
138
+ name: "search_testcases",
139
+ description: "Search and list test cases in a project. Supports text search, filters, sorting, and pagination.",
96
140
  inputSchema: {
97
141
  type: "object",
98
142
  properties: {
99
- search: { type: "string", description: "Search term to filter folders" },
143
+ projectId: { type: "string", description: "Project ID" },
144
+ search: { type: "string", description: "Search text (title or summary)" },
145
+ filters: {
146
+ type: "string",
147
+ description: 'JSON string of filters. Example: {"status":["active"],"priority":["high","critical"]}',
148
+ },
149
+ page: { type: "number", description: "Page number (default: 1)" },
150
+ limit: { type: "number", description: "Items per page (default: 20)" },
151
+ sortBy: { type: "string", description: "Field to sort by (e.g. created_at, title)" },
152
+ sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
100
153
  },
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: [],
154
+ required: ["projectId"],
121
155
  },
122
156
  };
123
- const CREATE_TESTCASE_TOOL = {
124
- name: "create_testcase",
125
- description: "Create a new testcase in the active project.",
157
+ const GET_TESTCASE_TOOL = {
158
+ name: "get_testcase",
159
+ description: "Get full details of a single test case including test steps and attachments.",
126
160
  inputSchema: {
127
161
  type: "object",
128
162
  properties: {
129
- title: { type: "string", description: "Testcase title (required)" },
130
- summary: { type: "string", description: "Description/summary" },
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\":\"...\"}]" },
163
+ projectId: { type: "string", description: "Project ID" },
164
+ testcaseId: { type: "string", description: "Test case ID (e.g. TEST-123)" },
134
165
  },
135
- required: ["title"],
166
+ required: ["projectId", "testcaseId"],
136
167
  },
137
168
  };
138
- const CREATE_BULK_TESTCASE_TOOL = {
139
- name: "create_bulk_testcase",
140
- description: "Create multiple testcases at once in the active project.",
169
+ const MANAGE_TESTCASE_TOOL = {
170
+ name: "manage_testcase",
171
+ description: "Create, bulk create, update, or delete test cases. " +
172
+ "Set 'action' to: create, create_bulk, update, or delete.",
141
173
  inputSchema: {
142
174
  type: "object",
143
175
  properties: {
144
- testcases: { type: "string", description: "JSON array of testcases, each with title, summary, priority, test_steps" },
176
+ projectId: { type: "string", description: "Project ID" },
177
+ action: {
178
+ type: "string",
179
+ enum: ["create", "create_bulk", "update", "delete"],
180
+ description: "Action to perform",
181
+ },
182
+ // create params
183
+ title: { type: "string", description: "[create] Test case title" },
184
+ summary: { type: "string", description: "[create] Description/summary" },
185
+ priority: {
186
+ type: "string",
187
+ description: "[create] Priority: low, medium, high, or critical",
188
+ },
189
+ folder_id: { type: "string", description: "[create] Folder ID to place the test case in" },
190
+ labels: {
191
+ type: "string",
192
+ description: "[create] Comma-separated label names",
193
+ },
194
+ test_steps: {
195
+ type: "string",
196
+ description: '[create] JSON array of test steps: [{"step":"...","expected_result":"..."}]',
197
+ },
198
+ // create_bulk params
199
+ testcases: {
200
+ type: "string",
201
+ description: "[create_bulk] JSON array of test cases, each with title, summary, priority, folder_id, test_steps",
202
+ },
203
+ // update params
204
+ ids: {
205
+ type: "string",
206
+ description: "[update/delete] Comma-separated test case IDs",
207
+ },
208
+ field: {
209
+ type: "string",
210
+ description: "[update] Field name to update (title, summary, priority, status, etc.)",
211
+ },
212
+ value: { type: "string", description: "[update] New value for the field" },
145
213
  },
146
- required: ["testcases"],
214
+ required: ["projectId", "action"],
147
215
  },
148
216
  };
149
- const UPDATE_TESTCASE_FIELD_TOOL = {
150
- name: "update_testcase_field",
151
- description: "Update a field for one or multiple testcases.",
217
+ const MANAGE_FOLDER_TOOL = {
218
+ name: "manage_folder",
219
+ description: "Create, rename, move, or delete folders. Works across all sections (TESTCASE, TEST_CYCLE, TEST_PLAN).",
152
220
  inputSchema: {
153
221
  type: "object",
154
222
  properties: {
155
- ids: { type: "string", description: "Comma-separated testcase IDs" },
156
- field: { type: "string", description: "Field name (title, summary, priority, status, etc.)" },
157
- value: { type: "string", description: "New value" },
223
+ projectId: { type: "string", description: "Project ID" },
224
+ action: {
225
+ type: "string",
226
+ enum: ["create", "rename", "move", "delete"],
227
+ description: "Action to perform",
228
+ },
229
+ section: {
230
+ type: "string",
231
+ enum: ["TESTCASE", "TEST_CYCLE", "TEST_PLAN"],
232
+ description: "Folder section (default: TESTCASE)",
233
+ },
234
+ name: { type: "string", description: "[create/rename] Folder name (max 50 chars)" },
235
+ folderId: {
236
+ type: "number",
237
+ description: "[rename/move/delete] ID of the folder to modify",
238
+ },
239
+ parentId: {
240
+ type: "number",
241
+ description: "[create] Parent folder ID for subfolder. [move] New parent ID (null for root).",
242
+ },
158
243
  },
159
- required: ["ids", "field", "value"],
244
+ required: ["projectId", "action"],
160
245
  },
161
246
  };
162
- const DELETE_TESTCASE_TOOL = {
163
- name: "delete_testcase",
164
- description: "Delete one or multiple testcases. ALWAYS confirm with user before deleting.",
247
+ const SEARCH_TEST_CYCLES_TOOL = {
248
+ name: "search_test_cycles",
249
+ description: "List and search test cycles in a project with execution progress.",
165
250
  inputSchema: {
166
251
  type: "object",
167
252
  properties: {
168
- ids: { type: "string", description: "Comma-separated testcase IDs to delete" },
253
+ projectId: { type: "string", description: "Project ID" },
254
+ search: { type: "string", description: "Search by title" },
255
+ folderId: { type: "number", description: "Filter by folder ID" },
256
+ status: { type: "string", description: "Filter by status (open, closed, etc.)" },
257
+ page: { type: "number", description: "Page number (default: 1)" },
258
+ limit: { type: "number", description: "Items per page (default: 20)" },
169
259
  },
170
- required: ["ids"],
260
+ required: ["projectId"],
171
261
  },
172
262
  };
173
- const CREATE_FOLDER_TOOL = {
174
- name: "create_folder",
175
- description: "Create a new folder in the active project for organizing testcases.",
263
+ const MANAGE_TEST_CYCLE_TOOL = {
264
+ name: "manage_test_cycle",
265
+ description: "Manage test cycles: create, update, delete, get details, list/link/unlink/assign test cases. " +
266
+ "Set 'action' to: create, update, delete, get_details, get_testcases, link_testcases, unlink_testcases, assign_testcases.",
176
267
  inputSchema: {
177
268
  type: "object",
178
269
  properties: {
179
- name: { type: "string", description: "Folder name (max 50 characters)" },
180
- parentId: { type: "number", description: "Parent folder ID for subfolder (optional)" },
270
+ projectId: { type: "string", description: "Project ID" },
271
+ action: {
272
+ type: "string",
273
+ enum: [
274
+ "create",
275
+ "update",
276
+ "delete",
277
+ "get_details",
278
+ "get_testcases",
279
+ "link_testcases",
280
+ "unlink_testcases",
281
+ "assign_testcases",
282
+ ],
283
+ description: "Action to perform",
284
+ },
285
+ // Identifiers
286
+ cycleId: {
287
+ type: "string",
288
+ description: "Test cycle ID (e.g. TCYCLE-1). Required for update/delete/get_details/get_testcases/link/unlink/assign.",
289
+ },
290
+ // create/update fields
291
+ title: { type: "string", description: "[create/update] Cycle title" },
292
+ summary: { type: "string", description: "[create/update] Description" },
293
+ status: {
294
+ type: "string",
295
+ description: "[create/update] Status: open, closed, in progress",
296
+ },
297
+ planned_start_date: {
298
+ type: "string",
299
+ description: "[create/update] Start date (YYYY-MM-DD)",
300
+ },
301
+ planned_end_date: {
302
+ type: "string",
303
+ description: "[create/update] End date (YYYY-MM-DD)",
304
+ },
305
+ folder_id: {
306
+ type: "number",
307
+ description: "[create/update] Folder ID",
308
+ },
309
+ // delete
310
+ ids: {
311
+ type: "string",
312
+ description: "[delete] Comma-separated cycle IDs to delete",
313
+ },
314
+ // link/unlink/assign
315
+ testcase_ids: {
316
+ type: "string",
317
+ description: "[link_testcases/unlink_testcases/assign_testcases] Comma-separated test case IDs",
318
+ },
319
+ assignee_id: {
320
+ type: "number",
321
+ description: "[assign_testcases] User ID to assign",
322
+ },
323
+ // get_testcases params
324
+ search: { type: "string", description: "[get_testcases] Search text" },
325
+ page: { type: "number", description: "[get_testcases] Page number" },
326
+ limit: { type: "number", description: "[get_testcases] Items per page" },
181
327
  },
182
- required: ["name"],
328
+ required: ["projectId", "action"],
183
329
  },
184
330
  };
185
- const CREATE_LABEL_TOOL = {
186
- name: "create_label",
187
- description: "Create new labels in the active project for categorizing testcases.",
331
+ const EXECUTE_TESTS_TOOL = {
332
+ name: "execute_tests",
333
+ description: "Record test execution results for test cases in a test cycle. " +
334
+ "Pass a single result or an array of results. " +
335
+ "Valid execution statuses: pass, fail, blocked, not_executed, in_progress.",
188
336
  inputSchema: {
189
337
  type: "object",
190
338
  properties: {
191
- values: { type: "string", description: "Comma-separated label names" },
339
+ projectId: { type: "string", description: "Project ID" },
340
+ cycleId: { type: "string", description: "Test cycle ID (e.g. TCYCLE-1)" },
341
+ results: {
342
+ type: "string",
343
+ description: 'JSON array of results: [{"testcase_id":"TEST-1","execution_status":"pass","actual_result":"Worked","environment":"staging"}]. ' +
344
+ "Each object must have testcase_id and execution_status. actual_result and environment are optional.",
345
+ },
192
346
  },
193
- required: ["values"],
347
+ required: ["projectId", "cycleId", "results"],
194
348
  },
195
349
  };
196
- const GET_USER_PROFILE_TOOL = {
197
- name: "get_user_profile",
198
- description: "Get current logged-in user profile information",
350
+ const MANAGE_TEST_PLAN_TOOL = {
351
+ name: "manage_test_plan",
352
+ description: "Manage test plans: create, update, delete, list, get details, link/unlink cycles, get cycles, get test cases. " +
353
+ "Set 'action' to: create, update, delete, list, get_details, link_cycles, unlink_cycles, get_cycles, get_testcases.",
199
354
  inputSchema: {
200
355
  type: "object",
201
- properties: {},
202
- required: [],
356
+ properties: {
357
+ projectId: { type: "string", description: "Project ID" },
358
+ action: {
359
+ type: "string",
360
+ enum: [
361
+ "create",
362
+ "update",
363
+ "delete",
364
+ "list",
365
+ "get_details",
366
+ "link_cycles",
367
+ "unlink_cycles",
368
+ "get_cycles",
369
+ "get_testcases",
370
+ ],
371
+ description: "Action to perform",
372
+ },
373
+ // Identifier
374
+ planId: {
375
+ type: "string",
376
+ description: "Test plan ID (e.g. TPLAN-1). Required for update/delete/get_details/link_cycles/unlink_cycles/get_cycles/get_testcases.",
377
+ },
378
+ // create/update fields
379
+ title: { type: "string", description: "[create/update] Plan title" },
380
+ summary: { type: "string", description: "[create/update] Description" },
381
+ status: { type: "string", description: "[create/update] Status" },
382
+ planned_start_date: {
383
+ type: "string",
384
+ description: "[create/update] Start date (YYYY-MM-DD)",
385
+ },
386
+ planned_end_date: {
387
+ type: "string",
388
+ description: "[create/update] End date (YYYY-MM-DD)",
389
+ },
390
+ folder_id: { type: "number", description: "[create/update] Folder ID" },
391
+ // delete
392
+ ids: {
393
+ type: "string",
394
+ description: "[delete] Comma-separated plan IDs to delete",
395
+ },
396
+ // link/unlink cycles
397
+ cycle_ids: {
398
+ type: "string",
399
+ description: "[link_cycles/unlink_cycles] Comma-separated test cycle IDs",
400
+ },
401
+ // list/get_cycles/get_testcases pagination
402
+ search: { type: "string", description: "[list/get_cycles/get_testcases] Search text" },
403
+ page: { type: "number", description: "[list/get_cycles/get_testcases] Page number" },
404
+ limit: { type: "number", description: "[list/get_cycles/get_testcases] Items per page" },
405
+ },
406
+ required: ["projectId", "action"],
203
407
  },
204
408
  };
205
- const SET_ACTIVE_PROJECT_TOOL = {
206
- name: "set_active_project",
207
- description: "Set active project AFTER user chooses from get_my_projects. All other tools use this project.",
409
+ const GET_REPORT_TOOL = {
410
+ name: "get_report",
411
+ description: "Get reports and analytics for a project. Covers execution, coverage, trends, team, defects, and AI insights. " +
412
+ "Available report types: execution_summary, execution_by_cycle, execution_by_tester, execution_by_priority, " +
413
+ "execution_by_environment, execution_by_folder, requirement_coverage, traceability_matrix, " +
414
+ "failed_requirements, uncovered_requirements, testcase_coverage, unlinked_testcases, " +
415
+ "defects_by_folder, tester_workload, testcase_distribution, " +
416
+ "execution_trend, execution_burnup, execution_burndown, test_creation, cycle_comparison, " +
417
+ "scorecard_by_folder, scorecard_by_tester, created_vs_executed, execution_by_automation, " +
418
+ "defects_by_cycle, defects_by_tester, requirement_coverage_trend, " +
419
+ "flaky_tests, release_readiness, risk_heatmap_folder, risk_heatmap_feature, " +
420
+ "testcase_quality, predictive_failure, smart_prioritization, tester_effectiveness, " +
421
+ "stale_tests, defect_hotspots, cycle_health, suite_optimization, " +
422
+ "requirement_risk_matrix, execution_velocity, project_health.",
208
423
  inputSchema: {
209
424
  type: "object",
210
425
  properties: {
211
- projectId: { type: "string", description: "Project ID chosen by user" },
426
+ projectId: { type: "string", description: "Project ID" },
427
+ report_type: {
428
+ type: "string",
429
+ description: "Report type (see description for full list)",
430
+ },
431
+ test_cycle_ids: {
432
+ type: "string",
433
+ description: "Comma-separated test cycle IDs to filter by",
434
+ },
435
+ test_plan_ids: {
436
+ type: "string",
437
+ description: "Comma-separated test plan IDs to filter by",
438
+ },
439
+ startDate: { type: "string", description: "Start date filter (YYYY-MM-DD)" },
440
+ endDate: { type: "string", description: "End date filter (YYYY-MM-DD)" },
441
+ granularity: {
442
+ type: "string",
443
+ enum: ["day", "week", "month"],
444
+ description: "Granularity for trend reports",
445
+ },
446
+ // cycle_comparison specific
447
+ cycle_ids: {
448
+ type: "string",
449
+ description: "[cycle_comparison] Comma-separated cycle IDs to compare",
450
+ },
212
451
  },
213
- required: ["projectId"],
452
+ required: ["projectId", "report_type"],
214
453
  },
215
454
  };
216
- const GET_ACTIVE_PROJECT_TOOL = {
217
- name: "get_active_project",
218
- description: "Get the currently active project ID that is being used for operations",
219
- inputSchema: {
220
- type: "object",
221
- properties: {},
222
- required: [],
223
- },
455
+ // ═══════════════════════════════════════════════════════════════════════════════
456
+ // REPORT TYPE → ENDPOINT MAPPING
457
+ // ═══════════════════════════════════════════════════════════════════════════════
458
+ const REPORT_TYPE_MAP = {
459
+ // Execution
460
+ execution_summary: "execution/summary",
461
+ execution_by_cycle: "execution/by-cycle",
462
+ execution_by_tester: "execution/by-tester",
463
+ execution_by_priority: "execution/by-priority",
464
+ execution_by_environment: "execution/by-environment",
465
+ execution_by_folder: "execution/by-folder",
466
+ // Coverage
467
+ requirement_coverage: "coverage/requirement-coverage",
468
+ traceability_matrix: "coverage/traceability-matrix",
469
+ failed_requirements: "coverage/failed-requirements",
470
+ uncovered_requirements: "coverage/uncovered-requirements",
471
+ testcase_coverage: "coverage/testcase-coverage",
472
+ unlinked_testcases: "coverage/unlinked-testcases",
473
+ // Defects
474
+ defects_by_folder: "defects/by-folder",
475
+ // Team
476
+ tester_workload: "team/tester-workload",
477
+ testcase_distribution: "team/testcase-distribution",
478
+ // Trends
479
+ execution_trend: "trends/execution-trend",
480
+ execution_burnup: "trends/execution-burnup",
481
+ execution_burndown: "trends/execution-burndown",
482
+ test_creation: "trends/test-creation",
483
+ cycle_comparison: "trends/cycle-comparison",
484
+ scorecard_by_folder: "trends/scorecard-by-folder",
485
+ scorecard_by_tester: "trends/scorecard-by-tester",
486
+ created_vs_executed: "trends/created-vs-executed",
487
+ execution_by_automation: "trends/execution-by-automation",
488
+ defects_by_cycle: "trends/defects-by-cycle",
489
+ defects_by_tester: "trends/defects-by-tester",
490
+ requirement_coverage_trend: "trends/requirement-coverage-trend",
491
+ // AI Insights
492
+ flaky_tests: "ai/flaky-tests",
493
+ release_readiness: "ai/release-readiness",
494
+ risk_heatmap_folder: "ai/risk-heatmap-folder",
495
+ risk_heatmap_feature: "ai/risk-heatmap-feature",
496
+ testcase_quality: "ai/testcase-quality",
497
+ predictive_failure: "ai/predictive-failure",
498
+ smart_prioritization: "ai/smart-prioritization",
499
+ tester_effectiveness: "ai/tester-effectiveness",
500
+ stale_tests: "ai/stale-tests",
501
+ defect_hotspots: "ai/defect-hotspots",
502
+ cycle_health: "ai/cycle-health",
503
+ suite_optimization: "ai/suite-optimization",
504
+ requirement_risk_matrix: "ai/requirement-risk-matrix",
505
+ execution_velocity: "ai/execution-velocity",
506
+ project_health: "ai/project-health",
224
507
  };
225
- // Create server instance
226
- const server = new Server({
227
- name: "testkase-mcp-server",
228
- version: "1.0.0",
229
- }, {
230
- capabilities: {
231
- tools: {},
232
- },
233
- });
234
- // List available tools (14 tools)
235
- server.setRequestHandler(ListToolsRequestSchema, async () => {
236
- return {
237
- tools: [
238
- GET_MY_PROJECTS_TOOL,
239
- SET_ACTIVE_PROJECT_TOOL,
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);
508
+ // ═══════════════════════════════════════════════════════════════════════════════
509
+ // TOOL HANDLERS
510
+ // ═══════════════════════════════════════════════════════════════════════════════
511
+ // 1. list_projects
512
+ async function handleListProjects(args) {
513
+ const data = await callApi("get", "projects/user/my-projects", {
514
+ query: {
515
+ search: p(args, "search"),
516
+ page: p(args, "page"),
517
+ limit: p(args, "limit"),
518
+ },
519
+ });
520
+ return jsonResponse(data);
521
+ }
522
+ // 2. get_project_structure
523
+ async function handleGetProjectStructure(args) {
524
+ const projectId = p(args, "projectId", "project_id");
525
+ if (!projectId)
526
+ throw new Error("projectId is required");
527
+ const section = p(args, "section") || "TESTCASE";
528
+ const includeRaw = p(args, "include") || "folders,labels,members";
529
+ const includes = includeRaw.split(",").map((s) => s.trim().toLowerCase());
530
+ const orgId = await getOrganizationId();
531
+ const result = {};
532
+ const promises = [];
533
+ if (includes.includes("folders")) {
534
+ promises.push(callApi("get", "folders/get-folders", {
535
+ query: { projectId, section },
536
+ }).then((data) => {
537
+ result.folders = data;
538
+ }));
539
+ }
540
+ if (includes.includes("labels")) {
541
+ promises.push(callApi("get", `projects/labels/get-label/${projectId}`).then((data) => {
542
+ result.labels = data;
543
+ }));
544
+ }
545
+ if (includes.includes("members")) {
546
+ promises.push(callApi("get", `projects/${projectId}/team-members`).then((data) => {
547
+ result.members = data;
548
+ }));
549
+ }
550
+ await Promise.all(promises);
551
+ return jsonResponse(result);
552
+ }
553
+ // 3. search_testcases
554
+ async function handleSearchTestcases(args) {
555
+ const projectId = p(args, "projectId", "project_id");
556
+ if (!projectId)
557
+ throw new Error("projectId is required");
558
+ const data = await callApi("get", `projects/testcases/get-testcase/${projectId}`, {
559
+ query: {
560
+ search: p(args, "search"),
561
+ filters: p(args, "filters"),
562
+ page: p(args, "page"),
563
+ limit: p(args, "limit"),
564
+ sortBy: p(args, "sortBy", "sort_by"),
565
+ sortOrder: p(args, "sortOrder", "sort_order"),
566
+ },
567
+ });
568
+ return jsonResponse(data);
569
+ }
570
+ // 4. get_testcase
571
+ async function handleGetTestcase(args) {
572
+ const projectId = p(args, "projectId", "project_id");
573
+ const testcaseId = p(args, "testcaseId", "testcase_id", "id");
574
+ if (!projectId)
575
+ throw new Error("projectId is required");
576
+ if (!testcaseId)
577
+ throw new Error("testcaseId is required");
578
+ const data = await callApi("get", `projects/testcases/get-testcase-detail/${projectId}/${testcaseId}`);
579
+ return jsonResponse(data);
580
+ }
581
+ // 5. manage_testcase
582
+ async function handleManageTestcase(args) {
583
+ const projectId = p(args, "projectId", "project_id");
584
+ const action = p(args, "action");
585
+ if (!projectId)
586
+ throw new Error("projectId is required");
587
+ if (!action)
588
+ throw new Error("action is required");
589
+ const orgId = await getOrganizationId();
590
+ switch (action) {
591
+ case "create": {
592
+ const title = p(args, "title", "name");
593
+ if (!title)
594
+ throw new Error("title is required for create action");
595
+ const testcaseData = { title };
596
+ if (p(args, "summary", "description"))
597
+ testcaseData.summary = p(args, "summary", "description");
598
+ if (p(args, "priority"))
599
+ testcaseData.priority = p(args, "priority");
600
+ if (p(args, "folder_id", "folderId"))
601
+ testcaseData.folder_id = p(args, "folder_id", "folderId");
602
+ const labelsRaw = p(args, "labels");
603
+ if (labelsRaw) {
604
+ testcaseData.labels =
605
+ typeof labelsRaw === "string"
606
+ ? labelsRaw.split(",").map((l) => l.trim())
607
+ : labelsRaw;
608
+ }
609
+ // Parse test_steps
610
+ const stepsRaw = p(args, "test_steps", "testSteps", "steps");
611
+ const testSteps = stepsRaw
612
+ ? typeof stepsRaw === "string"
613
+ ? JSON.parse(stepsRaw)
614
+ : stepsRaw
615
+ : null;
616
+ // Step 1: Create testcase
617
+ const createResult = await callApi("post", `projects/testcases/create-testcase/${projectId}`, { body: testcaseData });
618
+ // Step 2: Add test steps if provided
619
+ const created = createResult?.data || createResult;
620
+ const tcId = created?.id || created?.testcaseId;
621
+ if (testSteps && Array.isArray(testSteps) && testSteps.length > 0 && tcId) {
622
+ await callApi("post", `projects/testcases/teststeps/${projectId}/${tcId}`, {
623
+ body: { steps: testSteps },
624
+ });
625
+ }
626
+ return jsonResponse(createResult);
270
627
  }
271
- else if (name === "get_testcase_detail") {
272
- return await handleGetTestcaseDetail(args);
628
+ case "create_bulk": {
629
+ const rawTestcases = p(args, "testcases", "test_cases", "items");
630
+ if (!rawTestcases)
631
+ throw new Error("testcases is required for create_bulk action");
632
+ const testcases = typeof rawTestcases === "string" ? JSON.parse(rawTestcases) : rawTestcases;
633
+ // Separate test_steps for post-creation
634
+ const withSteps = testcases.map((tc) => {
635
+ const normalized = { title: tc.title || tc.name };
636
+ if (tc.summary || tc.description)
637
+ normalized.summary = tc.summary || tc.description;
638
+ if (tc.priority)
639
+ normalized.priority = tc.priority;
640
+ if (tc.folder_id || tc.folderId)
641
+ normalized.folder_id = tc.folder_id || tc.folderId;
642
+ return {
643
+ testcase: normalized,
644
+ test_steps: tc.test_steps || tc.testSteps || tc.steps,
645
+ };
646
+ });
647
+ const cleanTestcases = withSteps.map((t) => t.testcase);
648
+ const bulkResult = await callApi("post", `projects/testcases/create-bulk-testcase/${projectId}`, { body: { testcases: cleanTestcases } });
649
+ // Add test steps for each created testcase
650
+ const results = bulkResult?.results || [];
651
+ let stepsCreated = 0;
652
+ for (let i = 0; i < results.length; i++) {
653
+ const r = results[i];
654
+ const steps = withSteps[i]?.test_steps;
655
+ if (r.status === "success" && steps?.length) {
656
+ const tcId = r.testcaseId || r.id;
657
+ if (tcId) {
658
+ try {
659
+ await callApi("post", `projects/testcases/teststeps/${projectId}/${tcId}`, {
660
+ body: { steps },
661
+ });
662
+ stepsCreated++;
663
+ }
664
+ catch {
665
+ // Non-fatal: steps failed but testcase was created
666
+ }
667
+ }
668
+ }
669
+ }
670
+ return jsonResponse({ ...bulkResult, steps_created_for: stepsCreated });
273
671
  }
274
- else if (name === "create_testcase") {
275
- return await handleCreateTestcase(args);
672
+ case "update": {
673
+ const ids = p(args, "ids", "testcase_ids", "testcaseIds");
674
+ const field = p(args, "field", "field_name", "fieldName");
675
+ const value = p(args, "value", "field_value", "fieldValue", "new_value");
676
+ if (!ids)
677
+ throw new Error("ids is required for update action");
678
+ if (!field)
679
+ throw new Error("field is required for update action");
680
+ if (value === undefined)
681
+ throw new Error("value is required for update action");
682
+ const idList = typeof ids === "string" ? ids.split(",").map((s) => s.trim()) : ids;
683
+ const data = await callApi("patch", `projects/testcases/update-field/${projectId}`, {
684
+ body: { ids: idList, field, value },
685
+ });
686
+ return jsonResponse(data);
276
687
  }
277
- else if (name === "create_bulk_testcase") {
278
- return await handleCreateBulkTestcase(args);
688
+ case "delete": {
689
+ const ids = p(args, "ids", "testcase_ids", "testcaseIds");
690
+ if (!ids)
691
+ throw new Error("ids is required for delete action");
692
+ const idList = typeof ids === "string" ? ids.split(",").map((s) => s.trim()) : ids;
693
+ const data = await callApi("delete", `projects/testcases/delete-testcase/${projectId}`, {
694
+ body: { ids: idList },
695
+ });
696
+ return jsonResponse(data);
279
697
  }
280
- else if (name === "update_testcase_field") {
281
- return await handleUpdateTestcaseField(args);
698
+ default:
699
+ throw new Error(`Unknown action: ${action}. Use: create, create_bulk, update, delete`);
700
+ }
701
+ }
702
+ // 6. manage_folder
703
+ async function handleManageFolder(args) {
704
+ const projectId = p(args, "projectId", "project_id");
705
+ const action = p(args, "action");
706
+ if (!projectId)
707
+ throw new Error("projectId is required");
708
+ if (!action)
709
+ throw new Error("action is required");
710
+ const section = p(args, "section") || "TESTCASE";
711
+ const orgId = await getOrganizationId();
712
+ switch (action) {
713
+ case "create": {
714
+ const name = p(args, "name", "folder_name");
715
+ if (!name)
716
+ throw new Error("name is required for create action");
717
+ const body = {
718
+ organizationId: orgId,
719
+ projectId,
720
+ section,
721
+ name,
722
+ };
723
+ const parentId = p(args, "parentId", "parent_id");
724
+ if (parentId)
725
+ body.parentId = parentId;
726
+ const data = await callApi("post", "folders/create-folders", { body, skipOrgId: true });
727
+ return jsonResponse(data);
282
728
  }
283
- else if (name === "delete_testcase") {
284
- return await handleDeleteTestcase(args);
729
+ case "rename": {
730
+ const folderId = p(args, "folderId", "folder_id", "id");
731
+ const name = p(args, "name", "folder_name");
732
+ if (!folderId)
733
+ throw new Error("folderId is required for rename action");
734
+ if (!name)
735
+ throw new Error("name is required for rename action");
736
+ const data = await callApi("patch", `folders/${folderId}`, {
737
+ query: { projectId, section },
738
+ body: { name },
739
+ });
740
+ return jsonResponse(data);
285
741
  }
286
- else if (name === "get_folders") {
287
- return await handleGetFolders(args);
742
+ case "move": {
743
+ const folderId = p(args, "folderId", "folder_id", "id");
744
+ if (!folderId)
745
+ throw new Error("folderId is required for move action");
746
+ const parentId = p(args, "parentId", "parent_id");
747
+ const data = await callApi("patch", `folders/${folderId}/move`, {
748
+ query: { projectId, section },
749
+ body: { parentId: parentId ?? null },
750
+ });
751
+ return jsonResponse(data);
288
752
  }
289
- else if (name === "create_folder") {
290
- return await handleCreateFolder(args);
753
+ case "delete": {
754
+ const folderId = p(args, "folderId", "folder_id", "id");
755
+ if (!folderId)
756
+ throw new Error("folderId is required for delete action");
757
+ const data = await callApi("delete", `folders/delete/${folderId}`, {
758
+ query: { projectId, section },
759
+ });
760
+ return jsonResponse(data);
291
761
  }
292
- else if (name === "get_labels") {
293
- return await handleGetLabels(args);
762
+ default:
763
+ throw new Error(`Unknown action: ${action}. Use: create, rename, move, delete`);
764
+ }
765
+ }
766
+ // 7. search_test_cycles
767
+ async function handleSearchTestCycles(args) {
768
+ const projectId = p(args, "projectId", "project_id");
769
+ if (!projectId)
770
+ throw new Error("projectId is required");
771
+ const data = await callApi("get", `projects/test-cycles/get-test-cycles/${projectId}`, {
772
+ query: {
773
+ search: p(args, "search"),
774
+ folderId: p(args, "folderId", "folder_id"),
775
+ status: p(args, "status"),
776
+ page: p(args, "page"),
777
+ limit: p(args, "limit"),
778
+ },
779
+ });
780
+ return jsonResponse(data);
781
+ }
782
+ // 8. manage_test_cycle
783
+ async function handleManageTestCycle(args) {
784
+ const projectId = p(args, "projectId", "project_id");
785
+ const action = p(args, "action");
786
+ if (!projectId)
787
+ throw new Error("projectId is required");
788
+ if (!action)
789
+ throw new Error("action is required");
790
+ switch (action) {
791
+ case "create": {
792
+ const title = p(args, "title");
793
+ if (!title)
794
+ throw new Error("title is required for create action");
795
+ const body = { title };
796
+ if (p(args, "summary"))
797
+ body.summary = p(args, "summary");
798
+ if (p(args, "status"))
799
+ body.status = p(args, "status");
800
+ if (p(args, "planned_start_date", "plannedStartDate"))
801
+ body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
802
+ if (p(args, "planned_end_date", "plannedEndDate"))
803
+ body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
804
+ if (p(args, "folder_id", "folderId"))
805
+ body.folder_id = p(args, "folder_id", "folderId");
806
+ const data = await callApi("post", `projects/test-cycles/create-test-cycle/${projectId}`, { body });
807
+ return jsonResponse(data);
294
808
  }
295
- else if (name === "create_label") {
296
- return await handleCreateLabel(args);
809
+ case "update": {
810
+ const cycleId = p(args, "cycleId", "cycle_id", "id");
811
+ if (!cycleId)
812
+ throw new Error("cycleId is required for update action");
813
+ const body = { id: cycleId };
814
+ if (p(args, "title"))
815
+ body.title = p(args, "title");
816
+ if (p(args, "summary"))
817
+ body.summary = p(args, "summary");
818
+ if (p(args, "status"))
819
+ body.status = p(args, "status");
820
+ if (p(args, "planned_start_date", "plannedStartDate"))
821
+ body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
822
+ if (p(args, "planned_end_date", "plannedEndDate"))
823
+ body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
824
+ if (p(args, "folder_id", "folderId"))
825
+ body.folder_id = p(args, "folder_id", "folderId");
826
+ const data = await callApi("post", `projects/test-cycles/create-test-cycle/${projectId}`, { body });
827
+ return jsonResponse(data);
297
828
  }
298
- else if (name === "get_user_profile") {
299
- return await handleGetUserProfile(args);
829
+ case "delete": {
830
+ const ids = p(args, "ids", "cycleId", "cycle_id");
831
+ if (!ids)
832
+ throw new Error("ids (or cycleId) is required for delete action");
833
+ const idList = typeof ids === "string" ? ids.split(",").map((s) => s.trim()) : ids;
834
+ const data = await callApi("delete", `projects/test-cycles/delete-test-cycle/${projectId}`, { body: { ids: idList } });
835
+ return jsonResponse(data);
300
836
  }
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
- };
837
+ case "get_details": {
838
+ const cycleId = p(args, "cycleId", "cycle_id", "id");
839
+ if (!cycleId)
840
+ throw new Error("cycleId is required for get_details action");
841
+ const data = await callApi("get", `projects/test-cycles/get-test-cycle-details/${projectId}/${cycleId}`);
842
+ return jsonResponse(data);
314
843
  }
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`;
844
+ case "get_testcases": {
845
+ const cycleId = p(args, "cycleId", "cycle_id", "id");
846
+ if (!cycleId)
847
+ throw new Error("cycleId is required for get_testcases action");
848
+ const data = await callApi("get", `projects/test-cycles/testcases/${projectId}/${cycleId}`, {
849
+ query: {
850
+ search: p(args, "search"),
851
+ page: p(args, "page"),
852
+ limit: p(args, "limit"),
853
+ },
401
854
  });
855
+ return jsonResponse(data);
402
856
  }
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`;
857
+ case "link_testcases": {
858
+ const cycleId = p(args, "cycleId", "cycle_id", "id");
859
+ const tcIds = p(args, "testcase_ids", "testcaseIds");
860
+ if (!cycleId)
861
+ throw new Error("cycleId is required for link_testcases action");
862
+ if (!tcIds)
863
+ throw new Error("testcase_ids is required for link_testcases action");
864
+ const idList = typeof tcIds === "string" ? tcIds.split(",").map((s) => s.trim()) : tcIds;
865
+ const data = await callApi("post", `projects/test-cycles/testcases/link/${projectId}/${cycleId}`, { body: { testcase_ids: idList } });
866
+ return jsonResponse(data);
406
867
  }
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);
868
+ case "unlink_testcases": {
869
+ const cycleId = p(args, "cycleId", "cycle_id", "id");
870
+ const tcIds = p(args, "testcase_ids", "testcaseIds");
871
+ if (!cycleId)
872
+ throw new Error("cycleId is required for unlink_testcases action");
873
+ if (!tcIds)
874
+ throw new Error("testcase_ids is required for unlink_testcases action");
875
+ const idList = typeof tcIds === "string" ? tcIds.split(",").map((s) => s.trim()) : tcIds;
876
+ const data = await callApi("delete", `projects/test-cycles/testcases/unlink/${projectId}/${cycleId}`, { body: { testcase_ids: idList } });
877
+ return jsonResponse(data);
425
878
  }
426
- throw error;
427
- }
428
- }
429
- // Handler for get_testcase_detail tool
430
- async function handleGetTestcaseDetail(args) {
431
- validateAuth();
432
- const projectId = getProjectId(args.projectId);
433
- const organizationId = await getOrganizationId(args.organizationId);
434
- // Handle different param names AI might use
435
- const testcaseId = args.testcaseId || args.testcase_id || args.id;
436
- if (!testcaseId) {
437
- throw new Error("testcaseId is required");
438
- }
439
- const queryParams = new URLSearchParams();
440
- queryParams.append("organizationId", organizationId.toString());
441
- const url = `${config.apiBaseUrl}/api/v1/projects/testcases/get-testcase-detail/${projectId}/${testcaseId}?${queryParams.toString()}`;
442
- try {
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}`);
451
- }
452
- catch (error) {
453
- throw handleApiError(error);
454
- }
455
- }
456
- // Handler for get_folders tool
457
- async function handleGetFolders(args) {
458
- validateAuth();
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);
481
- }
482
- }
483
- // Handler for get_labels tool
484
- async function handleGetLabels(args) {
485
- validateAuth();
486
- const projectId = getProjectId(args.projectId);
487
- const organizationId = await getOrganizationId(args.organizationId);
488
- const queryParams = new URLSearchParams();
489
- queryParams.append("organizationId", organizationId.toString());
490
- const url = `${config.apiBaseUrl}/api/v1/projects/labels/get-label/${projectId}?${queryParams.toString()}`;
491
- try {
492
- const response = await axios.get(url, {
493
- headers: {
494
- "Content-Type": "application/json",
495
- "Authorization": `Bearer ${config.patToken}`,
496
- },
497
- });
498
- const data = response.data;
499
- return formatResponse(data, `Labels for Project: ${args.projectId}`);
500
- }
501
- catch (error) {
502
- throw handleApiError(error);
879
+ case "assign_testcases": {
880
+ const cycleId = p(args, "cycleId", "cycle_id", "id");
881
+ const tcIds = p(args, "testcase_ids", "testcaseIds");
882
+ const assigneeId = p(args, "assignee_id", "assigneeId");
883
+ if (!cycleId)
884
+ throw new Error("cycleId is required for assign_testcases action");
885
+ if (!tcIds)
886
+ throw new Error("testcase_ids is required for assign_testcases action");
887
+ if (!assigneeId)
888
+ throw new Error("assignee_id is required for assign_testcases action");
889
+ const idList = typeof tcIds === "string" ? tcIds.split(",").map((s) => s.trim()) : tcIds;
890
+ const data = await callApi("patch", `projects/test-cycles/testcases/assign/${projectId}/${cycleId}`, { body: { testcase_ids: idList, assignee_id: assigneeId } });
891
+ return jsonResponse(data);
892
+ }
893
+ default:
894
+ throw new Error(`Unknown action: ${action}. Use: create, update, delete, get_details, get_testcases, link_testcases, unlink_testcases, assign_testcases`);
503
895
  }
504
896
  }
505
- // Handler for get_my_projects tool
506
- async function handleGetMyProjects(args) {
507
- validateAuth();
508
- const queryParams = new URLSearchParams();
509
- if (args.page)
510
- queryParams.append("page", args.page.toString());
511
- if (args.limit)
512
- queryParams.append("limit", args.limit.toString());
513
- const url = `${config.apiBaseUrl}/api/v1/projects/user/my-projects?${queryParams.toString()}`;
514
- try {
515
- const response = await axios.get(url, {
516
- headers: {
517
- "Content-Type": "application/json",
518
- "Authorization": `Bearer ${config.patToken}`,
519
- },
520
- });
521
- const data = response.data;
522
- return formatResponse(data, "My Projects");
897
+ // 9. execute_tests
898
+ async function handleExecuteTests(args) {
899
+ const projectId = p(args, "projectId", "project_id");
900
+ const cycleId = p(args, "cycleId", "cycle_id");
901
+ const resultsRaw = p(args, "results");
902
+ if (!projectId)
903
+ throw new Error("projectId is required");
904
+ if (!cycleId)
905
+ throw new Error("cycleId is required");
906
+ if (!resultsRaw)
907
+ throw new Error("results is required");
908
+ const results = typeof resultsRaw === "string" ? JSON.parse(resultsRaw) : resultsRaw;
909
+ if (!Array.isArray(results) || results.length === 0) {
910
+ throw new Error("results must be a non-empty array");
523
911
  }
524
- catch (error) {
525
- throw handleApiError(error);
912
+ // Single result → use PATCH update-execution endpoint
913
+ if (results.length === 1) {
914
+ const r = results[0];
915
+ const testcaseId = r.testcase_id || r.testcaseId;
916
+ if (!testcaseId)
917
+ throw new Error("testcase_id is required in each result");
918
+ const body = {
919
+ execution_status: r.execution_status || r.executionStatus,
920
+ };
921
+ if (r.actual_result || r.actualResult)
922
+ body.actual_result = r.actual_result || r.actualResult;
923
+ if (r.environment)
924
+ body.environment = r.environment;
925
+ const data = await callApi("patch", `projects/test-cycles/testcases/update-execution/${projectId}/${cycleId}/${testcaseId}`, { body });
926
+ return jsonResponse(data);
526
927
  }
928
+ // Multiple results → use POST execute-bulk endpoint
929
+ const bulkResults = results.map((r) => ({
930
+ testcase_id: r.testcase_id || r.testcaseId,
931
+ execution_status: r.execution_status || r.executionStatus,
932
+ ...(r.actual_result || r.actualResult
933
+ ? { actual_result: r.actual_result || r.actualResult }
934
+ : {}),
935
+ ...(r.environment ? { environment: r.environment } : {}),
936
+ }));
937
+ const data = await callApi("post", `projects/test-cycles/execute-bulk/${projectId}/${cycleId}`, { body: { results: bulkResults } });
938
+ return jsonResponse(data);
527
939
  }
528
- // Handler for create_testcase tool (handles test_steps internally)
529
- async function handleCreateTestcase(args) {
530
- validateAuth();
531
- const projectId = getProjectId(args.projectId);
532
- const organizationId = await getOrganizationId(args.organizationId);
533
- // Extract and parse test_steps (JSON string) before sending to API
534
- const { test_steps: test_steps_raw, testSteps, labels: labels_raw, ...restArgs } = args;
535
- // Handle different param names AI might use
536
- const test_steps_input = test_steps_raw || testSteps;
537
- const test_steps = test_steps_input ? (typeof test_steps_input === 'string' ? JSON.parse(test_steps_input) : test_steps_input) : null;
538
- // Build testcaseData with normalized field names
539
- const testcaseData = {};
540
- // Title (required)
541
- testcaseData.title = restArgs.title || restArgs.name || restArgs.testcase_title;
542
- if (!testcaseData.title)
543
- throw new Error("title is required");
544
- // Summary/description
545
- if (restArgs.summary || restArgs.description) {
546
- testcaseData.summary = restArgs.summary || restArgs.description;
547
- }
548
- // Priority
549
- if (restArgs.priority) {
550
- testcaseData.priority = restArgs.priority;
551
- }
552
- // Folder ID - handle both snake_case and camelCase
553
- const folderId = restArgs.folder_id || restArgs.folderId;
554
- if (folderId) {
555
- testcaseData.folder_id = folderId;
556
- }
557
- // Parse labels from comma-separated string
558
- if (labels_raw) {
559
- testcaseData.labels = typeof labels_raw === 'string' ? labels_raw.split(',').map((l) => l.trim()) : labels_raw;
560
- }
561
- const queryParams = new URLSearchParams();
562
- queryParams.append("organizationId", organizationId.toString());
563
- const url = `${config.apiBaseUrl}/api/v1/projects/testcases/create-testcase/${projectId}?${queryParams.toString()}`;
564
- try {
565
- // Step 1: Create testcase
566
- const response = await axios.post(url, testcaseData, {
567
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
568
- });
569
- const createdTestcase = response.data?.data || response.data;
570
- const testcaseId = createdTestcase?.id || createdTestcase?.testcaseId;
571
- // Step 2: If test_steps provided, create them
572
- if (test_steps && Array.isArray(test_steps) && test_steps.length > 0 && testcaseId) {
573
- const stepsUrl = `${config.apiBaseUrl}/api/v1/projects/testcases/teststeps/${projectId}/${testcaseId}?organizationId=${organizationId}`;
574
- await axios.post(stepsUrl, { steps: test_steps }, {
575
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
940
+ // 10. manage_test_plan
941
+ async function handleManageTestPlan(args) {
942
+ const projectId = p(args, "projectId", "project_id");
943
+ const action = p(args, "action");
944
+ if (!projectId)
945
+ throw new Error("projectId is required");
946
+ if (!action)
947
+ throw new Error("action is required");
948
+ switch (action) {
949
+ case "create": {
950
+ const title = p(args, "title");
951
+ if (!title)
952
+ throw new Error("title is required for create action");
953
+ const body = { title };
954
+ if (p(args, "summary"))
955
+ body.summary = p(args, "summary");
956
+ if (p(args, "status"))
957
+ body.status = p(args, "status");
958
+ if (p(args, "planned_start_date", "plannedStartDate"))
959
+ body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
960
+ if (p(args, "planned_end_date", "plannedEndDate"))
961
+ body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
962
+ if (p(args, "folder_id", "folderId"))
963
+ body.folder_id = p(args, "folder_id", "folderId");
964
+ const data = await callApi("post", `projects/test-plans/create-test-plan/${projectId}`, { body });
965
+ return jsonResponse(data);
966
+ }
967
+ case "update": {
968
+ const planId = p(args, "planId", "plan_id", "id");
969
+ if (!planId)
970
+ throw new Error("planId is required for update action");
971
+ const body = { id: planId };
972
+ if (p(args, "title"))
973
+ body.title = p(args, "title");
974
+ if (p(args, "summary"))
975
+ body.summary = p(args, "summary");
976
+ if (p(args, "status"))
977
+ body.status = p(args, "status");
978
+ if (p(args, "planned_start_date", "plannedStartDate"))
979
+ body.planned_start_date = p(args, "planned_start_date", "plannedStartDate");
980
+ if (p(args, "planned_end_date", "plannedEndDate"))
981
+ body.planned_end_date = p(args, "planned_end_date", "plannedEndDate");
982
+ if (p(args, "folder_id", "folderId"))
983
+ body.folder_id = p(args, "folder_id", "folderId");
984
+ const data = await callApi("post", `projects/test-plans/create-test-plan/${projectId}`, { body });
985
+ return jsonResponse(data);
986
+ }
987
+ case "delete": {
988
+ const ids = p(args, "ids", "planId", "plan_id");
989
+ if (!ids)
990
+ throw new Error("ids (or planId) is required for delete action");
991
+ const idList = typeof ids === "string" ? ids.split(",").map((s) => s.trim()) : ids;
992
+ const data = await callApi("delete", `projects/test-plans/delete-test-plan/${projectId}`, { body: { ids: idList } });
993
+ return jsonResponse(data);
994
+ }
995
+ case "list": {
996
+ const data = await callApi("get", `projects/test-plans/get-test-plans/${projectId}`, {
997
+ query: {
998
+ search: p(args, "search"),
999
+ page: p(args, "page"),
1000
+ limit: p(args, "limit"),
1001
+ },
576
1002
  });
1003
+ return jsonResponse(data);
577
1004
  }
578
- return formatResponse(response.data, "Testcase Created" + (test_steps?.length ? ` with ${test_steps.length} test steps` : ""));
579
- }
580
- catch (error) {
581
- throw handleApiError(error);
582
- }
583
- }
584
- // Handler for create_bulk_testcase tool (handles test_steps internally)
585
- async function handleCreateBulkTestcase(args) {
586
- validateAuth();
587
- const projectId = getProjectId(args.projectId);
588
- const organizationId = await getOrganizationId(args.organizationId);
589
- // Handle different param names AI might use
590
- const rawTestcases = args.testcases || args.test_cases || args.items;
591
- if (!rawTestcases)
592
- throw new Error("testcases is required (JSON array of testcases)");
593
- const testcases = typeof rawTestcases === 'string' ? JSON.parse(rawTestcases) : rawTestcases;
594
- // Normalize each testcase and separate test_steps for later processing
595
- const testcasesWithSteps = testcases.map((tc) => {
596
- // Normalize field names for each testcase
597
- const normalized = {
598
- title: tc.title || tc.name || tc.testcase_title,
599
- };
600
- if (tc.summary || tc.description)
601
- normalized.summary = tc.summary || tc.description;
602
- if (tc.priority)
603
- normalized.priority = tc.priority;
604
- if (tc.folder_id || tc.folderId)
605
- normalized.folder_id = tc.folder_id || tc.folderId;
606
- return {
607
- testcase: normalized,
608
- test_steps: tc.test_steps || tc.testSteps || tc.steps,
609
- };
610
- });
611
- const queryParams = new URLSearchParams();
612
- queryParams.append("organizationId", organizationId.toString());
613
- const url = `${config.apiBaseUrl}/api/v1/projects/testcases/create-bulk-testcase/${projectId}?${queryParams.toString()}`;
614
- try {
615
- // Step 1: Create all testcases (without steps)
616
- const cleanTestcases = testcasesWithSteps.map((t) => t.testcase);
617
- const response = await axios.post(url, { testcases: cleanTestcases }, {
618
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
619
- });
620
- const results = response.data?.results || [];
621
- // Step 2: Create test steps for each successfully created testcase
622
- let stepsCreated = 0;
623
- for (let i = 0; i < results.length; i++) {
624
- const result = results[i];
625
- const steps = testcasesWithSteps[i]?.test_steps;
626
- if (result.status === 'success' && steps && Array.isArray(steps) && steps.length > 0) {
627
- const testcaseId = result.testcaseId || result.id;
628
- if (testcaseId) {
629
- try {
630
- const stepsUrl = `${config.apiBaseUrl}/api/v1/projects/testcases/teststeps/${projectId}/${testcaseId}?organizationId=${organizationId}`;
631
- await axios.post(stepsUrl, { steps }, {
632
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
633
- });
634
- stepsCreated++;
635
- }
636
- catch (e) {
637
- // Log but don't fail the whole operation
638
- console.error(`Failed to create steps for testcase ${testcaseId}`);
639
- }
640
- }
641
- }
1005
+ case "get_details": {
1006
+ const planId = p(args, "planId", "plan_id", "id");
1007
+ if (!planId)
1008
+ throw new Error("planId is required for get_details action");
1009
+ const data = await callApi("get", `projects/test-plans/get-test-plan-details/${projectId}/${planId}`);
1010
+ return jsonResponse(data);
642
1011
  }
643
- return formatResponse(response.data, `Bulk Testcases Created (${stepsCreated} with test steps)`);
644
- }
645
- catch (error) {
646
- throw handleApiError(error);
647
- }
648
- }
649
- // Handler for update_testcase_field tool
650
- async function handleUpdateTestcaseField(args) {
651
- validateAuth();
652
- const projectId = getProjectId(args.projectId);
653
- const organizationId = await getOrganizationId(args.organizationId);
654
- // Handle different param names AI might use
655
- const ids = args.ids || args.testcase_ids || args.testcaseIds;
656
- const field = args.field || args.field_name || args.fieldName;
657
- const value = args.value || args.field_value || args.fieldValue || args.new_value;
658
- if (!ids)
659
- throw new Error("ids is required (comma-separated testcase IDs)");
660
- if (!field)
661
- throw new Error("field is required");
662
- if (value === undefined)
663
- throw new Error("value is required");
664
- const queryParams = new URLSearchParams();
665
- queryParams.append("organizationId", organizationId.toString());
666
- const url = `${config.apiBaseUrl}/api/v1/projects/testcases/update-field/${projectId}?${queryParams.toString()}`;
667
- try {
668
- const response = await axios.patch(url, {
669
- ids: ids.split(','),
670
- field: field,
671
- value: value,
672
- }, {
673
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
674
- });
675
- return formatResponse(response.data, "Testcase Field Updated");
676
- }
677
- catch (error) {
678
- throw handleApiError(error);
679
- }
680
- }
681
- // Handler for delete_testcase tool
682
- async function handleDeleteTestcase(args) {
683
- validateAuth();
684
- const projectId = getProjectId(args.projectId);
685
- const organizationId = await getOrganizationId(args.organizationId);
686
- // Handle different param names AI might use
687
- const ids = args.ids || args.testcase_ids || args.testcaseIds;
688
- if (!ids)
689
- throw new Error("ids is required (comma-separated testcase IDs)");
690
- const queryParams = new URLSearchParams();
691
- queryParams.append("organizationId", organizationId.toString());
692
- const url = `${config.apiBaseUrl}/api/v1/projects/testcases/delete-testcase/${projectId}?${queryParams.toString()}`;
693
- try {
694
- const response = await axios.delete(url, {
695
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
696
- data: { ids: ids.split(',') },
697
- });
698
- return formatResponse(response.data, "Testcase Deleted");
699
- }
700
- catch (error) {
701
- throw handleApiError(error);
702
- }
703
- }
704
- // Handler for create_folder tool
705
- async function handleCreateFolder(args) {
706
- validateAuth();
707
- const projectId = getProjectId(args.projectId);
708
- const organizationId = await getOrganizationId(args.organizationId);
709
- const url = `${config.apiBaseUrl}/api/v1/folders/create-folders`;
710
- // Handle different param names AI might use
711
- const folderName = args.name || args.folder_name;
712
- if (!folderName) {
713
- throw new Error("Folder name is required. Provide 'name' parameter.");
714
- }
715
- const parentId = args.parentId || args.parent_id;
716
- const body = {
717
- organizationId: organizationId,
718
- projectId: projectId,
719
- section: "TESTCASE",
720
- name: folderName,
721
- ...(parentId && { parentId: parentId }),
722
- };
723
- try {
724
- const response = await axios.post(url, body, {
725
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
726
- });
727
- return formatResponse(response.data, "Folder Created");
728
- }
729
- catch (error) {
730
- throw handleApiError(error);
1012
+ case "link_cycles": {
1013
+ const planId = p(args, "planId", "plan_id", "id");
1014
+ const cycleIds = p(args, "cycle_ids", "cycleIds");
1015
+ if (!planId)
1016
+ throw new Error("planId is required for link_cycles action");
1017
+ if (!cycleIds)
1018
+ throw new Error("cycle_ids is required for link_cycles action");
1019
+ const idList = typeof cycleIds === "string" ? cycleIds.split(",").map((s) => s.trim()) : cycleIds;
1020
+ const data = await callApi("post", `projects/test-plans/testcycles/link/${projectId}/${planId}`, { body: { test_cycle_ids: idList } });
1021
+ return jsonResponse(data);
1022
+ }
1023
+ case "unlink_cycles": {
1024
+ const planId = p(args, "planId", "plan_id", "id");
1025
+ const cycleIds = p(args, "cycle_ids", "cycleIds");
1026
+ if (!planId)
1027
+ throw new Error("planId is required for unlink_cycles action");
1028
+ if (!cycleIds)
1029
+ throw new Error("cycle_ids is required for unlink_cycles action");
1030
+ const idList = typeof cycleIds === "string" ? cycleIds.split(",").map((s) => s.trim()) : cycleIds;
1031
+ const data = await callApi("delete", `projects/test-plans/testcycles/unlink/${projectId}/${planId}`, { body: { test_cycle_ids: idList } });
1032
+ return jsonResponse(data);
1033
+ }
1034
+ case "get_cycles": {
1035
+ const planId = p(args, "planId", "plan_id", "id");
1036
+ if (!planId)
1037
+ throw new Error("planId is required for get_cycles action");
1038
+ const data = await callApi("get", `projects/test-plans/testcycles/list/${projectId}/${planId}`, {
1039
+ query: {
1040
+ search: p(args, "search"),
1041
+ page: p(args, "page"),
1042
+ limit: p(args, "limit"),
1043
+ },
1044
+ });
1045
+ return jsonResponse(data);
1046
+ }
1047
+ case "get_testcases": {
1048
+ const planId = p(args, "planId", "plan_id", "id");
1049
+ if (!planId)
1050
+ throw new Error("planId is required for get_testcases action");
1051
+ const data = await callApi("get", `projects/test-plans/testcases/list/${projectId}/${planId}`, {
1052
+ query: {
1053
+ search: p(args, "search"),
1054
+ page: p(args, "page"),
1055
+ limit: p(args, "limit"),
1056
+ },
1057
+ });
1058
+ return jsonResponse(data);
1059
+ }
1060
+ default:
1061
+ throw new Error(`Unknown action: ${action}. Use: create, update, delete, list, get_details, link_cycles, unlink_cycles, get_cycles, get_testcases`);
731
1062
  }
732
1063
  }
733
- // Handler for create_label tool
734
- async function handleCreateLabel(args) {
735
- validateAuth();
736
- const projectId = getProjectId(args.projectId);
737
- const organizationId = await getOrganizationId(args.organizationId);
738
- const queryParams = new URLSearchParams();
739
- queryParams.append("organizationId", organizationId.toString());
740
- const url = `${config.apiBaseUrl}/api/v1/projects/labels/create/${projectId}?${queryParams.toString()}`;
741
- // Handle different param names AI might use
742
- const rawValues = args.values || args.labels || args.label_names || args.names;
743
- if (!rawValues)
744
- throw new Error("values is required (comma-separated label names)");
745
- // Parse values from comma-separated string
746
- const values = typeof rawValues === 'string' ? rawValues.split(',').map((v) => v.trim()) : rawValues;
747
- try {
748
- const response = await axios.post(url, { values }, {
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);
1064
+ // 11. get_report
1065
+ async function handleGetReport(args) {
1066
+ const projectId = p(args, "projectId", "project_id");
1067
+ const reportType = p(args, "report_type", "reportType");
1068
+ if (!projectId)
1069
+ throw new Error("projectId is required");
1070
+ if (!reportType)
1071
+ throw new Error("report_type is required");
1072
+ const endpointPath = REPORT_TYPE_MAP[reportType];
1073
+ if (!endpointPath) {
1074
+ throw new Error(`Unknown report_type: ${reportType}. Valid types: ${Object.keys(REPORT_TYPE_MAP).join(", ")}`);
755
1075
  }
1076
+ const query = {};
1077
+ // Common filters
1078
+ if (p(args, "test_cycle_ids", "testCycleIds"))
1079
+ query.test_cycle_ids = p(args, "test_cycle_ids", "testCycleIds");
1080
+ if (p(args, "test_plan_ids", "testPlanIds"))
1081
+ query.test_plan_ids = p(args, "test_plan_ids", "testPlanIds");
1082
+ if (p(args, "startDate", "start_date"))
1083
+ query.startDate = p(args, "startDate", "start_date");
1084
+ if (p(args, "endDate", "end_date"))
1085
+ query.endDate = p(args, "endDate", "end_date");
1086
+ if (p(args, "granularity"))
1087
+ query.granularity = p(args, "granularity");
1088
+ // cycle_comparison specific
1089
+ if (p(args, "cycle_ids", "cycleIds"))
1090
+ query.cycle_ids = p(args, "cycle_ids", "cycleIds");
1091
+ const data = await callApi("get", `projects/reports/${endpointPath}/${projectId}`, {
1092
+ query,
1093
+ });
1094
+ return jsonResponse(data);
756
1095
  }
757
- // Handler for get_user_profile tool
758
- async function handleGetUserProfile(args) {
759
- validateAuth();
760
- const url = `${config.apiBaseUrl}/api/v1/auth/me`;
1096
+ // ═══════════════════════════════════════════════════════════════════════════════
1097
+ // SERVER SETUP
1098
+ // ═══════════════════════════════════════════════════════════════════════════════
1099
+ const server = new Server({ name: "testkase-mcp-server", version: "2.1.0" }, { capabilities: { tools: {} } });
1100
+ // List all 11 tools
1101
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1102
+ tools: [
1103
+ LIST_PROJECTS_TOOL,
1104
+ GET_PROJECT_STRUCTURE_TOOL,
1105
+ SEARCH_TESTCASES_TOOL,
1106
+ GET_TESTCASE_TOOL,
1107
+ MANAGE_TESTCASE_TOOL,
1108
+ MANAGE_FOLDER_TOOL,
1109
+ SEARCH_TEST_CYCLES_TOOL,
1110
+ MANAGE_TEST_CYCLE_TOOL,
1111
+ EXECUTE_TESTS_TOOL,
1112
+ MANAGE_TEST_PLAN_TOOL,
1113
+ GET_REPORT_TOOL,
1114
+ ],
1115
+ }));
1116
+ // Route tool calls
1117
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1118
+ const { name, arguments: args } = request.params;
761
1119
  try {
762
- const response = await axios.get(url, {
763
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.patToken}` },
764
- });
765
- return formatResponse(response.data, "User Profile");
1120
+ switch (name) {
1121
+ case "list_projects":
1122
+ return await handleListProjects(args);
1123
+ case "get_project_structure":
1124
+ return await handleGetProjectStructure(args);
1125
+ case "search_testcases":
1126
+ return await handleSearchTestcases(args);
1127
+ case "get_testcase":
1128
+ return await handleGetTestcase(args);
1129
+ case "manage_testcase":
1130
+ return await handleManageTestcase(args);
1131
+ case "manage_folder":
1132
+ return await handleManageFolder(args);
1133
+ case "search_test_cycles":
1134
+ return await handleSearchTestCycles(args);
1135
+ case "manage_test_cycle":
1136
+ return await handleManageTestCycle(args);
1137
+ case "execute_tests":
1138
+ return await handleExecuteTests(args);
1139
+ case "manage_test_plan":
1140
+ return await handleManageTestPlan(args);
1141
+ case "get_report":
1142
+ return await handleGetReport(args);
1143
+ default:
1144
+ throw new Error(`Unknown tool: ${name}`);
1145
+ }
766
1146
  }
767
1147
  catch (error) {
768
- throw handleApiError(error);
769
- }
770
- }
771
- // Handler for set_active_project tool
772
- async function handleSetActiveProject(args) {
773
- // Handle different param names AI might use
774
- const projectId = args.projectId || args.project_id;
775
- if (!projectId) {
776
- throw new Error("projectId is required");
777
- }
778
- setActiveProject(projectId);
779
- return {
780
- content: [
781
- {
782
- type: "text",
783
- text: `Active project set to: ${projectId}\n\nYou can now use other tools without specifying projectId.`,
784
- },
785
- ],
786
- };
787
- }
788
- // Handler for get_active_project tool
789
- async function handleGetActiveProject() {
790
- const projectId = cachedProjectId;
791
- if (!projectId) {
1148
+ const err = error instanceof Error ? error : handleApiError(error);
792
1149
  return {
793
- content: [
794
- {
795
- type: "text",
796
- text: "No active project is set. Use set_active_project or specify projectId in your next command.",
797
- },
798
- ],
1150
+ content: [{ type: "text", text: `Error: ${err.message}` }],
1151
+ isError: true,
799
1152
  };
800
1153
  }
801
- return {
802
- content: [
803
- {
804
- type: "text",
805
- text: `Current active project: ${projectId}`,
806
- },
807
- ],
808
- };
809
- }
810
- // Helper function to validate authentication
811
- function validateAuth() {
812
- if (!config.patToken) {
813
- throw new Error("Authentication required. Please set TESTKASE_PAT_TOKEN environment variable. " +
814
- "Generate a Personal Access Token (PAT) from your TestKase account settings.");
815
- }
816
- if (!config.patToken.startsWith("xyz_")) {
817
- throw new Error("Invalid PAT token format. TestKase PAT tokens must start with 'xyz_' prefix. " +
818
- "Please generate a valid PAT from your TestKase account settings.");
819
- }
820
- }
821
- // Helper function to format API response
822
- function formatResponse(data, title) {
823
- let resultText = `# ${title}\n\n`;
824
- resultText += "```json\n";
825
- resultText += JSON.stringify(data, null, 2);
826
- resultText += "\n```\n";
827
- return {
828
- content: [
829
- {
830
- type: "text",
831
- text: resultText,
832
- },
833
- ],
834
- };
835
- }
836
- // Helper function to handle API errors
837
- function handleApiError(error) {
838
- if (axios.isAxiosError(error)) {
839
- const axiosError = error;
840
- let errorMessage = `API request failed: ${axiosError.message}`;
841
- if (axiosError.response) {
842
- errorMessage += `\nStatus: ${axiosError.response.status}`;
843
- errorMessage += `\nResponse: ${JSON.stringify(axiosError.response.data)}`;
844
- }
845
- return new Error(errorMessage);
846
- }
847
- return error;
848
- }
849
- // Start the server
1154
+ });
1155
+ // Start
850
1156
  async function main() {
851
1157
  const transport = new StdioServerTransport();
852
1158
  await server.connect(transport);
853
- console.error("TestKase MCP Server running on stdio");
1159
+ console.error("TestKase MCP Server v2.1.0 running on stdio (11 tools)");
854
1160
  }
855
1161
  main().catch((error) => {
856
1162
  console.error("Fatal error in main():", error);