zephyr-scale-mcp-server 0.6.0 → 0.7.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 +3 -1
- package/build/index.js +2 -0
- package/build/tool-handlers.js +94 -6
- package/build/tool-schemas.js +26 -0
- package/build/utils.js +17 -8
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/tool-handlers.ts +109 -6
- package/src/tool-schemas.ts +26 -0
- package/src/utils.ts +22 -10
package/README.md
CHANGED
|
@@ -107,12 +107,14 @@ The server provides access to various resources through URI schemes:
|
|
|
107
107
|
|
|
108
108
|
### Test Run Management
|
|
109
109
|
- `create_test_run`: Create a new test run.
|
|
110
|
-
- `get_test_run`: Get detailed information about a specific test run.
|
|
110
|
+
- `get_test_run`: Get detailed information about a specific test run, including resolved status name.
|
|
111
|
+
- `update_test_run`: Update an existing test cycle — set owner, name, description, dates, or status. *(Cloud only)*
|
|
111
112
|
- `get_test_run_cases`: Get test case keys from a test run.
|
|
112
113
|
- `add_test_cases_to_run`: Add test cases to an existing test run. *(Cloud only)*
|
|
113
114
|
|
|
114
115
|
### Test Execution & Search
|
|
115
116
|
- `get_test_execution`: Get detailed individual test execution results.
|
|
117
|
+
- `list_executions_by_cycle`: List all test executions for a specific test cycle with status, executor, and date. *(Cloud only)*
|
|
116
118
|
- `search_test_cases_by_folder`: Search for test cases in a specific folder.
|
|
117
119
|
- `search_test_runs`: Search for test runs by project key and/or folder path.
|
|
118
120
|
|
package/build/index.js
CHANGED
|
@@ -51,6 +51,8 @@ class ZephyrServer {
|
|
|
51
51
|
return await this.toolHandlers.updateTestCaseBdd(args);
|
|
52
52
|
case 'create_folder':
|
|
53
53
|
return await this.toolHandlers.createFolder(args);
|
|
54
|
+
case 'get_folders':
|
|
55
|
+
return await this.toolHandlers.getFolders(args);
|
|
54
56
|
case 'get_test_run_cases':
|
|
55
57
|
return await this.toolHandlers.getTestRunCases(args);
|
|
56
58
|
case 'delete_test_case':
|
package/build/tool-handlers.js
CHANGED
|
@@ -743,6 +743,79 @@ export class ZephyrToolHandlers {
|
|
|
743
743
|
throw new McpError(ErrorCode.InternalError, `Failed to create test run: ${this.formatError(error)}`);
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
|
+
async getFolders(args) {
|
|
747
|
+
const { project_key, folder_type, folder_path, max_results } = args;
|
|
748
|
+
try {
|
|
749
|
+
const pageSize = 1000;
|
|
750
|
+
const allFolders = [];
|
|
751
|
+
let startAt = 0;
|
|
752
|
+
let isLast = false;
|
|
753
|
+
while (!isLast) {
|
|
754
|
+
const params = { maxResults: pageSize, startAt };
|
|
755
|
+
if (project_key)
|
|
756
|
+
params.projectKey = project_key;
|
|
757
|
+
if (folder_type)
|
|
758
|
+
params.folderType = folder_type;
|
|
759
|
+
const response = await this.axiosInstance.get('/folders', { params });
|
|
760
|
+
const page = Array.isArray(response.data)
|
|
761
|
+
? response.data
|
|
762
|
+
: response.data?.values ?? [];
|
|
763
|
+
allFolders.push(...page);
|
|
764
|
+
isLast = response.data?.isLast === true || page.length < pageSize;
|
|
765
|
+
startAt += page.length;
|
|
766
|
+
if (max_results && allFolders.length >= max_results)
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
let results;
|
|
770
|
+
if (folder_path) {
|
|
771
|
+
// Resolve the root folder ID from the path, then collect full subtree via BFS
|
|
772
|
+
const rootId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder_path, folder_type ?? 'TEST_CASE');
|
|
773
|
+
if (rootId === null) {
|
|
774
|
+
return {
|
|
775
|
+
content: [{
|
|
776
|
+
type: 'text',
|
|
777
|
+
text: `⚠️ Folder not found: "${folder_path}" in project ${project_key}.`,
|
|
778
|
+
}],
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
// BFS over the already-fetched flat list — no extra API calls
|
|
782
|
+
const subtreeIds = new Set([rootId]);
|
|
783
|
+
const queue = [rootId];
|
|
784
|
+
while (queue.length > 0) {
|
|
785
|
+
const current = queue.shift();
|
|
786
|
+
for (const f of allFolders) {
|
|
787
|
+
if ((f.parentId ?? null) === current && !subtreeIds.has(f.id)) {
|
|
788
|
+
subtreeIds.add(f.id);
|
|
789
|
+
queue.push(f.id);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
results = allFolders.filter(f => subtreeIds.has(f.id));
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
results = allFolders;
|
|
797
|
+
}
|
|
798
|
+
if (max_results)
|
|
799
|
+
results = results.slice(0, max_results);
|
|
800
|
+
return {
|
|
801
|
+
content: [{
|
|
802
|
+
type: 'text',
|
|
803
|
+
text: JSON.stringify({
|
|
804
|
+
totalCount: results.length,
|
|
805
|
+
folders: results.map((f) => ({
|
|
806
|
+
id: f.id,
|
|
807
|
+
name: f.name,
|
|
808
|
+
parentId: f.parentId ?? null,
|
|
809
|
+
folderType: f.folderType,
|
|
810
|
+
})),
|
|
811
|
+
}, null, 2),
|
|
812
|
+
}],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get folders: ${this.formatError(error)}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
746
819
|
async resolveStatusName(statusId) {
|
|
747
820
|
try {
|
|
748
821
|
const response = await this.axiosInstance.get(`/statuses/${statusId}`);
|
|
@@ -837,12 +910,27 @@ export class ZephyrToolHandlers {
|
|
|
837
910
|
}],
|
|
838
911
|
};
|
|
839
912
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
913
|
+
// Paginate through all results — the API returns up to 1000 per page
|
|
914
|
+
const pageSize = Math.min(max_results, 1000);
|
|
915
|
+
const allTestCases = [];
|
|
916
|
+
let startAt = 0;
|
|
917
|
+
let isLast = false;
|
|
918
|
+
while (!isLast && allTestCases.length < max_results) {
|
|
919
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testcase, {
|
|
920
|
+
params: { projectKey: project_key, folderId, maxResults: pageSize, startAt },
|
|
921
|
+
});
|
|
922
|
+
const page = Array.isArray(response.data)
|
|
923
|
+
? response.data
|
|
924
|
+
: response.data?.values ?? [];
|
|
925
|
+
allTestCases.push(...page);
|
|
926
|
+
// Stop if the API signals last page, or we got fewer results than requested
|
|
927
|
+
isLast = response.data?.isLast === true || page.length < pageSize;
|
|
928
|
+
startAt += page.length;
|
|
929
|
+
// Safety: never exceed max_results
|
|
930
|
+
if (allTestCases.length >= max_results)
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
const testCases = allTestCases.slice(0, max_results);
|
|
846
934
|
return {
|
|
847
935
|
content: [{
|
|
848
936
|
type: 'text',
|
package/build/tool-schemas.js
CHANGED
|
@@ -196,6 +196,32 @@ export const toolSchemas = [
|
|
|
196
196
|
required: ['project_key', 'name'],
|
|
197
197
|
},
|
|
198
198
|
},
|
|
199
|
+
{
|
|
200
|
+
name: 'get_folders',
|
|
201
|
+
description: 'Get folders from Zephyr Scale. All parameters are optional. If folder_path is provided, returns the full subtree (the folder and all its descendants at every depth) under that path. Otherwise returns all folders matching the filters.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
project_key: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'Jira project key filter (e.g., "PROJ"). Optional.',
|
|
208
|
+
},
|
|
209
|
+
folder_type: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'Folder type filter (optional)',
|
|
212
|
+
enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
|
|
213
|
+
},
|
|
214
|
+
folder_path: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Folder path to filter by (e.g., "/CRM on Wechat"). Returns the matching folder and all folders nested beneath it at any depth. Requires project_key and folder_type to resolve the path.',
|
|
217
|
+
},
|
|
218
|
+
max_results: {
|
|
219
|
+
type: 'number',
|
|
220
|
+
description: 'Maximum total number of folders to return (optional). Defaults to all folders.',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
199
225
|
{
|
|
200
226
|
name: 'get_test_run_cases',
|
|
201
227
|
description: 'Get test case keys from a test run',
|
package/build/utils.js
CHANGED
|
@@ -4,18 +4,27 @@ export async function resolveFolderIdByPath(axiosInstance, projectKey, folderPat
|
|
|
4
4
|
if (segments.length === 0)
|
|
5
5
|
return null;
|
|
6
6
|
try {
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
// Paginate through all folders for the project + type
|
|
8
|
+
const pageSize = 1000;
|
|
9
|
+
const allFolders = [];
|
|
10
|
+
let startAt = 0;
|
|
11
|
+
let isLast = false;
|
|
12
|
+
while (!isLast) {
|
|
13
|
+
const response = await axiosInstance.get('/folders', {
|
|
14
|
+
params: { projectKey, folderType, maxResults: pageSize, startAt },
|
|
15
|
+
});
|
|
16
|
+
const page = Array.isArray(response.data)
|
|
17
|
+
? response.data
|
|
18
|
+
: response.data?.values ?? [];
|
|
19
|
+
allFolders.push(...page);
|
|
20
|
+
isLast = response.data?.isLast === true || page.length < pageSize;
|
|
21
|
+
startAt += page.length;
|
|
22
|
+
}
|
|
14
23
|
// Walk segments top-down, matching by name under the correct parent
|
|
15
24
|
let parentId = null;
|
|
16
25
|
let matchedId = null;
|
|
17
26
|
for (const segment of segments) {
|
|
18
|
-
const match =
|
|
27
|
+
const match = allFolders.find((f) => f.name === segment && (f.parentId ?? null) === parentId);
|
|
19
28
|
if (!match)
|
|
20
29
|
return null;
|
|
21
30
|
matchedId = match.id;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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
|
@@ -71,6 +71,8 @@ class ZephyrServer {
|
|
|
71
71
|
return await this.toolHandlers.updateTestCaseBdd(args as any);
|
|
72
72
|
case 'create_folder':
|
|
73
73
|
return await this.toolHandlers.createFolder(args as any);
|
|
74
|
+
case 'get_folders':
|
|
75
|
+
return await this.toolHandlers.getFolders(args as any);
|
|
74
76
|
case 'get_test_run_cases':
|
|
75
77
|
return await this.toolHandlers.getTestRunCases(args);
|
|
76
78
|
case 'delete_test_case':
|
package/src/tool-handlers.ts
CHANGED
|
@@ -800,6 +800,90 @@ export class ZephyrToolHandlers {
|
|
|
800
800
|
}
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
+
async getFolders(args: any) {
|
|
804
|
+
const { project_key, folder_type, folder_path, max_results } = args;
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const pageSize = 1000;
|
|
808
|
+
const allFolders: any[] = [];
|
|
809
|
+
let startAt = 0;
|
|
810
|
+
let isLast = false;
|
|
811
|
+
|
|
812
|
+
while (!isLast) {
|
|
813
|
+
const params: Record<string, any> = { maxResults: pageSize, startAt };
|
|
814
|
+
if (project_key) params.projectKey = project_key;
|
|
815
|
+
if (folder_type) params.folderType = folder_type;
|
|
816
|
+
|
|
817
|
+
const response = await this.axiosInstance.get('/folders', { params });
|
|
818
|
+
|
|
819
|
+
const page: any[] = Array.isArray(response.data)
|
|
820
|
+
? response.data
|
|
821
|
+
: response.data?.values ?? [];
|
|
822
|
+
|
|
823
|
+
allFolders.push(...page);
|
|
824
|
+
|
|
825
|
+
isLast = response.data?.isLast === true || page.length < pageSize;
|
|
826
|
+
startAt += page.length;
|
|
827
|
+
|
|
828
|
+
if (max_results && allFolders.length >= max_results) break;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
let results: any[];
|
|
832
|
+
|
|
833
|
+
if (folder_path) {
|
|
834
|
+
// Resolve the root folder ID from the path, then collect full subtree via BFS
|
|
835
|
+
const rootId = await resolveFolderIdByPath(
|
|
836
|
+
this.axiosInstance, project_key, folder_path, folder_type ?? 'TEST_CASE'
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
if (rootId === null) {
|
|
840
|
+
return {
|
|
841
|
+
content: [{
|
|
842
|
+
type: 'text',
|
|
843
|
+
text: `⚠️ Folder not found: "${folder_path}" in project ${project_key}.`,
|
|
844
|
+
}],
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// BFS over the already-fetched flat list — no extra API calls
|
|
849
|
+
const subtreeIds = new Set<number>([rootId]);
|
|
850
|
+
const queue = [rootId];
|
|
851
|
+
while (queue.length > 0) {
|
|
852
|
+
const current = queue.shift()!;
|
|
853
|
+
for (const f of allFolders) {
|
|
854
|
+
if ((f.parentId ?? null) === current && !subtreeIds.has(f.id)) {
|
|
855
|
+
subtreeIds.add(f.id);
|
|
856
|
+
queue.push(f.id);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
results = allFolders.filter(f => subtreeIds.has(f.id));
|
|
862
|
+
} else {
|
|
863
|
+
results = allFolders;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (max_results) results = results.slice(0, max_results);
|
|
867
|
+
|
|
868
|
+
return {
|
|
869
|
+
content: [{
|
|
870
|
+
type: 'text',
|
|
871
|
+
text: JSON.stringify({
|
|
872
|
+
totalCount: results.length,
|
|
873
|
+
folders: results.map((f: any) => ({
|
|
874
|
+
id: f.id,
|
|
875
|
+
name: f.name,
|
|
876
|
+
parentId: f.parentId ?? null,
|
|
877
|
+
folderType: f.folderType,
|
|
878
|
+
})),
|
|
879
|
+
}, null, 2),
|
|
880
|
+
}],
|
|
881
|
+
};
|
|
882
|
+
} catch (error) {
|
|
883
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get folders: ${this.formatError(error)}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
803
887
|
private async resolveStatusName(statusId: number): Promise<string | null> {
|
|
804
888
|
try {
|
|
805
889
|
const response = await this.axiosInstance.get(`/statuses/${statusId}`);
|
|
@@ -908,13 +992,32 @@ export class ZephyrToolHandlers {
|
|
|
908
992
|
};
|
|
909
993
|
}
|
|
910
994
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
995
|
+
// Paginate through all results — the API returns up to 1000 per page
|
|
996
|
+
const pageSize = Math.min(max_results, 1000);
|
|
997
|
+
const allTestCases: any[] = [];
|
|
998
|
+
let startAt = 0;
|
|
999
|
+
let isLast = false;
|
|
914
1000
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1001
|
+
while (!isLast && allTestCases.length < max_results) {
|
|
1002
|
+
const response = await this.axiosInstance.get(this.jiraConfig.apiEndpoints.testcase, {
|
|
1003
|
+
params: { projectKey: project_key, folderId, maxResults: pageSize, startAt },
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
const page = Array.isArray(response.data)
|
|
1007
|
+
? response.data
|
|
1008
|
+
: response.data?.values ?? [];
|
|
1009
|
+
|
|
1010
|
+
allTestCases.push(...page);
|
|
1011
|
+
|
|
1012
|
+
// Stop if the API signals last page, or we got fewer results than requested
|
|
1013
|
+
isLast = response.data?.isLast === true || page.length < pageSize;
|
|
1014
|
+
startAt += page.length;
|
|
1015
|
+
|
|
1016
|
+
// Safety: never exceed max_results
|
|
1017
|
+
if (allTestCases.length >= max_results) break;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const testCases = allTestCases.slice(0, max_results);
|
|
918
1021
|
|
|
919
1022
|
return {
|
|
920
1023
|
content: [{
|
package/src/tool-schemas.ts
CHANGED
|
@@ -196,6 +196,32 @@ export const toolSchemas = [
|
|
|
196
196
|
required: ['project_key', 'name'],
|
|
197
197
|
},
|
|
198
198
|
},
|
|
199
|
+
{
|
|
200
|
+
name: 'get_folders',
|
|
201
|
+
description: 'Get folders from Zephyr Scale. All parameters are optional. If folder_path is provided, returns the full subtree (the folder and all its descendants at every depth) under that path. Otherwise returns all folders matching the filters.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
project_key: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'Jira project key filter (e.g., "PROJ"). Optional.',
|
|
208
|
+
},
|
|
209
|
+
folder_type: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'Folder type filter (optional)',
|
|
212
|
+
enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
|
|
213
|
+
},
|
|
214
|
+
folder_path: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Folder path to filter by (e.g., "/CRM on Wechat"). Returns the matching folder and all folders nested beneath it at any depth. Requires project_key and folder_type to resolve the path.',
|
|
217
|
+
},
|
|
218
|
+
max_results: {
|
|
219
|
+
type: 'number',
|
|
220
|
+
description: 'Maximum total number of folders to return (optional). Defaults to all folders.',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
199
225
|
{
|
|
200
226
|
name: 'get_test_run_cases',
|
|
201
227
|
description: 'Get test case keys from a test run',
|
package/src/utils.ts
CHANGED
|
@@ -11,22 +11,34 @@ export async function resolveFolderIdByPath(
|
|
|
11
11
|
if (segments.length === 0) return null;
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
:
|
|
14
|
+
// Paginate through all folders for the project + type
|
|
15
|
+
const pageSize = 1000;
|
|
16
|
+
const allFolders: Array<{ id: number; parentId: number | null; name: string }> = [];
|
|
17
|
+
let startAt = 0;
|
|
18
|
+
let isLast = false;
|
|
19
|
+
|
|
20
|
+
while (!isLast) {
|
|
21
|
+
const response = await axiosInstance.get('/folders', {
|
|
22
|
+
params: { projectKey, folderType, maxResults: pageSize, startAt },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const page: Array<{ id: number; parentId: number | null; name: string }> =
|
|
26
|
+
Array.isArray(response.data)
|
|
27
|
+
? response.data
|
|
28
|
+
: response.data?.values ?? [];
|
|
29
|
+
|
|
30
|
+
allFolders.push(...page);
|
|
31
|
+
|
|
32
|
+
isLast = response.data?.isLast === true || page.length < pageSize;
|
|
33
|
+
startAt += page.length;
|
|
34
|
+
}
|
|
23
35
|
|
|
24
36
|
// Walk segments top-down, matching by name under the correct parent
|
|
25
37
|
let parentId: number | null = null;
|
|
26
38
|
let matchedId: number | null = null;
|
|
27
39
|
|
|
28
40
|
for (const segment of segments) {
|
|
29
|
-
const match =
|
|
41
|
+
const match = allFolders.find(
|
|
30
42
|
(f) => f.name === segment && (f.parentId ?? null) === parentId
|
|
31
43
|
);
|
|
32
44
|
if (!match) return null;
|