zephyr-scale-mcp-server 0.3.2 → 0.3.3
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 +464 -303
- 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 +551 -364
- 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,104 @@ 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 to the cycle
|
|
462
|
+
if (test_case_keys && test_case_keys.length > 0) {
|
|
463
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/testcases`, { items: test_case_keys.map((k) => ({ testCaseKey: k })) });
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
content: [{
|
|
467
|
+
type: 'text',
|
|
468
|
+
text: `✅ Test run (cycle) created successfully: ${cycleKey}\n${JSON.stringify({
|
|
469
|
+
key: cycleKey,
|
|
470
|
+
name,
|
|
471
|
+
testCaseCount: test_case_keys?.length || 0,
|
|
472
|
+
}, null, 2)}`,
|
|
473
|
+
}],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (error instanceof McpError)
|
|
478
|
+
throw error;
|
|
479
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async createTestRunDC(args) {
|
|
297
483
|
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
|
|
484
|
+
const payload = { projectKey: project_key, name };
|
|
304
485
|
if (test_case_keys && test_case_keys.length > 0) {
|
|
305
|
-
payload.items = test_case_keys.map((testCaseKey) => ({
|
|
306
|
-
testCaseKey: testCaseKey
|
|
307
|
-
}));
|
|
486
|
+
payload.items = test_case_keys.map((testCaseKey) => ({ testCaseKey }));
|
|
308
487
|
}
|
|
309
488
|
if (folder)
|
|
310
489
|
payload.folder = folder;
|
|
@@ -328,144 +507,129 @@ export class ZephyrToolHandlers {
|
|
|
328
507
|
payload.testPlanKey = test_plan_key;
|
|
329
508
|
try {
|
|
330
509
|
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 {
|
|
510
|
+
if (response.status !== 201)
|
|
348
511
|
throw new Error(`Unexpected status code: ${response.status}`);
|
|
349
|
-
|
|
512
|
+
const testRunKey = response.data.key || 'Unknown';
|
|
513
|
+
return {
|
|
514
|
+
content: [{
|
|
515
|
+
type: 'text',
|
|
516
|
+
text: `✅ Test run created successfully: ${testRunKey}\n${JSON.stringify({
|
|
517
|
+
key: testRunKey, name, testCaseCount: test_case_keys?.length || 0,
|
|
518
|
+
environment: environment || 'Not specified'
|
|
519
|
+
}, null, 2)}`,
|
|
520
|
+
}],
|
|
521
|
+
};
|
|
350
522
|
}
|
|
351
523
|
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}`);
|
|
524
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
|
|
364
525
|
}
|
|
365
526
|
}
|
|
366
527
|
async getTestRun(args) {
|
|
367
528
|
const { test_run_key } = args;
|
|
529
|
+
// Both Cloud (/testcycles/{key}) and DC (/rest/atm/1.0/testrun/{key}) handled
|
|
530
|
+
// via apiEndpoints.testrun which now correctly maps to /testcycles for Cloud
|
|
368
531
|
try {
|
|
369
532
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
370
533
|
return {
|
|
371
|
-
content: [
|
|
372
|
-
{
|
|
373
|
-
type: 'text',
|
|
374
|
-
text: JSON.stringify(response.data, null, 2),
|
|
375
|
-
},
|
|
376
|
-
],
|
|
534
|
+
content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
|
|
377
535
|
};
|
|
378
536
|
}
|
|
379
537
|
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}`);
|
|
538
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test run: ${this.formatError(error)}`);
|
|
397
539
|
}
|
|
398
540
|
}
|
|
399
541
|
async getTestExecution(args) {
|
|
400
542
|
const { execution_id, test_run_keys } = args;
|
|
401
|
-
|
|
543
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
544
|
+
// Cloud v2: direct fetch by ID or key (e.g. PROJ-E123)
|
|
545
|
+
try {
|
|
546
|
+
const response = await this.axiosInstance.get(`/testexecutions/${execution_id}`);
|
|
547
|
+
return {
|
|
548
|
+
content: [{
|
|
549
|
+
type: 'text',
|
|
550
|
+
text: `✅ Test execution ${execution_id} found:\n${JSON.stringify(response.data, null, 2)}`,
|
|
551
|
+
}],
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${this.formatError(error)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Data Center: iterate test runs searching testresults
|
|
402
559
|
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.
|
|
560
|
+
throw new McpError(ErrorCode.InvalidParams, 'test_run_keys is required for Data Center. Provide an array of test run keys to search in.');
|
|
404
561
|
}
|
|
405
562
|
try {
|
|
406
|
-
const testRunsToTry = test_run_keys;
|
|
407
563
|
const searchResults = [];
|
|
408
|
-
for (const testRunKey of
|
|
564
|
+
for (const testRunKey of test_run_keys) {
|
|
409
565
|
try {
|
|
410
566
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${testRunKey}/testresults`);
|
|
411
567
|
if (response.status === 200 && response.data) {
|
|
412
|
-
// Look for the specific execution_id in the results
|
|
413
568
|
const results = Array.isArray(response.data) ? response.data : [response.data];
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
569
|
+
const match = results.find((r) => r.id && r.id.toString() === execution_id);
|
|
570
|
+
if (match) {
|
|
416
571
|
return {
|
|
417
|
-
content: [
|
|
418
|
-
{
|
|
572
|
+
content: [{
|
|
419
573
|
type: 'text',
|
|
420
|
-
text: `✅ Test execution ${execution_id} found in ${testRunKey}:\n${JSON.stringify(
|
|
421
|
-
},
|
|
422
|
-
],
|
|
574
|
+
text: `✅ Test execution ${execution_id} found in ${testRunKey}:\n${JSON.stringify(match, null, 2)}`,
|
|
575
|
+
}],
|
|
423
576
|
};
|
|
424
577
|
}
|
|
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
|
-
});
|
|
578
|
+
searchResults.push({ testRunKey, executionCount: results.length, executionIds: results.map((r) => r.id).slice(0, 5) });
|
|
431
579
|
}
|
|
432
580
|
}
|
|
433
581
|
catch (runError) {
|
|
434
|
-
|
|
435
|
-
searchResults.push({
|
|
436
|
-
testRunKey,
|
|
437
|
-
error: runError instanceof Error ? runError.message : String(runError)
|
|
438
|
-
});
|
|
439
|
-
continue;
|
|
582
|
+
searchResults.push({ testRunKey, error: runError instanceof Error ? runError.message : String(runError) });
|
|
440
583
|
}
|
|
441
584
|
}
|
|
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)}`);
|
|
585
|
+
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
586
|
}
|
|
445
587
|
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}`);
|
|
588
|
+
if (error instanceof McpError)
|
|
589
|
+
throw error;
|
|
590
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test execution: ${this.formatError(error)}`);
|
|
454
591
|
}
|
|
455
592
|
}
|
|
456
593
|
async searchTestCasesByFolder(args) {
|
|
457
594
|
const { project_key, folder_path, max_results = 100 } = args;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
595
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
596
|
+
try {
|
|
597
|
+
// Cloud v2: GET /testcases?projectKey=X&folderId=Y
|
|
598
|
+
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder_path, 'TEST_CASE');
|
|
599
|
+
if (folderId === null) {
|
|
600
|
+
return {
|
|
601
|
+
content: [{
|
|
602
|
+
type: 'text',
|
|
603
|
+
text: `⚠️ Folder not found: "${folder_path}" in project ${project_key}. No test cases returned.`,
|
|
604
|
+
}],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testcase, {
|
|
608
|
+
params: { projectKey: project_key, folderId, maxResults: max_results },
|
|
609
|
+
});
|
|
610
|
+
const testCases = Array.isArray(response.data)
|
|
611
|
+
? response.data
|
|
612
|
+
: response.data?.values ?? [];
|
|
613
|
+
return {
|
|
614
|
+
content: [{
|
|
615
|
+
type: 'text',
|
|
616
|
+
text: `✅ Found ${testCases.length} test cases in folder "${folder_path}" (folderId: ${folderId}):\n${JSON.stringify({
|
|
617
|
+
folder: folder_path, folderId, testCaseKeys: testCases.map((tc) => tc.key), totalCount: testCases.length,
|
|
618
|
+
}, null, 2)}`,
|
|
619
|
+
}],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${this.formatError(error)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// Data Center: query-based search
|
|
466
627
|
try {
|
|
467
|
-
const
|
|
468
|
-
|
|
628
|
+
const escapedFolderPath = folder_path.replace(/"/g, '\\"');
|
|
629
|
+
const query = `projectKey = "${project_key}" AND folder = "${escapedFolderPath}"`;
|
|
630
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.search, {
|
|
631
|
+
params: { query, maxResults: max_results },
|
|
632
|
+
});
|
|
469
633
|
let testCases = [];
|
|
470
634
|
if (Array.isArray(response.data)) {
|
|
471
635
|
testCases = response.data;
|
|
@@ -477,58 +641,66 @@ export class ZephyrToolHandlers {
|
|
|
477
641
|
testCases = response.data.results;
|
|
478
642
|
}
|
|
479
643
|
return {
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
644
|
+
content: [{
|
|
482
645
|
type: 'text',
|
|
483
646
|
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
|
|
647
|
+
folder: folder_path, query, testCaseKeys: testCases.map((tc) => tc.key), totalCount: testCases.length,
|
|
488
648
|
}, null, 2)}`,
|
|
489
|
-
},
|
|
490
|
-
],
|
|
649
|
+
}],
|
|
491
650
|
};
|
|
492
651
|
}
|
|
493
652
|
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}`);
|
|
653
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${this.formatError(error)}`);
|
|
511
654
|
}
|
|
512
655
|
}
|
|
513
656
|
async searchTestRuns(args) {
|
|
514
657
|
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) {
|
|
658
|
+
if (!project_key && !folder) {
|
|
522
659
|
throw new McpError(ErrorCode.InvalidParams, 'At least one of project_key or folder must be provided.');
|
|
523
660
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
661
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
662
|
+
try {
|
|
663
|
+
// Cloud v2: GET /testcycles?projectKey=X&folderId=Y
|
|
664
|
+
const params = { maxResults: max_results };
|
|
665
|
+
if (project_key)
|
|
666
|
+
params.projectKey = project_key;
|
|
667
|
+
if (folder && project_key) {
|
|
668
|
+
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
|
|
669
|
+
if (folderId !== null)
|
|
670
|
+
params.folderId = folderId;
|
|
671
|
+
}
|
|
672
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testrun, { params });
|
|
673
|
+
const testRuns = Array.isArray(response.data)
|
|
674
|
+
? response.data
|
|
675
|
+
: response.data?.values ?? [];
|
|
676
|
+
return {
|
|
677
|
+
content: [{
|
|
678
|
+
type: 'text',
|
|
679
|
+
text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
|
|
680
|
+
totalCount: testRuns.length,
|
|
681
|
+
testRuns: testRuns.map((tr) => ({
|
|
682
|
+
key: tr.key, name: tr.name, status: tr.status?.name, folder: tr.folder?.name,
|
|
683
|
+
})),
|
|
684
|
+
}, null, 2)}`,
|
|
685
|
+
}],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${this.formatError(error)}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// Data Center: query-based search
|
|
531
693
|
try {
|
|
694
|
+
const queryParts = [];
|
|
695
|
+
if (project_key)
|
|
696
|
+
queryParts.push(`projectKey = "${project_key}"`);
|
|
697
|
+
if (folder)
|
|
698
|
+
queryParts.push(`folder = "${folder}"`);
|
|
699
|
+
const query = queryParts.join(' AND ');
|
|
700
|
+
const searchEndpoint = '/rest/atm/1.0/testrun/search';
|
|
701
|
+
const params = { query, maxResults: max_results };
|
|
702
|
+
if (fields)
|
|
703
|
+
params.fields = fields;
|
|
532
704
|
const response = await this.axiosInstance.get(searchEndpoint, { params });
|
|
533
705
|
let testRuns = [];
|
|
534
706
|
if (Array.isArray(response.data)) {
|
|
@@ -541,50 +713,32 @@ export class ZephyrToolHandlers {
|
|
|
541
713
|
testRuns = response.data.results;
|
|
542
714
|
}
|
|
543
715
|
return {
|
|
544
|
-
content: [
|
|
545
|
-
{
|
|
716
|
+
content: [{
|
|
546
717
|
type: 'text',
|
|
547
718
|
text: `✅ Found ${testRuns.length} test run(s) matching query "${query}":\n${JSON.stringify({
|
|
548
|
-
query,
|
|
549
|
-
totalCount: testRuns.length,
|
|
719
|
+
query, totalCount: testRuns.length,
|
|
550
720
|
testRuns: testRuns.map((tr) => ({
|
|
551
|
-
key: tr.key,
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
folder: tr.folder,
|
|
555
|
-
testCaseCount: tr.testCaseCount,
|
|
556
|
-
issueKey: tr.issueKey,
|
|
557
|
-
}))
|
|
721
|
+
key: tr.key, name: tr.name, status: tr.status, folder: tr.folder,
|
|
722
|
+
testCaseCount: tr.testCaseCount, issueKey: tr.issueKey,
|
|
723
|
+
})),
|
|
558
724
|
}, null, 2)}`,
|
|
559
|
-
},
|
|
560
|
-
],
|
|
725
|
+
}],
|
|
561
726
|
};
|
|
562
727
|
}
|
|
563
728
|
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}`);
|
|
729
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${this.formatError(error)}`);
|
|
576
730
|
}
|
|
577
731
|
}
|
|
578
732
|
async addTestCasesToRun(args) {
|
|
579
|
-
const { test_run_key, test_case_keys } = args;
|
|
580
733
|
if (this.jiraConfig.type === 'datacenter') {
|
|
581
734
|
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
735
|
}
|
|
736
|
+
const { test_run_key, test_case_keys } = args;
|
|
583
737
|
try {
|
|
584
738
|
const payload = {
|
|
585
739
|
items: test_case_keys.map(key => ({ testCaseKey: key }))
|
|
586
740
|
};
|
|
587
|
-
const response = await this.axiosInstance.post(
|
|
741
|
+
const response = await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`, payload);
|
|
588
742
|
if (response.status === 200 || response.status === 201 || response.status === 204) {
|
|
589
743
|
return {
|
|
590
744
|
content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
|
|
@@ -592,11 +746,18 @@ export class ZephyrToolHandlers {
|
|
|
592
746
|
}
|
|
593
747
|
}
|
|
594
748
|
catch (error) {
|
|
595
|
-
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${
|
|
749
|
+
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
596
750
|
}
|
|
597
751
|
return {
|
|
598
752
|
content: [{ type: 'text', text: 'An unexpected error occurred.' }],
|
|
599
753
|
isError: true,
|
|
600
754
|
};
|
|
601
755
|
}
|
|
756
|
+
formatError(error) {
|
|
757
|
+
if (error instanceof Error && 'response' in error) {
|
|
758
|
+
const axiosError = error;
|
|
759
|
+
return `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
760
|
+
}
|
|
761
|
+
return error instanceof Error ? error.message : String(error);
|
|
762
|
+
}
|
|
602
763
|
}
|