zephyr-scale-mcp-server 0.3.3 → 0.4.1

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
@@ -142,6 +142,20 @@ The server provides access to various resources through URI schemes:
142
142
  - `ZEPHYR_BASE_URL`: `https://your-company.atlassian.net`
143
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
144
 
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).
148
+
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.
158
+
145
159
  ### Jira Data Center Configuration
146
160
  - `ZEPHYR_BASE_URL`: `https://your-jira-server.com`
147
161
  - `ZEPHYR_API_KEY`: Your Zephyr Scale API token from your Jira profile settings.
@@ -20,6 +20,14 @@ export class ZephyrToolHandlers {
20
20
  }
21
21
  }
22
22
  async createTestCase(args) {
23
+ // Some MCP clients (e.g. Claude Code) pass nested object parameters as JSON strings.
24
+ // Parse test_script if it arrived as a string.
25
+ if (typeof args.test_script === 'string') {
26
+ try {
27
+ args.test_script = JSON.parse(args.test_script);
28
+ }
29
+ catch { }
30
+ }
23
31
  if (this.jiraConfig.type === 'cloud') {
24
32
  return this.createTestCaseCloud(args);
25
33
  }
@@ -169,40 +177,22 @@ export class ZephyrToolHandlers {
169
177
  }
170
178
  async updateTestCaseBddCloud(args) {
171
179
  const { test_case_key, bdd_content, name } = args;
180
+ const converted = convertToGherkin(bdd_content);
181
+ const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
172
182
  try {
173
- // Step 1: GET existing test case to extract required fields for PUT
174
- const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
175
- const tc = getResponse.data;
176
- // Cloud v2 PUT requires: id, key, name, project, priority, status
177
- const requiredFields = ['id', 'key', 'name', 'project', 'priority', 'status'];
178
- for (const field of requiredFields) {
179
- if (tc[field] === undefined || tc[field] === null) {
180
- throw new McpError(ErrorCode.InternalError, `Existing test case is missing required field '${field}' needed for Cloud v2 update.`);
181
- }
182
- }
183
- const putPayload = {
184
- id: tc.id,
185
- key: tc.key,
186
- name: typeof name === 'string' && name.trim().length > 0 ? name : tc.name,
187
- project: tc.project,
188
- priority: tc.priority,
189
- status: tc.status,
190
- };
191
- // Preserve optional fields
192
- for (const field of ['objective', 'precondition', 'estimatedTime', 'folder', 'component', 'owner']) {
193
- if (tc[field] !== undefined)
194
- putPayload[field] = tc[field];
195
- }
196
- if (Array.isArray(tc.labels))
197
- putPayload.labels = tc.labels;
198
- if (tc.customFields)
199
- putPayload.customFields = tc.customFields;
200
- // Step 2: PUT to update metadata
201
- await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
202
- // Step 3: POST testscript with BDD content
203
- const converted = convertToGherkin(bdd_content);
204
- const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
205
183
  await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`, { type: 'bdd', text: finalText });
184
+ // Only fetch and PUT metadata when the caller also wants to rename the test case
185
+ if (typeof name === 'string' && name.trim().length > 0) {
186
+ const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
187
+ const tc = getResponse.data;
188
+ const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
189
+ await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
190
+ projectKey,
191
+ name,
192
+ status: tc.status,
193
+ priority: tc.priority,
194
+ });
195
+ }
206
196
  return {
207
197
  content: [{
208
198
  type: 'text',
@@ -224,8 +214,9 @@ export class ZephyrToolHandlers {
224
214
  const converted = convertToGherkin(bdd_content);
225
215
  const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
226
216
  const payload = {};
217
+ const projectKey = testCaseData.projectKey ?? test_case_key.replace(/-T\d+$/, '');
227
218
  const requiredFields = [
228
- ['projectKey', testCaseData.projectKey],
219
+ ['projectKey', projectKey],
229
220
  ['name', testCaseData.name],
230
221
  ['status', testCaseData.status],
231
222
  ['priority', testCaseData.priority]
@@ -458,9 +449,16 @@ export class ZephyrToolHandlers {
458
449
  if (response.status !== 201)
459
450
  throw new Error(`Unexpected status code: ${response.status}`);
460
451
  const cycleKey = response.data.key || 'Unknown';
461
- // Step 2: add test cases to the cycle
452
+ // Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
462
453
  if (test_case_keys && test_case_keys.length > 0) {
463
- await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/testcases`, { items: test_case_keys.map((k) => ({ testCaseKey: k })) });
454
+ for (const testCaseKey of test_case_keys) {
455
+ await this.axiosInstance.post('/testexecutions', {
456
+ projectKey: project_key,
457
+ testCaseKey,
458
+ testCycleKey: cycleKey,
459
+ statusName: 'Not Executed',
460
+ });
461
+ }
464
462
  }
465
463
  return {
466
464
  content: [{
@@ -734,24 +732,24 @@ export class ZephyrToolHandlers {
734
732
  throw new McpError(ErrorCode.InvalidRequest, 'add_test_cases_to_run is only supported on Zephyr Scale Cloud. The Data Center API (v1) does not provide an endpoint to modify test runs after creation.');
735
733
  }
736
734
  const { test_run_key, test_case_keys } = args;
735
+ // Derive project key from the test run key (e.g. PROJ-R123 → PROJ)
736
+ const project_key = test_run_key.split('-')[0];
737
737
  try {
738
- const payload = {
739
- items: test_case_keys.map(key => ({ testCaseKey: key }))
740
- };
741
- const response = await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`, payload);
742
- if (response.status === 200 || response.status === 201 || response.status === 204) {
743
- return {
744
- content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
745
- };
738
+ for (const testCaseKey of test_case_keys) {
739
+ await this.axiosInstance.post('/testexecutions', {
740
+ projectKey: project_key,
741
+ testCaseKey,
742
+ testCycleKey: test_run_key,
743
+ statusName: 'Not Executed',
744
+ });
746
745
  }
746
+ return {
747
+ content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
748
+ };
747
749
  }
748
750
  catch (error) {
749
751
  throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
750
752
  }
751
- return {
752
- content: [{ type: 'text', text: 'An unexpected error occurred.' }],
753
- isError: true,
754
- };
755
753
  }
756
754
  formatError(error) {
757
755
  if (error instanceof Error && 'response' in error) {
package/build/utils.js CHANGED
@@ -30,28 +30,48 @@ export async function resolveFolderIdByPath(axiosInstance, projectKey, folderPat
30
30
  export function convertToGherkin(bddContent) {
31
31
  const bddLines = [];
32
32
  const lines = bddContent.split('\n');
33
+ // Bold-markdown step keywords (e.g. **Given**, **When**, etc.)
34
+ const boldStepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
35
+ // Plain step keyword prefixes
36
+ const stepKeywords = ['Given ', 'When ', 'Then ', 'And ', 'But '];
37
+ // Zephyr Scale Cloud only accepts steps and table rows — Feature:/Scenario: wrappers
38
+ // cause a 400 "Invalid Gherkin script" error and must be stripped.
39
+ const strippedPrefixes = [
40
+ 'Feature:',
41
+ 'Background:',
42
+ 'Scenario Outline:',
43
+ 'Scenario:',
44
+ 'Examples:',
45
+ ];
33
46
  for (const line of lines) {
34
47
  const trimmedLine = line.trim();
35
48
  if (!trimmedLine || trimmedLine.startsWith('---'))
36
49
  continue;
37
- if (trimmedLine.startsWith('**Given**')) {
38
- bddLines.push(`Given ${trimmedLine.replace('**Given**', '').trim()}`);
39
- }
40
- else if (trimmedLine.startsWith('**When**')) {
41
- bddLines.push(`When ${trimmedLine.replace('**When**', '').trim()}`);
42
- }
43
- else if (trimmedLine.startsWith('**Then**')) {
44
- bddLines.push(`Then ${trimmedLine.replace('**Then**', '').trim()}`);
50
+ // Strip structural Gherkin keywords — not accepted by the Zephyr Scale Cloud testscript API
51
+ if (strippedPrefixes.some(p => trimmedLine.startsWith(p)))
52
+ continue;
53
+ // Convert **Keyword** markdown bold to plain Gherkin keyword
54
+ let matchedBold = false;
55
+ for (const kw of boldStepKeywords) {
56
+ if (trimmedLine.startsWith(`**${kw}**`)) {
57
+ bddLines.push(`${kw} ${trimmedLine.replace(`**${kw}**`, '').trim()}`);
58
+ matchedBold = true;
59
+ break;
60
+ }
45
61
  }
46
- else if (trimmedLine.startsWith('**And**')) {
47
- bddLines.push(`And ${trimmedLine.replace('**And**', '').trim()}`);
62
+ if (matchedBold)
63
+ continue;
64
+ // Plain step keywords — pass through unchanged
65
+ if (stepKeywords.some(k => trimmedLine.startsWith(k))) {
66
+ bddLines.push(trimmedLine);
67
+ continue;
48
68
  }
49
- else if (trimmedLine.startsWith('Given ') || trimmedLine.startsWith('When ') ||
50
- trimmedLine.startsWith('Then ') || trimmedLine.startsWith('And ')) {
69
+ // Table rows pass through unchanged
70
+ if (trimmedLine.startsWith('|')) {
51
71
  bddLines.push(trimmedLine);
52
72
  }
53
73
  }
54
- return bddLines.length > 0 ? ' ' + bddLines.join('\n ') : '';
74
+ return bddLines.join('\n');
55
75
  }
56
76
  export const customPriorityMapping = {
57
77
  'High': 'P0',
@@ -111,8 +131,11 @@ export function createJiraConfig() {
111
131
  }
112
132
  const type = detectJiraType(jiraBaseUrl);
113
133
  const apiEndpoints = getApiEndpoints(type);
134
+ // Cloud: use ZEPHYR_API_BASE_URL if set (e.g. for EU: https://eu.api.zephyrscale.smartbear.com/v2), else default US
135
+ const defaultCloudBaseUrl = 'https://api.zephyrscale.smartbear.com/v2';
136
+ const cloudBaseUrl = process.env.ZEPHYR_API_BASE_URL?.trim();
114
137
  const baseUrl = type === 'cloud'
115
- ? 'https://api.zephyrscale.smartbear.com/v2'
138
+ ? (cloudBaseUrl || defaultCloudBaseUrl)
116
139
  : jiraBaseUrl;
117
140
  const authHeaders = {
118
141
  'Authorization': `Bearer ${apiKey}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zephyr-scale-mcp-server",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
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",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@modelcontextprotocol/sdk": "0.6.0",
60
- "axios": "^1.10.0"
60
+ "axios": "^1.15.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^20.11.24",
@@ -32,6 +32,11 @@ export class ZephyrToolHandlers {
32
32
  }
33
33
 
34
34
  async createTestCase(args: TestCaseArgs) {
35
+ // Some MCP clients (e.g. Claude Code) pass nested object parameters as JSON strings.
36
+ // Parse test_script if it arrived as a string.
37
+ if (typeof (args as any).test_script === 'string') {
38
+ try { (args as any).test_script = JSON.parse((args as any).test_script); } catch {}
39
+ }
35
40
  if (this.jiraConfig.type === 'cloud') {
36
41
  return this.createTestCaseCloud(args);
37
42
  }
@@ -185,47 +190,28 @@ export class ZephyrToolHandlers {
185
190
  private async updateTestCaseBddCloud(args: UpdateBddArgs) {
186
191
  const { test_case_key, bdd_content, name } = args;
187
192
 
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
- }
200
- }
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];
214
- }
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);
193
+ const converted = convertToGherkin(bdd_content);
194
+ const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
220
195
 
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;
196
+ try {
224
197
  await this.axiosInstance.post(
225
198
  `${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`,
226
199
  { type: 'bdd', text: finalText }
227
200
  );
228
201
 
202
+ // Only fetch and PUT metadata when the caller also wants to rename the test case
203
+ if (typeof name === 'string' && name.trim().length > 0) {
204
+ const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
205
+ const tc = getResponse.data;
206
+ const projectKey = tc.projectKey ?? test_case_key.replace(/-T\d+$/, '');
207
+ await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
208
+ projectKey,
209
+ name,
210
+ status: tc.status,
211
+ priority: tc.priority,
212
+ });
213
+ }
214
+
229
215
  return {
230
216
  content: [{
231
217
  type: 'text',
@@ -249,8 +235,9 @@ export class ZephyrToolHandlers {
249
235
  const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
250
236
 
251
237
  const payload: any = {};
238
+ const projectKey = testCaseData.projectKey ?? test_case_key.replace(/-T\d+$/, '');
252
239
  const requiredFields: Array<[string, any]> = [
253
- ['projectKey', testCaseData.projectKey],
240
+ ['projectKey', projectKey],
254
241
  ['name', testCaseData.name],
255
242
  ['status', testCaseData.status],
256
243
  ['priority', testCaseData.priority]
@@ -513,12 +500,16 @@ export class ZephyrToolHandlers {
513
500
 
514
501
  const cycleKey = response.data.key || 'Unknown';
515
502
 
516
- // Step 2: add test cases to the cycle
503
+ // Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
517
504
  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
- );
505
+ for (const testCaseKey of test_case_keys) {
506
+ await this.axiosInstance.post('/testexecutions', {
507
+ projectKey: project_key,
508
+ testCaseKey,
509
+ testCycleKey: cycleKey,
510
+ statusName: 'Not Executed',
511
+ });
512
+ }
522
513
  }
523
514
 
524
515
  return {
@@ -813,28 +804,24 @@ export class ZephyrToolHandlers {
813
804
 
814
805
  const { test_run_key, test_case_keys } = args;
815
806
 
816
- try {
817
- const payload = {
818
- items: test_case_keys.map(key => ({ testCaseKey: key }))
819
- };
820
- const response = await this.axiosInstance.post(
821
- `${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`,
822
- payload
823
- );
807
+ // Derive project key from the test run key (e.g. PROJ-R123 → PROJ)
808
+ const project_key = test_run_key.split('-')[0];
824
809
 
825
- if (response.status === 200 || response.status === 201 || response.status === 204) {
826
- return {
827
- content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
828
- };
810
+ try {
811
+ for (const testCaseKey of test_case_keys) {
812
+ await this.axiosInstance.post('/testexecutions', {
813
+ projectKey: project_key,
814
+ testCaseKey,
815
+ testCycleKey: test_run_key,
816
+ statusName: 'Not Executed',
817
+ });
829
818
  }
819
+ return {
820
+ content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
821
+ };
830
822
  } catch (error) {
831
823
  throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
832
824
  }
833
-
834
- return {
835
- content: [{ type: 'text', text: 'An unexpected error occurred.' }],
836
- isError: true,
837
- };
838
825
  }
839
826
 
840
827
  private formatError(error: unknown): string {
package/src/utils.ts CHANGED
@@ -43,28 +43,53 @@ export async function resolveFolderIdByPath(
43
43
  export function convertToGherkin(bddContent: string): string {
44
44
  const bddLines: string[] = [];
45
45
  const lines = bddContent.split('\n');
46
-
46
+
47
+ // Bold-markdown step keywords (e.g. **Given**, **When**, etc.)
48
+ const boldStepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
49
+ // Plain step keyword prefixes
50
+ const stepKeywords = ['Given ', 'When ', 'Then ', 'And ', 'But '];
51
+ // Zephyr Scale Cloud only accepts steps and table rows — Feature:/Scenario: wrappers
52
+ // cause a 400 "Invalid Gherkin script" error and must be stripped.
53
+ const strippedPrefixes = [
54
+ 'Feature:',
55
+ 'Background:',
56
+ 'Scenario Outline:',
57
+ 'Scenario:',
58
+ 'Examples:',
59
+ ];
60
+
47
61
  for (const line of lines) {
48
62
  const trimmedLine = line.trim();
49
63
  if (!trimmedLine || trimmedLine.startsWith('---')) continue;
50
-
51
- if (trimmedLine.startsWith('**Given**')) {
52
- bddLines.push(`Given ${trimmedLine.replace('**Given**', '').trim()}`);
53
- } else if (trimmedLine.startsWith('**When**')) {
54
- bddLines.push(`When ${trimmedLine.replace('**When**', '').trim()}`);
55
- } else if (trimmedLine.startsWith('**Then**')) {
56
- bddLines.push(`Then ${trimmedLine.replace('**Then**', '').trim()}`);
57
- } else if (trimmedLine.startsWith('**And**')) {
58
- bddLines.push(`And ${trimmedLine.replace('**And**', '').trim()}`);
59
- } else if (trimmedLine.startsWith('Given ') || trimmedLine.startsWith('When ') ||
60
- trimmedLine.startsWith('Then ') || trimmedLine.startsWith('And ')) {
64
+
65
+ // Strip structural Gherkin keywords — not accepted by the Zephyr Scale Cloud testscript API
66
+ if (strippedPrefixes.some(p => trimmedLine.startsWith(p))) continue;
67
+
68
+ // Convert **Keyword** markdown bold to plain Gherkin keyword
69
+ let matchedBold = false;
70
+ for (const kw of boldStepKeywords) {
71
+ if (trimmedLine.startsWith(`**${kw}**`)) {
72
+ bddLines.push(`${kw} ${trimmedLine.replace(`**${kw}**`, '').trim()}`);
73
+ matchedBold = true;
74
+ break;
75
+ }
76
+ }
77
+ if (matchedBold) continue;
78
+
79
+ // Plain step keywords — pass through unchanged
80
+ if (stepKeywords.some(k => trimmedLine.startsWith(k))) {
81
+ bddLines.push(trimmedLine);
82
+ continue;
83
+ }
84
+
85
+ // Table rows — pass through unchanged
86
+ if (trimmedLine.startsWith('|')) {
61
87
  bddLines.push(trimmedLine);
62
88
  }
63
89
  }
64
-
65
- return bddLines.length > 0 ? ' ' + bddLines.join('\n ') : '';
66
- }
67
90
 
91
+ return bddLines.join('\n');
92
+ }
68
93
  export const customPriorityMapping: { [key: string]: string } = {
69
94
  'High': 'P0',
70
95
  'Normal': 'P1',
@@ -128,9 +153,12 @@ export function createJiraConfig() {
128
153
 
129
154
  const type = detectJiraType(jiraBaseUrl);
130
155
  const apiEndpoints = getApiEndpoints(type);
131
-
132
- const baseUrl = type === 'cloud'
133
- ? 'https://api.zephyrscale.smartbear.com/v2'
156
+
157
+ // Cloud: use ZEPHYR_API_BASE_URL if set (e.g. for EU: https://eu.api.zephyrscale.smartbear.com/v2), else default US
158
+ const defaultCloudBaseUrl = 'https://api.zephyrscale.smartbear.com/v2';
159
+ const cloudBaseUrl = process.env.ZEPHYR_API_BASE_URL?.trim();
160
+ const baseUrl = type === 'cloud'
161
+ ? (cloudBaseUrl || defaultCloudBaseUrl)
134
162
  : jiraBaseUrl;
135
163
 
136
164
  const authHeaders = {