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 +14 -0
- package/build/tool-handlers.js +24 -33
- package/build/utils.js +37 -14
- package/package.json +2 -2
- package/src/tool-handlers.ts +23 -36
- package/src/utils.ts +46 -18
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.
|
package/build/tool-handlers.js
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
69
|
+
// Table rows — pass through unchanged
|
|
70
|
+
if (trimmedLine.startsWith('|')) {
|
|
51
71
|
bddLines.push(trimmedLine);
|
|
52
72
|
}
|
|
53
73
|
}
|
|
54
|
-
return bddLines.
|
|
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
|
-
?
|
|
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.
|
|
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.
|
|
60
|
+
"axios": "^1.15.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^20.11.24",
|
package/src/tool-handlers.ts
CHANGED
|
@@ -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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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 = {
|