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 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
- This may result in slightly different behavior for some tools, such as `add_test_cases_to_run`.
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.2.9',
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:
@@ -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 addTestCasesToRun(args) {
494
- const { test_run_key, test_case_keys } = args;
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
- // For Data Center, we need to get the current items and then update
497
- if (this.jiraConfig.type === 'datacenter') {
498
- const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
499
- const existingItems = getResponse.data.items || [];
500
- const existingKeys = new Set(existingItems.map((item) => item.testCaseKey));
501
- const newItems = test_case_keys
502
- .filter(key => !existingKeys.has(key))
503
- .map(key => ({ testCaseKey: key, testResultStatus: 'Not Executed' }));
504
- if (newItems.length > 0) {
505
- let response;
506
- if (this.jiraConfig.type === 'datacenter') {
507
- const minimalPayload = {
508
- items: [...existingItems, ...newItems]
509
- };
510
- response = await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`, minimalPayload);
511
- }
512
- else {
513
- const postPayload = { items: newItems.map(item => item.testCaseKey) };
514
- response = await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}/testcases`, postPayload);
515
- }
516
- if (response.status === 200 || response.status === 201 || response.status === 204) {
517
- return {
518
- content: [{ type: 'text', text: `Added ${newItems.length} new test cases to test run ${test_run_key}.` }],
519
- };
520
- }
521
- }
522
- else {
523
- return {
524
- content: [{ type: 'text', text: 'All specified test cases are already in the test run.' }],
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
- // For Cloud, we can just post the new test case keys
530
- const fullPayload = { items: test_case_keys };
531
- const response = await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`, fullPayload);
532
- if (response.status === 200 || response.status === 204) {
533
- return {
534
- content: [{ type: 'text', text: `Successfully updated test cases for test run ${test_run_key}.` }],
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) {
@@ -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.2.9",
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.2.9',
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:
@@ -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
- // For Data Center, we need to get the current items and then update
550
- if (this.jiraConfig.type === 'datacenter') {
551
- const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testrun}/${test_run_key}`);
552
- const existingItems = getResponse.data.items || [];
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
- if (response.status === 200 || response.status === 201 || response.status === 204) {
572
- return {
573
- content: [{ type: 'text', text: `Added ${newItems.length} new test cases to test run ${test_run_key}.` }],
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)}`);
@@ -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 {