zephyr-scale-mcp-server 0.3.2 → 0.4.0
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 +4 -8
- package/build/tool-handlers.js +482 -314
- package/build/tool-schemas.js +8 -8
- package/build/utils.js +30 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/tool-handlers.ts +563 -376
- package/src/tool-schemas.ts +8 -8
- package/src/types.ts +6 -0
- package/src/utils.ts +43 -1
package/build/tool-handlers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { convertToGherkin } from './utils.js';
|
|
2
|
+
import { convertToGherkin, resolveFolderIdByPath } from './utils.js';
|
|
3
3
|
export class ZephyrToolHandlers {
|
|
4
4
|
axiosInstance;
|
|
5
5
|
jiraConfig;
|
|
@@ -16,17 +16,83 @@ export class ZephyrToolHandlers {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
catch (error) {
|
|
19
|
-
throw new McpError(ErrorCode.InternalError, `Failed to get test case: ${
|
|
19
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test case: ${this.formatError(error)}`);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
async createTestCase(args) {
|
|
23
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
24
|
+
return this.createTestCaseCloud(args);
|
|
25
|
+
}
|
|
26
|
+
return this.createTestCaseDC(args);
|
|
27
|
+
}
|
|
28
|
+
async createTestCaseCloud(args) {
|
|
29
|
+
const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, } = args;
|
|
30
|
+
const payload = { projectKey: project_key, name };
|
|
31
|
+
// Cloud v2 uses statusName/priorityName (strings), folderId (integer)
|
|
32
|
+
payload.statusName = 'Draft';
|
|
33
|
+
if (priority)
|
|
34
|
+
payload.priorityName = priority;
|
|
35
|
+
if (precondition)
|
|
36
|
+
payload.precondition = precondition;
|
|
37
|
+
if (objective)
|
|
38
|
+
payload.objective = objective;
|
|
39
|
+
if (estimated_time)
|
|
40
|
+
payload.estimatedTime = estimated_time;
|
|
41
|
+
if (labels && labels.length > 0)
|
|
42
|
+
payload.labels = labels;
|
|
43
|
+
if (custom_fields)
|
|
44
|
+
payload.customFields = custom_fields;
|
|
45
|
+
// Resolve folder path → folderId
|
|
46
|
+
if (folder) {
|
|
47
|
+
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CASE');
|
|
48
|
+
if (folderId !== null)
|
|
49
|
+
payload.folderId = folderId;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testcase, payload);
|
|
53
|
+
if (response.status !== 201) {
|
|
54
|
+
throw new Error(`Unexpected status code: ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
const testKey = response.data.key || 'Unknown';
|
|
57
|
+
// Step 2: add test script via dedicated endpoint
|
|
58
|
+
if (test_script) {
|
|
59
|
+
await this.upsertTestScriptCloud(testKey, test_script);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
|
|
65
|
+
}],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create test case: ${this.formatError(error)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async upsertTestScriptCloud(testKey, test_script) {
|
|
73
|
+
if (!test_script)
|
|
74
|
+
return;
|
|
75
|
+
if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
|
|
76
|
+
const items = test_script.steps.map((step) => ({
|
|
77
|
+
inline: {
|
|
78
|
+
description: step.description || '',
|
|
79
|
+
testData: step.testData || null,
|
|
80
|
+
expectedResult: step.expectedResult || null,
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`, { mode: 'OVERWRITE', items });
|
|
84
|
+
}
|
|
85
|
+
else if (test_script.type === 'BDD' && test_script.text) {
|
|
86
|
+
const gherkin = convertToGherkin(test_script.text) || test_script.text;
|
|
87
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/testscript`, { type: 'bdd', text: gherkin });
|
|
88
|
+
}
|
|
89
|
+
else if (test_script.type === 'PLAIN_TEXT' && test_script.text) {
|
|
90
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/testscript`, { type: 'plain', text: test_script.text });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async createTestCaseDC(args) {
|
|
23
94
|
const { project_key, name, test_script, folder, status, priority, precondition, objective, component, owner, estimated_time, labels, issue_links, custom_fields, parameters } = args;
|
|
24
|
-
|
|
25
|
-
const payload = {
|
|
26
|
-
projectKey: project_key,
|
|
27
|
-
name: name
|
|
28
|
-
};
|
|
29
|
-
// Add optional fields
|
|
95
|
+
const payload = { projectKey: project_key, name };
|
|
30
96
|
if (folder)
|
|
31
97
|
payload.folder = folder;
|
|
32
98
|
if (status)
|
|
@@ -51,87 +117,113 @@ export class ZephyrToolHandlers {
|
|
|
51
117
|
payload.customFields = custom_fields;
|
|
52
118
|
if (parameters)
|
|
53
119
|
payload.parameters = parameters;
|
|
54
|
-
// Handle test script
|
|
55
120
|
if (test_script) {
|
|
56
|
-
payload.testScript = {
|
|
57
|
-
type: test_script.type
|
|
58
|
-
};
|
|
121
|
+
payload.testScript = { type: test_script.type };
|
|
59
122
|
if (test_script.type === 'STEP_BY_STEP' && test_script.steps) {
|
|
60
123
|
payload.testScript.steps = test_script.steps.map((step) => {
|
|
61
|
-
const
|
|
124
|
+
const s = {};
|
|
62
125
|
if (step.description)
|
|
63
|
-
|
|
126
|
+
s.description = step.description;
|
|
64
127
|
if (step.testData)
|
|
65
|
-
|
|
128
|
+
s.testData = step.testData;
|
|
66
129
|
if (step.expectedResult)
|
|
67
|
-
|
|
130
|
+
s.expectedResult = step.expectedResult;
|
|
68
131
|
if (step.testCaseKey)
|
|
69
|
-
|
|
70
|
-
return
|
|
132
|
+
s.testCaseKey = step.testCaseKey;
|
|
133
|
+
return s;
|
|
71
134
|
});
|
|
72
135
|
}
|
|
73
136
|
else if ((test_script.type === 'PLAIN_TEXT' || test_script.type === 'BDD') && test_script.text) {
|
|
74
137
|
if (test_script.type === 'BDD') {
|
|
75
|
-
const
|
|
76
|
-
payload.testScript.text =
|
|
138
|
+
const gherkin = convertToGherkin(test_script.text);
|
|
139
|
+
payload.testScript.text = gherkin || test_script.text;
|
|
77
140
|
}
|
|
78
141
|
else {
|
|
79
142
|
payload.testScript.text = test_script.text;
|
|
80
143
|
}
|
|
81
144
|
}
|
|
82
145
|
}
|
|
83
|
-
// Always
|
|
146
|
+
// Always Draft for new test cases
|
|
84
147
|
payload.status = 'Draft';
|
|
85
148
|
try {
|
|
86
149
|
const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testcase, payload);
|
|
87
|
-
if (response.status
|
|
88
|
-
const testKey = response.data.key || 'Unknown';
|
|
89
|
-
return {
|
|
90
|
-
content: [
|
|
91
|
-
{
|
|
92
|
-
type: 'text',
|
|
93
|
-
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({
|
|
94
|
-
key: testKey,
|
|
95
|
-
type: test_script?.type || 'none',
|
|
96
|
-
hasSteps: test_script?.type === 'STEP_BY_STEP' ? test_script.steps?.length || 0 : undefined,
|
|
97
|
-
hasText: (test_script?.type === 'PLAIN_TEXT' || test_script?.type === 'BDD') ? !!test_script.text : undefined
|
|
98
|
-
}, null, 2)}`,
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
150
|
+
if (response.status !== 201)
|
|
104
151
|
throw new Error(`Unexpected status code: ${response.status}`);
|
|
105
|
-
|
|
152
|
+
const testKey = response.data.key || 'Unknown';
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
106
159
|
}
|
|
107
160
|
catch (error) {
|
|
108
|
-
|
|
109
|
-
if (error instanceof Error && 'response' in error) {
|
|
110
|
-
const axiosError = error;
|
|
111
|
-
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
112
|
-
}
|
|
113
|
-
else if (error instanceof Error) {
|
|
114
|
-
errorMessage = error.message;
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
errorMessage = String(error);
|
|
118
|
-
}
|
|
119
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create test case: ${errorMessage}`);
|
|
161
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create test case: ${this.formatError(error)}`);
|
|
120
162
|
}
|
|
121
163
|
}
|
|
122
164
|
async updateTestCaseBdd(args) {
|
|
165
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
166
|
+
return this.updateTestCaseBddCloud(args);
|
|
167
|
+
}
|
|
168
|
+
return this.updateTestCaseBddDC(args);
|
|
169
|
+
}
|
|
170
|
+
async updateTestCaseBddCloud(args) {
|
|
171
|
+
const { test_case_key, bdd_content, name } = args;
|
|
172
|
+
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
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`, { type: 'bdd', text: finalText });
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: 'text',
|
|
209
|
+
text: `✅ Updated ${test_case_key} with BDD content successfully (Cloud v2)`,
|
|
210
|
+
}],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error instanceof McpError)
|
|
215
|
+
throw error;
|
|
216
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update test case BDD: ${this.formatError(error)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async updateTestCaseBddDC(args) {
|
|
123
220
|
const { test_case_key, bdd_content, name } = args;
|
|
124
221
|
try {
|
|
125
|
-
// First, get the existing test case data
|
|
126
222
|
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
127
223
|
const testCaseData = getResponse.data;
|
|
128
|
-
// Convert incoming BDD markdown (fallback to raw content if conversion returns empty)
|
|
129
224
|
const converted = convertToGherkin(bdd_content);
|
|
130
225
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
131
|
-
// Build a payload aligned with Zephyr Scale Server's update schema.
|
|
132
|
-
// Only include fields that exist to avoid accidental nulling; required fields must be present.
|
|
133
226
|
const payload = {};
|
|
134
|
-
// Required base fields (schema requires these on Server/Data Center). If missing, throw.
|
|
135
227
|
const requiredFields = [
|
|
136
228
|
['projectKey', testCaseData.projectKey],
|
|
137
229
|
['name', testCaseData.name],
|
|
@@ -144,66 +236,94 @@ export class ZephyrToolHandlers {
|
|
|
144
236
|
}
|
|
145
237
|
payload[field] = value;
|
|
146
238
|
}
|
|
147
|
-
|
|
148
|
-
if (typeof name === 'string' && name.trim().length > 0) {
|
|
239
|
+
if (typeof name === 'string' && name.trim().length > 0)
|
|
149
240
|
payload.name = name;
|
|
150
|
-
|
|
151
|
-
// Optional simple scalar/string fields
|
|
152
|
-
const optionalScalarFields = [
|
|
153
|
-
'objective',
|
|
154
|
-
'precondition',
|
|
155
|
-
'folder',
|
|
156
|
-
'component',
|
|
157
|
-
'owner',
|
|
158
|
-
'estimatedTime'
|
|
159
|
-
];
|
|
160
|
-
for (const field of optionalScalarFields) {
|
|
241
|
+
for (const field of ['objective', 'precondition', 'folder', 'component', 'owner', 'estimatedTime']) {
|
|
161
242
|
if (testCaseData[field] !== undefined)
|
|
162
243
|
payload[field] = testCaseData[field];
|
|
163
244
|
}
|
|
164
|
-
// Arrays / objects
|
|
165
245
|
if (Array.isArray(testCaseData.labels))
|
|
166
246
|
payload.labels = testCaseData.labels;
|
|
167
247
|
if (testCaseData.customFields)
|
|
168
248
|
payload.customFields = testCaseData.customFields;
|
|
169
249
|
if (testCaseData.parameters)
|
|
170
250
|
payload.parameters = testCaseData.parameters;
|
|
171
|
-
// issueLinks preferred; map deprecated issueKey if present and issueLinks absent
|
|
172
251
|
if (Array.isArray(testCaseData.issueLinks)) {
|
|
173
252
|
payload.issueLinks = testCaseData.issueLinks;
|
|
174
253
|
}
|
|
175
254
|
else if (testCaseData.issueKey) {
|
|
176
255
|
payload.issueLinks = [testCaseData.issueKey];
|
|
177
256
|
}
|
|
178
|
-
|
|
179
|
-
payload.testScript = {
|
|
180
|
-
type: 'BDD',
|
|
181
|
-
text: finalText
|
|
182
|
-
};
|
|
183
|
-
// PUT update
|
|
257
|
+
payload.testScript = { type: 'BDD', text: finalText };
|
|
184
258
|
const updateResponse = await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, payload);
|
|
185
|
-
if (updateResponse.status
|
|
259
|
+
if (updateResponse.status !== 200) {
|
|
260
|
+
throw new Error(`Failed to update ${test_case_key}: ${updateResponse.status}`);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
content: [{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: `✅ Updated ${test_case_key} with BDD content successfully\nPayload summary: ${JSON.stringify({ textLength: finalText.length, projectKey: payload.projectKey, name: payload.name, preservedLabels: payload.labels?.length || 0 }, null, 2)}`,
|
|
266
|
+
}],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof McpError)
|
|
271
|
+
throw error;
|
|
272
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update test case BDD: ${this.formatError(error)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async createFolder(args) {
|
|
276
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
277
|
+
return this.createFolderCloud(args);
|
|
278
|
+
}
|
|
279
|
+
return this.createFolderDC(args);
|
|
280
|
+
}
|
|
281
|
+
async createFolderCloud(args) {
|
|
282
|
+
const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
|
|
283
|
+
// Cloud v2 uses folderType (not type) and parentId integer
|
|
284
|
+
// Map TEST_RUN → TEST_CYCLE for Cloud v2
|
|
285
|
+
const cloudFolderType = folder_type === 'TEST_RUN' ? 'TEST_CYCLE' : folder_type;
|
|
286
|
+
const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
287
|
+
if (segments.length === 0) {
|
|
288
|
+
throw new McpError(ErrorCode.InvalidParams, 'folder name/path cannot be empty');
|
|
289
|
+
}
|
|
290
|
+
const leafName = segments[segments.length - 1];
|
|
291
|
+
let parentId = null;
|
|
292
|
+
// Resolve parent if nested path
|
|
293
|
+
if (segments.length > 1) {
|
|
294
|
+
const parentPath = '/' + segments.slice(0, -1).join('/');
|
|
295
|
+
parentId = await resolveFolderIdByPath(this.axiosInstance, project_key, parentPath, cloudFolderType);
|
|
296
|
+
if (parentId === null) {
|
|
297
|
+
throw new McpError(ErrorCode.InternalError, `Parent folder not found for path: ${parentPath}. Create parent folders first.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const payload = {
|
|
301
|
+
projectKey: project_key,
|
|
302
|
+
name: leafName,
|
|
303
|
+
folderType: cloudFolderType,
|
|
304
|
+
};
|
|
305
|
+
if (parentId !== null)
|
|
306
|
+
payload.parentId = parentId;
|
|
307
|
+
try {
|
|
308
|
+
const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.folder, payload);
|
|
309
|
+
if (response.status === 201 || response.status === 200) {
|
|
186
310
|
return {
|
|
187
|
-
content: [
|
|
188
|
-
{
|
|
311
|
+
content: [{
|
|
189
312
|
type: 'text',
|
|
190
|
-
text: `✅
|
|
191
|
-
},
|
|
192
|
-
],
|
|
313
|
+
text: `✅ Folder created successfully: ${leafName} (ID: ${response.data.id || 'N/A'})\n${JSON.stringify(response.data, null, 2)}`,
|
|
314
|
+
}],
|
|
193
315
|
};
|
|
194
316
|
}
|
|
195
|
-
throw new Error(`
|
|
317
|
+
throw new Error(`Unexpected status code: ${response.status}`);
|
|
196
318
|
}
|
|
197
319
|
catch (error) {
|
|
198
|
-
|
|
320
|
+
if (error instanceof McpError)
|
|
321
|
+
throw error;
|
|
322
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create folder: ${this.formatError(error)}`);
|
|
199
323
|
}
|
|
200
324
|
}
|
|
201
|
-
async
|
|
325
|
+
async createFolderDC(args) {
|
|
202
326
|
const { project_key, name, folder_type = 'TEST_CASE' } = args;
|
|
203
|
-
// According to Zephyr Scale API documentation:
|
|
204
|
-
// - projectKey: Project key (required)
|
|
205
|
-
// - name: Full folder path including parent folders (e.g., "/folder/subfolder")
|
|
206
|
-
// - type: Folder type (TEST_CASE, TEST_PLAN, or TEST_RUN)
|
|
207
327
|
const payload = {
|
|
208
328
|
projectKey: project_key,
|
|
209
329
|
name: name,
|
|
@@ -213,35 +333,51 @@ export class ZephyrToolHandlers {
|
|
|
213
333
|
const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.folder, payload);
|
|
214
334
|
if (response.status === 201 || response.status === 200) {
|
|
215
335
|
return {
|
|
216
|
-
content: [
|
|
217
|
-
{
|
|
336
|
+
content: [{
|
|
218
337
|
type: 'text',
|
|
219
338
|
text: `✅ Folder created successfully: ${response.data.name || name} (ID: ${response.data.id || 'N/A'})\n${JSON.stringify(response.data, null, 2)}`,
|
|
220
|
-
},
|
|
221
|
-
],
|
|
339
|
+
}],
|
|
222
340
|
};
|
|
223
341
|
}
|
|
224
|
-
|
|
225
|
-
throw new Error(`Unexpected status code: ${response.status}`);
|
|
226
|
-
}
|
|
342
|
+
throw new Error(`Unexpected status code: ${response.status}`);
|
|
227
343
|
}
|
|
228
344
|
catch (error) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}, Payload sent: ${JSON.stringify(payload)}`;
|
|
233
|
-
}
|
|
234
|
-
else if (error instanceof Error) {
|
|
235
|
-
errorMessage = error.message;
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
errorMessage = String(error);
|
|
239
|
-
}
|
|
240
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create folder: ${errorMessage}`);
|
|
345
|
+
if (error instanceof McpError)
|
|
346
|
+
throw error;
|
|
347
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create folder: ${this.formatError(error)}`);
|
|
241
348
|
}
|
|
242
349
|
}
|
|
243
350
|
async getTestRunCases(args) {
|
|
244
351
|
const { test_run_key } = args;
|
|
352
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
353
|
+
try {
|
|
354
|
+
// Cloud: test cases are retrieved via executions associated with the cycle
|
|
355
|
+
const response = await this.axiosInstance.get('/testexecutions', {
|
|
356
|
+
params: { testCycle: test_run_key, maxResults: 1000 },
|
|
357
|
+
});
|
|
358
|
+
const executions = Array.isArray(response.data)
|
|
359
|
+
? response.data
|
|
360
|
+
: response.data?.values ?? [];
|
|
361
|
+
// Deduplicate by test case key
|
|
362
|
+
const seen = new Set();
|
|
363
|
+
const testCaseKeys = [];
|
|
364
|
+
for (const exec of executions) {
|
|
365
|
+
const key = exec.testCase?.key
|
|
366
|
+
?? exec.testCase?.self?.match(/testcases\/([^/]+)/)?.[1];
|
|
367
|
+
if (key && !seen.has(key)) {
|
|
368
|
+
seen.add(key);
|
|
369
|
+
testCaseKeys.push(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: 'text', text: JSON.stringify(testCaseKeys, null, 2) }],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${this.formatError(error)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Data Center: items[] in the run response
|
|
245
381
|
try {
|
|
246
382
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
247
383
|
const testCases = response.data.items?.map((item) => item.testCaseKey) || [];
|
|
@@ -250,61 +386,111 @@ export class ZephyrToolHandlers {
|
|
|
250
386
|
};
|
|
251
387
|
}
|
|
252
388
|
catch (error) {
|
|
253
|
-
throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${
|
|
389
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${this.formatError(error)}`);
|
|
254
390
|
}
|
|
255
391
|
}
|
|
256
392
|
async deleteTestCase(args) {
|
|
393
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
394
|
+
throw new McpError(ErrorCode.InvalidRequest, 'delete_test_case is not supported by the Zephyr Scale Cloud v2 API.');
|
|
395
|
+
}
|
|
257
396
|
const { test_case_key } = args;
|
|
258
397
|
try {
|
|
259
398
|
const response = await this.axiosInstance.delete(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
260
399
|
if (response.status === 204) {
|
|
261
|
-
return {
|
|
262
|
-
content: [{ type: 'text', text: `Test case ${test_case_key} deleted successfully.` }],
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
return {
|
|
267
|
-
content: [{ type: 'text', text: `Failed to delete test case. Status: ${response.status}` }],
|
|
268
|
-
isError: true,
|
|
269
|
-
};
|
|
400
|
+
return { content: [{ type: 'text', text: `Test case ${test_case_key} deleted successfully.` }] };
|
|
270
401
|
}
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: 'text', text: `Failed to delete test case. Status: ${response.status}` }],
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
271
406
|
}
|
|
272
407
|
catch (error) {
|
|
273
|
-
throw new McpError(ErrorCode.InternalError, `Failed to delete test case: ${
|
|
408
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete test case: ${this.formatError(error)}`);
|
|
274
409
|
}
|
|
275
410
|
}
|
|
276
411
|
async deleteTestRun(args) {
|
|
412
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
413
|
+
throw new McpError(ErrorCode.InvalidRequest, 'delete_test_run is not supported by the Zephyr Scale Cloud v2 API.');
|
|
414
|
+
}
|
|
277
415
|
const { test_run_key } = args;
|
|
278
416
|
try {
|
|
279
417
|
const response = await this.axiosInstance.delete(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
280
418
|
if (response.status === 204) {
|
|
281
|
-
return {
|
|
282
|
-
content: [{ type: 'text', text: `Test run ${test_run_key} deleted successfully.` }],
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
return {
|
|
287
|
-
content: [{ type: 'text', text: `Failed to delete test run. Status: ${response.status}` }],
|
|
288
|
-
isError: true,
|
|
289
|
-
};
|
|
419
|
+
return { content: [{ type: 'text', text: `Test run ${test_run_key} deleted successfully.` }] };
|
|
290
420
|
}
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: 'text', text: `Failed to delete test run. Status: ${response.status}` }],
|
|
423
|
+
isError: true,
|
|
424
|
+
};
|
|
291
425
|
}
|
|
292
426
|
catch (error) {
|
|
293
|
-
throw new McpError(ErrorCode.InternalError, `Failed to delete test run: ${
|
|
427
|
+
throw new McpError(ErrorCode.InternalError, `Failed to delete test run: ${this.formatError(error)}`);
|
|
294
428
|
}
|
|
295
429
|
}
|
|
296
430
|
async createTestRun(args) {
|
|
431
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
432
|
+
return this.createTestRunCloud(args);
|
|
433
|
+
}
|
|
434
|
+
return this.createTestRunDC(args);
|
|
435
|
+
}
|
|
436
|
+
async createTestRunCloud(args) {
|
|
437
|
+
const { project_key, name, test_case_keys, folder, planned_start_date, planned_end_date, description, owner, environment, custom_fields, } = args;
|
|
438
|
+
// Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
|
|
439
|
+
// plannedEndDate, statusName, folderId, ownerId, customFields
|
|
440
|
+
const payload = { projectKey: project_key, name };
|
|
441
|
+
if (description)
|
|
442
|
+
payload.description = description;
|
|
443
|
+
if (planned_start_date)
|
|
444
|
+
payload.plannedStartDate = planned_start_date;
|
|
445
|
+
if (planned_end_date)
|
|
446
|
+
payload.plannedEndDate = planned_end_date;
|
|
447
|
+
if (custom_fields)
|
|
448
|
+
payload.customFields = custom_fields;
|
|
449
|
+
// Resolve folder path → folderId
|
|
450
|
+
if (folder) {
|
|
451
|
+
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
|
|
452
|
+
if (folderId !== null)
|
|
453
|
+
payload.folderId = folderId;
|
|
454
|
+
}
|
|
455
|
+
// Note: environment and owner (by account ID) not mapped here — Cloud requires IDs
|
|
456
|
+
try {
|
|
457
|
+
const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testrun, payload);
|
|
458
|
+
if (response.status !== 201)
|
|
459
|
+
throw new Error(`Unexpected status code: ${response.status}`);
|
|
460
|
+
const cycleKey = response.data.key || 'Unknown';
|
|
461
|
+
// Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
|
|
462
|
+
if (test_case_keys && test_case_keys.length > 0) {
|
|
463
|
+
for (const testCaseKey of test_case_keys) {
|
|
464
|
+
await this.axiosInstance.post('/testexecutions', {
|
|
465
|
+
projectKey: project_key,
|
|
466
|
+
testCaseKey,
|
|
467
|
+
testCycleKey: cycleKey,
|
|
468
|
+
statusName: 'Not Executed',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
content: [{
|
|
474
|
+
type: 'text',
|
|
475
|
+
text: `✅ Test run (cycle) created successfully: ${cycleKey}\n${JSON.stringify({
|
|
476
|
+
key: cycleKey,
|
|
477
|
+
name,
|
|
478
|
+
testCaseCount: test_case_keys?.length || 0,
|
|
479
|
+
}, null, 2)}`,
|
|
480
|
+
}],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (error instanceof McpError)
|
|
485
|
+
throw error;
|
|
486
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async createTestRunDC(args) {
|
|
297
490
|
const { project_key, name, test_case_keys, test_plan_key, folder, planned_start_date, planned_end_date, description, owner, environment, issue_key, issue_links, custom_fields } = args;
|
|
298
|
-
|
|
299
|
-
const payload = {
|
|
300
|
-
projectKey: project_key,
|
|
301
|
-
name: name,
|
|
302
|
-
};
|
|
303
|
-
// Add optional fields
|
|
491
|
+
const payload = { projectKey: project_key, name };
|
|
304
492
|
if (test_case_keys && test_case_keys.length > 0) {
|
|
305
|
-
payload.items = test_case_keys.map((testCaseKey) => ({
|
|
306
|
-
testCaseKey: testCaseKey
|
|
307
|
-
}));
|
|
493
|
+
payload.items = test_case_keys.map((testCaseKey) => ({ testCaseKey }));
|
|
308
494
|
}
|
|
309
495
|
if (folder)
|
|
310
496
|
payload.folder = folder;
|
|
@@ -328,144 +514,129 @@ export class ZephyrToolHandlers {
|
|
|
328
514
|
payload.testPlanKey = test_plan_key;
|
|
329
515
|
try {
|
|
330
516
|
const response = await this.axiosInstance.post(this.jiraConfig.apiEndpoints.testrun, payload);
|
|
331
|
-
if (response.status
|
|
332
|
-
const testRunKey = response.data.key || 'Unknown';
|
|
333
|
-
return {
|
|
334
|
-
content: [
|
|
335
|
-
{
|
|
336
|
-
type: 'text',
|
|
337
|
-
text: `✅ Test run created successfully: ${testRunKey}\n${JSON.stringify({
|
|
338
|
-
key: testRunKey,
|
|
339
|
-
name: name,
|
|
340
|
-
testCaseCount: test_case_keys?.length || 0,
|
|
341
|
-
environment: environment || 'Not specified'
|
|
342
|
-
}, null, 2)}`,
|
|
343
|
-
},
|
|
344
|
-
],
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
517
|
+
if (response.status !== 201)
|
|
348
518
|
throw new Error(`Unexpected status code: ${response.status}`);
|
|
349
|
-
|
|
519
|
+
const testRunKey = response.data.key || 'Unknown';
|
|
520
|
+
return {
|
|
521
|
+
content: [{
|
|
522
|
+
type: 'text',
|
|
523
|
+
text: `✅ Test run created successfully: ${testRunKey}\n${JSON.stringify({
|
|
524
|
+
key: testRunKey, name, testCaseCount: test_case_keys?.length || 0,
|
|
525
|
+
environment: environment || 'Not specified'
|
|
526
|
+
}, null, 2)}`,
|
|
527
|
+
}],
|
|
528
|
+
};
|
|
350
529
|
}
|
|
351
530
|
catch (error) {
|
|
352
|
-
|
|
353
|
-
if (error instanceof Error && 'response' in error) {
|
|
354
|
-
const axiosError = error;
|
|
355
|
-
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
356
|
-
}
|
|
357
|
-
else if (error instanceof Error) {
|
|
358
|
-
errorMessage = error.message;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
errorMessage = String(error);
|
|
362
|
-
}
|
|
363
|
-
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${errorMessage}`);
|
|
531
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
|
|
364
532
|
}
|
|
365
533
|
}
|
|
366
534
|
async getTestRun(args) {
|
|
367
535
|
const { test_run_key } = args;
|
|
536
|
+
// Both Cloud (/testcycles/{key}) and DC (/rest/atm/1.0/testrun/{key}) handled
|
|
537
|
+
// via apiEndpoints.testrun which now correctly maps to /testcycles for Cloud
|
|
368
538
|
try {
|
|
369
539
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
370
540
|
return {
|
|
371
|
-
content: [
|
|
372
|
-
{
|
|
373
|
-
type: 'text',
|
|
374
|
-
text: JSON.stringify(response.data, null, 2),
|
|
375
|
-
},
|
|
376
|
-
],
|
|
541
|
+
content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
|
|
377
542
|
};
|
|
378
543
|
}
|
|
379
544
|
catch (error) {
|
|
380
|
-
|
|
381
|
-
if (error instanceof Error && 'response' in error) {
|
|
382
|
-
const axiosError = error;
|
|
383
|
-
if (axiosError.response?.status === 404) {
|
|
384
|
-
errorMessage = `Test run ${test_run_key} not found`;
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
else if (error instanceof Error) {
|
|
391
|
-
errorMessage = error.message;
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
errorMessage = String(error);
|
|
395
|
-
}
|
|
396
|
-
throw new McpError(ErrorCode.InternalError, `Failed to get test run: ${errorMessage}`);
|
|
545
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test run: ${this.formatError(error)}`);
|
|
397
546
|
}
|
|
398
547
|
}
|
|
399
548
|
async getTestExecution(args) {
|
|
400
549
|
const { execution_id, test_run_keys } = args;
|
|
401
|
-
|
|
550
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
551
|
+
// Cloud v2: direct fetch by ID or key (e.g. PROJ-E123)
|
|
552
|
+
try {
|
|
553
|
+
const response = await this.axiosInstance.get(`/testexecutions/${execution_id}`);
|
|
554
|
+
return {
|
|
555
|
+
content: [{
|
|
556
|
+
type: 'text',
|
|
557
|
+
text: `✅ Test execution ${execution_id} found:\n${JSON.stringify(response.data, null, 2)}`,
|
|
558
|
+
}],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${this.formatError(error)}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Data Center: iterate test runs searching testresults
|
|
402
566
|
if (!test_run_keys || !Array.isArray(test_run_keys) || test_run_keys.length === 0) {
|
|
403
|
-
throw new McpError(ErrorCode.InvalidParams, 'test_run_keys is required.
|
|
567
|
+
throw new McpError(ErrorCode.InvalidParams, 'test_run_keys is required for Data Center. Provide an array of test run keys to search in.');
|
|
404
568
|
}
|
|
405
569
|
try {
|
|
406
|
-
const testRunsToTry = test_run_keys;
|
|
407
570
|
const searchResults = [];
|
|
408
|
-
for (const testRunKey of
|
|
571
|
+
for (const testRunKey of test_run_keys) {
|
|
409
572
|
try {
|
|
410
573
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${testRunKey}/testresults`);
|
|
411
574
|
if (response.status === 200 && response.data) {
|
|
412
|
-
// Look for the specific execution_id in the results
|
|
413
575
|
const results = Array.isArray(response.data) ? response.data : [response.data];
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
576
|
+
const match = results.find((r) => r.id && r.id.toString() === execution_id);
|
|
577
|
+
if (match) {
|
|
416
578
|
return {
|
|
417
|
-
content: [
|
|
418
|
-
{
|
|
579
|
+
content: [{
|
|
419
580
|
type: 'text',
|
|
420
|
-
text: `✅ Test execution ${execution_id} found in ${testRunKey}:\n${JSON.stringify(
|
|
421
|
-
},
|
|
422
|
-
],
|
|
581
|
+
text: `✅ Test execution ${execution_id} found in ${testRunKey}:\n${JSON.stringify(match, null, 2)}`,
|
|
582
|
+
}],
|
|
423
583
|
};
|
|
424
584
|
}
|
|
425
|
-
|
|
426
|
-
searchResults.push({
|
|
427
|
-
testRunKey,
|
|
428
|
-
executionCount: results.length,
|
|
429
|
-
executionIds: results.map((r) => r.id).slice(0, 5) // Show first 5 IDs
|
|
430
|
-
});
|
|
585
|
+
searchResults.push({ testRunKey, executionCount: results.length, executionIds: results.map((r) => r.id).slice(0, 5) });
|
|
431
586
|
}
|
|
432
587
|
}
|
|
433
588
|
catch (runError) {
|
|
434
|
-
|
|
435
|
-
searchResults.push({
|
|
436
|
-
testRunKey,
|
|
437
|
-
error: runError instanceof Error ? runError.message : String(runError)
|
|
438
|
-
});
|
|
439
|
-
continue;
|
|
589
|
+
searchResults.push({ testRunKey, error: runError instanceof Error ? runError.message : String(runError) });
|
|
440
590
|
}
|
|
441
591
|
}
|
|
442
|
-
|
|
443
|
-
throw new Error(`Test execution ${execution_id} not found in any of the ${testRunsToTry.length} test runs searched. Search results: ${JSON.stringify(searchResults, null, 2)}`);
|
|
592
|
+
throw new Error(`Test execution ${execution_id} not found in any of the ${test_run_keys.length} test runs. Search results: ${JSON.stringify(searchResults, null, 2)}`);
|
|
444
593
|
}
|
|
445
594
|
catch (error) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
errorMessage = String(error);
|
|
452
|
-
}
|
|
453
|
-
throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${errorMessage}`);
|
|
595
|
+
if (error instanceof McpError)
|
|
596
|
+
throw error;
|
|
597
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${this.formatError(error)}`);
|
|
454
598
|
}
|
|
455
599
|
}
|
|
456
600
|
async searchTestCasesByFolder(args) {
|
|
457
601
|
const { project_key, folder_path, max_results = 100 } = args;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
602
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
603
|
+
try {
|
|
604
|
+
// Cloud v2: GET /testcases?projectKey=X&folderId=Y
|
|
605
|
+
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder_path, 'TEST_CASE');
|
|
606
|
+
if (folderId === null) {
|
|
607
|
+
return {
|
|
608
|
+
content: [{
|
|
609
|
+
type: 'text',
|
|
610
|
+
text: `⚠️ Folder not found: "${folder_path}" in project ${project_key}. No test cases returned.`,
|
|
611
|
+
}],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testcase, {
|
|
615
|
+
params: { projectKey: project_key, folderId, maxResults: max_results },
|
|
616
|
+
});
|
|
617
|
+
const testCases = Array.isArray(response.data)
|
|
618
|
+
? response.data
|
|
619
|
+
: response.data?.values ?? [];
|
|
620
|
+
return {
|
|
621
|
+
content: [{
|
|
622
|
+
type: 'text',
|
|
623
|
+
text: `✅ Found ${testCases.length} test cases in folder "${folder_path}" (folderId: ${folderId}):\n${JSON.stringify({
|
|
624
|
+
folder: folder_path, folderId, testCaseKeys: testCases.map((tc) => tc.key), totalCount: testCases.length,
|
|
625
|
+
}, null, 2)}`,
|
|
626
|
+
}],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${this.formatError(error)}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Data Center: query-based search
|
|
466
634
|
try {
|
|
467
|
-
const
|
|
468
|
-
|
|
635
|
+
const escapedFolderPath = folder_path.replace(/"/g, '\\"');
|
|
636
|
+
const query = `projectKey = "${project_key}" AND folder = "${escapedFolderPath}"`;
|
|
637
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.search, {
|
|
638
|
+
params: { query, maxResults: max_results },
|
|
639
|
+
});
|
|
469
640
|
let testCases = [];
|
|
470
641
|
if (Array.isArray(response.data)) {
|
|
471
642
|
testCases = response.data;
|
|
@@ -477,58 +648,66 @@ export class ZephyrToolHandlers {
|
|
|
477
648
|
testCases = response.data.results;
|
|
478
649
|
}
|
|
479
650
|
return {
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
651
|
+
content: [{
|
|
482
652
|
type: 'text',
|
|
483
653
|
text: `✅ Found ${testCases.length} test cases in folder "${folder_path}":\n${JSON.stringify({
|
|
484
|
-
folder: folder_path,
|
|
485
|
-
query: query,
|
|
486
|
-
testCaseKeys: testCases.map((tc) => tc.key),
|
|
487
|
-
totalCount: testCases.length
|
|
654
|
+
folder: folder_path, query, testCaseKeys: testCases.map((tc) => tc.key), totalCount: testCases.length,
|
|
488
655
|
}, null, 2)}`,
|
|
489
|
-
},
|
|
490
|
-
],
|
|
656
|
+
}],
|
|
491
657
|
};
|
|
492
658
|
}
|
|
493
659
|
catch (error) {
|
|
494
|
-
|
|
495
|
-
if (error instanceof Error && 'response' in error) {
|
|
496
|
-
const axiosError = error;
|
|
497
|
-
if (axiosError.response?.status === 404) {
|
|
498
|
-
errorMessage = `Folder "${folder_path}" not found or no test cases found`;
|
|
499
|
-
}
|
|
500
|
-
else {
|
|
501
|
-
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
else if (error instanceof Error) {
|
|
505
|
-
errorMessage = error.message;
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
errorMessage = String(error);
|
|
509
|
-
}
|
|
510
|
-
throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${errorMessage}`);
|
|
660
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${this.formatError(error)}`);
|
|
511
661
|
}
|
|
512
662
|
}
|
|
513
663
|
async searchTestRuns(args) {
|
|
514
664
|
const { project_key, folder, max_results = 200, fields } = args;
|
|
515
|
-
|
|
516
|
-
const queryParts = [];
|
|
517
|
-
if (project_key)
|
|
518
|
-
queryParts.push(`projectKey = "${project_key}"`);
|
|
519
|
-
if (folder)
|
|
520
|
-
queryParts.push(`folder = "${folder}"`);
|
|
521
|
-
if (queryParts.length === 0) {
|
|
665
|
+
if (!project_key && !folder) {
|
|
522
666
|
throw new McpError(ErrorCode.InvalidParams, 'At least one of project_key or folder must be provided.');
|
|
523
667
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
668
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
669
|
+
try {
|
|
670
|
+
// Cloud v2: GET /testcycles?projectKey=X&folderId=Y
|
|
671
|
+
const params = { maxResults: max_results };
|
|
672
|
+
if (project_key)
|
|
673
|
+
params.projectKey = project_key;
|
|
674
|
+
if (folder && project_key) {
|
|
675
|
+
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
|
|
676
|
+
if (folderId !== null)
|
|
677
|
+
params.folderId = folderId;
|
|
678
|
+
}
|
|
679
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testrun, { params });
|
|
680
|
+
const testRuns = Array.isArray(response.data)
|
|
681
|
+
? response.data
|
|
682
|
+
: response.data?.values ?? [];
|
|
683
|
+
return {
|
|
684
|
+
content: [{
|
|
685
|
+
type: 'text',
|
|
686
|
+
text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
|
|
687
|
+
totalCount: testRuns.length,
|
|
688
|
+
testRuns: testRuns.map((tr) => ({
|
|
689
|
+
key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
|
|
690
|
+
})),
|
|
691
|
+
}, null, 2)}`,
|
|
692
|
+
}],
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${this.formatError(error)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Data Center: query-based search
|
|
531
700
|
try {
|
|
701
|
+
const queryParts = [];
|
|
702
|
+
if (project_key)
|
|
703
|
+
queryParts.push(`projectKey = "${project_key}"`);
|
|
704
|
+
if (folder)
|
|
705
|
+
queryParts.push(`folder = "${folder}"`);
|
|
706
|
+
const query = queryParts.join(' AND ');
|
|
707
|
+
const searchEndpoint = '/rest/atm/1.0/testrun/search';
|
|
708
|
+
const params = { query, maxResults: max_results };
|
|
709
|
+
if (fields)
|
|
710
|
+
params.fields = fields;
|
|
532
711
|
const response = await this.axiosInstance.get(searchEndpoint, { params });
|
|
533
712
|
let testRuns = [];
|
|
534
713
|
if (Array.isArray(response.data)) {
|
|
@@ -541,62 +720,51 @@ export class ZephyrToolHandlers {
|
|
|
541
720
|
testRuns = response.data.results;
|
|
542
721
|
}
|
|
543
722
|
return {
|
|
544
|
-
content: [
|
|
545
|
-
{
|
|
723
|
+
content: [{
|
|
546
724
|
type: 'text',
|
|
547
725
|
text: `✅ Found ${testRuns.length} test run(s) matching query "${query}":\n${JSON.stringify({
|
|
548
|
-
query,
|
|
549
|
-
totalCount: testRuns.length,
|
|
726
|
+
query, totalCount: testRuns.length,
|
|
550
727
|
testRuns: testRuns.map((tr) => ({
|
|
551
|
-
key: tr.key,
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
folder: tr.folder,
|
|
555
|
-
testCaseCount: tr.testCaseCount,
|
|
556
|
-
issueKey: tr.issueKey,
|
|
557
|
-
}))
|
|
728
|
+
key: tr.key, name: tr.name, status: tr.status, folder: tr.folder,
|
|
729
|
+
testCaseCount: tr.testCaseCount, issueKey: tr.issueKey,
|
|
730
|
+
})),
|
|
558
731
|
}, null, 2)}`,
|
|
559
|
-
},
|
|
560
|
-
],
|
|
732
|
+
}],
|
|
561
733
|
};
|
|
562
734
|
}
|
|
563
735
|
catch (error) {
|
|
564
|
-
|
|
565
|
-
if (error instanceof Error && 'response' in error) {
|
|
566
|
-
const axiosError = error;
|
|
567
|
-
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
568
|
-
}
|
|
569
|
-
else if (error instanceof Error) {
|
|
570
|
-
errorMessage = error.message;
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
errorMessage = String(error);
|
|
574
|
-
}
|
|
575
|
-
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${errorMessage}`);
|
|
736
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${this.formatError(error)}`);
|
|
576
737
|
}
|
|
577
738
|
}
|
|
578
739
|
async addTestCasesToRun(args) {
|
|
579
|
-
const { test_run_key, test_case_keys } = args;
|
|
580
740
|
if (this.jiraConfig.type === 'datacenter') {
|
|
581
741
|
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.');
|
|
582
742
|
}
|
|
743
|
+
const { test_run_key, test_case_keys } = args;
|
|
744
|
+
// Derive project key from the test run key (e.g. PROJ-R123 → PROJ)
|
|
745
|
+
const project_key = test_run_key.split('-')[0];
|
|
583
746
|
try {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
};
|
|
747
|
+
for (const testCaseKey of test_case_keys) {
|
|
748
|
+
await this.axiosInstance.post('/testexecutions', {
|
|
749
|
+
projectKey: project_key,
|
|
750
|
+
testCaseKey,
|
|
751
|
+
testCycleKey: test_run_key,
|
|
752
|
+
statusName: 'Not Executed',
|
|
753
|
+
});
|
|
592
754
|
}
|
|
755
|
+
return {
|
|
756
|
+
content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
|
|
757
|
+
};
|
|
593
758
|
}
|
|
594
759
|
catch (error) {
|
|
595
|
-
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${
|
|
760
|
+
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
596
761
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
762
|
+
}
|
|
763
|
+
formatError(error) {
|
|
764
|
+
if (error instanceof Error && 'response' in error) {
|
|
765
|
+
const axiosError = error;
|
|
766
|
+
return `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
767
|
+
}
|
|
768
|
+
return error instanceof Error ? error.message : String(error);
|
|
601
769
|
}
|
|
602
770
|
}
|