zephyr-scale-mcp-server 0.4.2 → 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.
package/README.md CHANGED
@@ -27,7 +27,29 @@ Configure your MCP client with the following structure.
27
27
  "args": ["zephyr-scale-mcp-server@latest"],
28
28
  "env": {
29
29
  "ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
30
- "ZEPHYR_API_KEY": "your-zephyr-api-key"
30
+ "ZEPHYR_API_KEY": "your-zephyr-api-key",
31
+ "JIRA_USERNAME": "your-email@company.com",
32
+ "JIRA_API_TOKEN": "your-jira-api-token"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+ > **Note**: `JIRA_USERNAME` and `JIRA_API_TOKEN` are optional but required if you want to use the `issue_links` field when creating test cases. Without them, issue linking will fail with a 401 warning (the test case is still created). Generate a Jira API token at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens).
39
+
40
+ **Jira Cloud (EU region):**
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "zephyr-server": {
45
+ "command": "npx",
46
+ "args": ["zephyr-scale-mcp-server@latest"],
47
+ "env": {
48
+ "ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
49
+ "ZEPHYR_API_KEY": "your-zephyr-api-key",
50
+ "JIRA_USERNAME": "your-email@company.com",
51
+ "JIRA_API_TOKEN": "your-jira-api-token",
52
+ "ZEPHYR_API_BASE_URL": "https://eu.api.zephyrscale.smartbear.com/v2"
31
53
  }
32
54
  }
33
55
  }
@@ -99,18 +121,19 @@ The server provides access to various resources through URI schemes:
99
121
 
100
122
  ## Usage Examples
101
123
 
102
- ### Create a BDD Test Case
124
+ ### Create a BDD Test Case with Issue Links
103
125
  ```json
104
126
  {
105
127
  "project_key": "PROJ",
106
128
  "name": "User Authentication",
107
129
  "test_script": {
108
130
  "type": "BDD",
109
- "text": "Feature: User Login\n\nScenario: Valid user login\n Given a user with valid credentials\n When the user attempts to log in\n Then the user should be authenticated successfully"
110
- }
131
+ "text": "Given a user with valid credentials\nWhen the user attempts to log in\nThen the user should be authenticated successfully"
132
+ },
133
+ "issue_links": ["PROJ-123", "PROJ-456"]
111
134
  }
112
135
  ```
113
- **Note**: The server will automatically convert markdown-style BDD to proper Gherkin format.
136
+ **Note**: `issue_links` requires `JIRA_USERNAME` and `JIRA_API_TOKEN` to be set (Cloud only). Link failures are reported as warnings — the test case is still created.
114
137
 
115
138
  ### Use a Live Test Case as a Template
116
139
  1. Fetch an existing test case: `zephyr://testcase/PROJ-T123`
@@ -139,26 +162,25 @@ The server provides access to various resources through URI schemes:
139
162
  ## Authentication
140
163
 
141
164
  ### Jira Cloud Configuration
142
- - `ZEPHYR_BASE_URL`: `https://your-company.atlassian.net`
143
- - `ZEPHYR_API_KEY`: Your Zephyr Scale API key (JWT). Generate it in Jira by clicking your profile picture (bottom left) → **Zephyr API keys**.
144
165
 
145
- ### Jira Cloud regional API endpoint (optional)
146
- Zephyr Scale Cloud has regional API hosts. By default the server uses the **US** endpoint (`https://api.zephyrscale.smartbear.com/v2`). If your instance uses the **EU** API (e.g. for EU data residency), set:
147
- - **`ZEPHYR_API_BASE_URL`**: Full base URL for the Zephyr Scale Cloud API (no trailing slash).
166
+ | Variable | Required | Description |
167
+ |---|---|---|
168
+ | `ZEPHYR_BASE_URL` | | Your Jira Cloud URL, e.g. `https://your-company.atlassian.net` |
169
+ | `ZEPHYR_API_KEY` | ✅ | Zephyr Scale API key (JWT). Generate in Jira: profile picture (bottom left) → **Zephyr API keys** |
170
+ | `JIRA_USERNAME` | ⚠️ Optional* | Your Jira account email address |
171
+ | `JIRA_API_TOKEN` | ⚠️ Optional* | Jira API token. Generate at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) |
172
+ | `ZEPHYR_API_BASE_URL` | Optional | Override the Zephyr API base URL (e.g. for EU: `https://eu.api.zephyrscale.smartbear.com/v2`). Defaults to US endpoint. |
173
+ | `JIRA_TYPE` | Optional | Force `"cloud"` or `"datacenter"` — overrides auto-detection |
148
174
 
149
- **Example EU:**
150
- ```json
151
- "env": {
152
- "ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
153
- "ZEPHYR_API_KEY": "your-zephyr-api-token",
154
- "ZEPHYR_API_BASE_URL": "https://eu.api.zephyrscale.smartbear.com/v2"
155
- }
156
- ```
157
- If `ZEPHYR_API_BASE_URL` is not set, the US URL is used. Omit this variable for US instances.
175
+ > **\* `JIRA_USERNAME` + `JIRA_API_TOKEN`**: Required only for the `issue_links` feature on Cloud. The Zephyr API key cannot authenticate against the Jira REST API, so a separate Jira credential is needed to resolve issue keys to numeric IDs. Without these, `issue_links` will fail with a 401 warning — the test case is still created successfully.
158
176
 
159
177
  ### Jira Data Center Configuration
160
- - `ZEPHYR_BASE_URL`: `https://your-jira-server.com`
161
- - `ZEPHYR_API_KEY`: Your Zephyr Scale API token from your Jira profile settings.
178
+
179
+ | Variable | Required | Description |
180
+ |---|---|---|
181
+ | `ZEPHYR_BASE_URL` | ✅ | Your Jira server URL, e.g. `https://your-jira-server.com` |
182
+ | `ZEPHYR_API_KEY` | ✅ | Zephyr Scale API token from your Jira profile settings |
183
+ | `JIRA_TYPE` | Optional | Set to `"datacenter"` to override auto-detection |
162
184
 
163
185
  ### Automatic Detection
164
186
  The server automatically detects your Jira type based on `ZEPHYR_BASE_URL` — URLs containing `.atlassian.net` are treated as Cloud, everything else as Data Center. Override with `JIRA_TYPE="cloud"` or `JIRA_TYPE="datacenter"`.
@@ -1,3 +1,4 @@
1
+ import axios from 'axios';
1
2
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
3
  import { convertToGherkin, resolveFolderIdByPath } from './utils.js';
3
4
  export class ZephyrToolHandlers {
@@ -34,7 +35,7 @@ export class ZephyrToolHandlers {
34
35
  return this.createTestCaseDC(args);
35
36
  }
36
37
  async createTestCaseCloud(args) {
37
- const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, issue_links, } = args;
38
+ const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, issue_links, owner_id, component_id, } = args;
38
39
  const payload = { projectKey: project_key, name };
39
40
  // Cloud v2 uses statusName/priorityName (strings), folderId (integer)
40
41
  payload.statusName = 'Draft';
@@ -50,6 +51,11 @@ export class ZephyrToolHandlers {
50
51
  payload.labels = labels;
51
52
  if (custom_fields)
52
53
  payload.customFields = custom_fields;
54
+ // Cloud v2 uses ownerId (Jira Account ID) and componentId (integer)
55
+ if (owner_id)
56
+ payload.ownerId = owner_id;
57
+ if (component_id)
58
+ payload.componentId = component_id;
53
59
  // Resolve folder path → folderId
54
60
  if (folder) {
55
61
  const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CASE');
@@ -80,8 +86,12 @@ export class ZephyrToolHandlers {
80
86
  }
81
87
  }
82
88
  }
89
+ const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
90
+ const credHint = missingCreds
91
+ ? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
92
+ : '';
83
93
  const warningText = linkWarnings.length > 0
84
- ? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}`
94
+ ? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
85
95
  : '';
86
96
  return {
87
97
  content: [{
@@ -98,13 +108,19 @@ export class ZephyrToolHandlers {
98
108
  if (!test_script)
99
109
  return;
100
110
  if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
101
- const items = test_script.steps.map((step) => ({
102
- inline: {
103
- description: step.description || '',
104
- testData: step.testData || null,
105
- expectedResult: step.expectedResult || null,
106
- },
107
- }));
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
+ });
108
124
  await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`, { mode: 'OVERWRITE', items });
109
125
  }
110
126
  else if (test_script.type === 'BDD' && test_script.text) {
@@ -202,13 +218,26 @@ export class ZephyrToolHandlers {
202
218
  if (typeof name === 'string' && name.trim().length > 0) {
203
219
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
204
220
  const tc = getResponse.data;
205
- const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
206
- await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
207
- projectKey,
221
+ // UpdateTestCaseInput requires: id, key, name, priority, project, status
222
+ // All optional fields must also be re-sent or the API will clear them
223
+ const putPayload = {
224
+ id: tc.id,
225
+ key: test_case_key,
208
226
  name,
209
227
  status: tc.status,
210
228
  priority: tc.priority,
211
- });
229
+ project: tc.project,
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);
212
241
  }
213
242
  return {
214
243
  content: [{
@@ -289,8 +318,7 @@ export class ZephyrToolHandlers {
289
318
  async createFolderCloud(args) {
290
319
  const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
291
320
  // Cloud v2 uses folderType (not type) and parentId integer
292
- // Map TEST_RUN → TEST_CYCLE for Cloud v2
293
- const cloudFolderType = folder_type === 'TEST_RUN' ? 'TEST_CYCLE' : folder_type;
321
+ const cloudFolderType = folder_type;
294
322
  const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
295
323
  if (segments.length === 0) {
296
324
  throw new McpError(ErrorCode.InvalidParams, 'folder name/path cannot be empty');
@@ -442,9 +470,10 @@ export class ZephyrToolHandlers {
442
470
  return this.createTestRunDC(args);
443
471
  }
444
472
  async createTestRunCloud(args) {
445
- 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;
446
474
  // Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
447
- // 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
448
477
  const payload = { projectKey: project_key, name };
449
478
  if (description)
450
479
  payload.description = description;
@@ -454,7 +483,12 @@ export class ZephyrToolHandlers {
454
483
  payload.plannedEndDate = planned_end_date;
455
484
  if (custom_fields)
456
485
  payload.customFields = custom_fields;
457
- // Resolve folder path folderId
486
+ // Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
487
+ if (owner)
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;
458
492
  if (folder) {
459
493
  const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
460
494
  if (folderId !== null)
@@ -469,14 +503,43 @@ export class ZephyrToolHandlers {
469
503
  // Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
470
504
  if (test_case_keys && test_case_keys.length > 0) {
471
505
  for (const testCaseKey of test_case_keys) {
472
- await this.axiosInstance.post('/testexecutions', {
506
+ const execPayload = {
473
507
  projectKey: project_key,
474
508
  testCaseKey,
475
509
  testCycleKey: cycleKey,
476
510
  statusName: 'Not Executed',
477
- });
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);
478
516
  }
479
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
+ }
534
+ }
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
+ : '';
480
543
  return {
481
544
  content: [{
482
545
  type: 'text',
@@ -484,7 +547,8 @@ export class ZephyrToolHandlers {
484
547
  key: cycleKey,
485
548
  name,
486
549
  testCaseCount: test_case_keys?.length || 0,
487
- }, null, 2)}`,
550
+ linkedIssues: allIssueLinks.length - linkWarnings.length,
551
+ }, null, 2)}${warningText}`,
488
552
  }],
489
553
  };
490
554
  }
@@ -694,7 +758,7 @@ export class ZephyrToolHandlers {
694
758
  text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
695
759
  totalCount: testRuns.length,
696
760
  testRuns: testRuns.map((tr) => ({
697
- key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
761
+ key: tr.key, name: tr.name, status: tr.status?.id, folder: tr.folder?.name,
698
762
  })),
699
763
  }, null, 2)}`,
700
764
  }],
@@ -769,11 +833,28 @@ export class ZephyrToolHandlers {
769
833
  }
770
834
  }
771
835
  async resolveJiraIssueId(issueKey) {
772
- // The Zephyr API key is a Jira-issued token it works against the Jira REST API too.
773
- // We call GET {jiraBaseUrl}/rest/api/3/issue/{key}?fields=id to get the numeric issue ID
774
- // required by POST /testcases/{key}/links/issues { issueId: <integer> }.
836
+ // The Zephyr API key is NOT valid for the Jira REST API Jira Cloud requires
837
+ // Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
838
+ const username = process.env.JIRA_USERNAME;
839
+ const apiToken = process.env.JIRA_API_TOKEN;
775
840
  const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
776
- const response = await this.axiosInstance.get(url, { baseURL: '' });
841
+ let response;
842
+ if (username && apiToken) {
843
+ // Jira Cloud Basic Auth
844
+ response = await axios.get(url, {
845
+ headers: { 'Accept': 'application/json' },
846
+ auth: { username, password: apiToken },
847
+ });
848
+ }
849
+ else {
850
+ // Fallback: try Bearer token (works for Data Center with PAT)
851
+ response = await axios.get(url, {
852
+ headers: {
853
+ 'Accept': 'application/json',
854
+ 'Authorization': `Bearer ${process.env.ZEPHYR_API_KEY}`,
855
+ },
856
+ });
857
+ }
777
858
  const id = parseInt(response.data.id, 10);
778
859
  if (!id || isNaN(id)) {
779
860
  throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
@@ -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',
@@ -91,13 +89,13 @@ export const toolSchemas = [
91
89
  type: 'string',
92
90
  description: 'Test objective (optional)',
93
91
  },
94
- component: {
95
- type: 'string',
96
- description: 'Component name (optional)',
92
+ component_id: {
93
+ type: 'integer',
94
+ description: 'Jira component ID (optional, Cloud only — use the numeric component ID, not the name)',
97
95
  },
98
- owner: {
96
+ owner_id: {
99
97
  type: 'string',
100
- description: 'Test case owner (optional)',
98
+ description: 'Test case owner Jira Account ID (optional, Cloud only — e.g. "5b10a2844c20165700ede21g")',
101
99
  },
102
100
  estimated_time: {
103
101
  type: 'number',
@@ -191,7 +189,7 @@ export const toolSchemas = [
191
189
  folder_type: {
192
190
  type: 'string',
193
191
  description: 'Type of folder',
194
- enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_RUN'],
192
+ enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
195
193
  default: 'TEST_CASE',
196
194
  },
197
195
  },
@@ -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.2",
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",
@@ -1,4 +1,5 @@
1
1
  import { AxiosInstance } from 'axios';
2
+ import axios from 'axios';
2
3
  import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
4
  import {
4
5
  TestCaseArgs,
@@ -47,6 +48,7 @@ export class ZephyrToolHandlers {
47
48
  const {
48
49
  project_key, name, test_script, folder, priority, precondition,
49
50
  objective, estimated_time, labels, custom_fields, issue_links,
51
+ owner_id, component_id,
50
52
  } = args;
51
53
 
52
54
  const payload: any = { projectKey: project_key, name };
@@ -58,6 +60,9 @@ export class ZephyrToolHandlers {
58
60
  if (estimated_time) payload.estimatedTime = estimated_time;
59
61
  if (labels && labels.length > 0) payload.labels = labels;
60
62
  if (custom_fields) payload.customFields = custom_fields;
63
+ // Cloud v2 uses ownerId (Jira Account ID) and componentId (integer)
64
+ if (owner_id) payload.ownerId = owner_id;
65
+ if (component_id) payload.componentId = component_id;
61
66
 
62
67
  // Resolve folder path → folderId
63
68
  if (folder) {
@@ -96,8 +101,12 @@ export class ZephyrToolHandlers {
96
101
  }
97
102
  }
98
103
 
104
+ const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
105
+ const credHint = missingCreds
106
+ ? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
107
+ : '';
99
108
  const warningText = linkWarnings.length > 0
100
- ? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}`
109
+ ? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
101
110
  : '';
102
111
 
103
112
  return {
@@ -115,13 +124,19 @@ export class ZephyrToolHandlers {
115
124
  if (!test_script) return;
116
125
 
117
126
  if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
118
- const items = test_script.steps.map((step: any) => ({
119
- inline: {
120
- description: step.description || '',
121
- testData: step.testData || null,
122
- expectedResult: step.expectedResult || null,
123
- },
124
- }));
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
+ });
125
140
  await this.axiosInstance.post(
126
141
  `${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`,
127
142
  { mode: 'OVERWRITE', items }
@@ -224,13 +239,23 @@ export class ZephyrToolHandlers {
224
239
  if (typeof name === 'string' && name.trim().length > 0) {
225
240
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
226
241
  const tc = getResponse.data;
227
- const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
228
- await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
229
- projectKey,
242
+ // UpdateTestCaseInput requires: id, key, name, priority, project, status
243
+ // All optional fields must also be re-sent or the API will clear them
244
+ const putPayload: any = {
245
+ id: tc.id,
246
+ key: test_case_key,
230
247
  name,
231
248
  status: tc.status,
232
249
  priority: tc.priority,
233
- });
250
+ project: tc.project,
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);
234
259
  }
235
260
 
236
261
  return {
@@ -318,8 +343,7 @@ export class ZephyrToolHandlers {
318
343
  const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
319
344
 
320
345
  // Cloud v2 uses folderType (not type) and parentId integer
321
- // Map TEST_RUN → TEST_CYCLE for Cloud v2
322
- const cloudFolderType = folder_type === 'TEST_RUN' ? 'TEST_CYCLE' : folder_type;
346
+ const cloudFolderType = folder_type;
323
347
 
324
348
  const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
325
349
  if (segments.length === 0) {
@@ -493,19 +517,23 @@ export class ZephyrToolHandlers {
493
517
  const {
494
518
  project_key, name, test_case_keys, folder,
495
519
  planned_start_date, planned_end_date, description,
496
- owner, environment, custom_fields,
520
+ owner, environment, custom_fields, issue_links, issue_key,
521
+ jira_project_version,
497
522
  } = args;
498
523
 
499
524
  // Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
500
- // 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
501
527
  const payload: any = { projectKey: project_key, name };
502
528
 
503
529
  if (description) payload.description = description;
504
530
  if (planned_start_date) payload.plannedStartDate = planned_start_date;
505
531
  if (planned_end_date) payload.plannedEndDate = planned_end_date;
506
532
  if (custom_fields) payload.customFields = custom_fields;
507
-
508
- // Resolve folder path → folderId
533
+ // Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
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;
509
537
  if (folder) {
510
538
  const folderId = await resolveFolderIdByPath(
511
539
  this.axiosInstance, project_key, folder, 'TEST_CYCLE'
@@ -524,15 +552,47 @@ export class ZephyrToolHandlers {
524
552
  // Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
525
553
  if (test_case_keys && test_case_keys.length > 0) {
526
554
  for (const testCaseKey of test_case_keys) {
527
- await this.axiosInstance.post('/testexecutions', {
555
+ const execPayload: any = {
528
556
  projectKey: project_key,
529
557
  testCaseKey,
530
558
  testCycleKey: cycleKey,
531
559
  statusName: 'Not Executed',
532
- });
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);
533
564
  }
534
565
  }
535
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
+ }
585
+ }
586
+ }
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
+
536
596
  return {
537
597
  content: [{
538
598
  type: 'text',
@@ -540,7 +600,8 @@ export class ZephyrToolHandlers {
540
600
  key: cycleKey,
541
601
  name,
542
602
  testCaseCount: test_case_keys?.length || 0,
543
- }, null, 2)}`,
603
+ linkedIssues: allIssueLinks.length - linkWarnings.length,
604
+ }, null, 2)}${warningText}`,
544
605
  }],
545
606
  };
546
607
  } catch (error) {
@@ -766,7 +827,7 @@ export class ZephyrToolHandlers {
766
827
  text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
767
828
  totalCount: testRuns.length,
768
829
  testRuns: testRuns.map((tr: any) => ({
769
- key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
830
+ key: tr.key, name: tr.name, status: tr.status?.id, folder: tr.folder?.name,
770
831
  })),
771
832
  }, null, 2)}`,
772
833
  }],
@@ -846,11 +907,30 @@ export class ZephyrToolHandlers {
846
907
  }
847
908
 
848
909
  private async resolveJiraIssueId(issueKey: string): Promise<number> {
849
- // The Zephyr API key is a Jira-issued token it works against the Jira REST API too.
850
- // We call GET {jiraBaseUrl}/rest/api/3/issue/{key}?fields=id to get the numeric issue ID
851
- // required by POST /testcases/{key}/links/issues { issueId: <integer> }.
910
+ // The Zephyr API key is NOT valid for the Jira REST API Jira Cloud requires
911
+ // Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
912
+ const username = process.env.JIRA_USERNAME;
913
+ const apiToken = process.env.JIRA_API_TOKEN;
914
+
852
915
  const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
853
- const response = await this.axiosInstance.get(url, { baseURL: '' });
916
+
917
+ let response;
918
+ if (username && apiToken) {
919
+ // Jira Cloud Basic Auth
920
+ response = await axios.get(url, {
921
+ headers: { 'Accept': 'application/json' },
922
+ auth: { username, password: apiToken },
923
+ });
924
+ } else {
925
+ // Fallback: try Bearer token (works for Data Center with PAT)
926
+ response = await axios.get(url, {
927
+ headers: {
928
+ 'Accept': 'application/json',
929
+ 'Authorization': `Bearer ${process.env.ZEPHYR_API_KEY}`,
930
+ },
931
+ });
932
+ }
933
+
854
934
  const id = parseInt(response.data.id, 10);
855
935
  if (!id || isNaN(id)) {
856
936
  throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
@@ -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',
@@ -91,13 +89,13 @@ export const toolSchemas = [
91
89
  type: 'string',
92
90
  description: 'Test objective (optional)',
93
91
  },
94
- component: {
95
- type: 'string',
96
- description: 'Component name (optional)',
92
+ component_id: {
93
+ type: 'integer',
94
+ description: 'Jira component ID (optional, Cloud only — use the numeric component ID, not the name)',
97
95
  },
98
- owner: {
96
+ owner_id: {
99
97
  type: 'string',
100
- description: 'Test case owner (optional)',
98
+ description: 'Test case owner Jira Account ID (optional, Cloud only — e.g. "5b10a2844c20165700ede21g")',
101
99
  },
102
100
  estimated_time: {
103
101
  type: 'number',
@@ -191,7 +189,7 @@ export const toolSchemas = [
191
189
  folder_type: {
192
190
  type: 'string',
193
191
  description: 'Type of folder',
194
- enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_RUN'],
192
+ enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
195
193
  default: 'TEST_CASE',
196
194
  },
197
195
  },
@@ -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
@@ -31,8 +31,10 @@ export interface TestCaseArgs {
31
31
  priority?: 'High' | 'Normal' | 'Low';
32
32
  precondition?: string;
33
33
  objective?: string;
34
- component?: string;
35
- owner?: string;
34
+ component?: string; // Data Center: component name
35
+ owner?: string; // Data Center: owner name
36
+ component_id?: number; // Cloud: Jira component ID (integer)
37
+ owner_id?: string; // Cloud: Jira Account ID
36
38
  estimated_time?: number;
37
39
  labels?: string[];
38
40
  issue_links?: string[];
@@ -49,7 +51,7 @@ export interface UpdateBddArgs {
49
51
  export interface FolderArgs {
50
52
  project_key: string;
51
53
  name: string; // Full folder path including parent folders (e.g., "/folder/subfolder")
52
- folder_type?: 'TEST_CASE' | 'TEST_PLAN' | 'TEST_RUN';
54
+ folder_type?: 'TEST_CASE' | 'TEST_PLAN' | 'TEST_CYCLE';
53
55
  }
54
56
 
55
57
  export interface TestRunArgs {
@@ -62,7 +64,8 @@ export interface TestRunArgs {
62
64
  planned_end_date?: string;
63
65
  description?: string;
64
66
  owner?: string;
65
- 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)
66
69
  issue_key?: string;
67
70
  issue_links?: string[];
68
71
  custom_fields?: Record<string, any>;