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