zephyr-scale-mcp-server 0.3.2 → 0.3.3

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.
@@ -8,9 +8,10 @@ import {
8
8
  SearchTestCasesArgs,
9
9
  AddTestCasesToRunArgs,
10
10
  SearchTestRunsArgs,
11
+ GetTestExecutionArgs,
11
12
  JiraConfig
12
13
  } from './types.js';
13
- import { convertToGherkin, customPriorityMapping, priorityMapping } from './utils.js';
14
+ import { convertToGherkin, resolveFolderIdByPath } from './utils.js';
14
15
 
15
16
  export class ZephyrToolHandlers {
16
17
  constructor(
@@ -26,36 +27,102 @@ export class ZephyrToolHandlers {
26
27
  content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
27
28
  };
28
29
  } catch (error) {
29
- throw new McpError(ErrorCode.InternalError, `Failed to get test case: ${error instanceof Error ? error.message : String(error)}`);
30
+ throw new McpError(ErrorCode.InternalError, `Failed to get test case: ${this.formatError(error)}`);
30
31
  }
31
32
  }
32
33
 
33
34
  async createTestCase(args: TestCaseArgs) {
35
+ if (this.jiraConfig.type === 'cloud') {
36
+ return this.createTestCaseCloud(args);
37
+ }
38
+ return this.createTestCaseDC(args);
39
+ }
40
+
41
+ private async createTestCaseCloud(args: TestCaseArgs) {
34
42
  const {
35
- project_key,
36
- name,
37
- test_script,
38
- folder,
39
- status,
40
- priority,
41
- precondition,
42
- objective,
43
- component,
44
- owner,
45
- estimated_time,
46
- labels,
47
- issue_links,
48
- custom_fields,
49
- parameters
43
+ project_key, name, test_script, folder, priority, precondition,
44
+ objective, estimated_time, labels, custom_fields,
50
45
  } = args;
51
46
 
52
- // Build the basic payload
53
- const payload: any = {
54
- projectKey: project_key,
55
- name: name
56
- };
47
+ const payload: any = { projectKey: project_key, name };
48
+ // Cloud v2 uses statusName/priorityName (strings), folderId (integer)
49
+ payload.statusName = 'Draft';
50
+ if (priority) payload.priorityName = priority;
51
+ if (precondition) payload.precondition = precondition;
52
+ if (objective) payload.objective = objective;
53
+ if (estimated_time) payload.estimatedTime = estimated_time;
54
+ if (labels && labels.length > 0) payload.labels = labels;
55
+ if (custom_fields) payload.customFields = custom_fields;
56
+
57
+ // Resolve folder path → folderId
58
+ if (folder) {
59
+ const folderId = await resolveFolderIdByPath(
60
+ this.axiosInstance, project_key, folder, 'TEST_CASE'
61
+ );
62
+ if (folderId !== null) payload.folderId = folderId;
63
+ }
64
+
65
+ try {
66
+ const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testcase, payload);
67
+ if (response.status !== 201) {
68
+ throw new Error(`Unexpected status code: ${response.status}`);
69
+ }
70
+ const testKey = response.data.key || 'Unknown';
71
+
72
+ // Step 2: add test script via dedicated endpoint
73
+ if (test_script) {
74
+ await this.upsertTestScriptCloud(testKey, test_script);
75
+ }
76
+
77
+ return {
78
+ content: [{
79
+ type: 'text',
80
+ text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
81
+ }],
82
+ };
83
+ } catch (error) {
84
+ throw new McpError(ErrorCode.InternalError, `Failed to create test case: ${this.formatError(error)}`);
85
+ }
86
+ }
87
+
88
+ private async upsertTestScriptCloud(testKey: string, test_script: TestCaseArgs['test_script']) {
89
+ if (!test_script) return;
90
+
91
+ if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
92
+ const items = test_script.steps.map((step: any) => ({
93
+ inline: {
94
+ description: step.description || '',
95
+ testData: step.testData || null,
96
+ expectedResult: step.expectedResult || null,
97
+ },
98
+ }));
99
+ await this.axiosInstance.post(
100
+ `${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`,
101
+ { mode: 'OVERWRITE', items }
102
+ );
103
+ } else if (test_script.type === 'BDD' && test_script.text) {
104
+ const gherkin = convertToGherkin(test_script.text) || test_script.text;
105
+ await this.axiosInstance.post(
106
+ `${this.jiraConfig.apiEndpoints.testcase}/${testKey}/testscript`,
107
+ { type: 'bdd', text: gherkin }
108
+ );
109
+ } else if (test_script.type === 'PLAIN_TEXT' && test_script.text) {
110
+ await this.axiosInstance.post(
111
+ `${this.jiraConfig.apiEndpoints.testcase}/${testKey}/testscript`,
112
+ { type: 'plain', text: test_script.text }
113
+ );
114
+ }
115
+ }
116
+
117
+ private async createTestCaseDC(args: TestCaseArgs) {
118
+ const {
119
+ project_key, name, test_script, folder, status, priority, precondition,
120
+ objective, component, owner, estimated_time, labels, issue_links,
121
+ custom_fields, parameters
122
+ } = args;
123
+
124
+ const payload: any = { projectKey: project_key, name };
57
125
 
58
- // Add optional fields
59
126
  if (folder) payload.folder = folder;
60
127
  if (status) payload.status = status;
61
128
  if (priority) payload.priority = priority;
@@ -69,168 +136,230 @@ export class ZephyrToolHandlers {
69
136
  if (custom_fields) payload.customFields = custom_fields;
70
137
  if (parameters) payload.parameters = parameters;
71
138
 
72
- // Handle test script
73
139
  if (test_script) {
74
- payload.testScript = {
75
- type: test_script.type
76
- };
77
-
140
+ payload.testScript = { type: test_script.type };
78
141
  if (test_script.type === 'STEP_BY_STEP' && test_script.steps) {
79
142
  payload.testScript.steps = test_script.steps.map((step: any) => {
80
- const stepObj: any = {};
81
- if (step.description) stepObj.description = step.description;
82
- if (step.testData) stepObj.testData = step.testData;
83
- if (step.expectedResult) stepObj.expectedResult = step.expectedResult;
84
- if (step.testCaseKey) stepObj.testCaseKey = step.testCaseKey;
85
- return stepObj;
143
+ const s: any = {};
144
+ if (step.description) s.description = step.description;
145
+ if (step.testData) s.testData = step.testData;
146
+ if (step.expectedResult) s.expectedResult = step.expectedResult;
147
+ if (step.testCaseKey) s.testCaseKey = step.testCaseKey;
148
+ return s;
86
149
  });
87
150
  } else if ((test_script.type === 'PLAIN_TEXT' || test_script.type === 'BDD') && test_script.text) {
88
151
  if (test_script.type === 'BDD') {
89
- const gherkinContent = convertToGherkin(test_script.text);
90
- payload.testScript.text = gherkinContent || test_script.text;
152
+ const gherkin = convertToGherkin(test_script.text);
153
+ payload.testScript.text = gherkin || test_script.text;
91
154
  } else {
92
155
  payload.testScript.text = test_script.text;
93
156
  }
94
157
  }
95
158
  }
96
159
 
97
- // Always set status to Draft for new test cases
160
+ // Always Draft for new test cases
98
161
  payload.status = 'Draft';
99
162
 
100
163
  try {
101
164
  const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testcase, payload);
165
+ if (response.status !== 201) throw new Error(`Unexpected status code: ${response.status}`);
166
+ const testKey = response.data.key || 'Unknown';
167
+ return {
168
+ content: [{
169
+ type: 'text',
170
+ text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
171
+ }],
172
+ };
173
+ } catch (error) {
174
+ throw new McpError(ErrorCode.InternalError, `Failed to create test case: ${this.formatError(error)}`);
175
+ }
176
+ }
102
177
 
103
- if (response.status === 201) {
104
- const testKey = response.data.key || 'Unknown';
105
- return {
106
- content: [
107
- {
108
- type: 'text',
109
- text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({
110
- key: testKey,
111
- type: test_script?.type || 'none',
112
- hasSteps: test_script?.type === 'STEP_BY_STEP' ? test_script.steps?.length || 0 : undefined,
113
- hasText: (test_script?.type === 'PLAIN_TEXT' || test_script?.type === 'BDD') ? !!test_script.text : undefined
114
- }, null, 2)}`,
115
- },
116
- ],
117
- };
118
- } else {
119
- throw new Error(`Unexpected status code: ${response.status}`);
178
+ async updateTestCaseBdd(args: UpdateBddArgs) {
179
+ if (this.jiraConfig.type === 'cloud') {
180
+ return this.updateTestCaseBddCloud(args);
181
+ }
182
+ return this.updateTestCaseBddDC(args);
183
+ }
184
+
185
+ private async updateTestCaseBddCloud(args: UpdateBddArgs) {
186
+ const { test_case_key, bdd_content, name } = args;
187
+
188
+ try {
189
+ // Step 1: GET existing test case to extract required fields for PUT
190
+ const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
191
+ const tc = getResponse.data;
192
+
193
+ // Cloud v2 PUT requires: id, key, name, project, priority, status
194
+ const requiredFields = ['id', 'key', 'name', 'project', 'priority', 'status'];
195
+ for (const field of requiredFields) {
196
+ if (tc[field] === undefined || tc[field] === null) {
197
+ throw new McpError(ErrorCode.InternalError,
198
+ `Existing test case is missing required field '${field}' needed for Cloud v2 update.`);
199
+ }
120
200
  }
121
- } catch (error) {
122
- let errorMessage = 'Unknown error';
123
- if (error instanceof Error && 'response' in error) {
124
- const axiosError = error as any;
125
- errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
126
- } else if (error instanceof Error) {
127
- errorMessage = error.message;
128
- } else {
129
- errorMessage = String(error);
201
+
202
+ const putPayload: any = {
203
+ id: tc.id,
204
+ key: tc.key,
205
+ name: typeof name === 'string' && name.trim().length > 0 ? name : tc.name,
206
+ project: tc.project,
207
+ priority: tc.priority,
208
+ status: tc.status,
209
+ };
210
+
211
+ // Preserve optional fields
212
+ for (const field of ['objective', 'precondition', 'estimatedTime', 'folder', 'component', 'owner']) {
213
+ if (tc[field] !== undefined) putPayload[field] = tc[field];
130
214
  }
131
- throw new McpError(
132
- ErrorCode.InternalError,
133
- `Failed to create test case: ${errorMessage}`
215
+ if (Array.isArray(tc.labels)) putPayload.labels = tc.labels;
216
+ if (tc.customFields) putPayload.customFields = tc.customFields;
217
+
218
+ // Step 2: PUT to update metadata
219
+ await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
220
+
221
+ // Step 3: POST testscript with BDD content
222
+ const converted = convertToGherkin(bdd_content);
223
+ const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
224
+ await this.axiosInstance.post(
225
+ `${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`,
226
+ { type: 'bdd', text: finalText }
134
227
  );
228
+
229
+ return {
230
+ content: [{
231
+ type: 'text',
232
+ text: `✅ Updated ${test_case_key} with BDD content successfully (Cloud v2)`,
233
+ }],
234
+ };
235
+ } catch (error) {
236
+ if (error instanceof McpError) throw error;
237
+ throw new McpError(ErrorCode.InternalError, `Failed to update test case BDD: ${this.formatError(error)}`);
135
238
  }
136
239
  }
137
240
 
138
- async updateTestCaseBdd(args: UpdateBddArgs) {
241
+ private async updateTestCaseBddDC(args: UpdateBddArgs) {
139
242
  const { test_case_key, bdd_content, name } = args;
140
243
 
141
244
  try {
142
- // First, get the existing test case data
143
245
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
144
246
  const testCaseData = getResponse.data;
145
247
 
146
- // Convert incoming BDD markdown (fallback to raw content if conversion returns empty)
147
248
  const converted = convertToGherkin(bdd_content);
148
249
  const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
149
250
 
150
- // Build a payload aligned with Zephyr Scale Server's update schema.
151
- // Only include fields that exist to avoid accidental nulling; required fields must be present.
152
251
  const payload: any = {};
153
-
154
- // Required base fields (schema requires these on Server/Data Center). If missing, throw.
155
252
  const requiredFields: Array<[string, any]> = [
156
253
  ['projectKey', testCaseData.projectKey],
157
254
  ['name', testCaseData.name],
158
255
  ['status', testCaseData.status],
159
256
  ['priority', testCaseData.priority]
160
257
  ];
161
-
162
258
  for (const [field, value] of requiredFields) {
163
259
  if (value === undefined || value === null || value === '') {
164
- throw new McpError(ErrorCode.InternalError, `Existing test case is missing required field '${field}' needed for update.`);
260
+ throw new McpError(ErrorCode.InternalError,
261
+ `Existing test case is missing required field '${field}' needed for update.`);
165
262
  }
166
263
  payload[field] = value;
167
264
  }
168
265
 
169
- // Optional name update
170
- if (typeof name === 'string' && name.trim().length > 0) {
171
- payload.name = name;
172
- }
266
+ if (typeof name === 'string' && name.trim().length > 0) payload.name = name;
173
267
 
174
- // Optional simple scalar/string fields
175
- const optionalScalarFields = [
176
- 'objective',
177
- 'precondition',
178
- 'folder',
179
- 'component',
180
- 'owner',
181
- 'estimatedTime'
182
- ];
183
- for (const field of optionalScalarFields) {
268
+ for (const field of ['objective', 'precondition', 'folder', 'component', 'owner', 'estimatedTime']) {
184
269
  if (testCaseData[field] !== undefined) payload[field] = testCaseData[field];
185
270
  }
186
-
187
- // Arrays / objects
188
271
  if (Array.isArray(testCaseData.labels)) payload.labels = testCaseData.labels;
189
272
  if (testCaseData.customFields) payload.customFields = testCaseData.customFields;
190
273
  if (testCaseData.parameters) payload.parameters = testCaseData.parameters;
191
- // issueLinks preferred; map deprecated issueKey if present and issueLinks absent
192
274
  if (Array.isArray(testCaseData.issueLinks)) {
193
275
  payload.issueLinks = testCaseData.issueLinks;
194
276
  } else if (testCaseData.issueKey) {
195
277
  payload.issueLinks = [testCaseData.issueKey];
196
278
  }
197
279
 
198
- // Build testScript. Force type to BDD when performing a BDD update.
199
- payload.testScript = {
200
- type: 'BDD',
201
- text: finalText
280
+ payload.testScript = { type: 'BDD', text: finalText };
281
+
282
+ const updateResponse = await this.axiosInstance.put(
283
+ `${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, payload
284
+ );
285
+
286
+ if (updateResponse.status !== 200) {
287
+ throw new Error(`Failed to update ${test_case_key}: ${updateResponse.status}`);
288
+ }
289
+
290
+ return {
291
+ content: [{
292
+ type: 'text',
293
+ text: `✅ Updated ${test_case_key} with BDD content successfully\nPayload summary: ${JSON.stringify({ textLength: finalText.length, projectKey: payload.projectKey, name: payload.name, preservedLabels: payload.labels?.length || 0 }, null, 2)}`,
294
+ }],
202
295
  };
296
+ } catch (error) {
297
+ if (error instanceof McpError) throw error;
298
+ throw new McpError(ErrorCode.InternalError, `Failed to update test case BDD: ${this.formatError(error)}`);
299
+ }
300
+ }
301
+
302
+ async createFolder(args: FolderArgs) {
303
+ if (this.jiraConfig.type === 'cloud') {
304
+ return this.createFolderCloud(args);
305
+ }
306
+ return this.createFolderDC(args);
307
+ }
308
+
309
+ private async createFolderCloud(args: FolderArgs) {
310
+ const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
311
+
312
+ // Cloud v2 uses folderType (not type) and parentId integer
313
+ // Map TEST_RUN → TEST_CYCLE for Cloud v2
314
+ const cloudFolderType = folder_type === 'TEST_RUN' ? 'TEST_CYCLE' : folder_type;
315
+
316
+ const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
317
+ if (segments.length === 0) {
318
+ throw new McpError(ErrorCode.InvalidParams, 'folder name/path cannot be empty');
319
+ }
320
+
321
+ const leafName = segments[segments.length - 1];
322
+ let parentId: number | null = null;
323
+
324
+ // Resolve parent if nested path
325
+ if (segments.length > 1) {
326
+ const parentPath = '/' + segments.slice(0, -1).join('/');
327
+ parentId = await resolveFolderIdByPath(
328
+ this.axiosInstance, project_key, parentPath, cloudFolderType
329
+ );
330
+ if (parentId === null) {
331
+ throw new McpError(ErrorCode.InternalError,
332
+ `Parent folder not found for path: ${parentPath}. Create parent folders first.`);
333
+ }
334
+ }
203
335
 
204
- // PUT update
205
- const updateResponse = await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, payload);
336
+ const payload: any = {
337
+ projectKey: project_key,
338
+ name: leafName,
339
+ folderType: cloudFolderType,
340
+ };
341
+ if (parentId !== null) payload.parentId = parentId;
206
342
 
207
- if (updateResponse.status === 200) {
343
+ try {
344
+ const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.folder, payload);
345
+ if (response.status === 201 || response.status === 200) {
208
346
  return {
209
- content: [
210
- {
211
- type: 'text',
212
- text: `✅ Updated ${test_case_key} with BDD content successfully\nPayload summary: ${JSON.stringify({ textLength: finalText.length, projectKey: payload.projectKey, name: payload.name, preservedLabels: payload.labels?.length || 0 }, null, 2)}`,
213
- },
214
- ],
347
+ content: [{
348
+ type: 'text',
349
+ text: `✅ Folder created successfully: ${leafName} (ID: ${response.data.id || 'N/A'})\n${JSON.stringify(response.data, null, 2)}`,
350
+ }],
215
351
  };
216
352
  }
217
-
218
- throw new Error(`Failed to update ${test_case_key}: ${updateResponse.status}`);
353
+ throw new Error(`Unexpected status code: ${response.status}`);
219
354
  } catch (error) {
220
- throw new McpError(
221
- ErrorCode.InternalError,
222
- `Failed to update test case BDD: ${error instanceof Error ? error.message : String(error)}`
223
- );
355
+ if (error instanceof McpError) throw error;
356
+ throw new McpError(ErrorCode.InternalError, `Failed to create folder: ${this.formatError(error)}`);
224
357
  }
225
358
  }
226
359
 
227
- async createFolder(args: FolderArgs) {
360
+ private async createFolderDC(args: FolderArgs) {
228
361
  const { project_key, name, folder_type = 'TEST_CASE' } = args;
229
362
 
230
- // According to Zephyr Scale API documentation:
231
- // - projectKey: Project key (required)
232
- // - name: Full folder path including parent folders (e.g., "/folder/subfolder")
233
- // - type: Folder type (TEST_CASE, TEST_PLAN, or TEST_RUN)
234
363
  const payload: any = {
235
364
  projectKey: project_key,
236
365
  name: name,
@@ -239,38 +368,55 @@ export class ZephyrToolHandlers {
239
368
 
240
369
  try {
241
370
  const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.folder, payload);
242
-
243
371
  if (response.status === 201 || response.status === 200) {
244
372
  return {
245
- content: [
246
- {
247
- type: 'text',
248
- text: `✅ Folder created successfully: ${response.data.name || name} (ID: ${response.data.id || 'N/A'})\n${JSON.stringify(response.data, null, 2)}`,
249
- },
250
- ],
373
+ content: [{
374
+ type: 'text',
375
+ text: `✅ Folder created successfully: ${response.data.name || name} (ID: ${response.data.id || 'N/A'})\n${JSON.stringify(response.data, null, 2)}`,
376
+ }],
251
377
  };
252
- } else {
253
- throw new Error(`Unexpected status code: ${response.status}`);
254
378
  }
379
+ throw new Error(`Unexpected status code: ${response.status}`);
255
380
  } catch (error) {
256
- let errorMessage = 'Unknown error';
257
- if (error instanceof Error && 'response' in error) {
258
- const axiosError = error as any;
259
- errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}, Payload sent: ${JSON.stringify(payload)}`;
260
- } else if (error instanceof Error) {
261
- errorMessage = error.message;
262
- } else {
263
- errorMessage = String(error);
264
- }
265
- throw new McpError(
266
- ErrorCode.InternalError,
267
- `Failed to create folder: ${errorMessage}`
268
- );
381
+ if (error instanceof McpError) throw error;
382
+ throw new McpError(ErrorCode.InternalError, `Failed to create folder: ${this.formatError(error)}`);
269
383
  }
270
384
  }
271
385
 
272
386
  async getTestRunCases(args: any) {
273
387
  const { test_run_key } = args;
388
+
389
+ if (this.jiraConfig.type === 'cloud') {
390
+ try {
391
+ // Cloud: test cases are retrieved via executions associated with the cycle
392
+ const response = await this.axiosInstance.get('/testexecutions', {
393
+ params: { testCycle: test_run_key, maxResults: 1000 },
394
+ });
395
+
396
+ const executions = Array.isArray(response.data)
397
+ ? response.data
398
+ : response.data?.values ?? [];
399
+
400
+ // Deduplicate by test case key
401
+ const seen = new Set<string>();
402
+ const testCaseKeys: string[] = [];
403
+ for (const exec of executions) {
404
+ const key = exec.testCase?.key
405
+ ?? exec.testCase?.self?.match(/testcases\/([^/]+)/)?.[1];
406
+ if (key && !seen.has(key)) {
407
+ seen.add(key);
408
+ testCaseKeys.push(key);
409
+ }
410
+ }
411
+ return {
412
+ content: [{ type: 'text', text: JSON.stringify(testCaseKeys, null, 2) }],
413
+ };
414
+ } catch (error) {
415
+ throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${this.formatError(error)}`);
416
+ }
417
+ }
418
+
419
+ // Data Center: items[] in the run response
274
420
  try {
275
421
  const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
276
422
  const testCases = response.data.items?.map((item: any) => item.testCaseKey) || [];
@@ -278,76 +424,130 @@ export class ZephyrToolHandlers {
278
424
  content: [{ type: 'text', text: JSON.stringify(testCases, null, 2) }],
279
425
  };
280
426
  } catch (error) {
281
- throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${error instanceof Error ? error.message : String(error)}`);
427
+ throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${this.formatError(error)}`);
282
428
  }
283
429
  }
284
430
 
285
431
  async deleteTestCase(args: any) {
432
+ if (this.jiraConfig.type === 'cloud') {
433
+ throw new McpError(
434
+ ErrorCode.InvalidRequest,
435
+ 'delete_test_case is not supported by the Zephyr Scale Cloud v2 API.'
436
+ );
437
+ }
438
+
286
439
  const { test_case_key } = args;
287
440
  try {
288
441
  const response = await this.axiosInstance.delete(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
289
442
  if (response.status === 204) {
290
- return {
291
- content: [{ type: 'text', text: `Test case ${test_case_key} deleted successfully.` }],
292
- };
293
- } else {
294
- return {
295
- content: [{ type: 'text', text: `Failed to delete test case. Status: ${response.status}` }],
296
- isError: true,
297
- };
443
+ return { content: [{ type: 'text', text: `Test case ${test_case_key} deleted successfully.` }] };
298
444
  }
445
+ return {
446
+ content: [{ type: 'text', text: `Failed to delete test case. Status: ${response.status}` }],
447
+ isError: true,
448
+ };
299
449
  } catch (error) {
300
- throw new McpError(ErrorCode.InternalError, `Failed to delete test case: ${error instanceof Error ? error.message : String(error)}`);
450
+ throw new McpError(ErrorCode.InternalError, `Failed to delete test case: ${this.formatError(error)}`);
301
451
  }
302
452
  }
303
453
 
304
454
  async deleteTestRun(args: any) {
455
+ if (this.jiraConfig.type === 'cloud') {
456
+ throw new McpError(
457
+ ErrorCode.InvalidRequest,
458
+ 'delete_test_run is not supported by the Zephyr Scale Cloud v2 API.'
459
+ );
460
+ }
461
+
305
462
  const { test_run_key } = args;
306
463
  try {
307
464
  const response = await this.axiosInstance.delete(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
308
465
  if (response.status === 204) {
309
- return {
310
- content: [{ type: 'text', text: `Test run ${test_run_key} deleted successfully.` }],
311
- };
312
- } else {
313
- return {
314
- content: [{ type: 'text', text: `Failed to delete test run. Status: ${response.status}` }],
315
- isError: true,
316
- };
466
+ return { content: [{ type: 'text', text: `Test run ${test_run_key} deleted successfully.` }] };
317
467
  }
468
+ return {
469
+ content: [{ type: 'text', text: `Failed to delete test run. Status: ${response.status}` }],
470
+ isError: true,
471
+ };
318
472
  } catch (error) {
319
- throw new McpError(ErrorCode.InternalError, `Failed to delete test run: ${error instanceof Error ? error.message : String(error)}`);
473
+ throw new McpError(ErrorCode.InternalError, `Failed to delete test run: ${this.formatError(error)}`);
320
474
  }
321
475
  }
322
476
 
323
477
  async createTestRun(args: TestRunArgs) {
478
+ if (this.jiraConfig.type === 'cloud') {
479
+ return this.createTestRunCloud(args);
480
+ }
481
+ return this.createTestRunDC(args);
482
+ }
483
+
484
+ private async createTestRunCloud(args: TestRunArgs) {
324
485
  const {
325
- project_key,
326
- name,
327
- test_case_keys,
328
- test_plan_key,
329
- folder,
330
- planned_start_date,
331
- planned_end_date,
332
- description,
333
- owner,
334
- environment,
335
- issue_key,
336
- issue_links,
337
- custom_fields
486
+ project_key, name, test_case_keys, folder,
487
+ planned_start_date, planned_end_date, description,
488
+ owner, environment, custom_fields,
338
489
  } = args;
339
490
 
340
- // Build the basic payload
341
- const payload: any = {
342
- projectKey: project_key,
343
- name: name,
344
- };
491
+ // Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
492
+ // plannedEndDate, statusName, folderId, ownerId, customFields
493
+ const payload: any = { projectKey: project_key, name };
494
+
495
+ if (description) payload.description = description;
496
+ if (planned_start_date) payload.plannedStartDate = planned_start_date;
497
+ if (planned_end_date) payload.plannedEndDate = planned_end_date;
498
+ if (custom_fields) payload.customFields = custom_fields;
499
+
500
+ // Resolve folder path → folderId
501
+ if (folder) {
502
+ const folderId = await resolveFolderIdByPath(
503
+ this.axiosInstance, project_key, folder, 'TEST_CYCLE'
504
+ );
505
+ if (folderId !== null) payload.folderId = folderId;
506
+ }
507
+
508
+ // Note: environment and owner (by account ID) not mapped here — Cloud requires IDs
509
+
510
+ try {
511
+ const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testrun, payload);
512
+ if (response.status !== 201) throw new Error(`Unexpected status code: ${response.status}`);
513
+
514
+ const cycleKey = response.data.key || 'Unknown';
515
+
516
+ // Step 2: add test cases to the cycle
517
+ if (test_case_keys && test_case_keys.length > 0) {
518
+ await this.axiosInstance.post(
519
+ `${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/testcases`,
520
+ { items: test_case_keys.map((k: string) => ({ testCaseKey: k })) }
521
+ );
522
+ }
523
+
524
+ return {
525
+ content: [{
526
+ type: 'text',
527
+ text: `✅ Test run (cycle) created successfully: ${cycleKey}\n${JSON.stringify({
528
+ key: cycleKey,
529
+ name,
530
+ testCaseCount: test_case_keys?.length || 0,
531
+ }, null, 2)}`,
532
+ }],
533
+ };
534
+ } catch (error) {
535
+ if (error instanceof McpError) throw error;
536
+ throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
537
+ }
538
+ }
539
+
540
+ private async createTestRunDC(args: TestRunArgs) {
541
+ const {
542
+ project_key, name, test_case_keys, test_plan_key, folder,
543
+ planned_start_date, planned_end_date, description,
544
+ owner, environment, issue_key, issue_links, custom_fields
545
+ } = args;
546
+
547
+ const payload: any = { projectKey: project_key, name };
345
548
 
346
- // Add optional fields
347
549
  if (test_case_keys && test_case_keys.length > 0) {
348
- payload.items = test_case_keys.map((testCaseKey: string) => ({
349
- testCaseKey: testCaseKey
350
- }));
550
+ payload.items = test_case_keys.map((testCaseKey: string) => ({ testCaseKey }));
351
551
  }
352
552
  if (folder) payload.folder = folder;
353
553
  if (planned_start_date) payload.plannedStartDate = planned_start_date;
@@ -362,163 +562,144 @@ export class ZephyrToolHandlers {
362
562
 
363
563
  try {
364
564
  const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testrun, payload);
365
-
366
- if (response.status === 201) {
367
- const testRunKey = response.data.key || 'Unknown';
368
- return {
369
- content: [
370
- {
371
- type: 'text',
372
- text: `✅ Test run created successfully: ${testRunKey}\n${JSON.stringify({
373
- key: testRunKey,
374
- name: name,
375
- testCaseCount: test_case_keys?.length || 0,
376
- environment: environment || 'Not specified'
377
- }, null, 2)}`,
378
- },
379
- ],
380
- };
381
- } else {
382
- throw new Error(`Unexpected status code: ${response.status}`);
383
- }
565
+ if (response.status !== 201) throw new Error(`Unexpected status code: ${response.status}`);
566
+ const testRunKey = response.data.key || 'Unknown';
567
+ return {
568
+ content: [{
569
+ type: 'text',
570
+ text: `✅ Test run created successfully: ${testRunKey}\n${JSON.stringify({
571
+ key: testRunKey, name, testCaseCount: test_case_keys?.length || 0,
572
+ environment: environment || 'Not specified'
573
+ }, null, 2)}`,
574
+ }],
575
+ };
384
576
  } catch (error) {
385
- let errorMessage = 'Unknown error';
386
- if (error instanceof Error && 'response' in error) {
387
- const axiosError = error as any;
388
- errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
389
- } else if (error instanceof Error) {
390
- errorMessage = error.message;
391
- } else {
392
- errorMessage = String(error);
393
- }
394
- throw new McpError(
395
- ErrorCode.InternalError,
396
- `Failed to create test run: ${errorMessage}`
397
- );
577
+ throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
398
578
  }
399
579
  }
400
580
 
401
581
  async getTestRun(args: any) {
402
582
  const { test_run_key } = args;
583
+ // Both Cloud (/testcycles/{key}) and DC (/rest/atm/1.0/testrun/{key}) handled
584
+ // via apiEndpoints.testrun which now correctly maps to /testcycles for Cloud
403
585
  try {
404
586
  const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
405
587
  return {
406
- content: [
407
- {
408
- type: 'text',
409
- text: JSON.stringify(response.data, null, 2),
410
- },
411
- ],
588
+ content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
412
589
  };
413
590
  } catch (error) {
414
- let errorMessage = 'Unknown error';
415
- if (error instanceof Error && 'response' in error) {
416
- const axiosError = error as any;
417
- if (axiosError.response?.status === 404) {
418
- errorMessage = `Test run ${test_run_key} not found`;
419
- } else {
420
- errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
421
- }
422
- } else if (error instanceof Error) {
423
- errorMessage = error.message;
424
- } else {
425
- errorMessage = String(error);
426
- }
427
- throw new McpError(
428
- ErrorCode.InternalError,
429
- `Failed to get test run: ${errorMessage}`
430
- );
591
+ throw new McpError(ErrorCode.InternalError, `Failed to get test run: ${this.formatError(error)}`);
431
592
  }
432
593
  }
433
594
 
434
- async getTestExecution(args: any) {
595
+ async getTestExecution(args: GetTestExecutionArgs) {
435
596
  const { execution_id, test_run_keys } = args;
436
597
 
437
- // Require users to specify test runs to search - fail immediately if not provided
598
+ if (this.jiraConfig.type === 'cloud') {
599
+ // Cloud v2: direct fetch by ID or key (e.g. PROJ-E123)
600
+ try {
601
+ const response = await this.axiosInstance.get(`/testexecutions/${execution_id}`);
602
+ return {
603
+ content: [{
604
+ type: 'text',
605
+ text: `✅ Test execution ${execution_id} found:\n${JSON.stringify(response.data, null, 2)}`,
606
+ }],
607
+ };
608
+ } catch (error) {
609
+ throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${this.formatError(error)}`);
610
+ }
611
+ }
612
+
613
+ // Data Center: iterate test runs searching testresults
438
614
  if (!test_run_keys || !Array.isArray(test_run_keys) || test_run_keys.length === 0) {
439
615
  throw new McpError(
440
616
  ErrorCode.InvalidParams,
441
- 'test_run_keys is required. Please provide an array of test run keys to search in (e.g., ["PROJ-C152", "PROJ-C161"]). Use get_test_run_cases to find test runs if needed.'
617
+ 'test_run_keys is required for Data Center. Provide an array of test run keys to search in.'
442
618
  );
443
619
  }
444
620
 
445
621
  try {
446
- const testRunsToTry = test_run_keys;
447
-
448
622
  const searchResults: any[] = [];
449
623
 
450
- for (const testRunKey of testRunsToTry) {
624
+ for (const testRunKey of test_run_keys) {
451
625
  try {
452
- const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${testRunKey}/testresults`);
626
+ const response = await this.axiosInstance.get(
627
+ `${this.jiraConfig.apiEndpoints.testrun}/${testRunKey}/testresults`
628
+ );
453
629
 
454
630
  if (response.status === 200 && response.data) {
455
- // Look for the specific execution_id in the results
456
631
  const results = Array.isArray(response.data) ? response.data : [response.data];
457
- const matchingExecution = results.find((result: any) =>
458
- result.id && result.id.toString() === execution_id
459
- );
460
-
461
- if (matchingExecution) {
632
+ const match = results.find((r: any) => r.id && r.id.toString() === execution_id);
633
+ if (match) {
462
634
  return {
463
- content: [
464
- {
465
- type: 'text',
466
- text: `✅ Test execution ${execution_id} found in ${testRunKey}:\n${JSON.stringify(matchingExecution, null, 2)}`,
467
- },
468
- ],
635
+ content: [{
636
+ type: 'text',
637
+ text: `✅ Test execution ${execution_id} found in ${testRunKey}:\n${JSON.stringify(match, null, 2)}`,
638
+ }],
469
639
  };
470
640
  }
471
-
472
- // Store search info for debugging
473
- searchResults.push({
474
- testRunKey,
475
- executionCount: results.length,
476
- executionIds: results.map((r: any) => r.id).slice(0, 5) // Show first 5 IDs
477
- });
641
+ searchResults.push({ testRunKey, executionCount: results.length, executionIds: results.map((r: any) => r.id).slice(0, 5) });
478
642
  }
479
643
  } catch (runError) {
480
- // Store error info for debugging
481
- searchResults.push({
482
- testRunKey,
483
- error: runError instanceof Error ? runError.message : String(runError)
484
- });
485
- continue;
644
+ searchResults.push({ testRunKey, error: runError instanceof Error ? runError.message : String(runError) });
486
645
  }
487
646
  }
488
647
 
489
- // If not found, provide helpful debugging info
490
- throw new Error(`Test execution ${execution_id} not found in any of the ${testRunsToTry.length} test runs searched. Search results: ${JSON.stringify(searchResults, null, 2)}`);
648
+ throw new Error(`Test execution ${execution_id} not found in any of the ${test_run_keys.length} test runs. Search results: ${JSON.stringify(searchResults, null, 2)}`);
491
649
  } catch (error) {
492
- let errorMessage = 'Unknown error';
493
- if (error instanceof Error) {
494
- errorMessage = error.message;
495
- } else {
496
- errorMessage = String(error);
497
- }
498
- throw new McpError(
499
- ErrorCode.InternalError,
500
- `Failed to get test execution: ${errorMessage}`
501
- );
650
+ if (error instanceof McpError) throw error;
651
+ throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${this.formatError(error)}`);
502
652
  }
503
653
  }
504
654
 
505
655
  async searchTestCasesByFolder(args: SearchTestCasesArgs) {
506
656
  const { project_key, folder_path, max_results = 100 } = args;
507
-
508
- // Build JQL-style query for Zephyr Scale API
509
- // Escape double quotes in folder path
510
- const escapedFolderPath = folder_path.replace(/"/g, '\\"');
511
- const query = `projectKey = "${project_key}" AND folder = "${escapedFolderPath}"`;
512
-
513
- const params = {
514
- query: query,
515
- maxResults: max_results,
516
- };
517
657
 
658
+ if (this.jiraConfig.type === 'cloud') {
659
+ try {
660
+ // Cloud v2: GET /testcases?projectKey=X&folderId=Y
661
+ const folderId = await resolveFolderIdByPath(
662
+ this.axiosInstance, project_key, folder_path, 'TEST_CASE'
663
+ );
664
+
665
+ if (folderId === null) {
666
+ return {
667
+ content: [{
668
+ type: 'text',
669
+ text: `⚠️ Folder not found: "${folder_path}" in project ${project_key}. No test cases returned.`,
670
+ }],
671
+ };
672
+ }
673
+
674
+ const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testcase, {
675
+ params: { projectKey: project_key, folderId, maxResults: max_results },
676
+ });
677
+
678
+ const testCases = Array.isArray(response.data)
679
+ ? response.data
680
+ : response.data?.values ?? [];
681
+
682
+ return {
683
+ content: [{
684
+ type: 'text',
685
+ text: `✅ Found ${testCases.length} test cases in folder "${folder_path}" (folderId: ${folderId}):\n${JSON.stringify({
686
+ folder: folder_path, folderId, testCaseKeys: testCases.map((tc: any) => tc.key), totalCount: testCases.length,
687
+ }, null, 2)}`,
688
+ }],
689
+ };
690
+ } catch (error) {
691
+ throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${this.formatError(error)}`);
692
+ }
693
+ }
694
+
695
+ // Data Center: query-based search
518
696
  try {
519
- const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.search, { params });
520
-
521
- // Handle different response structures
697
+ const escapedFolderPath = folder_path.replace(/"/g, '\\"');
698
+ const query = `projectKey = "${project_key}" AND folder = "${escapedFolderPath}"`;
699
+ const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.search, {
700
+ params: { query, maxResults: max_results },
701
+ });
702
+
522
703
  let testCases = [];
523
704
  if (Array.isArray(response.data)) {
524
705
  testCases = response.data;
@@ -527,62 +708,73 @@ export class ZephyrToolHandlers {
527
708
  } else if (response.data.results && Array.isArray(response.data.results)) {
528
709
  testCases = response.data.results;
529
710
  }
530
-
711
+
531
712
  return {
532
- content: [
533
- {
534
- type: 'text',
535
- text: `✅ Found ${testCases.length} test cases in folder "${folder_path}":\n${JSON.stringify({
536
- folder: folder_path,
537
- query: query,
538
- testCaseKeys: testCases.map((tc: any) => tc.key),
539
- totalCount: testCases.length
540
- }, null, 2)}`,
541
- },
542
- ],
713
+ content: [{
714
+ type: 'text',
715
+ text: `✅ Found ${testCases.length} test cases in folder "${folder_path}":\n${JSON.stringify({
716
+ folder: folder_path, query, testCaseKeys: testCases.map((tc: any) => tc.key), totalCount: testCases.length,
717
+ }, null, 2)}`,
718
+ }],
543
719
  };
544
720
  } catch (error) {
545
- let errorMessage = 'Unknown error';
546
- if (error instanceof Error && 'response' in error) {
547
- const axiosError = error as any;
548
- if (axiosError.response?.status === 404) {
549
- errorMessage = `Folder "${folder_path}" not found or no test cases found`;
550
- } else {
551
- errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
552
- }
553
- } else if (error instanceof Error) {
554
- errorMessage = error.message;
555
- } else {
556
- errorMessage = String(error);
557
- }
558
- throw new McpError(
559
- ErrorCode.InternalError,
560
- `Failed to search test cases by folder: ${errorMessage}`
561
- );
721
+ throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${this.formatError(error)}`);
562
722
  }
563
723
  }
564
724
 
565
725
  async searchTestRuns(args: SearchTestRunsArgs) {
566
726
  const { project_key, folder, max_results = 200, fields } = args;
567
727
 
568
- // Build query string per Zephyr Scale API docs
569
- const queryParts: string[] = [];
570
- if (project_key) queryParts.push(`projectKey = "${project_key}"`);
571
- if (folder) queryParts.push(`folder = "${folder}"`);
572
-
573
- if (queryParts.length === 0) {
728
+ if (!project_key && !folder) {
574
729
  throw new McpError(ErrorCode.InvalidParams, 'At least one of project_key or folder must be provided.');
575
730
  }
576
731
 
577
- const query = queryParts.join(' AND ');
578
- const params: Record<string, any> = { query, maxResults: max_results };
579
- if (fields) params.fields = fields;
732
+ if (this.jiraConfig.type === 'cloud') {
733
+ try {
734
+ // Cloud v2: GET /testcycles?projectKey=X&folderId=Y
735
+ const params: Record<string, any> = { maxResults: max_results };
736
+ if (project_key) params.projectKey = project_key;
737
+
738
+ if (folder && project_key) {
739
+ const folderId = await resolveFolderIdByPath(
740
+ this.axiosInstance, project_key, folder, 'TEST_CYCLE'
741
+ );
742
+ if (folderId !== null) params.folderId = folderId;
743
+ }
744
+
745
+ const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testrun, { params });
580
746
 
581
- const searchEndpoint = this.jiraConfig.type === 'cloud'
582
- ? '/testruns/search'
583
- : '/rest/atm/1.0/testrun/search';
747
+ const testRuns = Array.isArray(response.data)
748
+ ? response.data
749
+ : response.data?.values ?? [];
584
750
 
751
+ return {
752
+ content: [{
753
+ type: 'text',
754
+ text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
755
+ totalCount: testRuns.length,
756
+ testRuns: testRuns.map((tr: any) => ({
757
+ key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
758
+ })),
759
+ }, null, 2)}`,
760
+ }],
761
+ };
762
+ } catch (error) {
763
+ throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${this.formatError(error)}`);
764
+ }
765
+ }
766
+
767
+ // Data Center: query-based search
585
768
  try {
769
+ const queryParts: string[] = [];
770
+ if (project_key) queryParts.push(`projectKey = "${project_key}"`);
771
+ if (folder) queryParts.push(`folder = "${folder}"`);
772
+ const query = queryParts.join(' AND ');
773
+
774
+ const searchEndpoint = '/rest/atm/1.0/testrun/search';
775
+ const params: Record<string, any> = { query, maxResults: max_results };
776
+ if (fields) params.fields = fields;
777
+
586
778
  const response = await this.axiosInstance.get(searchEndpoint, { params });
587
779
 
588
780
  let testRuns: any[] = [];
@@ -595,41 +787,23 @@ export class ZephyrToolHandlers {
595
787
  }
596
788
 
597
789
  return {
598
- content: [
599
- {
600
- type: 'text',
601
- text: `✅ Found ${testRuns.length} test run(s) matching query "${query}":\n${JSON.stringify({
602
- query,
603
- totalCount: testRuns.length,
604
- testRuns: testRuns.map((tr: any) => ({
605
- key: tr.key,
606
- name: tr.name,
607
- status: tr.status,
608
- folder: tr.folder,
609
- testCaseCount: tr.testCaseCount,
610
- issueKey: tr.issueKey,
611
- }))
612
- }, null, 2)}`,
613
- },
614
- ],
790
+ content: [{
791
+ type: 'text',
792
+ text: `✅ Found ${testRuns.length} test run(s) matching query "${query}":\n${JSON.stringify({
793
+ query, totalCount: testRuns.length,
794
+ testRuns: testRuns.map((tr: any) => ({
795
+ key: tr.key, name: tr.name, status: tr.status, folder: tr.folder,
796
+ testCaseCount: tr.testCaseCount, issueKey: tr.issueKey,
797
+ })),
798
+ }, null, 2)}`,
799
+ }],
615
800
  };
616
801
  } catch (error) {
617
- let errorMessage = 'Unknown error';
618
- if (error instanceof Error && 'response' in error) {
619
- const axiosError = error as any;
620
- errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
621
- } else if (error instanceof Error) {
622
- errorMessage = error.message;
623
- } else {
624
- errorMessage = String(error);
625
- }
626
- throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${errorMessage}`);
802
+ throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${this.formatError(error)}`);
627
803
  }
628
804
  }
629
805
 
630
806
  async addTestCasesToRun(args: AddTestCasesToRunArgs) {
631
- const { test_run_key, test_case_keys } = args;
632
-
633
807
  if (this.jiraConfig.type === 'datacenter') {
634
808
  throw new McpError(
635
809
  ErrorCode.InvalidRequest,
@@ -637,11 +811,16 @@ export class ZephyrToolHandlers {
637
811
  );
638
812
  }
639
813
 
814
+ const { test_run_key, test_case_keys } = args;
815
+
640
816
  try {
641
817
  const payload = {
642
818
  items: test_case_keys.map(key => ({ testCaseKey: key }))
643
819
  };
644
- const response = await this.axiosInstance.post(`/testcycles/${test_run_key}/testcases`, payload);
820
+ const response = await this.axiosInstance.post(
821
+ `${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`,
822
+ payload
823
+ );
645
824
 
646
825
  if (response.status === 200 || response.status === 201 || response.status === 204) {
647
826
  return {
@@ -649,7 +828,7 @@ export class ZephyrToolHandlers {
649
828
  };
650
829
  }
651
830
  } catch (error) {
652
- throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${error instanceof Error ? error.message : String(error)}`);
831
+ throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
653
832
  }
654
833
 
655
834
  return {
@@ -657,4 +836,12 @@ export class ZephyrToolHandlers {
657
836
  isError: true,
658
837
  };
659
838
  }
660
- }
839
+
840
+ private formatError(error: unknown): string {
841
+ if (error instanceof Error && 'response' in error) {
842
+ const axiosError = error as any;
843
+ return `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
844
+ }
845
+ return error instanceof Error ? error.message : String(error);
846
+ }
847
+ }