@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/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
- // 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
+ // ─── 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
- // If cached, return cached value
22
- if (cachedOrganizationId !== null) {
23
- return cachedOrganizationId;
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
- // Fetch from API using PAT token
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
- const orgId = orgData.organization_id;
37
- cachedOrganizationId = orgId;
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
- throw new Error("Failed to fetch organization. Please provide organizationId or check your PAT token.");
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
- // 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;
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
- // If cached, return cached value
54
- if (cachedProjectId !== null) {
55
- return cachedProjectId;
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
- // Helper to set active project (for explicit project switching)
63
- function setActiveProject(projectId) {
64
- cachedProjectId = projectId;
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
- // 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.",
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 term to filter by title or summary" },
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 GET_TESTCASE_DETAIL_TOOL = {
83
- name: "get_testcase_detail",
84
- description: "Get full details of a testcase including test steps and attachments.",
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
- testcaseId: { type: "string", description: "The testcase ID" },
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: ["testcaseId"],
230
+ required: ["projectId"],
91
231
  },
92
232
  };
93
- const GET_FOLDERS_TOOL = {
94
- name: "get_folders",
95
- description: "Get folder structure of the active project. Use folder IDs when creating testcases.",
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
- search: { type: "string", description: "Search term to filter folders" },
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 CREATE_TESTCASE_TOOL = {
124
- name: "create_testcase",
125
- description: "Create a new testcase in the active project.",
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
- 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\":\"...\"}]" },
259
+ projectId: { type: "string", description: "Project ID" },
260
+ testcaseId: { type: "string", description: "Test case ID (e.g. TEST-123)" },
134
261
  },
135
- required: ["title"],
262
+ required: ["projectId", "testcaseId"],
136
263
  },
137
264
  };
138
- const CREATE_BULK_TESTCASE_TOOL = {
139
- name: "create_bulk_testcase",
140
- description: "Create multiple testcases at once in the active project.",
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
- testcases: { type: "string", description: "JSON array of testcases, each with title, summary, priority, test_steps" },
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: ["testcases"],
342
+ required: ["projectId", "action"],
147
343
  },
148
344
  };
149
- const UPDATE_TESTCASE_FIELD_TOOL = {
150
- name: "update_testcase_field",
151
- description: "Update a field for one or multiple testcases.",
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
- 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" },
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: ["ids", "field", "value"],
377
+ required: ["projectId", "action"],
160
378
  },
161
379
  };
162
- const DELETE_TESTCASE_TOOL = {
163
- name: "delete_testcase",
164
- description: "Delete one or multiple testcases. ALWAYS confirm with user before deleting.",
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
- ids: { type: "string", description: "Comma-separated testcase IDs to delete" },
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: ["ids"],
393
+ required: ["projectId"],
171
394
  },
172
395
  };
173
- const CREATE_FOLDER_TOOL = {
174
- name: "create_folder",
175
- description: "Create a new folder in the active project for organizing testcases.",
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
- name: { type: "string", description: "Folder name (max 50 characters)" },
180
- parentId: { type: "number", description: "Parent folder ID for subfolder (optional)" },
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: ["name"],
470
+ required: ["projectId", "action"],
183
471
  },
184
472
  };
185
- const CREATE_LABEL_TOOL = {
186
- name: "create_label",
187
- description: "Create new labels in the active project for categorizing testcases.",
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
- values: { type: "string", description: "Comma-separated label names" },
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: ["values"],
489
+ required: ["projectId", "cycleId", "results"],
194
490
  },
195
491
  };
196
- const GET_USER_PROFILE_TOOL = {
197
- name: "get_user_profile",
198
- description: "Get current logged-in user profile information",
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
- required: [],
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 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.",
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 chosen by user" },
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
- 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
- },
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
- // 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);
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
- // 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");
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
- 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}`);
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
- catch (error) {
453
- throw handleApiError(error);
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
- // 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);
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
- // 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
- },
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
- // 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");
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
- // 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}` },
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
- 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++;
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
- catch (e) {
637
- // Log but don't fail the whole operation
638
- console.error(`Failed to create steps for testcase ${testcaseId}`);
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
- return formatResponse(response.data, `Bulk Testcases Created (${stepsCreated} with test steps)`);
644
- }
645
- catch (error) {
646
- throw handleApiError(error);
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
- // 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);
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
- // 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(',') },
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
- // 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);
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
- // 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);
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
- // Handler for get_user_profile tool
758
- async function handleGetUserProfile(args) {
759
- validateAuth();
760
- const url = `${config.apiBaseUrl}/api/v1/auth/me`;
761
- 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");
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
- catch (error) {
768
- throw handleApiError(error);
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
- // 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) {
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
- 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) {
792
- 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.",
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
- // 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.");
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
- // 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)}`;
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
- return error;
848
- }
849
- // Start the server
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);