zephyr-scale-mcp-server 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yong Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Zephyr Scale MCP Server
2
+
3
+ Model Context Protocol server for Zephyr Scale test management. Create, read, and manage test cases through the Atlassian REST API.
4
+
5
+ ## Quick Start
6
+
7
+ ### Option 1: Install from npm
8
+ ```bash
9
+ npm install -g zephyr-scale-mcp-server
10
+ ```
11
+
12
+ ### Option 2: Build locally
13
+ ```bash
14
+ # Clone and build
15
+ git clone <repository-url>
16
+ cd zephyr-scale-mcp-server
17
+ npm install
18
+ npm run build
19
+
20
+ # Configure environment
21
+ export ZEPHYR_API_KEY="your-api-token"
22
+ export ZEPHYR_BASE_URL="https://your-company.atlassian.net"
23
+
24
+ # Test the server
25
+ node build/index.js
26
+ ```
27
+
28
+ ## MCP Configuration
29
+
30
+ ### For npm installation:
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "zephyr-server": {
35
+ "command": "zephyr-scale-mcp",
36
+ "env": {
37
+ "ZEPHYR_API_KEY": "your-api-token",
38
+ "ZEPHYR_BASE_URL": "https://your-company.atlassian.net"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### For local build:
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "zephyr-server": {
50
+ "command": "node",
51
+ "args": ["/path/to/zephyr-scale-mcp-server/build/index.js"],
52
+ "env": {
53
+ "ZEPHYR_API_KEY": "your-api-token",
54
+ "ZEPHYR_BASE_URL": "https://your-company.atlassian.net"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Available Tools
62
+
63
+ - `create_test_case` - Create STEP_BY_STEP or PLAIN_TEXT test cases
64
+ - `create_test_case_with_bdd` - Create BDD/Gherkin test cases
65
+ - `get_test_case` - Get test case details
66
+ - `update_test_case_bdd` - Update BDD test cases
67
+ - `create_folder` - Create test case folders
68
+ - `get_test_run_cases` - Get test cases from test runs
69
+
70
+ ## Examples
71
+
72
+ ### Simple Test Case
73
+ ```json
74
+ {
75
+ "project_key": "PROJ",
76
+ "name": "Login Test"
77
+ }
78
+ ```
79
+
80
+ ### Step-by-Step Test Case
81
+ ```json
82
+ {
83
+ "project_key": "PROJ",
84
+ "name": "User Login Flow",
85
+ "test_script_type": "STEP_BY_STEP",
86
+ "steps": [
87
+ {
88
+ "description": "Navigate to login page",
89
+ "testData": "URL: https://app.example.com/login",
90
+ "expectedResult": "Login form is displayed"
91
+ }
92
+ ]
93
+ }
94
+ ```
95
+
96
+ ### BDD Test Case
97
+ ```json
98
+ {
99
+ "project_key": "PROJ",
100
+ "name": "User Authentication",
101
+ "bdd_content": "**Given** a user with valid credentials\n**When** the user attempts to log in\n**Then** the user should be authenticated successfully"
102
+ }
103
+ ```
104
+
105
+ ## Authentication
106
+
107
+ Get your API token from:
108
+ 1. Atlassian account settings
109
+ 2. Security → API tokens
110
+ 3. Create new token for Zephyr Scale
111
+
112
+ ## License
113
+
114
+ MIT
package/build/index.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
+ import axios from 'axios';
6
+ const BASE_URL = process.env.ZEPHYR_BASE_URL;
7
+ const ACCESS_TOKEN = process.env.ZEPHYR_API_KEY;
8
+ if (!ACCESS_TOKEN) {
9
+ throw new Error('ZEPHYR_API_KEY environment variable is required');
10
+ }
11
+ const headers = {
12
+ 'Accept': '*/*',
13
+ 'Authorization': `Bearer ${ACCESS_TOKEN}`,
14
+ 'Content-Type': 'application/json',
15
+ };
16
+ class ZephyrServer {
17
+ server;
18
+ axiosInstance;
19
+ constructor() {
20
+ this.server = new Server({
21
+ name: 'zephyr-server',
22
+ version: '0.1.0',
23
+ }, {
24
+ capabilities: {
25
+ tools: {},
26
+ },
27
+ });
28
+ this.axiosInstance = axios.create({
29
+ baseURL: BASE_URL,
30
+ headers,
31
+ });
32
+ this.setupToolHandlers();
33
+ this.server.onerror = (error) => console.error('[MCP Error]', error);
34
+ process.on('SIGINT', async () => {
35
+ await this.server.close();
36
+ process.exit(0);
37
+ });
38
+ }
39
+ setupToolHandlers() {
40
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
41
+ tools: [
42
+ {
43
+ name: 'get_test_case',
44
+ description: 'Get detailed information about a specific test case',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ test_case_key: {
49
+ type: 'string',
50
+ description: 'Test case key (e.g., PROJ-T123)',
51
+ },
52
+ },
53
+ required: ['test_case_key'],
54
+ },
55
+ },
56
+ {
57
+ name: 'create_test_case',
58
+ description: 'Create a new test case with STEP_BY_STEP or PLAIN_TEXT content',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ project_key: {
63
+ type: 'string',
64
+ description: 'Project key (required)',
65
+ },
66
+ name: {
67
+ type: 'string',
68
+ description: 'Test case name (required)',
69
+ },
70
+ test_script_type: {
71
+ type: 'string',
72
+ description: 'Type of test script',
73
+ enum: ['STEP_BY_STEP', 'PLAIN_TEXT'],
74
+ default: 'STEP_BY_STEP',
75
+ },
76
+ steps: {
77
+ type: 'array',
78
+ description: 'Test steps for STEP_BY_STEP (array of objects with description, testData, expectedResult)',
79
+ items: {
80
+ type: 'object',
81
+ properties: {
82
+ description: { type: 'string' },
83
+ testData: { type: 'string' },
84
+ expectedResult: { type: 'string' },
85
+ testCaseKey: { type: 'string' }
86
+ }
87
+ }
88
+ },
89
+ plain_text: {
90
+ type: 'string',
91
+ description: 'Plain text content for PLAIN_TEXT type',
92
+ },
93
+ folder: {
94
+ type: 'string',
95
+ description: 'Folder path (optional)',
96
+ },
97
+ status: {
98
+ type: 'string',
99
+ description: 'Test case status (optional)',
100
+ enum: ['Draft', 'Approved', 'Deprecated'],
101
+ default: 'Draft',
102
+ },
103
+ priority: {
104
+ type: 'string',
105
+ description: 'Test case priority (optional)',
106
+ enum: ['High', 'Medium', 'Low'],
107
+ default: 'High',
108
+ },
109
+ precondition: {
110
+ type: 'string',
111
+ description: 'Test precondition (optional)',
112
+ },
113
+ objective: {
114
+ type: 'string',
115
+ description: 'Test objective (optional)',
116
+ },
117
+ component: {
118
+ type: 'string',
119
+ description: 'Component name (optional)',
120
+ },
121
+ owner: {
122
+ type: 'string',
123
+ description: 'Test case owner (optional)',
124
+ },
125
+ estimated_time: {
126
+ type: 'number',
127
+ description: 'Estimated time in milliseconds (optional)',
128
+ },
129
+ labels: {
130
+ type: 'array',
131
+ description: 'Array of labels (optional)',
132
+ items: { type: 'string' }
133
+ },
134
+ issue_links: {
135
+ type: 'array',
136
+ description: 'Array of issue links (optional)',
137
+ items: { type: 'string' }
138
+ },
139
+ custom_fields: {
140
+ type: 'object',
141
+ description: 'Custom fields object (optional)',
142
+ },
143
+ },
144
+ required: ['project_key', 'name'],
145
+ },
146
+ },
147
+ {
148
+ name: 'create_test_case_with_bdd',
149
+ description: 'Create a new test case with BDD content',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ project_key: {
154
+ type: 'string',
155
+ description: 'Project key (required)',
156
+ },
157
+ name: {
158
+ type: 'string',
159
+ description: 'Test case name (required)',
160
+ },
161
+ bdd_content: {
162
+ type: 'string',
163
+ description: 'BDD content in markdown format',
164
+ },
165
+ folder: {
166
+ type: 'string',
167
+ description: 'Folder name (optional)',
168
+ },
169
+ priority: {
170
+ type: 'string',
171
+ description: 'Test case priority (High, Medium, Low)',
172
+ enum: ['High', 'Medium', 'Low'],
173
+ },
174
+ },
175
+ required: ['project_key', 'name', 'bdd_content'],
176
+ },
177
+ },
178
+ {
179
+ name: 'update_test_case_bdd',
180
+ description: 'Update an existing test case with BDD content',
181
+ inputSchema: {
182
+ type: 'object',
183
+ properties: {
184
+ test_case_key: {
185
+ type: 'string',
186
+ description: 'Test case key to update',
187
+ },
188
+ bdd_content: {
189
+ type: 'string',
190
+ description: 'BDD content in markdown format',
191
+ },
192
+ },
193
+ required: ['test_case_key', 'bdd_content'],
194
+ },
195
+ },
196
+ {
197
+ name: 'create_folder',
198
+ description: 'Create a new folder in Zephyr Scale',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ project_key: {
203
+ type: 'string',
204
+ description: 'Project key (required)',
205
+ },
206
+ name: {
207
+ type: 'string',
208
+ description: 'Folder name (required)',
209
+ },
210
+ parent_folder_path: {
211
+ type: 'string',
212
+ description: 'Parent folder path (optional)',
213
+ },
214
+ folder_type: {
215
+ type: 'string',
216
+ description: 'Type of folder',
217
+ enum: ['TEST_CASE', 'TEST_PLAN'],
218
+ default: 'TEST_CASE',
219
+ },
220
+ },
221
+ required: ['project_key', 'name'],
222
+ },
223
+ },
224
+ {
225
+ name: 'get_test_run_cases',
226
+ description: 'Get test case keys from a test run',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: {
230
+ test_run_key: {
231
+ type: 'string',
232
+ description: 'Test run key (e.g., PROJ-C123)',
233
+ },
234
+ },
235
+ required: ['test_run_key'],
236
+ },
237
+ },
238
+ ],
239
+ }));
240
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
241
+ try {
242
+ switch (request.params.name) {
243
+ case 'get_test_case':
244
+ return await this.getTestCase(request.params.arguments);
245
+ case 'create_test_case':
246
+ return await this.createTestCase(request.params.arguments);
247
+ case 'create_test_case_with_bdd':
248
+ return await this.createTestCaseWithBdd(request.params.arguments);
249
+ case 'update_test_case_bdd':
250
+ return await this.updateTestCaseBdd(request.params.arguments);
251
+ case 'create_folder':
252
+ return await this.createFolder(request.params.arguments);
253
+ case 'get_test_run_cases':
254
+ return await this.getTestRunCases(request.params.arguments);
255
+ default:
256
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
257
+ }
258
+ }
259
+ catch (error) {
260
+ if (error instanceof McpError) {
261
+ throw error;
262
+ }
263
+ return {
264
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
265
+ isError: true,
266
+ };
267
+ }
268
+ });
269
+ }
270
+ async getTestCase(args) {
271
+ const { test_case_key } = args;
272
+ try {
273
+ const response = await this.axiosInstance.get(`/rest/atm/1.0/testcase/${test_case_key}`);
274
+ return {
275
+ content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
276
+ };
277
+ }
278
+ catch (error) {
279
+ throw new McpError(ErrorCode.InternalError, `Failed to get test case: ${error instanceof Error ? error.message : String(error)}`);
280
+ }
281
+ }
282
+ convertToGherkin(bddContent) {
283
+ const bddLines = [];
284
+ const lines = bddContent.split('\n');
285
+ for (const line of lines) {
286
+ const trimmedLine = line.trim();
287
+ if (!trimmedLine || trimmedLine.startsWith('---'))
288
+ continue;
289
+ if (trimmedLine.startsWith('**Given**')) {
290
+ bddLines.push(`Given ${trimmedLine.replace('**Given**', '').trim()}`);
291
+ }
292
+ else if (trimmedLine.startsWith('**When**')) {
293
+ bddLines.push(`When ${trimmedLine.replace('**When**', '').trim()}`);
294
+ }
295
+ else if (trimmedLine.startsWith('**Then**')) {
296
+ bddLines.push(`Then ${trimmedLine.replace('**Then**', '').trim()}`);
297
+ }
298
+ else if (trimmedLine.startsWith('**And**')) {
299
+ bddLines.push(`And ${trimmedLine.replace('**And**', '').trim()}`);
300
+ }
301
+ else if (trimmedLine.startsWith('Given ') || trimmedLine.startsWith('When ') ||
302
+ trimmedLine.startsWith('Then ') || trimmedLine.startsWith('And ')) {
303
+ bddLines.push(trimmedLine);
304
+ }
305
+ }
306
+ return bddLines.length > 0 ? ' ' + bddLines.join('\n ') : '';
307
+ }
308
+ async createTestCase(args) {
309
+ const { project_key, name, test_script_type = 'STEP_BY_STEP', steps, plain_text, folder, status = 'Draft', priority = 'High', precondition, objective, component, owner, estimated_time, labels, issue_links, custom_fields } = args;
310
+ // Map priority to custom priority
311
+ const customPriorityMapping = {
312
+ 'High': 'P0',
313
+ 'Medium': 'P1',
314
+ 'Low': 'P2'
315
+ };
316
+ // Build the basic payload
317
+ const payload = {
318
+ projectKey: project_key,
319
+ name: name,
320
+ status: status,
321
+ priority: priority,
322
+ customFields: {
323
+ 'Type': 'Functional',
324
+ 'Priority': customPriorityMapping[priority] || 'P0',
325
+ 'Execution Type': 'Manual - To Be Automated'
326
+ }
327
+ };
328
+ // Add optional fields
329
+ if (folder)
330
+ payload.folder = folder;
331
+ if (precondition)
332
+ payload.precondition = precondition;
333
+ if (objective)
334
+ payload.objective = objective;
335
+ if (component)
336
+ payload.component = component;
337
+ if (owner)
338
+ payload.owner = owner;
339
+ if (estimated_time)
340
+ payload.estimatedTime = estimated_time;
341
+ if (labels && labels.length > 0)
342
+ payload.labels = labels;
343
+ if (issue_links && issue_links.length > 0)
344
+ payload.issueLinks = issue_links;
345
+ // Merge custom fields if provided
346
+ if (custom_fields) {
347
+ payload.customFields = { ...payload.customFields, ...custom_fields };
348
+ }
349
+ // Handle test script based on type
350
+ if (test_script_type === 'STEP_BY_STEP' && steps && steps.length > 0) {
351
+ payload.testScript = {
352
+ type: 'STEP_BY_STEP',
353
+ steps: steps.map((step) => {
354
+ const stepObj = {};
355
+ if (step.description)
356
+ stepObj.description = step.description;
357
+ if (step.testData)
358
+ stepObj.testData = step.testData;
359
+ if (step.expectedResult)
360
+ stepObj.expectedResult = step.expectedResult;
361
+ if (step.testCaseKey)
362
+ stepObj.testCaseKey = step.testCaseKey;
363
+ return stepObj;
364
+ })
365
+ };
366
+ }
367
+ else if (test_script_type === 'PLAIN_TEXT' && plain_text) {
368
+ payload.testScript = {
369
+ type: 'PLAIN_TEXT',
370
+ text: plain_text
371
+ };
372
+ }
373
+ try {
374
+ const response = await this.axiosInstance.post('/rest/atm/1.0/testcase', payload);
375
+ if (response.status === 201) {
376
+ const testKey = response.data.key || 'Unknown';
377
+ return {
378
+ content: [
379
+ {
380
+ type: 'text',
381
+ text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({
382
+ key: testKey,
383
+ type: test_script_type,
384
+ steps: test_script_type === 'STEP_BY_STEP' ? steps?.length || 0 : undefined,
385
+ plainText: test_script_type === 'PLAIN_TEXT' ? !!plain_text : undefined
386
+ }, null, 2)}`,
387
+ },
388
+ ],
389
+ };
390
+ }
391
+ else {
392
+ throw new Error(`Unexpected status code: ${response.status}`);
393
+ }
394
+ }
395
+ catch (error) {
396
+ let errorMessage = 'Unknown error';
397
+ if (error instanceof Error && 'response' in error) {
398
+ const axiosError = error;
399
+ errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
400
+ }
401
+ else if (error instanceof Error) {
402
+ errorMessage = error.message;
403
+ }
404
+ else {
405
+ errorMessage = String(error);
406
+ }
407
+ throw new McpError(ErrorCode.InternalError, `Failed to create test case: ${errorMessage}`);
408
+ }
409
+ }
410
+ async createTestCaseWithBdd(args) {
411
+ const { project_key, name, bdd_content, folder, priority = 'High' } = args;
412
+ const priorityMapping = {
413
+ 'High': 'High',
414
+ 'Medium': 'High',
415
+ 'Low': 'High'
416
+ };
417
+ const customPriorityMapping = {
418
+ 'High': 'P0',
419
+ 'Medium': 'P1',
420
+ 'Low': 'P2'
421
+ };
422
+ const payload = {
423
+ projectKey: project_key,
424
+ name: name,
425
+ status: 'Draft',
426
+ priority: priorityMapping[priority] || 'High',
427
+ customFields: {
428
+ 'Type': 'Functional',
429
+ 'Priority': customPriorityMapping[priority] || 'P0',
430
+ 'Execution Type': 'Manual - To Be Automated'
431
+ }
432
+ };
433
+ if (folder) {
434
+ payload.folder = folder;
435
+ }
436
+ const gherkinContent = this.convertToGherkin(bdd_content);
437
+ if (gherkinContent) {
438
+ payload.testScript = {
439
+ type: 'BDD',
440
+ text: gherkinContent
441
+ };
442
+ }
443
+ try {
444
+ const response = await this.axiosInstance.post('/rest/atm/1.0/testcase', payload);
445
+ if (response.status === 201) {
446
+ const testKey = response.data.key || 'Unknown';
447
+ return {
448
+ content: [
449
+ {
450
+ type: 'text',
451
+ text: `✅ Test case with BDD created successfully: ${testKey}\n${JSON.stringify({ key: testKey }, null, 2)}`,
452
+ },
453
+ ],
454
+ };
455
+ }
456
+ else {
457
+ throw new Error(`Unexpected status code: ${response.status}`);
458
+ }
459
+ }
460
+ catch (error) {
461
+ throw new McpError(ErrorCode.InternalError, `Failed to create test case with BDD: ${error instanceof Error ? error.message : String(error)}`);
462
+ }
463
+ }
464
+ async updateTestCaseBdd(args) {
465
+ const { test_case_key, bdd_content } = args;
466
+ try {
467
+ // First get the current test case
468
+ const getResponse = await this.axiosInstance.get(`/rest/atm/1.0/testcase/${test_case_key}`);
469
+ if (getResponse.status !== 200) {
470
+ throw new Error(`Failed to get test case ${test_case_key}`);
471
+ }
472
+ const gherkinContent = this.convertToGherkin(bdd_content);
473
+ const payload = {
474
+ testScript: {
475
+ type: 'BDD',
476
+ text: gherkinContent
477
+ }
478
+ };
479
+ const updateResponse = await this.axiosInstance.put(`/rest/atm/1.0/testcase/${test_case_key}`, payload);
480
+ if (updateResponse.status === 200) {
481
+ return {
482
+ content: [
483
+ {
484
+ type: 'text',
485
+ text: `✅ Updated ${test_case_key} with BDD content successfully`,
486
+ },
487
+ ],
488
+ };
489
+ }
490
+ else {
491
+ throw new Error(`Failed to update ${test_case_key}: ${updateResponse.status}`);
492
+ }
493
+ }
494
+ catch (error) {
495
+ throw new McpError(ErrorCode.InternalError, `Failed to update test case BDD: ${error instanceof Error ? error.message : String(error)}`);
496
+ }
497
+ }
498
+ async createFolder(args) {
499
+ const { project_key, name, parent_folder_path, folder_type = 'TEST_CASE' } = args;
500
+ let folderName = name;
501
+ if (parent_folder_path && !name.startsWith('/')) {
502
+ const parentPath = parent_folder_path.startsWith('/') ? parent_folder_path : `/${parent_folder_path}`;
503
+ folderName = `${parentPath}/${name}`;
504
+ }
505
+ else if (!name.startsWith('/')) {
506
+ folderName = `/${name}`;
507
+ }
508
+ const payload = {
509
+ projectKey: project_key,
510
+ name: folderName,
511
+ type: folder_type,
512
+ };
513
+ try {
514
+ const response = await this.axiosInstance.post('/rest/atm/1.0/folder', payload);
515
+ return {
516
+ content: [
517
+ {
518
+ type: 'text',
519
+ text: `✅ Folder created successfully: ${response.data.name} (ID: ${response.data.id})\n${JSON.stringify(response.data, null, 2)}`,
520
+ },
521
+ ],
522
+ };
523
+ }
524
+ catch (error) {
525
+ throw new McpError(ErrorCode.InternalError, `Failed to create folder: ${error instanceof Error ? error.message : String(error)}`);
526
+ }
527
+ }
528
+ async getTestRunCases(args) {
529
+ const { test_run_key } = args;
530
+ try {
531
+ const response = await this.axiosInstance.get(`/rest/atm/1.0/testrun/${test_run_key}`);
532
+ if (response.status === 200) {
533
+ const data = response.data;
534
+ const testCaseKeys = data.items.map((item) => item.testCaseKey);
535
+ const statuses = data.items.map((item) => item.status);
536
+ const runIds = data.items.map((item) => item.id);
537
+ return {
538
+ content: [
539
+ {
540
+ type: 'text',
541
+ text: `✅ Retrieved test cases from ${test_run_key}:\nTest Case Keys: ${JSON.stringify(testCaseKeys, null, 2)}\nStatuses: ${JSON.stringify(statuses, null, 2)}\nRun IDs: ${JSON.stringify(runIds, null, 2)}`,
542
+ },
543
+ ],
544
+ };
545
+ }
546
+ else {
547
+ throw new Error(`Failed to retrieve data: ${response.status}`);
548
+ }
549
+ }
550
+ catch (error) {
551
+ throw new McpError(ErrorCode.InternalError, `Failed to get test run cases: ${error instanceof Error ? error.message : String(error)}`);
552
+ }
553
+ }
554
+ async run() {
555
+ const transport = new StdioServerTransport();
556
+ await this.server.connect(transport);
557
+ console.error('Zephyr MCP server running on stdio');
558
+ }
559
+ }
560
+ const server = new ZephyrServer();
561
+ server.run().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "zephyr-scale-mcp-server",
3
+ "version": "0.0.1",
4
+ "description": "Model Context Protocol (MCP) server for Zephyr Scale test case management with comprehensive STEP_BY_STEP, PLAIN_TEXT, and BDD support",
5
+ "type": "module",
6
+ "main": "./build/index.js",
7
+ "bin": {
8
+ "zephyr-scale-mcp": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build/",
12
+ "src/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
18
+ "prepare": "npm run build",
19
+ "watch": "tsc --watch",
20
+ "start": "node build/index.js",
21
+ "inspector": "npx @modelcontextprotocol/inspector build/index.js",
22
+ "test": "echo \"Error: no test specified\" && exit 1",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "zephyr",
29
+ "zephyr-scale",
30
+ "test-management",
31
+ "test-cases",
32
+ "atlassian",
33
+ "jira",
34
+ "bdd",
35
+ "testing",
36
+ "qa",
37
+ "automation"
38
+ ],
39
+ "author": {
40
+ "name": "Yong Wang",
41
+ "email": "your-email@example.com"
42
+ },
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/your-username/zephyr-scale-mcp-server.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/your-username/zephyr-scale-mcp-server/issues"
50
+ },
51
+ "homepage": "https://github.com/your-username/zephyr-scale-mcp-server#readme",
52
+ "engines": {
53
+ "node": ">=16.0.0"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "0.6.0",
57
+ "axios": "^1.10.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.11.24",
61
+ "typescript": "^5.8.3"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,616 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ErrorCode,
7
+ ListToolsRequestSchema,
8
+ McpError,
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import axios from 'axios';
11
+
12
+ const BASE_URL = process.env.ZEPHYR_BASE_URL;
13
+ const ACCESS_TOKEN = process.env.ZEPHYR_API_KEY;
14
+
15
+ if (!ACCESS_TOKEN) {
16
+ throw new Error('ZEPHYR_API_KEY environment variable is required');
17
+ }
18
+
19
+ const headers = {
20
+ 'Accept': '*/*',
21
+ 'Authorization': `Bearer ${ACCESS_TOKEN}`,
22
+ 'Content-Type': 'application/json',
23
+ };
24
+
25
+ class ZephyrServer {
26
+ private server: Server;
27
+ private axiosInstance;
28
+
29
+ constructor() {
30
+ this.server = new Server(
31
+ {
32
+ name: 'zephyr-server',
33
+ version: '0.1.0',
34
+ },
35
+ {
36
+ capabilities: {
37
+ tools: {},
38
+ },
39
+ }
40
+ );
41
+
42
+ this.axiosInstance = axios.create({
43
+ baseURL: BASE_URL,
44
+ headers,
45
+ });
46
+
47
+ this.setupToolHandlers();
48
+
49
+ this.server.onerror = (error) => console.error('[MCP Error]', error);
50
+ process.on('SIGINT', async () => {
51
+ await this.server.close();
52
+ process.exit(0);
53
+ });
54
+ }
55
+
56
+ private setupToolHandlers() {
57
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
58
+ tools: [
59
+ {
60
+ name: 'get_test_case',
61
+ description: 'Get detailed information about a specific test case',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ test_case_key: {
66
+ type: 'string',
67
+ description: 'Test case key (e.g., PROJ-T123)',
68
+ },
69
+ },
70
+ required: ['test_case_key'],
71
+ },
72
+ },
73
+ {
74
+ name: 'create_test_case',
75
+ description: 'Create a new test case with STEP_BY_STEP or PLAIN_TEXT content',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ project_key: {
80
+ type: 'string',
81
+ description: 'Project key (required)',
82
+ },
83
+ name: {
84
+ type: 'string',
85
+ description: 'Test case name (required)',
86
+ },
87
+ test_script_type: {
88
+ type: 'string',
89
+ description: 'Type of test script',
90
+ enum: ['STEP_BY_STEP', 'PLAIN_TEXT'],
91
+ default: 'STEP_BY_STEP',
92
+ },
93
+ steps: {
94
+ type: 'array',
95
+ description: 'Test steps for STEP_BY_STEP (array of objects with description, testData, expectedResult)',
96
+ items: {
97
+ type: 'object',
98
+ properties: {
99
+ description: { type: 'string' },
100
+ testData: { type: 'string' },
101
+ expectedResult: { type: 'string' },
102
+ testCaseKey: { type: 'string' }
103
+ }
104
+ }
105
+ },
106
+ plain_text: {
107
+ type: 'string',
108
+ description: 'Plain text content for PLAIN_TEXT type',
109
+ },
110
+ folder: {
111
+ type: 'string',
112
+ description: 'Folder path (optional)',
113
+ },
114
+ status: {
115
+ type: 'string',
116
+ description: 'Test case status (optional)',
117
+ enum: ['Draft', 'Approved', 'Deprecated'],
118
+ default: 'Draft',
119
+ },
120
+ priority: {
121
+ type: 'string',
122
+ description: 'Test case priority (optional)',
123
+ enum: ['High', 'Medium', 'Low'],
124
+ default: 'High',
125
+ },
126
+ precondition: {
127
+ type: 'string',
128
+ description: 'Test precondition (optional)',
129
+ },
130
+ objective: {
131
+ type: 'string',
132
+ description: 'Test objective (optional)',
133
+ },
134
+ component: {
135
+ type: 'string',
136
+ description: 'Component name (optional)',
137
+ },
138
+ owner: {
139
+ type: 'string',
140
+ description: 'Test case owner (optional)',
141
+ },
142
+ estimated_time: {
143
+ type: 'number',
144
+ description: 'Estimated time in milliseconds (optional)',
145
+ },
146
+ labels: {
147
+ type: 'array',
148
+ description: 'Array of labels (optional)',
149
+ items: { type: 'string' }
150
+ },
151
+ issue_links: {
152
+ type: 'array',
153
+ description: 'Array of issue links (optional)',
154
+ items: { type: 'string' }
155
+ },
156
+ custom_fields: {
157
+ type: 'object',
158
+ description: 'Custom fields object (optional)',
159
+ },
160
+ },
161
+ required: ['project_key', 'name'],
162
+ },
163
+ },
164
+ {
165
+ name: 'create_test_case_with_bdd',
166
+ description: 'Create a new test case with BDD content',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ project_key: {
171
+ type: 'string',
172
+ description: 'Project key (required)',
173
+ },
174
+ name: {
175
+ type: 'string',
176
+ description: 'Test case name (required)',
177
+ },
178
+ bdd_content: {
179
+ type: 'string',
180
+ description: 'BDD content in markdown format',
181
+ },
182
+ folder: {
183
+ type: 'string',
184
+ description: 'Folder name (optional)',
185
+ },
186
+ priority: {
187
+ type: 'string',
188
+ description: 'Test case priority (High, Medium, Low)',
189
+ enum: ['High', 'Medium', 'Low'],
190
+ },
191
+ },
192
+ required: ['project_key', 'name', 'bdd_content'],
193
+ },
194
+ },
195
+ {
196
+ name: 'update_test_case_bdd',
197
+ description: 'Update an existing test case with BDD content',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ test_case_key: {
202
+ type: 'string',
203
+ description: 'Test case key to update',
204
+ },
205
+ bdd_content: {
206
+ type: 'string',
207
+ description: 'BDD content in markdown format',
208
+ },
209
+ },
210
+ required: ['test_case_key', 'bdd_content'],
211
+ },
212
+ },
213
+ {
214
+ name: 'create_folder',
215
+ description: 'Create a new folder in Zephyr Scale',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ project_key: {
220
+ type: 'string',
221
+ description: 'Project key (required)',
222
+ },
223
+ name: {
224
+ type: 'string',
225
+ description: 'Folder name (required)',
226
+ },
227
+ parent_folder_path: {
228
+ type: 'string',
229
+ description: 'Parent folder path (optional)',
230
+ },
231
+ folder_type: {
232
+ type: 'string',
233
+ description: 'Type of folder',
234
+ enum: ['TEST_CASE', 'TEST_PLAN'],
235
+ default: 'TEST_CASE',
236
+ },
237
+ },
238
+ required: ['project_key', 'name'],
239
+ },
240
+ },
241
+ {
242
+ name: 'get_test_run_cases',
243
+ description: 'Get test case keys from a test run',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ test_run_key: {
248
+ type: 'string',
249
+ description: 'Test run key (e.g., PROJ-C123)',
250
+ },
251
+ },
252
+ required: ['test_run_key'],
253
+ },
254
+ },
255
+ ],
256
+ }));
257
+
258
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
259
+ try {
260
+ switch (request.params.name) {
261
+ case 'get_test_case':
262
+ return await this.getTestCase(request.params.arguments);
263
+ case 'create_test_case':
264
+ return await this.createTestCase(request.params.arguments);
265
+ case 'create_test_case_with_bdd':
266
+ return await this.createTestCaseWithBdd(request.params.arguments);
267
+ case 'update_test_case_bdd':
268
+ return await this.updateTestCaseBdd(request.params.arguments);
269
+ case 'create_folder':
270
+ return await this.createFolder(request.params.arguments);
271
+ case 'get_test_run_cases':
272
+ return await this.getTestRunCases(request.params.arguments);
273
+ default:
274
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
275
+ }
276
+ } catch (error) {
277
+ if (error instanceof McpError) {
278
+ throw error;
279
+ }
280
+ return {
281
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
282
+ isError: true,
283
+ };
284
+ }
285
+ });
286
+ }
287
+
288
+ private async getTestCase(args: any) {
289
+ const { test_case_key } = args;
290
+ try {
291
+ const response = await this.axiosInstance.get(`/rest/atm/1.0/testcase/${test_case_key}`);
292
+ return {
293
+ content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
294
+ };
295
+ } catch (error) {
296
+ throw new McpError(ErrorCode.InternalError, `Failed to get test case: ${error instanceof Error ? error.message : String(error)}`);
297
+ }
298
+ }
299
+
300
+ private convertToGherkin(bddContent: string): string {
301
+ const bddLines: string[] = [];
302
+ const lines = bddContent.split('\n');
303
+
304
+ for (const line of lines) {
305
+ const trimmedLine = line.trim();
306
+ if (!trimmedLine || trimmedLine.startsWith('---')) continue;
307
+
308
+ if (trimmedLine.startsWith('**Given**')) {
309
+ bddLines.push(`Given ${trimmedLine.replace('**Given**', '').trim()}`);
310
+ } else if (trimmedLine.startsWith('**When**')) {
311
+ bddLines.push(`When ${trimmedLine.replace('**When**', '').trim()}`);
312
+ } else if (trimmedLine.startsWith('**Then**')) {
313
+ bddLines.push(`Then ${trimmedLine.replace('**Then**', '').trim()}`);
314
+ } else if (trimmedLine.startsWith('**And**')) {
315
+ bddLines.push(`And ${trimmedLine.replace('**And**', '').trim()}`);
316
+ } else if (trimmedLine.startsWith('Given ') || trimmedLine.startsWith('When ') ||
317
+ trimmedLine.startsWith('Then ') || trimmedLine.startsWith('And ')) {
318
+ bddLines.push(trimmedLine);
319
+ }
320
+ }
321
+
322
+ return bddLines.length > 0 ? ' ' + bddLines.join('\n ') : '';
323
+ }
324
+
325
+ private async createTestCase(args: any) {
326
+ const {
327
+ project_key,
328
+ name,
329
+ test_script_type = 'STEP_BY_STEP',
330
+ steps,
331
+ plain_text,
332
+ folder,
333
+ status = 'Draft',
334
+ priority = 'High',
335
+ precondition,
336
+ objective,
337
+ component,
338
+ owner,
339
+ estimated_time,
340
+ labels,
341
+ issue_links,
342
+ custom_fields
343
+ } = args;
344
+
345
+ // Map priority to custom priority
346
+ const customPriorityMapping: { [key: string]: string } = {
347
+ 'High': 'P0',
348
+ 'Medium': 'P1',
349
+ 'Low': 'P2'
350
+ };
351
+
352
+ // Build the basic payload
353
+ const payload: any = {
354
+ projectKey: project_key,
355
+ name: name,
356
+ status: status,
357
+ priority: priority,
358
+ customFields: {
359
+ 'Type': 'Functional',
360
+ 'Priority': customPriorityMapping[priority] || 'P0',
361
+ 'Execution Type': 'Manual - To Be Automated'
362
+ }
363
+ };
364
+
365
+ // Add optional fields
366
+ if (folder) payload.folder = folder;
367
+ if (precondition) payload.precondition = precondition;
368
+ if (objective) payload.objective = objective;
369
+ if (component) payload.component = component;
370
+ if (owner) payload.owner = owner;
371
+ if (estimated_time) payload.estimatedTime = estimated_time;
372
+ if (labels && labels.length > 0) payload.labels = labels;
373
+ if (issue_links && issue_links.length > 0) payload.issueLinks = issue_links;
374
+
375
+ // Merge custom fields if provided
376
+ if (custom_fields) {
377
+ payload.customFields = { ...payload.customFields, ...custom_fields };
378
+ }
379
+
380
+ // Handle test script based on type
381
+ if (test_script_type === 'STEP_BY_STEP' && steps && steps.length > 0) {
382
+ payload.testScript = {
383
+ type: 'STEP_BY_STEP',
384
+ steps: steps.map((step: any) => {
385
+ const stepObj: any = {};
386
+ if (step.description) stepObj.description = step.description;
387
+ if (step.testData) stepObj.testData = step.testData;
388
+ if (step.expectedResult) stepObj.expectedResult = step.expectedResult;
389
+ if (step.testCaseKey) stepObj.testCaseKey = step.testCaseKey;
390
+ return stepObj;
391
+ })
392
+ };
393
+ } else if (test_script_type === 'PLAIN_TEXT' && plain_text) {
394
+ payload.testScript = {
395
+ type: 'PLAIN_TEXT',
396
+ text: plain_text
397
+ };
398
+ }
399
+
400
+ try {
401
+ const response = await this.axiosInstance.post('/rest/atm/1.0/testcase', payload);
402
+
403
+ if (response.status === 201) {
404
+ const testKey = response.data.key || 'Unknown';
405
+ return {
406
+ content: [
407
+ {
408
+ type: 'text',
409
+ text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({
410
+ key: testKey,
411
+ type: test_script_type,
412
+ steps: test_script_type === 'STEP_BY_STEP' ? steps?.length || 0 : undefined,
413
+ plainText: test_script_type === 'PLAIN_TEXT' ? !!plain_text : undefined
414
+ }, null, 2)}`,
415
+ },
416
+ ],
417
+ };
418
+ } else {
419
+ throw new Error(`Unexpected status code: ${response.status}`);
420
+ }
421
+ } catch (error) {
422
+ let errorMessage = 'Unknown error';
423
+ if (error instanceof Error && 'response' in error) {
424
+ const axiosError = error as any;
425
+ errorMessage = `Status: ${axiosError.response?.status}, Data: ${JSON.stringify(axiosError.response?.data)}`;
426
+ } else if (error instanceof Error) {
427
+ errorMessage = error.message;
428
+ } else {
429
+ errorMessage = String(error);
430
+ }
431
+ throw new McpError(
432
+ ErrorCode.InternalError,
433
+ `Failed to create test case: ${errorMessage}`
434
+ );
435
+ }
436
+ }
437
+
438
+ private async createTestCaseWithBdd(args: any) {
439
+ const { project_key, name, bdd_content, folder, priority = 'High' } = args;
440
+
441
+ const priorityMapping: { [key: string]: string } = {
442
+ 'High': 'High',
443
+ 'Medium': 'High',
444
+ 'Low': 'High'
445
+ };
446
+
447
+ const customPriorityMapping: { [key: string]: string } = {
448
+ 'High': 'P0',
449
+ 'Medium': 'P1',
450
+ 'Low': 'P2'
451
+ };
452
+
453
+ const payload: any = {
454
+ projectKey: project_key,
455
+ name: name,
456
+ status: 'Draft',
457
+ priority: priorityMapping[priority] || 'High',
458
+ customFields: {
459
+ 'Type': 'Functional',
460
+ 'Priority': customPriorityMapping[priority] || 'P0',
461
+ 'Execution Type': 'Manual - To Be Automated'
462
+ }
463
+ };
464
+
465
+ if (folder) {
466
+ payload.folder = folder;
467
+ }
468
+
469
+ const gherkinContent = this.convertToGherkin(bdd_content);
470
+ if (gherkinContent) {
471
+ payload.testScript = {
472
+ type: 'BDD',
473
+ text: gherkinContent
474
+ };
475
+ }
476
+
477
+ try {
478
+ const response = await this.axiosInstance.post('/rest/atm/1.0/testcase', payload);
479
+
480
+ if (response.status === 201) {
481
+ const testKey = response.data.key || 'Unknown';
482
+ return {
483
+ content: [
484
+ {
485
+ type: 'text',
486
+ text: `✅ Test case with BDD created successfully: ${testKey}\n${JSON.stringify({ key: testKey }, null, 2)}`,
487
+ },
488
+ ],
489
+ };
490
+ } else {
491
+ throw new Error(`Unexpected status code: ${response.status}`);
492
+ }
493
+ } catch (error) {
494
+ throw new McpError(
495
+ ErrorCode.InternalError,
496
+ `Failed to create test case with BDD: ${error instanceof Error ? error.message : String(error)}`
497
+ );
498
+ }
499
+ }
500
+
501
+ private async updateTestCaseBdd(args: any) {
502
+ const { test_case_key, bdd_content } = args;
503
+
504
+ try {
505
+ // First get the current test case
506
+ const getResponse = await this.axiosInstance.get(`/rest/atm/1.0/testcase/${test_case_key}`);
507
+ if (getResponse.status !== 200) {
508
+ throw new Error(`Failed to get test case ${test_case_key}`);
509
+ }
510
+
511
+ const gherkinContent = this.convertToGherkin(bdd_content);
512
+ const payload = {
513
+ testScript: {
514
+ type: 'BDD',
515
+ text: gherkinContent
516
+ }
517
+ };
518
+
519
+ const updateResponse = await this.axiosInstance.put(`/rest/atm/1.0/testcase/${test_case_key}`, payload);
520
+
521
+ if (updateResponse.status === 200) {
522
+ return {
523
+ content: [
524
+ {
525
+ type: 'text',
526
+ text: `✅ Updated ${test_case_key} with BDD content successfully`,
527
+ },
528
+ ],
529
+ };
530
+ } else {
531
+ throw new Error(`Failed to update ${test_case_key}: ${updateResponse.status}`);
532
+ }
533
+ } catch (error) {
534
+ throw new McpError(
535
+ ErrorCode.InternalError,
536
+ `Failed to update test case BDD: ${error instanceof Error ? error.message : String(error)}`
537
+ );
538
+ }
539
+ }
540
+
541
+ private async createFolder(args: any) {
542
+ const { project_key, name, parent_folder_path, folder_type = 'TEST_CASE' } = args;
543
+
544
+ let folderName = name;
545
+ if (parent_folder_path && !name.startsWith('/')) {
546
+ const parentPath = parent_folder_path.startsWith('/') ? parent_folder_path : `/${parent_folder_path}`;
547
+ folderName = `${parentPath}/${name}`;
548
+ } else if (!name.startsWith('/')) {
549
+ folderName = `/${name}`;
550
+ }
551
+
552
+ const payload = {
553
+ projectKey: project_key,
554
+ name: folderName,
555
+ type: folder_type,
556
+ };
557
+
558
+ try {
559
+ const response = await this.axiosInstance.post('/rest/atm/1.0/folder', payload);
560
+
561
+ return {
562
+ content: [
563
+ {
564
+ type: 'text',
565
+ text: `✅ Folder created successfully: ${response.data.name} (ID: ${response.data.id})\n${JSON.stringify(response.data, null, 2)}`,
566
+ },
567
+ ],
568
+ };
569
+ } catch (error) {
570
+ throw new McpError(
571
+ ErrorCode.InternalError,
572
+ `Failed to create folder: ${error instanceof Error ? error.message : String(error)}`
573
+ );
574
+ }
575
+ }
576
+
577
+ private async getTestRunCases(args: any) {
578
+ const { test_run_key } = args;
579
+
580
+ try {
581
+ const response = await this.axiosInstance.get(`/rest/atm/1.0/testrun/${test_run_key}`);
582
+
583
+ if (response.status === 200) {
584
+ const data = response.data;
585
+ const testCaseKeys = data.items.map((item: any) => item.testCaseKey);
586
+ const statuses = data.items.map((item: any) => item.status);
587
+ const runIds = data.items.map((item: any) => item.id);
588
+
589
+ return {
590
+ content: [
591
+ {
592
+ type: 'text',
593
+ text: `✅ Retrieved test cases from ${test_run_key}:\nTest Case Keys: ${JSON.stringify(testCaseKeys, null, 2)}\nStatuses: ${JSON.stringify(statuses, null, 2)}\nRun IDs: ${JSON.stringify(runIds, null, 2)}`,
594
+ },
595
+ ],
596
+ };
597
+ } else {
598
+ throw new Error(`Failed to retrieve data: ${response.status}`);
599
+ }
600
+ } catch (error) {
601
+ throw new McpError(
602
+ ErrorCode.InternalError,
603
+ `Failed to get test run cases: ${error instanceof Error ? error.message : String(error)}`
604
+ );
605
+ }
606
+ }
607
+
608
+ async run() {
609
+ const transport = new StdioServerTransport();
610
+ await this.server.connect(transport);
611
+ console.error('Zephyr MCP server running on stdio');
612
+ }
613
+ }
614
+
615
+ const server = new ZephyrServer();
616
+ server.run().catch(console.error);