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 +21 -0
- package/README.md +114 -0
- package/build/index.js +561 -0
- package/package.json +66 -0
- package/src/index.ts +616 -0
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);
|