zephyr-scale-mcp-server 0.2.9 → 0.3.1
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 -2
- package/build/index.js +3 -1
- package/build/tool-handlers.js +76 -41
- package/build/tool-schemas.js +27 -1
- package/package.json +2 -1
- package/src/index.ts +3 -1
- package/src/tool-handlers.ts +81 -41
- package/src/tool-schemas.ts +27 -1
- package/src/types.ts +7 -0
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ The server automatically detects your Jira environment and uses the appropriate
|
|
|
68
68
|
- **Jira Cloud**: Uses Zephyr Scale API v2.
|
|
69
69
|
- **Jira Data Center**: Uses Zephyr Scale API v1.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Some tools are platform-specific. For example, `add_test_cases_to_run` is only available on Cloud, as the Data Center API (v1) does not support modifying test runs after creation.
|
|
72
72
|
|
|
73
73
|
### Resource System
|
|
74
74
|
The server provides access to various resources through URI schemes:
|
|
@@ -88,11 +88,12 @@ The server provides access to various resources through URI schemes:
|
|
|
88
88
|
- `create_test_run`: Create a new test run.
|
|
89
89
|
- `get_test_run`: Get detailed information about a specific test run.
|
|
90
90
|
- `get_test_run_cases`: Get test case keys from a test run.
|
|
91
|
-
- `add_test_cases_to_run`: Add test cases to an existing test run.
|
|
91
|
+
- `add_test_cases_to_run`: Add test cases to an existing test run. *(Cloud only)*
|
|
92
92
|
|
|
93
93
|
### Test Execution & Search
|
|
94
94
|
- `get_test_execution`: Get detailed individual test execution results.
|
|
95
95
|
- `search_test_cases_by_folder`: Search for test cases in a specific folder.
|
|
96
|
+
- `search_test_runs`: Search for test runs by project key and/or folder path.
|
|
96
97
|
|
|
97
98
|
### Organization
|
|
98
99
|
- `create_folder`: Create a new folder in Zephyr Scale.
|
package/build/index.js
CHANGED
|
@@ -14,7 +14,7 @@ class ZephyrServer {
|
|
|
14
14
|
const jiraConfig = createJiraConfig();
|
|
15
15
|
this.server = new Server({
|
|
16
16
|
name: 'zephyr-server',
|
|
17
|
-
version: '0.
|
|
17
|
+
version: '0.3.1',
|
|
18
18
|
}, {
|
|
19
19
|
capabilities: {
|
|
20
20
|
tools: {},
|
|
@@ -63,6 +63,8 @@ class ZephyrServer {
|
|
|
63
63
|
return await this.toolHandlers.getTestExecution(args);
|
|
64
64
|
case 'search_test_cases_by_folder':
|
|
65
65
|
return await this.toolHandlers.searchTestCasesByFolder(args);
|
|
66
|
+
case 'search_test_runs':
|
|
67
|
+
return await this.toolHandlers.searchTestRuns(args);
|
|
66
68
|
case 'add_test_cases_to_run':
|
|
67
69
|
return await this.toolHandlers.addTestCasesToRun(args);
|
|
68
70
|
default:
|
package/build/tool-handlers.js
CHANGED
|
@@ -490,50 +490,85 @@ export class ZephyrToolHandlers {
|
|
|
490
490
|
throw new McpError(ErrorCode.InternalError, `Failed to search test cases by folder: ${errorMessage}`);
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
-
async
|
|
494
|
-
const {
|
|
493
|
+
async searchTestRuns(args) {
|
|
494
|
+
const { project_key, folder, max_results = 200, fields } = args;
|
|
495
|
+
// Build query string per Zephyr Scale API docs
|
|
496
|
+
const queryParts = [];
|
|
497
|
+
if (project_key)
|
|
498
|
+
queryParts.push(`projectKey = "${project_key}"`);
|
|
499
|
+
if (folder)
|
|
500
|
+
queryParts.push(`folder = "${folder}"`);
|
|
501
|
+
if (queryParts.length === 0) {
|
|
502
|
+
throw new McpError(ErrorCode.InvalidParams, 'At least one of project_key or folder must be provided.');
|
|
503
|
+
}
|
|
504
|
+
const query = queryParts.join(' AND ');
|
|
505
|
+
const params = { query, maxResults: max_results };
|
|
506
|
+
if (fields)
|
|
507
|
+
params.fields = fields;
|
|
508
|
+
const searchEndpoint = this.jiraConfig.type === 'cloud'
|
|
509
|
+
? '/testruns/search'
|
|
510
|
+
: '/rest/atm/1.0/testrun/search';
|
|
495
511
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
512
|
+
const response = await this.axiosInstance.get(searchEndpoint, { params });
|
|
513
|
+
let testRuns = [];
|
|
514
|
+
if (Array.isArray(response.data)) {
|
|
515
|
+
testRuns = response.data;
|
|
516
|
+
}
|
|
517
|
+
else if (response.data.values && Array.isArray(response.data.values)) {
|
|
518
|
+
testRuns = response.data.values;
|
|
519
|
+
}
|
|
520
|
+
else if (response.data.results && Array.isArray(response.data.results)) {
|
|
521
|
+
testRuns = response.data.results;
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
content: [
|
|
525
|
+
{
|
|
526
|
+
type: 'text',
|
|
527
|
+
text: `✅ Found ${testRuns.length} test run(s) matching query "${query}":\n${JSON.stringify({
|
|
528
|
+
query,
|
|
529
|
+
totalCount: testRuns.length,
|
|
530
|
+
testRuns: testRuns.map((tr) => ({
|
|
531
|
+
key: tr.key,
|
|
532
|
+
name: tr.name,
|
|
533
|
+
status: tr.status,
|
|
534
|
+
folder: tr.folder,
|
|
535
|
+
testCaseCount: tr.testCaseCount,
|
|
536
|
+
issueKey: tr.issueKey,
|
|
537
|
+
}))
|
|
538
|
+
}, null, 2)}`,
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
let errorMessage = 'Unknown error';
|
|
545
|
+
if (error instanceof Error && 'response' in error) {
|
|
546
|
+
const axiosError = error;
|
|
547
|
+
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
548
|
+
}
|
|
549
|
+
else if (error instanceof Error) {
|
|
550
|
+
errorMessage = error.message;
|
|
527
551
|
}
|
|
528
552
|
else {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
553
|
+
errorMessage = String(error);
|
|
554
|
+
}
|
|
555
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${errorMessage}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async addTestCasesToRun(args) {
|
|
559
|
+
const { test_run_key, test_case_keys } = args;
|
|
560
|
+
if (this.jiraConfig.type === 'datacenter') {
|
|
561
|
+
throw new McpError(ErrorCode.InvalidRequest, 'add_test_cases_to_run is only supported on Zephyr Scale Cloud. The Data Center API (v1) does not provide an endpoint to modify test runs after creation.');
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
const payload = {
|
|
565
|
+
items: test_case_keys.map(key => ({ testCaseKey: key }))
|
|
566
|
+
};
|
|
567
|
+
const response = await this.axiosInstance.post(`/testcycles/${test_run_key}/testcases`, payload);
|
|
568
|
+
if (response.status === 200 || response.status === 201 || response.status === 204) {
|
|
569
|
+
return {
|
|
570
|
+
content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
|
|
571
|
+
};
|
|
537
572
|
}
|
|
538
573
|
}
|
|
539
574
|
catch (error) {
|
package/build/tool-schemas.js
CHANGED
|
@@ -347,9 +347,35 @@ export const toolSchemas = [
|
|
|
347
347
|
required: ['project_key', 'folder_path'],
|
|
348
348
|
},
|
|
349
349
|
},
|
|
350
|
+
{
|
|
351
|
+
name: 'search_test_runs',
|
|
352
|
+
description: 'Search for test runs using a query. Supports filtering by projectKey and/or folder path.',
|
|
353
|
+
inputSchema: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {
|
|
356
|
+
project_key: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
description: 'Project key to filter by (e.g., "PROJ"). Can be a single key or omitted if using folder only.',
|
|
359
|
+
},
|
|
360
|
+
folder: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
description: 'Folder path to filter test runs by (e.g., "/MyFolder/SubFolder")',
|
|
363
|
+
},
|
|
364
|
+
max_results: {
|
|
365
|
+
type: 'number',
|
|
366
|
+
description: 'Maximum number of results to return (optional, default 200)',
|
|
367
|
+
default: 200,
|
|
368
|
+
},
|
|
369
|
+
fields: {
|
|
370
|
+
type: 'string',
|
|
371
|
+
description: 'Comma-separated list of fields to include in the response (optional, e.g., "key,name,status,folder"). If not set, all fields are returned.',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
350
376
|
{
|
|
351
377
|
name: 'add_test_cases_to_run',
|
|
352
|
-
description: 'Add test cases to an existing test run',
|
|
378
|
+
description: 'Add test cases to an existing test run (Cloud only — not supported on Data Center)',
|
|
353
379
|
inputSchema: {
|
|
354
380
|
type: 'object',
|
|
355
381
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"test": "node test/run-tests.cjs",
|
|
23
23
|
"test:unit": "node test/zephyr-server.test.cjs",
|
|
24
24
|
"test:integration": "node test/integration.test.cjs",
|
|
25
|
+
"report:weekly": "node scripts/weekly-report.cjs",
|
|
25
26
|
"prepublishOnly": "npm run build"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ class ZephyrServer {
|
|
|
25
25
|
this.server = new Server(
|
|
26
26
|
{
|
|
27
27
|
name: 'zephyr-server',
|
|
28
|
-
version: '0.
|
|
28
|
+
version: '0.3.1',
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
capabilities: {
|
|
@@ -83,6 +83,8 @@ class ZephyrServer {
|
|
|
83
83
|
return await this.toolHandlers.getTestExecution(args);
|
|
84
84
|
case 'search_test_cases_by_folder':
|
|
85
85
|
return await this.toolHandlers.searchTestCasesByFolder(args as any);
|
|
86
|
+
case 'search_test_runs':
|
|
87
|
+
return await this.toolHandlers.searchTestRuns(args as any);
|
|
86
88
|
case 'add_test_cases_to_run':
|
|
87
89
|
return await this.toolHandlers.addTestCasesToRun(args as any);
|
|
88
90
|
default:
|
package/src/tool-handlers.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TestRunArgs,
|
|
8
8
|
SearchTestCasesArgs,
|
|
9
9
|
AddTestCasesToRunArgs,
|
|
10
|
+
SearchTestRunsArgs,
|
|
10
11
|
JiraConfig
|
|
11
12
|
} from './types.js';
|
|
12
13
|
import { convertToGherkin, customPriorityMapping, priorityMapping } from './utils.js';
|
|
@@ -542,52 +543,91 @@ export class ZephyrToolHandlers {
|
|
|
542
543
|
}
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
async searchTestRuns(args: SearchTestRunsArgs) {
|
|
547
|
+
const { project_key, folder, max_results = 200, fields } = args;
|
|
548
|
+
|
|
549
|
+
// Build query string per Zephyr Scale API docs
|
|
550
|
+
const queryParts: string[] = [];
|
|
551
|
+
if (project_key) queryParts.push(`projectKey = "${project_key}"`);
|
|
552
|
+
if (folder) queryParts.push(`folder = "${folder}"`);
|
|
553
|
+
|
|
554
|
+
if (queryParts.length === 0) {
|
|
555
|
+
throw new McpError(ErrorCode.InvalidParams, 'At least one of project_key or folder must be provided.');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const query = queryParts.join(' AND ');
|
|
559
|
+
const params: Record<string, any> = { query, maxResults: max_results };
|
|
560
|
+
if (fields) params.fields = fields;
|
|
561
|
+
|
|
562
|
+
const searchEndpoint = this.jiraConfig.type === 'cloud'
|
|
563
|
+
? '/testruns/search'
|
|
564
|
+
: '/rest/atm/1.0/testrun/search';
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
const response = await this.axiosInstance.get(searchEndpoint, { params });
|
|
568
|
+
|
|
569
|
+
let testRuns: any[] = [];
|
|
570
|
+
if (Array.isArray(response.data)) {
|
|
571
|
+
testRuns = response.data;
|
|
572
|
+
} else if (response.data.values && Array.isArray(response.data.values)) {
|
|
573
|
+
testRuns = response.data.values;
|
|
574
|
+
} else if (response.data.results && Array.isArray(response.data.results)) {
|
|
575
|
+
testRuns = response.data.results;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
content: [
|
|
580
|
+
{
|
|
581
|
+
type: 'text',
|
|
582
|
+
text: `✅ Found ${testRuns.length} test run(s) matching query "${query}":\n${JSON.stringify({
|
|
583
|
+
query,
|
|
584
|
+
totalCount: testRuns.length,
|
|
585
|
+
testRuns: testRuns.map((tr: any) => ({
|
|
586
|
+
key: tr.key,
|
|
587
|
+
name: tr.name,
|
|
588
|
+
status: tr.status,
|
|
589
|
+
folder: tr.folder,
|
|
590
|
+
testCaseCount: tr.testCaseCount,
|
|
591
|
+
issueKey: tr.issueKey,
|
|
592
|
+
}))
|
|
593
|
+
}, null, 2)}`,
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
};
|
|
597
|
+
} catch (error) {
|
|
598
|
+
let errorMessage = 'Unknown error';
|
|
599
|
+
if (error instanceof Error && 'response' in error) {
|
|
600
|
+
const axiosError = error as any;
|
|
601
|
+
errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
|
|
602
|
+
} else if (error instanceof Error) {
|
|
603
|
+
errorMessage = error.message;
|
|
604
|
+
} else {
|
|
605
|
+
errorMessage = String(error);
|
|
606
|
+
}
|
|
607
|
+
throw new McpError(ErrorCode.InternalError, `Failed to search test runs: ${errorMessage}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
545
611
|
async addTestCasesToRun(args: AddTestCasesToRunArgs) {
|
|
546
612
|
const { test_run_key, test_case_keys } = args;
|
|
547
613
|
|
|
614
|
+
if (this.jiraConfig.type === 'datacenter') {
|
|
615
|
+
throw new McpError(
|
|
616
|
+
ErrorCode.InvalidRequest,
|
|
617
|
+
'add_test_cases_to_run is only supported on Zephyr Scale Cloud. The Data Center API (v1) does not provide an endpoint to modify test runs after creation.'
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
548
621
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const existingKeys = new Set(existingItems.map((item: any) => item.testCaseKey));
|
|
554
|
-
|
|
555
|
-
const newItems = test_case_keys
|
|
556
|
-
.filter(key => !existingKeys.has(key))
|
|
557
|
-
.map(key => ({ testCaseKey: key, testResultStatus: 'Not Executed' }));
|
|
558
|
-
|
|
559
|
-
if (newItems.length > 0) {
|
|
560
|
-
let response;
|
|
561
|
-
if (this.jiraConfig.type === 'datacenter') {
|
|
562
|
-
const minimalPayload = {
|
|
563
|
-
items: [...existingItems, ...newItems]
|
|
564
|
-
};
|
|
565
|
-
response = await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`, minimalPayload);
|
|
566
|
-
} else {
|
|
567
|
-
const postPayload = { items: newItems.map(item => item.testCaseKey) };
|
|
568
|
-
response = await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`, postPayload);
|
|
569
|
-
}
|
|
622
|
+
const payload = {
|
|
623
|
+
items: test_case_keys.map(key => ({ testCaseKey: key }))
|
|
624
|
+
};
|
|
625
|
+
const response = await this.axiosInstance.post(`/testcycles/${test_run_key}/testcases`, payload);
|
|
570
626
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
} else {
|
|
577
|
-
return {
|
|
578
|
-
content: [{ type: 'text', text: 'All specified test cases are already in the test run.' }],
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
// For Cloud, we can just post the new test case keys
|
|
583
|
-
const fullPayload = { items: test_case_keys };
|
|
584
|
-
const response = await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`, fullPayload);
|
|
585
|
-
|
|
586
|
-
if (response.status === 200 || response.status === 204) {
|
|
587
|
-
return {
|
|
588
|
-
content: [{ type: 'text', text: `Successfully updated test cases for test run ${test_run_key}.` }],
|
|
589
|
-
};
|
|
590
|
-
}
|
|
627
|
+
if (response.status === 200 || response.status === 201 || response.status === 204) {
|
|
628
|
+
return {
|
|
629
|
+
content: [{ type: 'text', text: `Added ${test_case_keys.length} test case(s) to test run ${test_run_key}.` }],
|
|
630
|
+
};
|
|
591
631
|
}
|
|
592
632
|
} catch (error) {
|
|
593
633
|
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${error instanceof Error ? error.message : String(error)}`);
|
package/src/tool-schemas.ts
CHANGED
|
@@ -347,9 +347,35 @@ export const toolSchemas = [
|
|
|
347
347
|
required: ['project_key', 'folder_path'],
|
|
348
348
|
},
|
|
349
349
|
},
|
|
350
|
+
{
|
|
351
|
+
name: 'search_test_runs',
|
|
352
|
+
description: 'Search for test runs using a query. Supports filtering by projectKey and/or folder path.',
|
|
353
|
+
inputSchema: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {
|
|
356
|
+
project_key: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
description: 'Project key to filter by (e.g., "PROJ"). Can be a single key or omitted if using folder only.',
|
|
359
|
+
},
|
|
360
|
+
folder: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
description: 'Folder path to filter test runs by (e.g., "/MyFolder/SubFolder")',
|
|
363
|
+
},
|
|
364
|
+
max_results: {
|
|
365
|
+
type: 'number',
|
|
366
|
+
description: 'Maximum number of results to return (optional, default 200)',
|
|
367
|
+
default: 200,
|
|
368
|
+
},
|
|
369
|
+
fields: {
|
|
370
|
+
type: 'string',
|
|
371
|
+
description: 'Comma-separated list of fields to include in the response (optional, e.g., "key,name,status,folder"). If not set, all fields are returned.',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
350
376
|
{
|
|
351
377
|
name: 'add_test_cases_to_run',
|
|
352
|
-
description: 'Add test cases to an existing test run',
|
|
378
|
+
description: 'Add test cases to an existing test run (Cloud only — not supported on Data Center)',
|
|
353
379
|
inputSchema: {
|
|
354
380
|
type: 'object',
|
|
355
381
|
properties: {
|
package/src/types.ts
CHANGED
|
@@ -79,6 +79,13 @@ export interface AddTestCasesToRunArgs {
|
|
|
79
79
|
test_case_keys: string[];
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export interface SearchTestRunsArgs {
|
|
83
|
+
project_key?: string;
|
|
84
|
+
folder?: string;
|
|
85
|
+
max_results?: number;
|
|
86
|
+
fields?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
export type JiraType = 'cloud' | 'datacenter';
|
|
83
90
|
|
|
84
91
|
export interface ApiEndpoints {
|