zephyr-scale-mcp-server 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/build/index.js +4 -0
- package/build/tool-handlers.js +165 -0
- package/build/tool-schemas.js +77 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/tool-handlers.ts +195 -0
- package/src/tool-schemas.ts +77 -0
- package/src/types.ts +28 -0
package/README.md
CHANGED
|
@@ -115,11 +115,14 @@ The server provides access to various resources through URI schemes:
|
|
|
115
115
|
### Test Execution & Search
|
|
116
116
|
- `get_test_execution`: Get detailed individual test execution results.
|
|
117
117
|
- `list_executions_by_cycle`: List all test executions for a specific test cycle with status, executor, and date. *(Cloud only)*
|
|
118
|
-
- `
|
|
118
|
+
- `update_test_execution`: Update a test case execution's status within a cycle (Pass/Fail/etc.), add a comment, and attach bug(s) as Jira issue links. Identify the execution by `execution_id`, or by `test_cycle_key` + `test_case_key`. *(Cloud only)*
|
|
119
|
+
- `search_test_cases_by_folder`: Search for test cases in a specific folder. Automatically paginates through all results.
|
|
119
120
|
- `search_test_runs`: Search for test runs by project key and/or folder path.
|
|
121
|
+
- `get_test_cycles_for_issue`: Get the Zephyr test cycles linked to a Jira issue (story/epic). Resolves each cycle ID to its key (e.g. `PROJ-R123`) and name so you can feed it straight into `list_executions_by_cycle` / `update_test_execution`. *(Cloud only)*
|
|
120
122
|
|
|
121
123
|
### Organization
|
|
122
124
|
- `create_folder`: Create a new folder in Zephyr Scale.
|
|
125
|
+
- `get_folders`: List folders, optionally filtered by project, type, and path. When `folder_path` is given, returns the matching folder and its full subtree at every depth.
|
|
123
126
|
|
|
124
127
|
## Usage Examples
|
|
125
128
|
|
|
@@ -161,6 +164,33 @@ The server provides access to various resources through URI schemes:
|
|
|
161
164
|
```
|
|
162
165
|
**Note**: The server will convert markdown-style BDD into Gherkin when possible and will preserve all other existing test case fields.
|
|
163
166
|
|
|
167
|
+
### Mark an Execution as Failed and Attach a Bug
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"test_cycle_key": "PROJ-R123",
|
|
171
|
+
"test_case_key": "PROJ-T456",
|
|
172
|
+
"status": "Fail",
|
|
173
|
+
"comment": "Login button unresponsive on submit.",
|
|
174
|
+
"bug_keys": ["PROJ-789"]
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
Or target an execution directly by key:
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"execution_id": "PROJ-E123",
|
|
181
|
+
"status": "Pass"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
**Note**: `update_test_execution` is Cloud only. `bug_keys` requires `JIRA_USERNAME` and `JIRA_API_TOKEN`; link failures are reported as warnings while the status update still succeeds.
|
|
185
|
+
|
|
186
|
+
### Find the Test Cycle Linked to a Jira Ticket
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"issue_key": "PROJ-6752"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
Returns the linked cycles with resolved keys, e.g. `[{ "id": "110702963", "key": "PROJ-R467", "name": "..." }]`. This is the bridge from a Jira ticket to its Zephyr cycle — the association is stored on the Zephyr side, not in Jira's issue fields. Chain it: `get_test_cycles_for_issue` → `list_executions_by_cycle` → `update_test_execution`. Pass `"resolve_keys": false` to skip the per-cycle key/name lookup and return raw IDs only. **Cloud only.**
|
|
193
|
+
|
|
164
194
|
## Authentication
|
|
165
195
|
|
|
166
196
|
### Jira Cloud Configuration
|
package/build/index.js
CHANGED
|
@@ -75,6 +75,10 @@ class ZephyrServer {
|
|
|
75
75
|
return await this.toolHandlers.addTestCasesToRun(args);
|
|
76
76
|
case 'list_executions_by_cycle':
|
|
77
77
|
return await this.toolHandlers.listExecutionsByCycle(args);
|
|
78
|
+
case 'update_test_execution':
|
|
79
|
+
return await this.toolHandlers.updateTestExecution(args);
|
|
80
|
+
case 'get_test_cycles_for_issue':
|
|
81
|
+
return await this.toolHandlers.getTestCyclesForIssue(args);
|
|
78
82
|
default:
|
|
79
83
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
80
84
|
}
|
package/build/tool-handlers.js
CHANGED
|
@@ -1151,6 +1151,171 @@ export class ZephyrToolHandlers {
|
|
|
1151
1151
|
throw new McpError(ErrorCode.InternalError, `Failed to list executions: ${this.formatError(error)}`);
|
|
1152
1152
|
}
|
|
1153
1153
|
}
|
|
1154
|
+
async updateTestExecution(args) {
|
|
1155
|
+
if (this.jiraConfig.type !== 'cloud') {
|
|
1156
|
+
throw new McpError(ErrorCode.InvalidRequest, 'update_test_execution is only supported on Zephyr Scale Cloud. The Data Center API (v1) uses a different test-result model.');
|
|
1157
|
+
}
|
|
1158
|
+
const { execution_id, test_cycle_key, test_case_key, project_key, status, comment, environment, execution_time, actual_end_date, executed_by_id, assigned_to_id, bug_keys, } = args;
|
|
1159
|
+
// Resolve which execution to update: explicit execution_id wins, otherwise cycle + test case.
|
|
1160
|
+
let execKey = execution_id;
|
|
1161
|
+
if (!execKey) {
|
|
1162
|
+
if (!test_cycle_key || !test_case_key) {
|
|
1163
|
+
throw new McpError(ErrorCode.InvalidParams, 'Provide either execution_id, or both test_cycle_key and test_case_key to identify the execution.');
|
|
1164
|
+
}
|
|
1165
|
+
execKey = await this.resolveExecutionByCycleAndCase(test_cycle_key, test_case_key, project_key);
|
|
1166
|
+
}
|
|
1167
|
+
// Build the update payload — the PUT ignores null/undefined and only touches provided fields.
|
|
1168
|
+
const payload = {};
|
|
1169
|
+
if (status !== undefined)
|
|
1170
|
+
payload.statusName = status;
|
|
1171
|
+
if (comment !== undefined)
|
|
1172
|
+
payload.comment = comment;
|
|
1173
|
+
if (environment !== undefined)
|
|
1174
|
+
payload.environmentName = environment;
|
|
1175
|
+
if (execution_time !== undefined)
|
|
1176
|
+
payload.executionTime = execution_time;
|
|
1177
|
+
if (actual_end_date !== undefined)
|
|
1178
|
+
payload.actualEndDate = actual_end_date;
|
|
1179
|
+
if (executed_by_id !== undefined)
|
|
1180
|
+
payload.executedById = executed_by_id;
|
|
1181
|
+
if (assigned_to_id !== undefined)
|
|
1182
|
+
payload.assignedToId = assigned_to_id;
|
|
1183
|
+
const hasBugs = Array.isArray(bug_keys) && bug_keys.length > 0;
|
|
1184
|
+
if (Object.keys(payload).length === 0 && !hasBugs) {
|
|
1185
|
+
throw new McpError(ErrorCode.InvalidParams, 'Nothing to update — provide at least one of: status, comment, environment, execution_time, actual_end_date, executed_by_id, assigned_to_id, or bug_keys.');
|
|
1186
|
+
}
|
|
1187
|
+
try {
|
|
1188
|
+
if (Object.keys(payload).length > 0) {
|
|
1189
|
+
await this.axiosInstance.put(`/testexecutions/${execKey}`, payload);
|
|
1190
|
+
}
|
|
1191
|
+
// Attach bugs / Jira issues. IssueLinkInput requires a numeric issueId, so resolve each
|
|
1192
|
+
// key via the Jira REST API (needs JIRA_USERNAME + JIRA_API_TOKEN on Cloud), mirroring create_test_case.
|
|
1193
|
+
const linkWarnings = [];
|
|
1194
|
+
let linkedCount = 0;
|
|
1195
|
+
if (hasBugs) {
|
|
1196
|
+
for (const bugKey of bug_keys) {
|
|
1197
|
+
try {
|
|
1198
|
+
const issueId = await this.resolveJiraIssueId(bugKey);
|
|
1199
|
+
await this.axiosInstance.post(`/testexecutions/${execKey}/links/issues`, { issueId });
|
|
1200
|
+
linkedCount++;
|
|
1201
|
+
}
|
|
1202
|
+
catch (e) {
|
|
1203
|
+
linkWarnings.push(`${bugKey}: ${this.formatError(e)}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
|
|
1208
|
+
const credHint = missingCreds && linkWarnings.length > 0
|
|
1209
|
+
? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable bug/issue linking on Cloud.'
|
|
1210
|
+
: '';
|
|
1211
|
+
const warningText = linkWarnings.length > 0
|
|
1212
|
+
? `\n⚠️ Some bug links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
|
|
1213
|
+
: '';
|
|
1214
|
+
return {
|
|
1215
|
+
content: [{
|
|
1216
|
+
type: 'text',
|
|
1217
|
+
text: `✅ Updated test execution ${execKey} successfully.\n${JSON.stringify({
|
|
1218
|
+
executionKey: execKey,
|
|
1219
|
+
status: status ?? '(unchanged)',
|
|
1220
|
+
updatedFields: Object.keys(payload),
|
|
1221
|
+
linkedBugs: linkedCount,
|
|
1222
|
+
}, null, 2)}${warningText}`,
|
|
1223
|
+
}],
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
catch (error) {
|
|
1227
|
+
if (error instanceof McpError)
|
|
1228
|
+
throw error;
|
|
1229
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update test execution: ${this.formatError(error)}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async getTestCyclesForIssue(args) {
|
|
1233
|
+
if (this.jiraConfig.type !== 'cloud') {
|
|
1234
|
+
throw new McpError(ErrorCode.InvalidRequest, 'get_test_cycles_for_issue is only supported on Zephyr Scale Cloud. The Data Center API (v1) does not expose issue-link lookups.');
|
|
1235
|
+
}
|
|
1236
|
+
const { issue_key, resolve_keys = true } = args;
|
|
1237
|
+
if (!issue_key) {
|
|
1238
|
+
throw new McpError(ErrorCode.InvalidParams, 'issue_key is required (e.g. "PROJ-123").');
|
|
1239
|
+
}
|
|
1240
|
+
try {
|
|
1241
|
+
// GET /issuelinks/{issueKey}/testcycles → TestCycleIdList: [{ id, self }, ...]
|
|
1242
|
+
const response = await this.axiosInstance.get(`/issuelinks/${issue_key}/testcycles`);
|
|
1243
|
+
const raw = Array.isArray(response.data)
|
|
1244
|
+
? response.data
|
|
1245
|
+
: response.data?.values ?? [];
|
|
1246
|
+
const cycleIds = raw
|
|
1247
|
+
.map((c) => c.id ?? c.self?.match(/testcycles\/(\d+)/)?.[1])
|
|
1248
|
+
.filter((id) => id !== undefined && id !== null)
|
|
1249
|
+
.map((id) => String(id));
|
|
1250
|
+
// Optionally resolve each numeric cycle ID to its human-readable key + name.
|
|
1251
|
+
let cycles;
|
|
1252
|
+
if (resolve_keys) {
|
|
1253
|
+
cycles = [];
|
|
1254
|
+
for (const id of cycleIds) {
|
|
1255
|
+
try {
|
|
1256
|
+
const cyc = await this.axiosInstance.get(`/testcycles/${id}`);
|
|
1257
|
+
cycles.push({ id, key: cyc.data?.key ?? null, name: cyc.data?.name ?? null });
|
|
1258
|
+
}
|
|
1259
|
+
catch (e) {
|
|
1260
|
+
cycles.push({ id, key: null, name: null, error: this.formatError(e) });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
cycles = cycleIds.map((id) => ({ id }));
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
content: [{
|
|
1269
|
+
type: 'text',
|
|
1270
|
+
text: `✅ Found ${cycles.length} test cycle(s) linked to ${issue_key}:\n${JSON.stringify({
|
|
1271
|
+
issueKey: issue_key,
|
|
1272
|
+
totalCount: cycles.length,
|
|
1273
|
+
testCycles: cycles,
|
|
1274
|
+
}, null, 2)}`,
|
|
1275
|
+
}],
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
if (error instanceof McpError)
|
|
1280
|
+
throw error;
|
|
1281
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test cycles for issue: ${this.formatError(error)}`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/** Find the latest execution key for a test case within a cycle (Cloud). */
|
|
1285
|
+
async resolveExecutionByCycleAndCase(cycleKey, caseKey, projectKey) {
|
|
1286
|
+
const derivedProject = projectKey || cycleKey.split('-')[0];
|
|
1287
|
+
try {
|
|
1288
|
+
const response = await this.axiosInstance.get('/testexecutions', {
|
|
1289
|
+
params: {
|
|
1290
|
+
projectKey: derivedProject,
|
|
1291
|
+
testCycle: cycleKey,
|
|
1292
|
+
onlyLastExecutions: true,
|
|
1293
|
+
maxResults: 1000,
|
|
1294
|
+
},
|
|
1295
|
+
});
|
|
1296
|
+
const executions = Array.isArray(response.data)
|
|
1297
|
+
? response.data
|
|
1298
|
+
: response.data?.values ?? [];
|
|
1299
|
+
const match = executions.find((ex) => {
|
|
1300
|
+
const key = ex.testCase?.self?.match(/testcases\/(.+?)\/versions/)?.[1]
|
|
1301
|
+
?? ex.testCase?.key;
|
|
1302
|
+
return key === caseKey;
|
|
1303
|
+
});
|
|
1304
|
+
if (!match) {
|
|
1305
|
+
throw new McpError(ErrorCode.InvalidParams, `No execution found for test case ${caseKey} in cycle ${cycleKey}. Ensure the test case is part of the cycle.`);
|
|
1306
|
+
}
|
|
1307
|
+
const execKey = match.key ?? (match.id !== undefined ? String(match.id) : undefined);
|
|
1308
|
+
if (!execKey) {
|
|
1309
|
+
throw new McpError(ErrorCode.InternalError, `Found a matching execution for ${caseKey} in ${cycleKey} but it has no key or id.`);
|
|
1310
|
+
}
|
|
1311
|
+
return execKey;
|
|
1312
|
+
}
|
|
1313
|
+
catch (error) {
|
|
1314
|
+
if (error instanceof McpError)
|
|
1315
|
+
throw error;
|
|
1316
|
+
throw new McpError(ErrorCode.InternalError, `Failed to resolve execution for ${caseKey} in ${cycleKey}: ${this.formatError(error)}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1154
1319
|
async resolveJiraIssueId(issueKey) {
|
|
1155
1320
|
// The Zephyr API key is NOT valid for the Jira REST API — Jira Cloud requires
|
|
1156
1321
|
// Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
|
package/build/tool-schemas.js
CHANGED
|
@@ -466,6 +466,83 @@ export const toolSchemas = [
|
|
|
466
466
|
required: ['test_run_key'],
|
|
467
467
|
},
|
|
468
468
|
},
|
|
469
|
+
{
|
|
470
|
+
name: 'update_test_execution',
|
|
471
|
+
description: 'Update a test case execution\'s status within a test cycle, and optionally attach bug(s) as Jira issue links. Identify the execution either by execution_id directly, or by test_cycle_key + test_case_key (the latest execution is used). Cloud only.',
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
execution_id: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'Test execution key or ID (e.g. "PROJ-E123" or 5805255). Takes precedence over test_cycle_key/test_case_key.',
|
|
478
|
+
},
|
|
479
|
+
test_cycle_key: {
|
|
480
|
+
type: 'string',
|
|
481
|
+
description: 'Test cycle key (e.g. "PROJ-R123"). Used together with test_case_key to locate the execution when execution_id is not given.',
|
|
482
|
+
},
|
|
483
|
+
test_case_key: {
|
|
484
|
+
type: 'string',
|
|
485
|
+
description: 'Test case key (e.g. "PROJ-T456"). Used together with test_cycle_key to locate the execution.',
|
|
486
|
+
},
|
|
487
|
+
project_key: {
|
|
488
|
+
type: 'string',
|
|
489
|
+
description: 'Project key for the cycle lookup (optional — derived from test_cycle_key when omitted).',
|
|
490
|
+
},
|
|
491
|
+
status: {
|
|
492
|
+
type: 'string',
|
|
493
|
+
description: 'New execution status name. Common values: "Pass", "Fail", "In Progress", "Blocked", "Not Executed". Must match a status configured in your Zephyr project.',
|
|
494
|
+
},
|
|
495
|
+
comment: {
|
|
496
|
+
type: 'string',
|
|
497
|
+
description: 'Comment against the overall execution (e.g. failure details).',
|
|
498
|
+
},
|
|
499
|
+
environment: {
|
|
500
|
+
type: 'string',
|
|
501
|
+
description: 'Environment name assigned to the execution (e.g. "Chrome Latest Version").',
|
|
502
|
+
},
|
|
503
|
+
execution_time: {
|
|
504
|
+
type: 'number',
|
|
505
|
+
description: 'Actual execution time in milliseconds (optional).',
|
|
506
|
+
},
|
|
507
|
+
actual_end_date: {
|
|
508
|
+
type: 'string',
|
|
509
|
+
description: 'Actual end date in ISO format, e.g. "2024-05-20T13:15:13Z" (optional).',
|
|
510
|
+
},
|
|
511
|
+
executed_by_id: {
|
|
512
|
+
type: 'string',
|
|
513
|
+
description: 'Jira Account ID of the user who executed the test (optional).',
|
|
514
|
+
},
|
|
515
|
+
assigned_to_id: {
|
|
516
|
+
type: 'string',
|
|
517
|
+
description: 'Jira Account ID of the user the execution is assigned to (optional).',
|
|
518
|
+
},
|
|
519
|
+
bug_keys: {
|
|
520
|
+
type: 'array',
|
|
521
|
+
description: 'Jira issue keys to attach to the execution as bugs (e.g. ["PROJ-789"]). Each key is resolved to a numeric ID via the Jira REST API (requires JIRA_USERNAME + JIRA_API_TOKEN) and linked via POST /testexecutions/{key}/links/issues. Failures are reported as warnings and do not fail the call.',
|
|
522
|
+
items: { type: 'string' },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'get_test_cycles_for_issue',
|
|
529
|
+
description: 'Get the Zephyr test cycles linked to a Jira issue (e.g. a story or epic). Calls GET /issuelinks/{issueKey}/testcycles and, by default, resolves each numeric cycle ID to its key (e.g. "PROJ-R123") and name. Use this to discover the test cycle referenced by a Jira ticket, then feed the key into list_executions_by_cycle / update_test_execution. Cloud only.',
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
properties: {
|
|
533
|
+
issue_key: {
|
|
534
|
+
type: 'string',
|
|
535
|
+
description: 'Jira issue key whose linked test cycles to fetch (e.g. "PROJ-123").',
|
|
536
|
+
},
|
|
537
|
+
resolve_keys: {
|
|
538
|
+
type: 'boolean',
|
|
539
|
+
description: 'When true (default), resolve each cycle ID to its key + name via GET /testcycles/{id}. Set false to return raw numeric IDs only (faster, no extra calls).',
|
|
540
|
+
default: true,
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
required: ['issue_key'],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
469
546
|
{
|
|
470
547
|
name: 'delete_test_run',
|
|
471
548
|
description: 'Delete a specific test run (Data Center only — not supported on Cloud v2)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Zephyr Scale test case management with comprehensive STEP_BY_STEP, PLAIN_TEXT, and BDD support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
package/src/index.ts
CHANGED
|
@@ -95,6 +95,10 @@ class ZephyrServer {
|
|
|
95
95
|
return await this.toolHandlers.addTestCasesToRun(args as any);
|
|
96
96
|
case 'list_executions_by_cycle':
|
|
97
97
|
return await this.toolHandlers.listExecutionsByCycle(args as any);
|
|
98
|
+
case 'update_test_execution':
|
|
99
|
+
return await this.toolHandlers.updateTestExecution(args as any);
|
|
100
|
+
case 'get_test_cycles_for_issue':
|
|
101
|
+
return await this.toolHandlers.getTestCyclesForIssue(args as any);
|
|
98
102
|
default:
|
|
99
103
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
100
104
|
}
|
package/src/tool-handlers.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
SearchTestRunsArgs,
|
|
12
12
|
GetTestExecutionArgs,
|
|
13
13
|
ListExecutionsByCycleArgs,
|
|
14
|
+
UpdateTestExecutionArgs,
|
|
15
|
+
GetTestCyclesForIssueArgs,
|
|
14
16
|
JiraConfig
|
|
15
17
|
} from './types.js';
|
|
16
18
|
import { convertToGherkin, resolveFolderIdByPath, getAccountIdFromApiKey } from './utils.js';
|
|
@@ -1259,6 +1261,199 @@ export class ZephyrToolHandlers {
|
|
|
1259
1261
|
}
|
|
1260
1262
|
}
|
|
1261
1263
|
|
|
1264
|
+
async updateTestExecution(args: UpdateTestExecutionArgs) {
|
|
1265
|
+
if (this.jiraConfig.type !== 'cloud') {
|
|
1266
|
+
throw new McpError(
|
|
1267
|
+
ErrorCode.InvalidRequest,
|
|
1268
|
+
'update_test_execution is only supported on Zephyr Scale Cloud. The Data Center API (v1) uses a different test-result model.'
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const {
|
|
1273
|
+
execution_id, test_cycle_key, test_case_key, project_key,
|
|
1274
|
+
status, comment, environment, execution_time, actual_end_date,
|
|
1275
|
+
executed_by_id, assigned_to_id, bug_keys,
|
|
1276
|
+
} = args;
|
|
1277
|
+
|
|
1278
|
+
// Resolve which execution to update: explicit execution_id wins, otherwise cycle + test case.
|
|
1279
|
+
let execKey = execution_id;
|
|
1280
|
+
if (!execKey) {
|
|
1281
|
+
if (!test_cycle_key || !test_case_key) {
|
|
1282
|
+
throw new McpError(
|
|
1283
|
+
ErrorCode.InvalidParams,
|
|
1284
|
+
'Provide either execution_id, or both test_cycle_key and test_case_key to identify the execution.'
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
execKey = await this.resolveExecutionByCycleAndCase(test_cycle_key, test_case_key, project_key);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Build the update payload — the PUT ignores null/undefined and only touches provided fields.
|
|
1291
|
+
const payload: any = {};
|
|
1292
|
+
if (status !== undefined) payload.statusName = status;
|
|
1293
|
+
if (comment !== undefined) payload.comment = comment;
|
|
1294
|
+
if (environment !== undefined) payload.environmentName = environment;
|
|
1295
|
+
if (execution_time !== undefined) payload.executionTime = execution_time;
|
|
1296
|
+
if (actual_end_date !== undefined) payload.actualEndDate = actual_end_date;
|
|
1297
|
+
if (executed_by_id !== undefined) payload.executedById = executed_by_id;
|
|
1298
|
+
if (assigned_to_id !== undefined) payload.assignedToId = assigned_to_id;
|
|
1299
|
+
|
|
1300
|
+
const hasBugs = Array.isArray(bug_keys) && bug_keys.length > 0;
|
|
1301
|
+
if (Object.keys(payload).length === 0 && !hasBugs) {
|
|
1302
|
+
throw new McpError(
|
|
1303
|
+
ErrorCode.InvalidParams,
|
|
1304
|
+
'Nothing to update — provide at least one of: status, comment, environment, execution_time, actual_end_date, executed_by_id, assigned_to_id, or bug_keys.'
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
try {
|
|
1309
|
+
if (Object.keys(payload).length > 0) {
|
|
1310
|
+
await this.axiosInstance.put(`/testexecutions/${execKey}`, payload);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Attach bugs / Jira issues. IssueLinkInput requires a numeric issueId, so resolve each
|
|
1314
|
+
// key via the Jira REST API (needs JIRA_USERNAME + JIRA_API_TOKEN on Cloud), mirroring create_test_case.
|
|
1315
|
+
const linkWarnings: string[] = [];
|
|
1316
|
+
let linkedCount = 0;
|
|
1317
|
+
if (hasBugs) {
|
|
1318
|
+
for (const bugKey of bug_keys!) {
|
|
1319
|
+
try {
|
|
1320
|
+
const issueId = await this.resolveJiraIssueId(bugKey);
|
|
1321
|
+
await this.axiosInstance.post(`/testexecutions/${execKey}/links/issues`, { issueId });
|
|
1322
|
+
linkedCount++;
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
linkWarnings.push(`${bugKey}: ${this.formatError(e)}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
|
|
1330
|
+
const credHint = missingCreds && linkWarnings.length > 0
|
|
1331
|
+
? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable bug/issue linking on Cloud.'
|
|
1332
|
+
: '';
|
|
1333
|
+
const warningText = linkWarnings.length > 0
|
|
1334
|
+
? `\n⚠️ Some bug links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
|
|
1335
|
+
: '';
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
content: [{
|
|
1339
|
+
type: 'text',
|
|
1340
|
+
text: `✅ Updated test execution ${execKey} successfully.\n${JSON.stringify({
|
|
1341
|
+
executionKey: execKey,
|
|
1342
|
+
status: status ?? '(unchanged)',
|
|
1343
|
+
updatedFields: Object.keys(payload),
|
|
1344
|
+
linkedBugs: linkedCount,
|
|
1345
|
+
}, null, 2)}${warningText}`,
|
|
1346
|
+
}],
|
|
1347
|
+
};
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
if (error instanceof McpError) throw error;
|
|
1350
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update test execution: ${this.formatError(error)}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async getTestCyclesForIssue(args: GetTestCyclesForIssueArgs) {
|
|
1355
|
+
if (this.jiraConfig.type !== 'cloud') {
|
|
1356
|
+
throw new McpError(
|
|
1357
|
+
ErrorCode.InvalidRequest,
|
|
1358
|
+
'get_test_cycles_for_issue is only supported on Zephyr Scale Cloud. The Data Center API (v1) does not expose issue-link lookups.'
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const { issue_key, resolve_keys = true } = args;
|
|
1363
|
+
if (!issue_key) {
|
|
1364
|
+
throw new McpError(ErrorCode.InvalidParams, 'issue_key is required (e.g. "PROJ-123").');
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
try {
|
|
1368
|
+
// GET /issuelinks/{issueKey}/testcycles → TestCycleIdList: [{ id, self }, ...]
|
|
1369
|
+
const response = await this.axiosInstance.get(`/issuelinks/${issue_key}/testcycles`);
|
|
1370
|
+
const raw = Array.isArray(response.data)
|
|
1371
|
+
? response.data
|
|
1372
|
+
: response.data?.values ?? [];
|
|
1373
|
+
|
|
1374
|
+
const cycleIds: string[] = raw
|
|
1375
|
+
.map((c: any) => c.id ?? c.self?.match(/testcycles\/(\d+)/)?.[1])
|
|
1376
|
+
.filter((id: any) => id !== undefined && id !== null)
|
|
1377
|
+
.map((id: any) => String(id));
|
|
1378
|
+
|
|
1379
|
+
// Optionally resolve each numeric cycle ID to its human-readable key + name.
|
|
1380
|
+
let cycles: any[];
|
|
1381
|
+
if (resolve_keys) {
|
|
1382
|
+
cycles = [];
|
|
1383
|
+
for (const id of cycleIds) {
|
|
1384
|
+
try {
|
|
1385
|
+
const cyc = await this.axiosInstance.get(`/testcycles/${id}`);
|
|
1386
|
+
cycles.push({ id, key: cyc.data?.key ?? null, name: cyc.data?.name ?? null });
|
|
1387
|
+
} catch (e) {
|
|
1388
|
+
cycles.push({ id, key: null, name: null, error: this.formatError(e) });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
} else {
|
|
1392
|
+
cycles = cycleIds.map((id) => ({ id }));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return {
|
|
1396
|
+
content: [{
|
|
1397
|
+
type: 'text',
|
|
1398
|
+
text: `✅ Found ${cycles.length} test cycle(s) linked to ${issue_key}:\n${JSON.stringify({
|
|
1399
|
+
issueKey: issue_key,
|
|
1400
|
+
totalCount: cycles.length,
|
|
1401
|
+
testCycles: cycles,
|
|
1402
|
+
}, null, 2)}`,
|
|
1403
|
+
}],
|
|
1404
|
+
};
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (error instanceof McpError) throw error;
|
|
1407
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get test cycles for issue: ${this.formatError(error)}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/** Find the latest execution key for a test case within a cycle (Cloud). */
|
|
1412
|
+
private async resolveExecutionByCycleAndCase(
|
|
1413
|
+
cycleKey: string, caseKey: string, projectKey?: string
|
|
1414
|
+
): Promise<string> {
|
|
1415
|
+
const derivedProject = projectKey || cycleKey.split('-')[0];
|
|
1416
|
+
try {
|
|
1417
|
+
const response = await this.axiosInstance.get('/testexecutions', {
|
|
1418
|
+
params: {
|
|
1419
|
+
projectKey: derivedProject,
|
|
1420
|
+
testCycle: cycleKey,
|
|
1421
|
+
onlyLastExecutions: true,
|
|
1422
|
+
maxResults: 1000,
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
const executions = Array.isArray(response.data)
|
|
1427
|
+
? response.data
|
|
1428
|
+
: response.data?.values ?? [];
|
|
1429
|
+
|
|
1430
|
+
const match = executions.find((ex: any) => {
|
|
1431
|
+
const key = ex.testCase?.self?.match(/testcases\/(.+?)\/versions/)?.[1]
|
|
1432
|
+
?? ex.testCase?.key;
|
|
1433
|
+
return key === caseKey;
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
if (!match) {
|
|
1437
|
+
throw new McpError(
|
|
1438
|
+
ErrorCode.InvalidParams,
|
|
1439
|
+
`No execution found for test case ${caseKey} in cycle ${cycleKey}. Ensure the test case is part of the cycle.`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const execKey = match.key ?? (match.id !== undefined ? String(match.id) : undefined);
|
|
1444
|
+
if (!execKey) {
|
|
1445
|
+
throw new McpError(
|
|
1446
|
+
ErrorCode.InternalError,
|
|
1447
|
+
`Found a matching execution for ${caseKey} in ${cycleKey} but it has no key or id.`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
return execKey;
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (error instanceof McpError) throw error;
|
|
1453
|
+
throw new McpError(ErrorCode.InternalError, `Failed to resolve execution for ${caseKey} in ${cycleKey}: ${this.formatError(error)}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1262
1457
|
private async resolveJiraIssueId(issueKey: string): Promise<number> {
|
|
1263
1458
|
// The Zephyr API key is NOT valid for the Jira REST API — Jira Cloud requires
|
|
1264
1459
|
// Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
|
package/src/tool-schemas.ts
CHANGED
|
@@ -466,6 +466,83 @@ export const toolSchemas = [
|
|
|
466
466
|
required: ['test_run_key'],
|
|
467
467
|
},
|
|
468
468
|
},
|
|
469
|
+
{
|
|
470
|
+
name: 'update_test_execution',
|
|
471
|
+
description: 'Update a test case execution\'s status within a test cycle, and optionally attach bug(s) as Jira issue links. Identify the execution either by execution_id directly, or by test_cycle_key + test_case_key (the latest execution is used). Cloud only.',
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
execution_id: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'Test execution key or ID (e.g. "PROJ-E123" or 5805255). Takes precedence over test_cycle_key/test_case_key.',
|
|
478
|
+
},
|
|
479
|
+
test_cycle_key: {
|
|
480
|
+
type: 'string',
|
|
481
|
+
description: 'Test cycle key (e.g. "PROJ-R123"). Used together with test_case_key to locate the execution when execution_id is not given.',
|
|
482
|
+
},
|
|
483
|
+
test_case_key: {
|
|
484
|
+
type: 'string',
|
|
485
|
+
description: 'Test case key (e.g. "PROJ-T456"). Used together with test_cycle_key to locate the execution.',
|
|
486
|
+
},
|
|
487
|
+
project_key: {
|
|
488
|
+
type: 'string',
|
|
489
|
+
description: 'Project key for the cycle lookup (optional — derived from test_cycle_key when omitted).',
|
|
490
|
+
},
|
|
491
|
+
status: {
|
|
492
|
+
type: 'string',
|
|
493
|
+
description: 'New execution status name. Common values: "Pass", "Fail", "In Progress", "Blocked", "Not Executed". Must match a status configured in your Zephyr project.',
|
|
494
|
+
},
|
|
495
|
+
comment: {
|
|
496
|
+
type: 'string',
|
|
497
|
+
description: 'Comment against the overall execution (e.g. failure details).',
|
|
498
|
+
},
|
|
499
|
+
environment: {
|
|
500
|
+
type: 'string',
|
|
501
|
+
description: 'Environment name assigned to the execution (e.g. "Chrome Latest Version").',
|
|
502
|
+
},
|
|
503
|
+
execution_time: {
|
|
504
|
+
type: 'number',
|
|
505
|
+
description: 'Actual execution time in milliseconds (optional).',
|
|
506
|
+
},
|
|
507
|
+
actual_end_date: {
|
|
508
|
+
type: 'string',
|
|
509
|
+
description: 'Actual end date in ISO format, e.g. "2024-05-20T13:15:13Z" (optional).',
|
|
510
|
+
},
|
|
511
|
+
executed_by_id: {
|
|
512
|
+
type: 'string',
|
|
513
|
+
description: 'Jira Account ID of the user who executed the test (optional).',
|
|
514
|
+
},
|
|
515
|
+
assigned_to_id: {
|
|
516
|
+
type: 'string',
|
|
517
|
+
description: 'Jira Account ID of the user the execution is assigned to (optional).',
|
|
518
|
+
},
|
|
519
|
+
bug_keys: {
|
|
520
|
+
type: 'array',
|
|
521
|
+
description: 'Jira issue keys to attach to the execution as bugs (e.g. ["PROJ-789"]). Each key is resolved to a numeric ID via the Jira REST API (requires JIRA_USERNAME + JIRA_API_TOKEN) and linked via POST /testexecutions/{key}/links/issues. Failures are reported as warnings and do not fail the call.',
|
|
522
|
+
items: { type: 'string' },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'get_test_cycles_for_issue',
|
|
529
|
+
description: 'Get the Zephyr test cycles linked to a Jira issue (e.g. a story or epic). Calls GET /issuelinks/{issueKey}/testcycles and, by default, resolves each numeric cycle ID to its key (e.g. "PROJ-R123") and name. Use this to discover the test cycle referenced by a Jira ticket, then feed the key into list_executions_by_cycle / update_test_execution. Cloud only.',
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
properties: {
|
|
533
|
+
issue_key: {
|
|
534
|
+
type: 'string',
|
|
535
|
+
description: 'Jira issue key whose linked test cycles to fetch (e.g. "PROJ-123").',
|
|
536
|
+
},
|
|
537
|
+
resolve_keys: {
|
|
538
|
+
type: 'boolean',
|
|
539
|
+
description: 'When true (default), resolve each cycle ID to its key + name via GET /testcycles/{id}. Set false to return raw numeric IDs only (faster, no extra calls).',
|
|
540
|
+
default: true,
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
required: ['issue_key'],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
469
546
|
{
|
|
470
547
|
name: 'delete_test_run',
|
|
471
548
|
description: 'Delete a specific test run (Data Center only — not supported on Cloud v2)',
|
package/src/types.ts
CHANGED
|
@@ -102,6 +102,34 @@ export interface ListExecutionsByCycleArgs {
|
|
|
102
102
|
max_results?: number;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
export interface UpdateTestExecutionArgs {
|
|
106
|
+
/** Direct execution key/ID (e.g. "PROJ-E123" or 5805255). Takes precedence over cycle+case lookup. */
|
|
107
|
+
execution_id?: string;
|
|
108
|
+
/** Test cycle key (e.g. "PROJ-R123") — used with test_case_key to locate the execution. */
|
|
109
|
+
test_cycle_key?: string;
|
|
110
|
+
/** Test case key (e.g. "PROJ-T456") — used with test_cycle_key to locate the execution. */
|
|
111
|
+
test_case_key?: string;
|
|
112
|
+
/** Project key for the cycle lookup. Derived from test_cycle_key when omitted. */
|
|
113
|
+
project_key?: string;
|
|
114
|
+
/** New execution status name (e.g. "Pass", "Fail", "In Progress", "Blocked", "Not Executed"). */
|
|
115
|
+
status?: string;
|
|
116
|
+
comment?: string;
|
|
117
|
+
environment?: string;
|
|
118
|
+
execution_time?: number;
|
|
119
|
+
actual_end_date?: string;
|
|
120
|
+
executed_by_id?: string;
|
|
121
|
+
assigned_to_id?: string;
|
|
122
|
+
/** Jira issue keys to link to the execution as bugs (e.g. ["PROJ-789"]). Resolved to numeric IDs via the Jira REST API. */
|
|
123
|
+
bug_keys?: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface GetTestCyclesForIssueArgs {
|
|
127
|
+
/** Jira issue key whose linked test cycles to fetch (e.g. "PROJ-123"). */
|
|
128
|
+
issue_key: string;
|
|
129
|
+
/** When true (default), resolve each cycle ID to its key + name via GET /testcycles/{id}. */
|
|
130
|
+
resolve_keys?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
export type JiraType = 'cloud' | 'datacenter';
|
|
106
134
|
|
|
107
135
|
export interface ApiEndpoints {
|