zephyr-scale-mcp-server 0.4.0 → 0.4.2
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 +55 -35
- package/build/tool-schemas.js +1 -1
- package/build/utils.js +38 -14
- package/package.json +2 -2
- package/src/tool-handlers.ts +59 -38
- package/src/tool-schemas.ts +1 -1
- package/src/types.ts +1 -0
- package/src/utils.ts +47 -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,13 +20,21 @@ 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
|
}
|
|
26
34
|
return this.createTestCaseDC(args);
|
|
27
35
|
}
|
|
28
36
|
async createTestCaseCloud(args) {
|
|
29
|
-
const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, } = args;
|
|
37
|
+
const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, issue_links, } = args;
|
|
30
38
|
const payload = { projectKey: project_key, name };
|
|
31
39
|
// Cloud v2 uses statusName/priorityName (strings), folderId (integer)
|
|
32
40
|
payload.statusName = 'Draft';
|
|
@@ -58,10 +66,27 @@ export class ZephyrToolHandlers {
|
|
|
58
66
|
if (test_script) {
|
|
59
67
|
await this.upsertTestScriptCloud(testKey, test_script);
|
|
60
68
|
}
|
|
69
|
+
// Step 3: link Jira issues via POST /testcases/{key}/links/issues
|
|
70
|
+
// IssueLinkInput requires a numeric issueId — resolve each key via Jira REST API
|
|
71
|
+
const linkWarnings = [];
|
|
72
|
+
if (issue_links && issue_links.length > 0) {
|
|
73
|
+
for (const issueKey of issue_links) {
|
|
74
|
+
try {
|
|
75
|
+
const issueId = await this.resolveJiraIssueId(issueKey);
|
|
76
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/links/issues`, { issueId });
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
linkWarnings.push(`${issueKey}: ${this.formatError(e)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const warningText = linkWarnings.length > 0
|
|
84
|
+
? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}`
|
|
85
|
+
: '';
|
|
61
86
|
return {
|
|
62
87
|
content: [{
|
|
63
88
|
type: 'text',
|
|
64
|
-
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
|
|
89
|
+
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none', linkedIssues: (issue_links ?? []).length - linkWarnings.length }, null, 2)}${warningText}`,
|
|
65
90
|
}],
|
|
66
91
|
};
|
|
67
92
|
}
|
|
@@ -169,40 +194,22 @@ export class ZephyrToolHandlers {
|
|
|
169
194
|
}
|
|
170
195
|
async updateTestCaseBddCloud(args) {
|
|
171
196
|
const { test_case_key, bdd_content, name } = args;
|
|
197
|
+
const converted = convertToGherkin(bdd_content);
|
|
198
|
+
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
172
199
|
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
200
|
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`, { type: 'bdd', text: finalText });
|
|
201
|
+
// Only fetch and PUT metadata when the caller also wants to rename the test case
|
|
202
|
+
if (typeof name === 'string' && name.trim().length > 0) {
|
|
203
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
204
|
+
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,
|
|
208
|
+
name,
|
|
209
|
+
status: tc.status,
|
|
210
|
+
priority: tc.priority,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
206
213
|
return {
|
|
207
214
|
content: [{
|
|
208
215
|
type: 'text',
|
|
@@ -224,8 +231,9 @@ export class ZephyrToolHandlers {
|
|
|
224
231
|
const converted = convertToGherkin(bdd_content);
|
|
225
232
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
226
233
|
const payload = {};
|
|
234
|
+
const projectKey = testCaseData.projectKey ?? test_case_key.replace(/-T\d+$/, '');
|
|
227
235
|
const requiredFields = [
|
|
228
|
-
['projectKey',
|
|
236
|
+
['projectKey', projectKey],
|
|
229
237
|
['name', testCaseData.name],
|
|
230
238
|
['status', testCaseData.status],
|
|
231
239
|
['priority', testCaseData.priority]
|
|
@@ -760,6 +768,18 @@ export class ZephyrToolHandlers {
|
|
|
760
768
|
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
761
769
|
}
|
|
762
770
|
}
|
|
771
|
+
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> }.
|
|
775
|
+
const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
|
|
776
|
+
const response = await this.axiosInstance.get(url, { baseURL: '' });
|
|
777
|
+
const id = parseInt(response.data.id, 10);
|
|
778
|
+
if (!id || isNaN(id)) {
|
|
779
|
+
throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
|
|
780
|
+
}
|
|
781
|
+
return id;
|
|
782
|
+
}
|
|
763
783
|
formatError(error) {
|
|
764
784
|
if (error instanceof Error && 'response' in error) {
|
|
765
785
|
const axiosError = error;
|
package/build/tool-schemas.js
CHANGED
|
@@ -110,7 +110,7 @@ export const toolSchemas = [
|
|
|
110
110
|
},
|
|
111
111
|
issue_links: {
|
|
112
112
|
type: 'array',
|
|
113
|
-
description: 'Array of issue
|
|
113
|
+
description: 'Array of Jira issue keys to link to this test case (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric Jira issue ID via the Jira REST API, then linked via POST /testcases/{key}/links/issues — failures are reported as warnings but do not fail the tool call. On Data Center, sent directly in the create payload.',
|
|
114
114
|
items: { type: 'string' }
|
|
115
115
|
},
|
|
116
116
|
custom_fields: {
|
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}`,
|
|
@@ -122,6 +145,7 @@ export function createJiraConfig() {
|
|
|
122
145
|
return {
|
|
123
146
|
type,
|
|
124
147
|
baseUrl,
|
|
148
|
+
jiraBaseUrl,
|
|
125
149
|
authHeaders,
|
|
126
150
|
apiEndpoints,
|
|
127
151
|
};
|
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.2",
|
|
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
|
}
|
|
@@ -41,7 +46,7 @@ export class ZephyrToolHandlers {
|
|
|
41
46
|
private async createTestCaseCloud(args: TestCaseArgs) {
|
|
42
47
|
const {
|
|
43
48
|
project_key, name, test_script, folder, priority, precondition,
|
|
44
|
-
objective, estimated_time, labels, custom_fields,
|
|
49
|
+
objective, estimated_time, labels, custom_fields, issue_links,
|
|
45
50
|
} = args;
|
|
46
51
|
|
|
47
52
|
const payload: any = { projectKey: project_key, name };
|
|
@@ -74,10 +79,31 @@ export class ZephyrToolHandlers {
|
|
|
74
79
|
await this.upsertTestScriptCloud(testKey, test_script);
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
// Step 3: link Jira issues via POST /testcases/{key}/links/issues
|
|
83
|
+
// IssueLinkInput requires a numeric issueId — resolve each key via Jira REST API
|
|
84
|
+
const linkWarnings: string[] = [];
|
|
85
|
+
if (issue_links && issue_links.length > 0) {
|
|
86
|
+
for (const issueKey of issue_links) {
|
|
87
|
+
try {
|
|
88
|
+
const issueId = await this.resolveJiraIssueId(issueKey);
|
|
89
|
+
await this.axiosInstance.post(
|
|
90
|
+
`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/links/issues`,
|
|
91
|
+
{ issueId }
|
|
92
|
+
);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
linkWarnings.push(`${issueKey}: ${this.formatError(e)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const warningText = linkWarnings.length > 0
|
|
100
|
+
? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}`
|
|
101
|
+
: '';
|
|
102
|
+
|
|
77
103
|
return {
|
|
78
104
|
content: [{
|
|
79
105
|
type: 'text',
|
|
80
|
-
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
|
|
106
|
+
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none', linkedIssues: (issue_links ?? []).length - linkWarnings.length }, null, 2)}${warningText}`,
|
|
81
107
|
}],
|
|
82
108
|
};
|
|
83
109
|
} catch (error) {
|
|
@@ -185,47 +211,28 @@ export class ZephyrToolHandlers {
|
|
|
185
211
|
private async updateTestCaseBddCloud(args: UpdateBddArgs) {
|
|
186
212
|
const { test_case_key, bdd_content, name } = args;
|
|
187
213
|
|
|
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;
|
|
214
|
+
const converted = convertToGherkin(bdd_content);
|
|
215
|
+
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
217
216
|
|
|
218
|
-
|
|
219
|
-
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
220
|
-
|
|
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;
|
|
217
|
+
try {
|
|
224
218
|
await this.axiosInstance.post(
|
|
225
219
|
`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`,
|
|
226
220
|
{ type: 'bdd', text: finalText }
|
|
227
221
|
);
|
|
228
222
|
|
|
223
|
+
// Only fetch and PUT metadata when the caller also wants to rename the test case
|
|
224
|
+
if (typeof name === 'string' && name.trim().length > 0) {
|
|
225
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
226
|
+
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,
|
|
230
|
+
name,
|
|
231
|
+
status: tc.status,
|
|
232
|
+
priority: tc.priority,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
229
236
|
return {
|
|
230
237
|
content: [{
|
|
231
238
|
type: 'text',
|
|
@@ -249,8 +256,9 @@ export class ZephyrToolHandlers {
|
|
|
249
256
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
250
257
|
|
|
251
258
|
const payload: any = {};
|
|
259
|
+
const projectKey = testCaseData.projectKey ?? test_case_key.replace(/-T\d+$/, '');
|
|
252
260
|
const requiredFields: Array<[string, any]> = [
|
|
253
|
-
['projectKey',
|
|
261
|
+
['projectKey', projectKey],
|
|
254
262
|
['name', testCaseData.name],
|
|
255
263
|
['status', testCaseData.status],
|
|
256
264
|
['priority', testCaseData.priority]
|
|
@@ -837,6 +845,19 @@ export class ZephyrToolHandlers {
|
|
|
837
845
|
}
|
|
838
846
|
}
|
|
839
847
|
|
|
848
|
+
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> }.
|
|
852
|
+
const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
|
|
853
|
+
const response = await this.axiosInstance.get(url, { baseURL: '' });
|
|
854
|
+
const id = parseInt(response.data.id, 10);
|
|
855
|
+
if (!id || isNaN(id)) {
|
|
856
|
+
throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
|
|
857
|
+
}
|
|
858
|
+
return id;
|
|
859
|
+
}
|
|
860
|
+
|
|
840
861
|
private formatError(error: unknown): string {
|
|
841
862
|
if (error instanceof Error && 'response' in error) {
|
|
842
863
|
const axiosError = error as any;
|
package/src/tool-schemas.ts
CHANGED
|
@@ -110,7 +110,7 @@ export const toolSchemas = [
|
|
|
110
110
|
},
|
|
111
111
|
issue_links: {
|
|
112
112
|
type: 'array',
|
|
113
|
-
description: 'Array of issue
|
|
113
|
+
description: 'Array of Jira issue keys to link to this test case (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric Jira issue ID via the Jira REST API, then linked via POST /testcases/{key}/links/issues — failures are reported as warnings but do not fail the tool call. On Data Center, sent directly in the create payload.',
|
|
114
114
|
items: { type: 'string' }
|
|
115
115
|
},
|
|
116
116
|
custom_fields: {
|
package/src/types.ts
CHANGED
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 = {
|
|
@@ -142,6 +170,7 @@ export function createJiraConfig() {
|
|
|
142
170
|
return {
|
|
143
171
|
type,
|
|
144
172
|
baseUrl,
|
|
173
|
+
jiraBaseUrl,
|
|
145
174
|
authHeaders,
|
|
146
175
|
apiEndpoints,
|
|
147
176
|
};
|