zephyr-scale-mcp-server 0.4.2 → 0.4.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.
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: [{
@@ -202,12 +212,15 @@ export class ZephyrToolHandlers {
202
212
  if (typeof name === 'string' && name.trim().length > 0) {
203
213
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
204
214
  const tc = getResponse.data;
205
- const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
215
+ // UpdateTestCaseInput requires: id, key, name, priority, project, status
216
+ // tc.project is a ProjectLink { id, self } — pass it back as-is
206
217
  await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
207
- projectKey,
218
+ id: tc.id,
219
+ key: test_case_key,
208
220
  name,
209
221
  status: tc.status,
210
222
  priority: tc.priority,
223
+ project: tc.project,
211
224
  });
212
225
  }
213
226
  return {
@@ -289,8 +302,7 @@ export class ZephyrToolHandlers {
289
302
  async createFolderCloud(args) {
290
303
  const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
291
304
  // 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;
305
+ const cloudFolderType = folder_type;
294
306
  const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
295
307
  if (segments.length === 0) {
296
308
  throw new McpError(ErrorCode.InvalidParams, 'folder name/path cannot be empty');
@@ -454,7 +466,9 @@ export class ZephyrToolHandlers {
454
466
  payload.plannedEndDate = planned_end_date;
455
467
  if (custom_fields)
456
468
  payload.customFields = custom_fields;
457
- // Resolve folder path folderId
469
+ // Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
470
+ if (owner)
471
+ payload.ownerId = owner;
458
472
  if (folder) {
459
473
  const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
460
474
  if (folderId !== null)
@@ -694,7 +708,7 @@ export class ZephyrToolHandlers {
694
708
  text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
695
709
  totalCount: testRuns.length,
696
710
  testRuns: testRuns.map((tr) => ({
697
- key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
711
+ key: tr.key, name: tr.name, status: tr.status?.id, folder: tr.folder?.name,
698
712
  })),
699
713
  }, null, 2)}`,
700
714
  }],
@@ -769,11 +783,28 @@ export class ZephyrToolHandlers {
769
783
  }
770
784
  }
771
785
  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> }.
786
+ // The Zephyr API key is NOT valid for the Jira REST API Jira Cloud requires
787
+ // Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
788
+ const username = process.env.JIRA_USERNAME;
789
+ const apiToken = process.env.JIRA_API_TOKEN;
775
790
  const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
776
- const response = await this.axiosInstance.get(url, { baseURL: '' });
791
+ let response;
792
+ if (username && apiToken) {
793
+ // Jira Cloud Basic Auth
794
+ response = await axios.get(url, {
795
+ headers: { 'Accept': 'application/json' },
796
+ auth: { username, password: apiToken },
797
+ });
798
+ }
799
+ else {
800
+ // Fallback: try Bearer token (works for Data Center with PAT)
801
+ response = await axios.get(url, {
802
+ headers: {
803
+ 'Accept': 'application/json',
804
+ 'Authorization': `Bearer ${process.env.ZEPHYR_API_KEY}`,
805
+ },
806
+ });
807
+ }
777
808
  const id = parseInt(response.data.id, 10);
778
809
  if (!id || isNaN(id)) {
779
810
  throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
@@ -91,13 +91,13 @@ export const toolSchemas = [
91
91
  type: 'string',
92
92
  description: 'Test objective (optional)',
93
93
  },
94
- component: {
95
- type: 'string',
96
- description: 'Component name (optional)',
94
+ component_id: {
95
+ type: 'integer',
96
+ description: 'Jira component ID (optional, Cloud only — use the numeric component ID, not the name)',
97
97
  },
98
- owner: {
98
+ owner_id: {
99
99
  type: 'string',
100
- description: 'Test case owner (optional)',
100
+ description: 'Test case owner Jira Account ID (optional, Cloud only — e.g. "5b10a2844c20165700ede21g")',
101
101
  },
102
102
  estimated_time: {
103
103
  type: 'number',
@@ -191,7 +191,7 @@ export const toolSchemas = [
191
191
  folder_type: {
192
192
  type: 'string',
193
193
  description: 'Type of folder',
194
- enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_RUN'],
194
+ enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
195
195
  default: 'TEST_CASE',
196
196
  },
197
197
  },
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.3",
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 {
@@ -224,12 +233,15 @@ export class ZephyrToolHandlers {
224
233
  if (typeof name === 'string' && name.trim().length > 0) {
225
234
  const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
226
235
  const tc = getResponse.data;
227
- const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
236
+ // UpdateTestCaseInput requires: id, key, name, priority, project, status
237
+ // tc.project is a ProjectLink { id, self } — pass it back as-is
228
238
  await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
229
- projectKey,
239
+ id: tc.id,
240
+ key: test_case_key,
230
241
  name,
231
242
  status: tc.status,
232
243
  priority: tc.priority,
244
+ project: tc.project,
233
245
  });
234
246
  }
235
247
 
@@ -318,8 +330,7 @@ export class ZephyrToolHandlers {
318
330
  const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
319
331
 
320
332
  // 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;
333
+ const cloudFolderType = folder_type;
323
334
 
324
335
  const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
325
336
  if (segments.length === 0) {
@@ -504,8 +515,8 @@ export class ZephyrToolHandlers {
504
515
  if (planned_start_date) payload.plannedStartDate = planned_start_date;
505
516
  if (planned_end_date) payload.plannedEndDate = planned_end_date;
506
517
  if (custom_fields) payload.customFields = custom_fields;
507
-
508
- // Resolve folder path → folderId
518
+ // Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
519
+ if (owner) payload.ownerId = owner;
509
520
  if (folder) {
510
521
  const folderId = await resolveFolderIdByPath(
511
522
  this.axiosInstance, project_key, folder, 'TEST_CYCLE'
@@ -766,7 +777,7 @@ export class ZephyrToolHandlers {
766
777
  text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
767
778
  totalCount: testRuns.length,
768
779
  testRuns: testRuns.map((tr: any) => ({
769
- key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
780
+ key: tr.key, name: tr.name, status: tr.status?.id, folder: tr.folder?.name,
770
781
  })),
771
782
  }, null, 2)}`,
772
783
  }],
@@ -846,11 +857,30 @@ export class ZephyrToolHandlers {
846
857
  }
847
858
 
848
859
  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> }.
860
+ // The Zephyr API key is NOT valid for the Jira REST API Jira Cloud requires
861
+ // Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
862
+ const username = process.env.JIRA_USERNAME;
863
+ const apiToken = process.env.JIRA_API_TOKEN;
864
+
852
865
  const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
853
- const response = await this.axiosInstance.get(url, { baseURL: '' });
866
+
867
+ let response;
868
+ if (username && apiToken) {
869
+ // Jira Cloud Basic Auth
870
+ response = await axios.get(url, {
871
+ headers: { 'Accept': 'application/json' },
872
+ auth: { username, password: apiToken },
873
+ });
874
+ } else {
875
+ // Fallback: try Bearer token (works for Data Center with PAT)
876
+ response = await axios.get(url, {
877
+ headers: {
878
+ 'Accept': 'application/json',
879
+ 'Authorization': `Bearer ${process.env.ZEPHYR_API_KEY}`,
880
+ },
881
+ });
882
+ }
883
+
854
884
  const id = parseInt(response.data.id, 10);
855
885
  if (!id || isNaN(id)) {
856
886
  throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
@@ -91,13 +91,13 @@ export const toolSchemas = [
91
91
  type: 'string',
92
92
  description: 'Test objective (optional)',
93
93
  },
94
- component: {
95
- type: 'string',
96
- description: 'Component name (optional)',
94
+ component_id: {
95
+ type: 'integer',
96
+ description: 'Jira component ID (optional, Cloud only — use the numeric component ID, not the name)',
97
97
  },
98
- owner: {
98
+ owner_id: {
99
99
  type: 'string',
100
- description: 'Test case owner (optional)',
100
+ description: 'Test case owner Jira Account ID (optional, Cloud only — e.g. "5b10a2844c20165700ede21g")',
101
101
  },
102
102
  estimated_time: {
103
103
  type: 'number',
@@ -191,7 +191,7 @@ export const toolSchemas = [
191
191
  folder_type: {
192
192
  type: 'string',
193
193
  description: 'Type of folder',
194
- enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_RUN'],
194
+ enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
195
195
  default: 'TEST_CASE',
196
196
  },
197
197
  },
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 {