zephyr-scale-mcp-server 0.4.3 → 0.4.4

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.
@@ -108,13 +108,19 @@ export class ZephyrToolHandlers {
108
108
  if (!test_script)
109
109
  return;
110
110
  if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
111
- const items = test_script.steps.map((step) => ({
112
- inline: {
113
- description: step.description || '',
114
- testData: step.testData || null,
115
- expectedResult: step.expectedResult || null,
116
- },
117
- }));
111
+ const items = test_script.steps.map((step) => {
112
+ // If step is a call-to-test (testCaseKey), use the testCase variant
113
+ if (step.testCaseKey) {
114
+ return { testCase: { testCaseKey: step.testCaseKey } };
115
+ }
116
+ return {
117
+ inline: {
118
+ description: step.description || '',
119
+ testData: step.testData || null,
120
+ expectedResult: step.expectedResult || null,
121
+ },
122
+ };
123
+ });
118
124
  await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`, { mode: 'OVERWRITE', items });
119
125
  }
120
126
  else if (test_script.type === 'BDD' && test_script.text) {
@@ -213,15 +219,25 @@ export class ZephyrToolHandlers {
213
219
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
214
220
  const tc = getResponse.data;
215
221
  // UpdateTestCaseInput requires: id, key, name, priority, project, status
216
- // tc.project is a ProjectLink { id, self } pass it back as-is
217
- await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
222
+ // All optional fields must also be re-sent or the API will clear them
223
+ const putPayload = {
218
224
  id: tc.id,
219
225
  key: test_case_key,
220
226
  name,
221
227
  status: tc.status,
222
228
  priority: tc.priority,
223
229
  project: tc.project,
224
- });
230
+ };
231
+ // Preserve all optional fields to avoid the API clearing them
232
+ for (const field of ['objective', 'precondition', 'estimatedTime', 'component', 'owner', 'folder']) {
233
+ if (tc[field] !== undefined && tc[field] !== null)
234
+ putPayload[field] = tc[field];
235
+ }
236
+ if (Array.isArray(tc.labels) && tc.labels.length > 0)
237
+ putPayload.labels = tc.labels;
238
+ if (tc.customFields && Object.keys(tc.customFields).length > 0)
239
+ putPayload.customFields = tc.customFields;
240
+ await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
225
241
  }
226
242
  return {
227
243
  content: [{
@@ -454,9 +470,10 @@ export class ZephyrToolHandlers {
454
470
  return this.createTestRunDC(args);
455
471
  }
456
472
  async createTestRunCloud(args) {
457
- const { project_key, name, test_case_keys, folder, planned_start_date, planned_end_date, description, owner, environment, custom_fields, } = args;
473
+ const { project_key, name, test_case_keys, folder, planned_start_date, planned_end_date, description, owner, environment, custom_fields, issue_links, issue_key, jira_project_version, } = args;
458
474
  // Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
459
- // plannedEndDate, statusName, folderId, ownerId, customFields
475
+ // plannedEndDate, statusName, folderId, ownerId, jiraProjectVersion, customFields
476
+ // Note: environment is NOT a TestCycleInput field on Cloud — it belongs on TestExecutionInput
460
477
  const payload = { projectKey: project_key, name };
461
478
  if (description)
462
479
  payload.description = description;
@@ -469,6 +486,9 @@ export class ZephyrToolHandlers {
469
486
  // Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
470
487
  if (owner)
471
488
  payload.ownerId = owner;
489
+ // Link to a Jira project version/release (integer ID)
490
+ if (jira_project_version)
491
+ payload.jiraProjectVersion = jira_project_version;
472
492
  if (folder) {
473
493
  const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
474
494
  if (folderId !== null)
@@ -483,14 +503,43 @@ export class ZephyrToolHandlers {
483
503
  // Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
484
504
  if (test_case_keys && test_case_keys.length > 0) {
485
505
  for (const testCaseKey of test_case_keys) {
486
- await this.axiosInstance.post('/testexecutions', {
506
+ const execPayload = {
487
507
  projectKey: project_key,
488
508
  testCaseKey,
489
509
  testCycleKey: cycleKey,
490
510
  statusName: 'Not Executed',
491
- });
511
+ };
512
+ // environment is set at execution level on Cloud, not cycle level
513
+ if (environment)
514
+ execPayload.environmentName = environment;
515
+ await this.axiosInstance.post('/testexecutions', execPayload);
516
+ }
517
+ }
518
+ // Step 3: link Jira issues via POST /testcycles/{key}/links/issues
519
+ // Merge issue_key (single) and issue_links (array) into one list
520
+ const allIssueLinks = [
521
+ ...(issue_key ? [issue_key] : []),
522
+ ...(issue_links ?? []),
523
+ ];
524
+ const linkWarnings = [];
525
+ if (allIssueLinks.length > 0) {
526
+ for (const ik of allIssueLinks) {
527
+ try {
528
+ const issueId = await this.resolveJiraIssueId(ik);
529
+ await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/links/issues`, { issueId });
530
+ }
531
+ catch (e) {
532
+ linkWarnings.push(`${ik}: ${this.formatError(e)}`);
533
+ }
492
534
  }
493
535
  }
536
+ const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
537
+ const credHint = missingCreds && linkWarnings.length > 0
538
+ ? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
539
+ : '';
540
+ const warningText = linkWarnings.length > 0
541
+ ? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
542
+ : '';
494
543
  return {
495
544
  content: [{
496
545
  type: 'text',
@@ -498,7 +547,8 @@ export class ZephyrToolHandlers {
498
547
  key: cycleKey,
499
548
  name,
500
549
  testCaseCount: test_case_keys?.length || 0,
501
- }, null, 2)}`,
550
+ linkedIssues: allIssueLinks.length - linkWarnings.length,
551
+ }, null, 2)}${warningText}`,
502
552
  }],
503
553
  };
504
554
  }
@@ -74,14 +74,12 @@ export const toolSchemas = [
74
74
  },
75
75
  status: {
76
76
  type: 'string',
77
- description: 'Test case status (optional)',
78
- enum: ['Draft', 'Approved', 'Deprecated'],
77
+ description: 'Test case status (optional, default: "Draft"). Value must match a status name configured in your Zephyr project (e.g. "Draft", "Approved", "Deprecated"). Note: always overridden to "Draft" on creation.',
79
78
  default: 'Draft',
80
79
  },
81
80
  priority: {
82
81
  type: 'string',
83
- description: 'Test case priority (optional)',
84
- enum: ['High', 'Normal', 'Low'],
82
+ description: 'Test case priority (optional). Value must match a priority name configured in your Zephyr project (e.g. "High", "Normal", "Low", "Critical"). Use zephyr://testcase/EXISTING-KEY to check your project\'s valid values.',
85
83
  },
86
84
  precondition: {
87
85
  type: 'string',
@@ -206,7 +204,7 @@ export const toolSchemas = [
206
204
  properties: {
207
205
  test_run_key: {
208
206
  type: 'string',
209
- description: 'Test run key (e.g., PROJ-C123)',
207
+ description: 'Test run key (e.g., PROJ-R123)',
210
208
  },
211
209
  },
212
210
  required: ['test_run_key'],
@@ -271,17 +269,21 @@ export const toolSchemas = [
271
269
  },
272
270
  environment: {
273
271
  type: 'string',
274
- description: 'Test environment (optional)',
272
+ description: 'Test environment name (optional). On Cloud, applied to each test execution (environmentName). On Data Center, set at cycle level.',
275
273
  },
276
274
  issue_key: {
277
275
  type: 'string',
278
- description: 'Single issue key to link to the test run (optional) - will be mapped to issueKey in API',
276
+ description: 'Single Jira issue key to link to the test cycle (e.g. "PROJ-123"). On Cloud, resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars.',
279
277
  },
280
278
  issue_links: {
281
279
  type: 'array',
282
- description: 'Array of issue links (optional) - will be mapped to issueLinks in API',
280
+ description: 'Array of Jira issue keys to link to the test cycle (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars. Failures are reported as warnings and do not fail the tool call.',
283
281
  items: { type: 'string' },
284
282
  },
283
+ jira_project_version: {
284
+ type: 'integer',
285
+ description: 'Jira project version/release ID to link this test cycle to (optional, Cloud only — use the numeric version ID).',
286
+ },
285
287
  custom_fields: {
286
288
  type: 'object',
287
289
  description: 'Custom fields object (optional)',
@@ -316,7 +318,7 @@ export const toolSchemas = [
316
318
  },
317
319
  test_run_keys: {
318
320
  type: 'array',
319
- description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-C152", "PROJ-C161"])',
321
+ description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-R152", "PROJ-R161"])',
320
322
  items: { type: 'string' },
321
323
  minItems: 1
322
324
  },
@@ -395,7 +397,7 @@ export const toolSchemas = [
395
397
  properties: {
396
398
  test_run_key: {
397
399
  type: 'string',
398
- description: 'Test run key (e.g., PROJ-C161)',
400
+ description: 'Test run key (e.g., PROJ-R161)',
399
401
  },
400
402
  test_case_keys: {
401
403
  type: 'array',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zephyr-scale-mcp-server",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Model Context Protocol (MCP) server for Zephyr Scale test case management with comprehensive STEP_BY_STEP, PLAIN_TEXT, and BDD support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",
@@ -124,13 +124,19 @@ export class ZephyrToolHandlers {
124
124
  if (!test_script) return;
125
125
 
126
126
  if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
127
- const items = test_script.steps.map((step: any) => ({
128
- inline: {
129
- description: step.description || '',
130
- testData: step.testData || null,
131
- expectedResult: step.expectedResult || null,
132
- },
133
- }));
127
+ const items = test_script.steps.map((step: any) => {
128
+ // If step is a call-to-test (testCaseKey), use the testCase variant
129
+ if (step.testCaseKey) {
130
+ return { testCase: { testCaseKey: step.testCaseKey } };
131
+ }
132
+ return {
133
+ inline: {
134
+ description: step.description || '',
135
+ testData: step.testData || null,
136
+ expectedResult: step.expectedResult || null,
137
+ },
138
+ };
139
+ });
134
140
  await this.axiosInstance.post(
135
141
  `${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`,
136
142
  { mode: 'OVERWRITE', items }
@@ -234,15 +240,22 @@ export class ZephyrToolHandlers {
234
240
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
235
241
  const tc = getResponse.data;
236
242
  // UpdateTestCaseInput requires: id, key, name, priority, project, status
237
- // tc.project is a ProjectLink { id, self } pass it back as-is
238
- await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
243
+ // All optional fields must also be re-sent or the API will clear them
244
+ const putPayload: any = {
239
245
  id: tc.id,
240
246
  key: test_case_key,
241
247
  name,
242
248
  status: tc.status,
243
249
  priority: tc.priority,
244
250
  project: tc.project,
245
- });
251
+ };
252
+ // Preserve all optional fields to avoid the API clearing them
253
+ for (const field of ['objective', 'precondition', 'estimatedTime', 'component', 'owner', 'folder']) {
254
+ if (tc[field] !== undefined && tc[field] !== null) putPayload[field] = tc[field];
255
+ }
256
+ if (Array.isArray(tc.labels) && tc.labels.length > 0) putPayload.labels = tc.labels;
257
+ if (tc.customFields && Object.keys(tc.customFields).length > 0) putPayload.customFields = tc.customFields;
258
+ await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
246
259
  }
247
260
 
248
261
  return {
@@ -504,11 +517,13 @@ export class ZephyrToolHandlers {
504
517
  const {
505
518
  project_key, name, test_case_keys, folder,
506
519
  planned_start_date, planned_end_date, description,
507
- owner, environment, custom_fields,
520
+ owner, environment, custom_fields, issue_links, issue_key,
521
+ jira_project_version,
508
522
  } = args;
509
523
 
510
524
  // Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
511
- // plannedEndDate, statusName, folderId, ownerId, customFields
525
+ // plannedEndDate, statusName, folderId, ownerId, jiraProjectVersion, customFields
526
+ // Note: environment is NOT a TestCycleInput field on Cloud — it belongs on TestExecutionInput
512
527
  const payload: any = { projectKey: project_key, name };
513
528
 
514
529
  if (description) payload.description = description;
@@ -517,6 +532,8 @@ export class ZephyrToolHandlers {
517
532
  if (custom_fields) payload.customFields = custom_fields;
518
533
  // Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
519
534
  if (owner) payload.ownerId = owner;
535
+ // Link to a Jira project version/release (integer ID)
536
+ if (jira_project_version) payload.jiraProjectVersion = jira_project_version;
520
537
  if (folder) {
521
538
  const folderId = await resolveFolderIdByPath(
522
539
  this.axiosInstance, project_key, folder, 'TEST_CYCLE'
@@ -535,15 +552,47 @@ export class ZephyrToolHandlers {
535
552
  // Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
536
553
  if (test_case_keys && test_case_keys.length > 0) {
537
554
  for (const testCaseKey of test_case_keys) {
538
- await this.axiosInstance.post('/testexecutions', {
555
+ const execPayload: any = {
539
556
  projectKey: project_key,
540
557
  testCaseKey,
541
558
  testCycleKey: cycleKey,
542
559
  statusName: 'Not Executed',
543
- });
560
+ };
561
+ // environment is set at execution level on Cloud, not cycle level
562
+ if (environment) execPayload.environmentName = environment;
563
+ await this.axiosInstance.post('/testexecutions', execPayload);
564
+ }
565
+ }
566
+
567
+ // Step 3: link Jira issues via POST /testcycles/{key}/links/issues
568
+ // Merge issue_key (single) and issue_links (array) into one list
569
+ const allIssueLinks = [
570
+ ...(issue_key ? [issue_key] : []),
571
+ ...(issue_links ?? []),
572
+ ];
573
+ const linkWarnings: string[] = [];
574
+ if (allIssueLinks.length > 0) {
575
+ for (const ik of allIssueLinks) {
576
+ try {
577
+ const issueId = await this.resolveJiraIssueId(ik);
578
+ await this.axiosInstance.post(
579
+ `${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/links/issues`,
580
+ { issueId }
581
+ );
582
+ } catch (e) {
583
+ linkWarnings.push(`${ik}: ${this.formatError(e)}`);
584
+ }
544
585
  }
545
586
  }
546
587
 
588
+ const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
589
+ const credHint = missingCreds && linkWarnings.length > 0
590
+ ? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
591
+ : '';
592
+ const warningText = linkWarnings.length > 0
593
+ ? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
594
+ : '';
595
+
547
596
  return {
548
597
  content: [{
549
598
  type: 'text',
@@ -551,7 +600,8 @@ export class ZephyrToolHandlers {
551
600
  key: cycleKey,
552
601
  name,
553
602
  testCaseCount: test_case_keys?.length || 0,
554
- }, null, 2)}`,
603
+ linkedIssues: allIssueLinks.length - linkWarnings.length,
604
+ }, null, 2)}${warningText}`,
555
605
  }],
556
606
  };
557
607
  } catch (error) {
@@ -74,14 +74,12 @@ export const toolSchemas = [
74
74
  },
75
75
  status: {
76
76
  type: 'string',
77
- description: 'Test case status (optional)',
78
- enum: ['Draft', 'Approved', 'Deprecated'],
77
+ description: 'Test case status (optional, default: "Draft"). Value must match a status name configured in your Zephyr project (e.g. "Draft", "Approved", "Deprecated"). Note: always overridden to "Draft" on creation.',
79
78
  default: 'Draft',
80
79
  },
81
80
  priority: {
82
81
  type: 'string',
83
- description: 'Test case priority (optional)',
84
- enum: ['High', 'Normal', 'Low'],
82
+ description: 'Test case priority (optional). Value must match a priority name configured in your Zephyr project (e.g. "High", "Normal", "Low", "Critical"). Use zephyr://testcase/EXISTING-KEY to check your project\'s valid values.',
85
83
  },
86
84
  precondition: {
87
85
  type: 'string',
@@ -206,7 +204,7 @@ export const toolSchemas = [
206
204
  properties: {
207
205
  test_run_key: {
208
206
  type: 'string',
209
- description: 'Test run key (e.g., PROJ-C123)',
207
+ description: 'Test run key (e.g., PROJ-R123)',
210
208
  },
211
209
  },
212
210
  required: ['test_run_key'],
@@ -271,17 +269,21 @@ export const toolSchemas = [
271
269
  },
272
270
  environment: {
273
271
  type: 'string',
274
- description: 'Test environment (optional)',
272
+ description: 'Test environment name (optional). On Cloud, applied to each test execution (environmentName). On Data Center, set at cycle level.',
275
273
  },
276
274
  issue_key: {
277
275
  type: 'string',
278
- description: 'Single issue key to link to the test run (optional) - will be mapped to issueKey in API',
276
+ description: 'Single Jira issue key to link to the test cycle (e.g. "PROJ-123"). On Cloud, resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars.',
279
277
  },
280
278
  issue_links: {
281
279
  type: 'array',
282
- description: 'Array of issue links (optional) - will be mapped to issueLinks in API',
280
+ description: 'Array of Jira issue keys to link to the test cycle (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars. Failures are reported as warnings and do not fail the tool call.',
283
281
  items: { type: 'string' },
284
282
  },
283
+ jira_project_version: {
284
+ type: 'integer',
285
+ description: 'Jira project version/release ID to link this test cycle to (optional, Cloud only — use the numeric version ID).',
286
+ },
285
287
  custom_fields: {
286
288
  type: 'object',
287
289
  description: 'Custom fields object (optional)',
@@ -316,7 +318,7 @@ export const toolSchemas = [
316
318
  },
317
319
  test_run_keys: {
318
320
  type: 'array',
319
- description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-C152", "PROJ-C161"])',
321
+ description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-R152", "PROJ-R161"])',
320
322
  items: { type: 'string' },
321
323
  minItems: 1
322
324
  },
@@ -395,7 +397,7 @@ export const toolSchemas = [
395
397
  properties: {
396
398
  test_run_key: {
397
399
  type: 'string',
398
- description: 'Test run key (e.g., PROJ-C161)',
400
+ description: 'Test run key (e.g., PROJ-R161)',
399
401
  },
400
402
  test_case_keys: {
401
403
  type: 'array',
package/src/types.ts CHANGED
@@ -64,7 +64,8 @@ export interface TestRunArgs {
64
64
  planned_end_date?: string;
65
65
  description?: string;
66
66
  owner?: string;
67
- environment?: string;
67
+ environment?: string; // Cloud: mapped to environmentName on each TestExecutionInput; DC: cycle-level field
68
+ jira_project_version?: number; // Cloud only: Jira project version/release ID (integer)
68
69
  issue_key?: string;
69
70
  issue_links?: string[];
70
71
  custom_fields?: Record<string, any>;