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 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
- - `search_test_cases_by_folder`: Search for test cases in a specific folder.
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
  }
@@ -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.
@@ -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.7.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
  }
@@ -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.
@@ -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 {