zephyr-scale-mcp-server 0.5.0 → 0.6.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/build/index.js +4 -0
- package/build/tool-handlers.js +162 -10
- package/build/tool-schemas.js +61 -0
- package/build/utils.js +23 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/tool-handlers.ts +172 -8
- package/src/tool-schemas.ts +61 -0
- package/src/types.ts +6 -0
- package/src/utils.ts +21 -0
package/build/index.js
CHANGED
|
@@ -55,6 +55,8 @@ class ZephyrServer {
|
|
|
55
55
|
return await this.toolHandlers.getTestRunCases(args);
|
|
56
56
|
case 'delete_test_case':
|
|
57
57
|
return await this.toolHandlers.deleteTestCase(args);
|
|
58
|
+
case 'update_test_run':
|
|
59
|
+
return await this.toolHandlers.updateTestRun(args);
|
|
58
60
|
case 'delete_test_run':
|
|
59
61
|
return await this.toolHandlers.deleteTestRun(args);
|
|
60
62
|
case 'create_test_run':
|
|
@@ -69,6 +71,8 @@ class ZephyrServer {
|
|
|
69
71
|
return await this.toolHandlers.searchTestRuns(args);
|
|
70
72
|
case 'add_test_cases_to_run':
|
|
71
73
|
return await this.toolHandlers.addTestCasesToRun(args);
|
|
74
|
+
case 'list_executions_by_cycle':
|
|
75
|
+
return await this.toolHandlers.listExecutionsByCycle(args);
|
|
72
76
|
default:
|
|
73
77
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
74
78
|
}
|
package/build/tool-handlers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { convertToGherkin, resolveFolderIdByPath } from './utils.js';
|
|
3
|
+
import { convertToGherkin, resolveFolderIdByPath, getAccountIdFromApiKey } from './utils.js';
|
|
4
4
|
export class ZephyrToolHandlers {
|
|
5
5
|
axiosInstance;
|
|
6
6
|
jiraConfig;
|
|
@@ -61,9 +61,10 @@ export class ZephyrToolHandlers {
|
|
|
61
61
|
payload.labels = labels;
|
|
62
62
|
if (custom_fields)
|
|
63
63
|
payload.customFields = custom_fields;
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Default ownerId to the account ID embedded in the Zephyr API key JWT
|
|
65
|
+
const resolvedOwner = owner_id ?? getAccountIdFromApiKey();
|
|
66
|
+
if (resolvedOwner)
|
|
67
|
+
payload.ownerId = resolvedOwner;
|
|
67
68
|
if (component_id)
|
|
68
69
|
payload.componentId = component_id;
|
|
69
70
|
// Resolve folder path → folderId
|
|
@@ -500,6 +501,69 @@ export class ZephyrToolHandlers {
|
|
|
500
501
|
throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${this.formatError(error)}`);
|
|
501
502
|
}
|
|
502
503
|
}
|
|
504
|
+
async updateTestRun(args) {
|
|
505
|
+
const { test_run_key, owner, name, description, planned_start_date, planned_end_date, status_id } = args;
|
|
506
|
+
if (this.jiraConfig.type !== 'cloud') {
|
|
507
|
+
throw new McpError(ErrorCode.InvalidRequest, 'update_test_run is only supported on Zephyr Scale Cloud.');
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
// Fetch current cycle to preserve required fields
|
|
511
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
512
|
+
const current = getResponse.data;
|
|
513
|
+
const payload = {
|
|
514
|
+
id: current.id,
|
|
515
|
+
key: test_run_key,
|
|
516
|
+
name: name ?? current.name,
|
|
517
|
+
project: current.project,
|
|
518
|
+
status: status_id ? { id: status_id } : current.status,
|
|
519
|
+
};
|
|
520
|
+
if (description !== undefined)
|
|
521
|
+
payload.description = description;
|
|
522
|
+
else if (current.description)
|
|
523
|
+
payload.description = current.description;
|
|
524
|
+
if (planned_start_date !== undefined)
|
|
525
|
+
payload.plannedStartDate = planned_start_date;
|
|
526
|
+
else if (current.plannedStartDate)
|
|
527
|
+
payload.plannedStartDate = current.plannedStartDate;
|
|
528
|
+
if (planned_end_date !== undefined)
|
|
529
|
+
payload.plannedEndDate = planned_end_date;
|
|
530
|
+
else if (current.plannedEndDate)
|
|
531
|
+
payload.plannedEndDate = current.plannedEndDate;
|
|
532
|
+
if (owner !== undefined)
|
|
533
|
+
payload.owner = { accountId: owner };
|
|
534
|
+
else if (current.owner)
|
|
535
|
+
payload.owner = current.owner;
|
|
536
|
+
if (current.jiraProjectVersion)
|
|
537
|
+
payload.jiraProjectVersion = current.jiraProjectVersion;
|
|
538
|
+
if (current.folder)
|
|
539
|
+
payload.folder = current.folder;
|
|
540
|
+
if (current.customFields && Object.keys(current.customFields).length > 0) {
|
|
541
|
+
payload.customFields = current.customFields;
|
|
542
|
+
}
|
|
543
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`, payload);
|
|
544
|
+
// Resolve status name for the response
|
|
545
|
+
const projectKey = test_run_key.replace(/-R\d+$/, '');
|
|
546
|
+
const statusName = payload.status?.id
|
|
547
|
+
? await this.resolveStatusName(payload.status.id)
|
|
548
|
+
: null;
|
|
549
|
+
return {
|
|
550
|
+
content: [{
|
|
551
|
+
type: 'text',
|
|
552
|
+
text: `✅ Updated test cycle ${test_run_key} successfully.\n${JSON.stringify({
|
|
553
|
+
key: test_run_key,
|
|
554
|
+
name: payload.name,
|
|
555
|
+
owner: payload.owner ?? null,
|
|
556
|
+
status: statusName ? { id: payload.status?.id, name: statusName } : payload.status,
|
|
557
|
+
}, null, 2)}`,
|
|
558
|
+
}],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
if (error instanceof McpError)
|
|
563
|
+
throw error;
|
|
564
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update test run: ${this.formatError(error)}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
503
567
|
async deleteTestCase(args) {
|
|
504
568
|
if (this.jiraConfig.type === 'cloud') {
|
|
505
569
|
throw new McpError(ErrorCode.InvalidRequest, 'delete_test_case is not supported by the Zephyr Scale Cloud v2 API.');
|
|
@@ -558,9 +622,10 @@ export class ZephyrToolHandlers {
|
|
|
558
622
|
payload.plannedEndDate = planned_end_date;
|
|
559
623
|
if (custom_fields)
|
|
560
624
|
payload.customFields = custom_fields;
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
625
|
+
// Default ownerId to the account ID embedded in the Zephyr API key JWT
|
|
626
|
+
const resolvedOwner = owner ?? getAccountIdFromApiKey();
|
|
627
|
+
if (resolvedOwner)
|
|
628
|
+
payload.ownerId = resolvedOwner;
|
|
564
629
|
// Link to a Jira project version/release (integer ID)
|
|
565
630
|
if (jira_project_version)
|
|
566
631
|
payload.jiraProjectVersion = jira_project_version;
|
|
@@ -678,14 +743,28 @@ export class ZephyrToolHandlers {
|
|
|
678
743
|
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
|
|
679
744
|
}
|
|
680
745
|
}
|
|
746
|
+
async resolveStatusName(statusId) {
|
|
747
|
+
try {
|
|
748
|
+
const response = await this.axiosInstance.get(`/statuses/${statusId}`);
|
|
749
|
+
return response.data?.name ?? null;
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
681
755
|
async getTestRun(args) {
|
|
682
756
|
const { test_run_key } = args;
|
|
683
|
-
// Both Cloud (/testcycles/{key}) and DC (/rest/atm/1.0/testrun/{key}) handled
|
|
684
|
-
// via apiEndpoints.testrun which now correctly maps to /testcycles for Cloud
|
|
685
757
|
try {
|
|
686
758
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
759
|
+
const data = response.data;
|
|
760
|
+
// Resolve status name — extract project key from the cycle key (e.g. DDCN-R377 → DDCN)
|
|
761
|
+
if (data?.status?.id) {
|
|
762
|
+
const statusName = await this.resolveStatusName(data.status.id);
|
|
763
|
+
if (statusName)
|
|
764
|
+
data.status.name = statusName;
|
|
765
|
+
}
|
|
687
766
|
return {
|
|
688
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
767
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
689
768
|
};
|
|
690
769
|
}
|
|
691
770
|
catch (error) {
|
|
@@ -911,6 +990,79 @@ export class ZephyrToolHandlers {
|
|
|
911
990
|
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
912
991
|
}
|
|
913
992
|
}
|
|
993
|
+
async listExecutionsByCycle(args) {
|
|
994
|
+
const { test_cycle_key, project_key, max_results = 100 } = args;
|
|
995
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
996
|
+
try {
|
|
997
|
+
// Cloud v2: GET /testexecutions?projectKey=X&testCycle=Y
|
|
998
|
+
const params = {
|
|
999
|
+
projectKey: project_key,
|
|
1000
|
+
testCycle: test_cycle_key,
|
|
1001
|
+
maxResults: max_results,
|
|
1002
|
+
};
|
|
1003
|
+
const response = await this.axiosInstance.get('/testexecutions', { params });
|
|
1004
|
+
const executions = Array.isArray(response.data)
|
|
1005
|
+
? response.data
|
|
1006
|
+
: response.data?.values ?? [];
|
|
1007
|
+
const summary = executions.map((ex) => ({
|
|
1008
|
+
key: ex.key,
|
|
1009
|
+
testCaseKey: ex.testCase?.self?.match(/testcases\/(.+?)\/versions/)?.[1] || ex.testCase?.id,
|
|
1010
|
+
status: ex.testExecutionStatus?.id,
|
|
1011
|
+
statusName: ex.testExecutionStatus?.name,
|
|
1012
|
+
executedById: ex.executedById,
|
|
1013
|
+
assignedToId: ex.assignedToId,
|
|
1014
|
+
actualEndDate: ex.actualEndDate,
|
|
1015
|
+
automated: ex.automated,
|
|
1016
|
+
comment: ex.comment,
|
|
1017
|
+
}));
|
|
1018
|
+
// Count statuses
|
|
1019
|
+
const statusCounts = {};
|
|
1020
|
+
for (const ex of executions) {
|
|
1021
|
+
const statusId = ex.testExecutionStatus?.id?.toString() || 'unknown';
|
|
1022
|
+
statusCounts[statusId] = (statusCounts[statusId] || 0) + 1;
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
content: [{
|
|
1026
|
+
type: 'text',
|
|
1027
|
+
text: `✅ Found ${executions.length} execution(s) for cycle ${test_cycle_key}:\n${JSON.stringify({
|
|
1028
|
+
cycleKey: test_cycle_key,
|
|
1029
|
+
totalExecutions: executions.length,
|
|
1030
|
+
statusCounts,
|
|
1031
|
+
executions: summary,
|
|
1032
|
+
}, null, 2)}`,
|
|
1033
|
+
}],
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list executions: ${this.formatError(error)}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// Data Center: GET /rest/atm/1.0/testrun/{key}/testresults
|
|
1041
|
+
try {
|
|
1042
|
+
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_cycle_key}/testresults`);
|
|
1043
|
+
const results = Array.isArray(response.data) ? response.data : [];
|
|
1044
|
+
return {
|
|
1045
|
+
content: [{
|
|
1046
|
+
type: 'text',
|
|
1047
|
+
text: `✅ Found ${results.length} execution(s) for cycle ${test_cycle_key}:\n${JSON.stringify({
|
|
1048
|
+
cycleKey: test_cycle_key,
|
|
1049
|
+
totalExecutions: results.length,
|
|
1050
|
+
executions: results.map((r) => ({
|
|
1051
|
+
id: r.id,
|
|
1052
|
+
testCaseKey: r.testCaseKey,
|
|
1053
|
+
status: r.status,
|
|
1054
|
+
executedBy: r.executedBy,
|
|
1055
|
+
executionDate: r.executionDate,
|
|
1056
|
+
automated: r.automated,
|
|
1057
|
+
})),
|
|
1058
|
+
}, null, 2)}`,
|
|
1059
|
+
}],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list executions: ${this.formatError(error)}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
914
1066
|
async resolveJiraIssueId(issueKey) {
|
|
915
1067
|
// The Zephyr API key is NOT valid for the Jira REST API — Jira Cloud requires
|
|
916
1068
|
// Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
|
package/build/tool-schemas.js
CHANGED
|
@@ -379,6 +379,67 @@ export const toolSchemas = [
|
|
|
379
379
|
},
|
|
380
380
|
},
|
|
381
381
|
},
|
|
382
|
+
{
|
|
383
|
+
name: 'list_executions_by_cycle',
|
|
384
|
+
description: 'List all test executions for a specific test cycle. Returns execution status, executedBy, actualEndDate for each test case in the cycle. Cloud only.',
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
test_cycle_key: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'Test cycle key (e.g., DDCN-R371)',
|
|
391
|
+
},
|
|
392
|
+
project_key: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'Project key (required for Cloud)',
|
|
395
|
+
},
|
|
396
|
+
max_results: {
|
|
397
|
+
type: 'number',
|
|
398
|
+
description: 'Maximum number of results to return (optional, default 100)',
|
|
399
|
+
default: 100,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ['test_cycle_key', 'project_key'],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'update_test_run',
|
|
407
|
+
description: 'Update an existing test cycle — set owner, name, description, dates, or status. Unspecified fields are preserved. Cloud only.',
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
test_run_key: {
|
|
412
|
+
type: 'string',
|
|
413
|
+
description: 'Test cycle key to update (e.g., PROJ-R123)',
|
|
414
|
+
},
|
|
415
|
+
owner: {
|
|
416
|
+
type: 'string',
|
|
417
|
+
description: 'Jira Account ID of the new owner (e.g., "6269ee89494f17007056d8f0")',
|
|
418
|
+
},
|
|
419
|
+
name: {
|
|
420
|
+
type: 'string',
|
|
421
|
+
description: 'New name for the test cycle (optional)',
|
|
422
|
+
},
|
|
423
|
+
description: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
description: 'New description (optional)',
|
|
426
|
+
},
|
|
427
|
+
planned_start_date: {
|
|
428
|
+
type: 'string',
|
|
429
|
+
description: 'Planned start date in ISO format (optional)',
|
|
430
|
+
},
|
|
431
|
+
planned_end_date: {
|
|
432
|
+
type: 'string',
|
|
433
|
+
description: 'Planned end date in ISO format (optional)',
|
|
434
|
+
},
|
|
435
|
+
status_id: {
|
|
436
|
+
type: 'number',
|
|
437
|
+
description: 'Numeric status ID to set (optional)',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
required: ['test_run_key'],
|
|
441
|
+
},
|
|
442
|
+
},
|
|
382
443
|
{
|
|
383
444
|
name: 'delete_test_run',
|
|
384
445
|
description: 'Delete a specific test run (Data Center only — not supported on Cloud v2)',
|
package/build/utils.js
CHANGED
|
@@ -83,6 +83,29 @@ export const priorityMapping = {
|
|
|
83
83
|
'Medium': 'High',
|
|
84
84
|
'Low': 'High'
|
|
85
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* Decodes the Atlassian Account ID from the Zephyr JWT API key.
|
|
88
|
+
* The JWT payload contains context.user.accountId — no extra API call needed.
|
|
89
|
+
* Returns null if the token is missing or malformed.
|
|
90
|
+
*/
|
|
91
|
+
export function getAccountIdFromApiKey(apiKey) {
|
|
92
|
+
try {
|
|
93
|
+
const token = apiKey ?? process.env.ZEPHYR_API_KEY;
|
|
94
|
+
if (!token)
|
|
95
|
+
return null;
|
|
96
|
+
const parts = token.split('.');
|
|
97
|
+
if (parts.length < 2)
|
|
98
|
+
return null;
|
|
99
|
+
// Base64url decode the payload (add padding as needed)
|
|
100
|
+
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
101
|
+
const padded = payload + '='.repeat((4 - payload.length % 4) % 4);
|
|
102
|
+
const decoded = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
103
|
+
return decoded?.context?.user?.accountId ?? null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
86
109
|
/**
|
|
87
110
|
* Detects whether the Jira instance is Cloud or Data Center based on the base URL.
|
|
88
111
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
@@ -75,6 +75,8 @@ class ZephyrServer {
|
|
|
75
75
|
return await this.toolHandlers.getTestRunCases(args);
|
|
76
76
|
case 'delete_test_case':
|
|
77
77
|
return await this.toolHandlers.deleteTestCase(args);
|
|
78
|
+
case 'update_test_run':
|
|
79
|
+
return await this.toolHandlers.updateTestRun(args);
|
|
78
80
|
case 'delete_test_run':
|
|
79
81
|
return await this.toolHandlers.deleteTestRun(args);
|
|
80
82
|
case 'create_test_run':
|
|
@@ -89,6 +91,8 @@ class ZephyrServer {
|
|
|
89
91
|
return await this.toolHandlers.searchTestRuns(args as any);
|
|
90
92
|
case 'add_test_cases_to_run':
|
|
91
93
|
return await this.toolHandlers.addTestCasesToRun(args as any);
|
|
94
|
+
case 'list_executions_by_cycle':
|
|
95
|
+
return await this.toolHandlers.listExecutionsByCycle(args as any);
|
|
92
96
|
default:
|
|
93
97
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
94
98
|
}
|
package/src/tool-handlers.ts
CHANGED
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
AddTestCasesToRunArgs,
|
|
11
11
|
SearchTestRunsArgs,
|
|
12
12
|
GetTestExecutionArgs,
|
|
13
|
+
ListExecutionsByCycleArgs,
|
|
13
14
|
JiraConfig
|
|
14
15
|
} from './types.js';
|
|
15
|
-
import { convertToGherkin, resolveFolderIdByPath } from './utils.js';
|
|
16
|
+
import { convertToGherkin, resolveFolderIdByPath, getAccountIdFromApiKey } from './utils.js';
|
|
16
17
|
|
|
17
18
|
export class ZephyrToolHandlers {
|
|
18
19
|
constructor(
|
|
@@ -73,8 +74,9 @@ export class ZephyrToolHandlers {
|
|
|
73
74
|
if (estimated_time) payload.estimatedTime = estimated_time;
|
|
74
75
|
if (labels && labels.length > 0) payload.labels = labels;
|
|
75
76
|
if (custom_fields) payload.customFields = custom_fields;
|
|
76
|
-
//
|
|
77
|
-
|
|
77
|
+
// Default ownerId to the account ID embedded in the Zephyr API key JWT
|
|
78
|
+
const resolvedOwner = owner_id ?? getAccountIdFromApiKey();
|
|
79
|
+
if (resolvedOwner) payload.ownerId = resolvedOwner;
|
|
78
80
|
if (component_id) payload.componentId = component_id;
|
|
79
81
|
|
|
80
82
|
// Resolve folder path → folderId
|
|
@@ -543,6 +545,69 @@ export class ZephyrToolHandlers {
|
|
|
543
545
|
}
|
|
544
546
|
}
|
|
545
547
|
|
|
548
|
+
async updateTestRun(args: any) {
|
|
549
|
+
const { test_run_key, owner, name, description, planned_start_date, planned_end_date, status_id } = args;
|
|
550
|
+
|
|
551
|
+
if (this.jiraConfig.type !== 'cloud') {
|
|
552
|
+
throw new McpError(ErrorCode.InvalidRequest, 'update_test_run is only supported on Zephyr Scale Cloud.');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
// Fetch current cycle to preserve required fields
|
|
557
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
558
|
+
const current = getResponse.data;
|
|
559
|
+
|
|
560
|
+
const payload: any = {
|
|
561
|
+
id: current.id,
|
|
562
|
+
key: test_run_key,
|
|
563
|
+
name: name ?? current.name,
|
|
564
|
+
project: current.project,
|
|
565
|
+
status: status_id ? { id: status_id } : current.status,
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
if (description !== undefined) payload.description = description;
|
|
569
|
+
else if (current.description) payload.description = current.description;
|
|
570
|
+
|
|
571
|
+
if (planned_start_date !== undefined) payload.plannedStartDate = planned_start_date;
|
|
572
|
+
else if (current.plannedStartDate) payload.plannedStartDate = current.plannedStartDate;
|
|
573
|
+
|
|
574
|
+
if (planned_end_date !== undefined) payload.plannedEndDate = planned_end_date;
|
|
575
|
+
else if (current.plannedEndDate) payload.plannedEndDate = current.plannedEndDate;
|
|
576
|
+
|
|
577
|
+
if (owner !== undefined) payload.owner = { accountId: owner };
|
|
578
|
+
else if (current.owner) payload.owner = current.owner;
|
|
579
|
+
|
|
580
|
+
if (current.jiraProjectVersion) payload.jiraProjectVersion = current.jiraProjectVersion;
|
|
581
|
+
if (current.folder) payload.folder = current.folder;
|
|
582
|
+
if (current.customFields && Object.keys(current.customFields).length > 0) {
|
|
583
|
+
payload.customFields = current.customFields;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`, payload);
|
|
587
|
+
|
|
588
|
+
// Resolve status name for the response
|
|
589
|
+
const projectKey = test_run_key.replace(/-R\d+$/, '');
|
|
590
|
+
const statusName = payload.status?.id
|
|
591
|
+
? await this.resolveStatusName(payload.status.id)
|
|
592
|
+
: null;
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
content: [{
|
|
596
|
+
type: 'text',
|
|
597
|
+
text: `✅ Updated test cycle ${test_run_key} successfully.\n${JSON.stringify({
|
|
598
|
+
key: test_run_key,
|
|
599
|
+
name: payload.name,
|
|
600
|
+
owner: payload.owner ?? null,
|
|
601
|
+
status: statusName ? { id: payload.status?.id, name: statusName } : payload.status,
|
|
602
|
+
}, null, 2)}`,
|
|
603
|
+
}],
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (error instanceof McpError) throw error;
|
|
607
|
+
throw new McpError(ErrorCode.InternalError, `Failed to update test run: ${this.formatError(error)}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
546
611
|
async deleteTestCase(args: any) {
|
|
547
612
|
if (this.jiraConfig.type === 'cloud') {
|
|
548
613
|
throw new McpError(
|
|
@@ -613,8 +678,9 @@ export class ZephyrToolHandlers {
|
|
|
613
678
|
if (planned_start_date) payload.plannedStartDate = planned_start_date;
|
|
614
679
|
if (planned_end_date) payload.plannedEndDate = planned_end_date;
|
|
615
680
|
if (custom_fields) payload.customFields = custom_fields;
|
|
616
|
-
//
|
|
617
|
-
|
|
681
|
+
// Default ownerId to the account ID embedded in the Zephyr API key JWT
|
|
682
|
+
const resolvedOwner = owner ?? getAccountIdFromApiKey();
|
|
683
|
+
if (resolvedOwner) payload.ownerId = resolvedOwner;
|
|
618
684
|
// Link to a Jira project version/release (integer ID)
|
|
619
685
|
if (jira_project_version) payload.jiraProjectVersion = jira_project_version;
|
|
620
686
|
if (folder) {
|
|
@@ -734,14 +800,29 @@ export class ZephyrToolHandlers {
|
|
|
734
800
|
}
|
|
735
801
|
}
|
|
736
802
|
|
|
803
|
+
private async resolveStatusName(statusId: number): Promise<string | null> {
|
|
804
|
+
try {
|
|
805
|
+
const response = await this.axiosInstance.get(`/statuses/${statusId}`);
|
|
806
|
+
return response.data?.name ?? null;
|
|
807
|
+
} catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
737
812
|
async getTestRun(args: any) {
|
|
738
813
|
const { test_run_key } = args;
|
|
739
|
-
// Both Cloud (/testcycles/{key}) and DC (/rest/atm/1.0/testrun/{key}) handled
|
|
740
|
-
// via apiEndpoints.testrun which now correctly maps to /testcycles for Cloud
|
|
741
814
|
try {
|
|
742
815
|
const response = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
|
|
816
|
+
const data = response.data;
|
|
817
|
+
|
|
818
|
+
// Resolve status name — extract project key from the cycle key (e.g. DDCN-R377 → DDCN)
|
|
819
|
+
if (data?.status?.id) {
|
|
820
|
+
const statusName = await this.resolveStatusName(data.status.id);
|
|
821
|
+
if (statusName) data.status.name = statusName;
|
|
822
|
+
}
|
|
823
|
+
|
|
743
824
|
return {
|
|
744
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
825
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
745
826
|
};
|
|
746
827
|
} catch (error) {
|
|
747
828
|
throw new McpError(ErrorCode.InternalError, `Failed to get test run: ${this.formatError(error)}`);
|
|
@@ -992,6 +1073,89 @@ export class ZephyrToolHandlers {
|
|
|
992
1073
|
}
|
|
993
1074
|
}
|
|
994
1075
|
|
|
1076
|
+
async listExecutionsByCycle(args: ListExecutionsByCycleArgs) {
|
|
1077
|
+
const { test_cycle_key, project_key, max_results = 100 } = args;
|
|
1078
|
+
|
|
1079
|
+
if (this.jiraConfig.type === 'cloud') {
|
|
1080
|
+
try {
|
|
1081
|
+
// Cloud v2: GET /testexecutions?projectKey=X&testCycle=Y
|
|
1082
|
+
const params: Record<string, any> = {
|
|
1083
|
+
projectKey: project_key,
|
|
1084
|
+
testCycle: test_cycle_key,
|
|
1085
|
+
maxResults: max_results,
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const response = await this.axiosInstance.get('/testexecutions', { params });
|
|
1089
|
+
|
|
1090
|
+
const executions = Array.isArray(response.data)
|
|
1091
|
+
? response.data
|
|
1092
|
+
: response.data?.values ?? [];
|
|
1093
|
+
|
|
1094
|
+
const summary = executions.map((ex: any) => ({
|
|
1095
|
+
key: ex.key,
|
|
1096
|
+
testCaseKey: ex.testCase?.self?.match(/testcases\/(.+?)\/versions/)?.[1] || ex.testCase?.id,
|
|
1097
|
+
status: ex.testExecutionStatus?.id,
|
|
1098
|
+
statusName: ex.testExecutionStatus?.name,
|
|
1099
|
+
executedById: ex.executedById,
|
|
1100
|
+
assignedToId: ex.assignedToId,
|
|
1101
|
+
actualEndDate: ex.actualEndDate,
|
|
1102
|
+
automated: ex.automated,
|
|
1103
|
+
comment: ex.comment,
|
|
1104
|
+
}));
|
|
1105
|
+
|
|
1106
|
+
// Count statuses
|
|
1107
|
+
const statusCounts: Record<string, number> = {};
|
|
1108
|
+
for (const ex of executions) {
|
|
1109
|
+
const statusId = ex.testExecutionStatus?.id?.toString() || 'unknown';
|
|
1110
|
+
statusCounts[statusId] = (statusCounts[statusId] || 0) + 1;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return {
|
|
1114
|
+
content: [{
|
|
1115
|
+
type: 'text',
|
|
1116
|
+
text: `✅ Found ${executions.length} execution(s) for cycle ${test_cycle_key}:\n${JSON.stringify({
|
|
1117
|
+
cycleKey: test_cycle_key,
|
|
1118
|
+
totalExecutions: executions.length,
|
|
1119
|
+
statusCounts,
|
|
1120
|
+
executions: summary,
|
|
1121
|
+
}, null, 2)}`,
|
|
1122
|
+
}],
|
|
1123
|
+
};
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list executions: ${this.formatError(error)}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Data Center: GET /rest/atm/1.0/testrun/{key}/testresults
|
|
1130
|
+
try {
|
|
1131
|
+
const response = await this.axiosInstance.get(
|
|
1132
|
+
`${this.jiraConfig.apiEndpoints.testrun}/${test_cycle_key}/testresults`
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
const results = Array.isArray(response.data) ? response.data : [];
|
|
1136
|
+
|
|
1137
|
+
return {
|
|
1138
|
+
content: [{
|
|
1139
|
+
type: 'text',
|
|
1140
|
+
text: `✅ Found ${results.length} execution(s) for cycle ${test_cycle_key}:\n${JSON.stringify({
|
|
1141
|
+
cycleKey: test_cycle_key,
|
|
1142
|
+
totalExecutions: results.length,
|
|
1143
|
+
executions: results.map((r: any) => ({
|
|
1144
|
+
id: r.id,
|
|
1145
|
+
testCaseKey: r.testCaseKey,
|
|
1146
|
+
status: r.status,
|
|
1147
|
+
executedBy: r.executedBy,
|
|
1148
|
+
executionDate: r.executionDate,
|
|
1149
|
+
automated: r.automated,
|
|
1150
|
+
})),
|
|
1151
|
+
}, null, 2)}`,
|
|
1152
|
+
}],
|
|
1153
|
+
};
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list executions: ${this.formatError(error)}`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
995
1159
|
private async resolveJiraIssueId(issueKey: string): Promise<number> {
|
|
996
1160
|
// The Zephyr API key is NOT valid for the Jira REST API — Jira Cloud requires
|
|
997
1161
|
// Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
|
package/src/tool-schemas.ts
CHANGED
|
@@ -379,6 +379,67 @@ export const toolSchemas = [
|
|
|
379
379
|
},
|
|
380
380
|
},
|
|
381
381
|
},
|
|
382
|
+
{
|
|
383
|
+
name: 'list_executions_by_cycle',
|
|
384
|
+
description: 'List all test executions for a specific test cycle. Returns execution status, executedBy, actualEndDate for each test case in the cycle. Cloud only.',
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
test_cycle_key: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'Test cycle key (e.g., DDCN-R371)',
|
|
391
|
+
},
|
|
392
|
+
project_key: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'Project key (required for Cloud)',
|
|
395
|
+
},
|
|
396
|
+
max_results: {
|
|
397
|
+
type: 'number',
|
|
398
|
+
description: 'Maximum number of results to return (optional, default 100)',
|
|
399
|
+
default: 100,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ['test_cycle_key', 'project_key'],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'update_test_run',
|
|
407
|
+
description: 'Update an existing test cycle — set owner, name, description, dates, or status. Unspecified fields are preserved. Cloud only.',
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
test_run_key: {
|
|
412
|
+
type: 'string',
|
|
413
|
+
description: 'Test cycle key to update (e.g., PROJ-R123)',
|
|
414
|
+
},
|
|
415
|
+
owner: {
|
|
416
|
+
type: 'string',
|
|
417
|
+
description: 'Jira Account ID of the new owner (e.g., "6269ee89494f17007056d8f0")',
|
|
418
|
+
},
|
|
419
|
+
name: {
|
|
420
|
+
type: 'string',
|
|
421
|
+
description: 'New name for the test cycle (optional)',
|
|
422
|
+
},
|
|
423
|
+
description: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
description: 'New description (optional)',
|
|
426
|
+
},
|
|
427
|
+
planned_start_date: {
|
|
428
|
+
type: 'string',
|
|
429
|
+
description: 'Planned start date in ISO format (optional)',
|
|
430
|
+
},
|
|
431
|
+
planned_end_date: {
|
|
432
|
+
type: 'string',
|
|
433
|
+
description: 'Planned end date in ISO format (optional)',
|
|
434
|
+
},
|
|
435
|
+
status_id: {
|
|
436
|
+
type: 'number',
|
|
437
|
+
description: 'Numeric status ID to set (optional)',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
required: ['test_run_key'],
|
|
441
|
+
},
|
|
442
|
+
},
|
|
382
443
|
{
|
|
383
444
|
name: 'delete_test_run',
|
|
384
445
|
description: 'Delete a specific test run (Data Center only — not supported on Cloud v2)',
|
package/src/types.ts
CHANGED
|
@@ -96,6 +96,12 @@ export interface SearchTestRunsArgs {
|
|
|
96
96
|
fields?: string;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export interface ListExecutionsByCycleArgs {
|
|
100
|
+
test_cycle_key: string;
|
|
101
|
+
project_key: string;
|
|
102
|
+
max_results?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
export type JiraType = 'cloud' | 'datacenter';
|
|
100
106
|
|
|
101
107
|
export interface ApiEndpoints {
|
package/src/utils.ts
CHANGED
|
@@ -102,6 +102,27 @@ export const priorityMapping: { [key: string]: string } = {
|
|
|
102
102
|
'Low': 'High'
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Decodes the Atlassian Account ID from the Zephyr JWT API key.
|
|
107
|
+
* The JWT payload contains context.user.accountId — no extra API call needed.
|
|
108
|
+
* Returns null if the token is missing or malformed.
|
|
109
|
+
*/
|
|
110
|
+
export function getAccountIdFromApiKey(apiKey?: string): string | null {
|
|
111
|
+
try {
|
|
112
|
+
const token = apiKey ?? process.env.ZEPHYR_API_KEY;
|
|
113
|
+
if (!token) return null;
|
|
114
|
+
const parts = token.split('.');
|
|
115
|
+
if (parts.length < 2) return null;
|
|
116
|
+
// Base64url decode the payload (add padding as needed)
|
|
117
|
+
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
118
|
+
const padded = payload + '='.repeat((4 - payload.length % 4) % 4);
|
|
119
|
+
const decoded = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
120
|
+
return decoded?.context?.user?.accountId ?? null;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
/**
|
|
106
127
|
* Detects whether the Jira instance is Cloud or Data Center based on the base URL.
|
|
107
128
|
*/
|