zephyr-scale-mcp-server 0.4.0 → 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]
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.4.0",
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]
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 = {