zephyr-scale-mcp-server 0.4.1 → 0.4.3
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 +43 -21
- package/build/tool-handlers.js +68 -8
- package/build/tool-schemas.js +7 -7
- package/build/utils.js +1 -0
- package/package.json +1 -1
- package/src/tool-handlers.ts +73 -9
- package/src/tool-schemas.ts +7 -7
- package/src/types.ts +6 -3
- package/src/utils.ts +1 -0
package/README.md
CHANGED
|
@@ -27,7 +27,29 @@ Configure your MCP client with the following structure.
|
|
|
27
27
|
"args": ["zephyr-scale-mcp-server@latest"],
|
|
28
28
|
"env": {
|
|
29
29
|
"ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
|
|
30
|
-
"ZEPHYR_API_KEY": "your-zephyr-api-key"
|
|
30
|
+
"ZEPHYR_API_KEY": "your-zephyr-api-key",
|
|
31
|
+
"JIRA_USERNAME": "your-email@company.com",
|
|
32
|
+
"JIRA_API_TOKEN": "your-jira-api-token"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
> **Note**: `JIRA_USERNAME` and `JIRA_API_TOKEN` are optional but required if you want to use the `issue_links` field when creating test cases. Without them, issue linking will fail with a 401 warning (the test case is still created). Generate a Jira API token at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens).
|
|
39
|
+
|
|
40
|
+
**Jira Cloud (EU region):**
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"zephyr-server": {
|
|
45
|
+
"command": "npx",
|
|
46
|
+
"args": ["zephyr-scale-mcp-server@latest"],
|
|
47
|
+
"env": {
|
|
48
|
+
"ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
|
|
49
|
+
"ZEPHYR_API_KEY": "your-zephyr-api-key",
|
|
50
|
+
"JIRA_USERNAME": "your-email@company.com",
|
|
51
|
+
"JIRA_API_TOKEN": "your-jira-api-token",
|
|
52
|
+
"ZEPHYR_API_BASE_URL": "https://eu.api.zephyrscale.smartbear.com/v2"
|
|
31
53
|
}
|
|
32
54
|
}
|
|
33
55
|
}
|
|
@@ -99,18 +121,19 @@ The server provides access to various resources through URI schemes:
|
|
|
99
121
|
|
|
100
122
|
## Usage Examples
|
|
101
123
|
|
|
102
|
-
### Create a BDD Test Case
|
|
124
|
+
### Create a BDD Test Case with Issue Links
|
|
103
125
|
```json
|
|
104
126
|
{
|
|
105
127
|
"project_key": "PROJ",
|
|
106
128
|
"name": "User Authentication",
|
|
107
129
|
"test_script": {
|
|
108
130
|
"type": "BDD",
|
|
109
|
-
"text": "
|
|
110
|
-
}
|
|
131
|
+
"text": "Given a user with valid credentials\nWhen the user attempts to log in\nThen the user should be authenticated successfully"
|
|
132
|
+
},
|
|
133
|
+
"issue_links": ["PROJ-123", "PROJ-456"]
|
|
111
134
|
}
|
|
112
135
|
```
|
|
113
|
-
**Note**:
|
|
136
|
+
**Note**: `issue_links` requires `JIRA_USERNAME` and `JIRA_API_TOKEN` to be set (Cloud only). Link failures are reported as warnings — the test case is still created.
|
|
114
137
|
|
|
115
138
|
### Use a Live Test Case as a Template
|
|
116
139
|
1. Fetch an existing test case: `zephyr://testcase/PROJ-T123`
|
|
@@ -139,26 +162,25 @@ The server provides access to various resources through URI schemes:
|
|
|
139
162
|
## Authentication
|
|
140
163
|
|
|
141
164
|
### Jira Cloud Configuration
|
|
142
|
-
- `ZEPHYR_BASE_URL`: `https://your-company.atlassian.net`
|
|
143
|
-
- `ZEPHYR_API_KEY`: Your Zephyr Scale API key (JWT). Generate it in Jira by clicking your profile picture (bottom left) → **Zephyr API keys**.
|
|
144
165
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
| Variable | Required | Description |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `ZEPHYR_BASE_URL` | ✅ | Your Jira Cloud URL, e.g. `https://your-company.atlassian.net` |
|
|
169
|
+
| `ZEPHYR_API_KEY` | ✅ | Zephyr Scale API key (JWT). Generate in Jira: profile picture (bottom left) → **Zephyr API keys** |
|
|
170
|
+
| `JIRA_USERNAME` | ⚠️ Optional* | Your Jira account email address |
|
|
171
|
+
| `JIRA_API_TOKEN` | ⚠️ Optional* | Jira API token. Generate at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) |
|
|
172
|
+
| `ZEPHYR_API_BASE_URL` | Optional | Override the Zephyr API base URL (e.g. for EU: `https://eu.api.zephyrscale.smartbear.com/v2`). Defaults to US endpoint. |
|
|
173
|
+
| `JIRA_TYPE` | Optional | Force `"cloud"` or `"datacenter"` — overrides auto-detection |
|
|
148
174
|
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
"env": {
|
|
152
|
-
"ZEPHYR_BASE_URL": "https://your-company.atlassian.net",
|
|
153
|
-
"ZEPHYR_API_KEY": "your-zephyr-api-token",
|
|
154
|
-
"ZEPHYR_API_BASE_URL": "https://eu.api.zephyrscale.smartbear.com/v2"
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
If `ZEPHYR_API_BASE_URL` is not set, the US URL is used. Omit this variable for US instances.
|
|
175
|
+
> **\* `JIRA_USERNAME` + `JIRA_API_TOKEN`**: Required only for the `issue_links` feature on Cloud. The Zephyr API key cannot authenticate against the Jira REST API, so a separate Jira credential is needed to resolve issue keys to numeric IDs. Without these, `issue_links` will fail with a 401 warning — the test case is still created successfully.
|
|
158
176
|
|
|
159
177
|
### Jira Data Center Configuration
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
|
|
179
|
+
| Variable | Required | Description |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `ZEPHYR_BASE_URL` | ✅ | Your Jira server URL, e.g. `https://your-jira-server.com` |
|
|
182
|
+
| `ZEPHYR_API_KEY` | ✅ | Zephyr Scale API token from your Jira profile settings |
|
|
183
|
+
| `JIRA_TYPE` | Optional | Set to `"datacenter"` to override auto-detection |
|
|
162
184
|
|
|
163
185
|
### Automatic Detection
|
|
164
186
|
The server automatically detects your Jira type based on `ZEPHYR_BASE_URL` — URLs containing `.atlassian.net` are treated as Cloud, everything else as Data Center. Override with `JIRA_TYPE="cloud"` or `JIRA_TYPE="datacenter"`.
|
package/build/tool-handlers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
1
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
3
|
import { convertToGherkin, resolveFolderIdByPath } from './utils.js';
|
|
3
4
|
export class ZephyrToolHandlers {
|
|
@@ -34,7 +35,7 @@ export class ZephyrToolHandlers {
|
|
|
34
35
|
return this.createTestCaseDC(args);
|
|
35
36
|
}
|
|
36
37
|
async createTestCaseCloud(args) {
|
|
37
|
-
const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, } = args;
|
|
38
|
+
const { project_key, name, test_script, folder, priority, precondition, objective, estimated_time, labels, custom_fields, issue_links, owner_id, component_id, } = args;
|
|
38
39
|
const payload = { projectKey: project_key, name };
|
|
39
40
|
// Cloud v2 uses statusName/priorityName (strings), folderId (integer)
|
|
40
41
|
payload.statusName = 'Draft';
|
|
@@ -50,6 +51,11 @@ export class ZephyrToolHandlers {
|
|
|
50
51
|
payload.labels = labels;
|
|
51
52
|
if (custom_fields)
|
|
52
53
|
payload.customFields = custom_fields;
|
|
54
|
+
// Cloud v2 uses ownerId (Jira Account ID) and componentId (integer)
|
|
55
|
+
if (owner_id)
|
|
56
|
+
payload.ownerId = owner_id;
|
|
57
|
+
if (component_id)
|
|
58
|
+
payload.componentId = component_id;
|
|
53
59
|
// Resolve folder path → folderId
|
|
54
60
|
if (folder) {
|
|
55
61
|
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CASE');
|
|
@@ -66,10 +72,31 @@ export class ZephyrToolHandlers {
|
|
|
66
72
|
if (test_script) {
|
|
67
73
|
await this.upsertTestScriptCloud(testKey, test_script);
|
|
68
74
|
}
|
|
75
|
+
// Step 3: link Jira issues via POST /testcases/{key}/links/issues
|
|
76
|
+
// IssueLinkInput requires a numeric issueId — resolve each key via Jira REST API
|
|
77
|
+
const linkWarnings = [];
|
|
78
|
+
if (issue_links && issue_links.length > 0) {
|
|
79
|
+
for (const issueKey of issue_links) {
|
|
80
|
+
try {
|
|
81
|
+
const issueId = await this.resolveJiraIssueId(issueKey);
|
|
82
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/links/issues`, { issueId });
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
linkWarnings.push(`${issueKey}: ${this.formatError(e)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
|
|
90
|
+
const credHint = missingCreds
|
|
91
|
+
? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
|
|
92
|
+
: '';
|
|
93
|
+
const warningText = linkWarnings.length > 0
|
|
94
|
+
? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
|
|
95
|
+
: '';
|
|
69
96
|
return {
|
|
70
97
|
content: [{
|
|
71
98
|
type: 'text',
|
|
72
|
-
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
|
|
99
|
+
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none', linkedIssues: (issue_links ?? []).length - linkWarnings.length }, null, 2)}${warningText}`,
|
|
73
100
|
}],
|
|
74
101
|
};
|
|
75
102
|
}
|
|
@@ -185,12 +212,15 @@ export class ZephyrToolHandlers {
|
|
|
185
212
|
if (typeof name === 'string' && name.trim().length > 0) {
|
|
186
213
|
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
187
214
|
const tc = getResponse.data;
|
|
188
|
-
|
|
215
|
+
// UpdateTestCaseInput requires: id, key, name, priority, project, status
|
|
216
|
+
// tc.project is a ProjectLink { id, self } — pass it back as-is
|
|
189
217
|
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
|
|
190
|
-
|
|
218
|
+
id: tc.id,
|
|
219
|
+
key: test_case_key,
|
|
191
220
|
name,
|
|
192
221
|
status: tc.status,
|
|
193
222
|
priority: tc.priority,
|
|
223
|
+
project: tc.project,
|
|
194
224
|
});
|
|
195
225
|
}
|
|
196
226
|
return {
|
|
@@ -272,8 +302,7 @@ export class ZephyrToolHandlers {
|
|
|
272
302
|
async createFolderCloud(args) {
|
|
273
303
|
const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
|
|
274
304
|
// Cloud v2 uses folderType (not type) and parentId integer
|
|
275
|
-
|
|
276
|
-
const cloudFolderType = folder_type === 'TEST_RUN' ? 'TEST_CYCLE' : folder_type;
|
|
305
|
+
const cloudFolderType = folder_type;
|
|
277
306
|
const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
278
307
|
if (segments.length === 0) {
|
|
279
308
|
throw new McpError(ErrorCode.InvalidParams, 'folder name/path cannot be empty');
|
|
@@ -437,7 +466,9 @@ export class ZephyrToolHandlers {
|
|
|
437
466
|
payload.plannedEndDate = planned_end_date;
|
|
438
467
|
if (custom_fields)
|
|
439
468
|
payload.customFields = custom_fields;
|
|
440
|
-
//
|
|
469
|
+
// Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
|
|
470
|
+
if (owner)
|
|
471
|
+
payload.ownerId = owner;
|
|
441
472
|
if (folder) {
|
|
442
473
|
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
|
|
443
474
|
if (folderId !== null)
|
|
@@ -677,7 +708,7 @@ export class ZephyrToolHandlers {
|
|
|
677
708
|
text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
|
|
678
709
|
totalCount: testRuns.length,
|
|
679
710
|
testRuns: testRuns.map((tr) => ({
|
|
680
|
-
key: tr.key, name: tr.name, status: tr.status?.
|
|
711
|
+
key: tr.key, name: tr.name, status: tr.status?.id, folder: tr.folder?.name,
|
|
681
712
|
})),
|
|
682
713
|
}, null, 2)}`,
|
|
683
714
|
}],
|
|
@@ -751,6 +782,35 @@ export class ZephyrToolHandlers {
|
|
|
751
782
|
throw new McpError(ErrorCode.InternalError, `Failed to add test cases: ${this.formatError(error)}`);
|
|
752
783
|
}
|
|
753
784
|
}
|
|
785
|
+
async resolveJiraIssueId(issueKey) {
|
|
786
|
+
// The Zephyr API key is NOT valid for the Jira REST API — Jira Cloud requires
|
|
787
|
+
// Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
|
|
788
|
+
const username = process.env.JIRA_USERNAME;
|
|
789
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
790
|
+
const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
|
|
791
|
+
let response;
|
|
792
|
+
if (username && apiToken) {
|
|
793
|
+
// Jira Cloud Basic Auth
|
|
794
|
+
response = await axios.get(url, {
|
|
795
|
+
headers: { 'Accept': 'application/json' },
|
|
796
|
+
auth: { username, password: apiToken },
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
// Fallback: try Bearer token (works for Data Center with PAT)
|
|
801
|
+
response = await axios.get(url, {
|
|
802
|
+
headers: {
|
|
803
|
+
'Accept': 'application/json',
|
|
804
|
+
'Authorization': `Bearer ${process.env.ZEPHYR_API_KEY}`,
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const id = parseInt(response.data.id, 10);
|
|
809
|
+
if (!id || isNaN(id)) {
|
|
810
|
+
throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
|
|
811
|
+
}
|
|
812
|
+
return id;
|
|
813
|
+
}
|
|
754
814
|
formatError(error) {
|
|
755
815
|
if (error instanceof Error && 'response' in error) {
|
|
756
816
|
const axiosError = error;
|
package/build/tool-schemas.js
CHANGED
|
@@ -91,13 +91,13 @@ export const toolSchemas = [
|
|
|
91
91
|
type: 'string',
|
|
92
92
|
description: 'Test objective (optional)',
|
|
93
93
|
},
|
|
94
|
-
|
|
95
|
-
type: '
|
|
96
|
-
description: '
|
|
94
|
+
component_id: {
|
|
95
|
+
type: 'integer',
|
|
96
|
+
description: 'Jira component ID (optional, Cloud only — use the numeric component ID, not the name)',
|
|
97
97
|
},
|
|
98
|
-
|
|
98
|
+
owner_id: {
|
|
99
99
|
type: 'string',
|
|
100
|
-
description: 'Test case owner (optional)',
|
|
100
|
+
description: 'Test case owner Jira Account ID (optional, Cloud only — e.g. "5b10a2844c20165700ede21g")',
|
|
101
101
|
},
|
|
102
102
|
estimated_time: {
|
|
103
103
|
type: 'number',
|
|
@@ -110,7 +110,7 @@ export const toolSchemas = [
|
|
|
110
110
|
},
|
|
111
111
|
issue_links: {
|
|
112
112
|
type: 'array',
|
|
113
|
-
description: 'Array of issue
|
|
113
|
+
description: 'Array of Jira issue keys to link to this test case (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric Jira issue ID via the Jira REST API, then linked via POST /testcases/{key}/links/issues — failures are reported as warnings but do not fail the tool call. On Data Center, sent directly in the create payload.',
|
|
114
114
|
items: { type: 'string' }
|
|
115
115
|
},
|
|
116
116
|
custom_fields: {
|
|
@@ -191,7 +191,7 @@ export const toolSchemas = [
|
|
|
191
191
|
folder_type: {
|
|
192
192
|
type: 'string',
|
|
193
193
|
description: 'Type of folder',
|
|
194
|
-
enum: ['TEST_CASE', 'TEST_PLAN', '
|
|
194
|
+
enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
|
|
195
195
|
default: 'TEST_CASE',
|
|
196
196
|
},
|
|
197
197
|
},
|
package/build/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Zephyr Scale test case management with comprehensive STEP_BY_STEP, PLAIN_TEXT, and BDD support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
package/src/tool-handlers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiosInstance } from 'axios';
|
|
2
|
+
import axios from 'axios';
|
|
2
3
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import {
|
|
4
5
|
TestCaseArgs,
|
|
@@ -46,7 +47,8 @@ export class ZephyrToolHandlers {
|
|
|
46
47
|
private async createTestCaseCloud(args: TestCaseArgs) {
|
|
47
48
|
const {
|
|
48
49
|
project_key, name, test_script, folder, priority, precondition,
|
|
49
|
-
objective, estimated_time, labels, custom_fields,
|
|
50
|
+
objective, estimated_time, labels, custom_fields, issue_links,
|
|
51
|
+
owner_id, component_id,
|
|
50
52
|
} = args;
|
|
51
53
|
|
|
52
54
|
const payload: any = { projectKey: project_key, name };
|
|
@@ -58,6 +60,9 @@ export class ZephyrToolHandlers {
|
|
|
58
60
|
if (estimated_time) payload.estimatedTime = estimated_time;
|
|
59
61
|
if (labels && labels.length > 0) payload.labels = labels;
|
|
60
62
|
if (custom_fields) payload.customFields = custom_fields;
|
|
63
|
+
// Cloud v2 uses ownerId (Jira Account ID) and componentId (integer)
|
|
64
|
+
if (owner_id) payload.ownerId = owner_id;
|
|
65
|
+
if (component_id) payload.componentId = component_id;
|
|
61
66
|
|
|
62
67
|
// Resolve folder path → folderId
|
|
63
68
|
if (folder) {
|
|
@@ -79,10 +84,35 @@ export class ZephyrToolHandlers {
|
|
|
79
84
|
await this.upsertTestScriptCloud(testKey, test_script);
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
// Step 3: link Jira issues via POST /testcases/{key}/links/issues
|
|
88
|
+
// IssueLinkInput requires a numeric issueId — resolve each key via Jira REST API
|
|
89
|
+
const linkWarnings: string[] = [];
|
|
90
|
+
if (issue_links && issue_links.length > 0) {
|
|
91
|
+
for (const issueKey of issue_links) {
|
|
92
|
+
try {
|
|
93
|
+
const issueId = await this.resolveJiraIssueId(issueKey);
|
|
94
|
+
await this.axiosInstance.post(
|
|
95
|
+
`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/links/issues`,
|
|
96
|
+
{ issueId }
|
|
97
|
+
);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
linkWarnings.push(`${issueKey}: ${this.formatError(e)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
|
|
105
|
+
const credHint = missingCreds
|
|
106
|
+
? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
|
|
107
|
+
: '';
|
|
108
|
+
const warningText = linkWarnings.length > 0
|
|
109
|
+
? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
|
|
110
|
+
: '';
|
|
111
|
+
|
|
82
112
|
return {
|
|
83
113
|
content: [{
|
|
84
114
|
type: 'text',
|
|
85
|
-
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none' }, null, 2)}`,
|
|
115
|
+
text: `✅ Test case created successfully: ${testKey}\n${JSON.stringify({ key: testKey, type: test_script?.type || 'none', linkedIssues: (issue_links ?? []).length - linkWarnings.length }, null, 2)}${warningText}`,
|
|
86
116
|
}],
|
|
87
117
|
};
|
|
88
118
|
} catch (error) {
|
|
@@ -203,12 +233,15 @@ export class ZephyrToolHandlers {
|
|
|
203
233
|
if (typeof name === 'string' && name.trim().length > 0) {
|
|
204
234
|
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
205
235
|
const tc = getResponse.data;
|
|
206
|
-
|
|
236
|
+
// UpdateTestCaseInput requires: id, key, name, priority, project, status
|
|
237
|
+
// tc.project is a ProjectLink { id, self } — pass it back as-is
|
|
207
238
|
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, {
|
|
208
|
-
|
|
239
|
+
id: tc.id,
|
|
240
|
+
key: test_case_key,
|
|
209
241
|
name,
|
|
210
242
|
status: tc.status,
|
|
211
243
|
priority: tc.priority,
|
|
244
|
+
project: tc.project,
|
|
212
245
|
});
|
|
213
246
|
}
|
|
214
247
|
|
|
@@ -297,8 +330,7 @@ export class ZephyrToolHandlers {
|
|
|
297
330
|
const { project_key, name: folderPath, folder_type = 'TEST_CASE' } = args;
|
|
298
331
|
|
|
299
332
|
// Cloud v2 uses folderType (not type) and parentId integer
|
|
300
|
-
|
|
301
|
-
const cloudFolderType = folder_type === 'TEST_RUN' ? 'TEST_CYCLE' : folder_type;
|
|
333
|
+
const cloudFolderType = folder_type;
|
|
302
334
|
|
|
303
335
|
const segments = folderPath.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
304
336
|
if (segments.length === 0) {
|
|
@@ -483,8 +515,8 @@ export class ZephyrToolHandlers {
|
|
|
483
515
|
if (planned_start_date) payload.plannedStartDate = planned_start_date;
|
|
484
516
|
if (planned_end_date) payload.plannedEndDate = planned_end_date;
|
|
485
517
|
if (custom_fields) payload.customFields = custom_fields;
|
|
486
|
-
|
|
487
|
-
|
|
518
|
+
// Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
|
|
519
|
+
if (owner) payload.ownerId = owner;
|
|
488
520
|
if (folder) {
|
|
489
521
|
const folderId = await resolveFolderIdByPath(
|
|
490
522
|
this.axiosInstance, project_key, folder, 'TEST_CYCLE'
|
|
@@ -745,7 +777,7 @@ export class ZephyrToolHandlers {
|
|
|
745
777
|
text: `✅ Found ${testRuns.length} test run(s):\n${JSON.stringify({
|
|
746
778
|
totalCount: testRuns.length,
|
|
747
779
|
testRuns: testRuns.map((tr: any) => ({
|
|
748
|
-
key: tr.key, name: tr.name, status: tr.status?.
|
|
780
|
+
key: tr.key, name: tr.name, status: tr.status?.id, folder: tr.folder?.name,
|
|
749
781
|
})),
|
|
750
782
|
}, null, 2)}`,
|
|
751
783
|
}],
|
|
@@ -824,6 +856,38 @@ export class ZephyrToolHandlers {
|
|
|
824
856
|
}
|
|
825
857
|
}
|
|
826
858
|
|
|
859
|
+
private async resolveJiraIssueId(issueKey: string): Promise<number> {
|
|
860
|
+
// The Zephyr API key is NOT valid for the Jira REST API — Jira Cloud requires
|
|
861
|
+
// Basic Auth: base64(email:api_token) via JIRA_USERNAME + JIRA_API_TOKEN env vars.
|
|
862
|
+
const username = process.env.JIRA_USERNAME;
|
|
863
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
864
|
+
|
|
865
|
+
const url = `${this.jiraConfig.jiraBaseUrl}/rest/api/3/issue/${issueKey}?fields=id`;
|
|
866
|
+
|
|
867
|
+
let response;
|
|
868
|
+
if (username && apiToken) {
|
|
869
|
+
// Jira Cloud Basic Auth
|
|
870
|
+
response = await axios.get(url, {
|
|
871
|
+
headers: { 'Accept': 'application/json' },
|
|
872
|
+
auth: { username, password: apiToken },
|
|
873
|
+
});
|
|
874
|
+
} else {
|
|
875
|
+
// Fallback: try Bearer token (works for Data Center with PAT)
|
|
876
|
+
response = await axios.get(url, {
|
|
877
|
+
headers: {
|
|
878
|
+
'Accept': 'application/json',
|
|
879
|
+
'Authorization': `Bearer ${process.env.ZEPHYR_API_KEY}`,
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const id = parseInt(response.data.id, 10);
|
|
885
|
+
if (!id || isNaN(id)) {
|
|
886
|
+
throw new Error(`Could not resolve numeric ID for Jira issue "${issueKey}"`);
|
|
887
|
+
}
|
|
888
|
+
return id;
|
|
889
|
+
}
|
|
890
|
+
|
|
827
891
|
private formatError(error: unknown): string {
|
|
828
892
|
if (error instanceof Error && 'response' in error) {
|
|
829
893
|
const axiosError = error as any;
|
package/src/tool-schemas.ts
CHANGED
|
@@ -91,13 +91,13 @@ export const toolSchemas = [
|
|
|
91
91
|
type: 'string',
|
|
92
92
|
description: 'Test objective (optional)',
|
|
93
93
|
},
|
|
94
|
-
|
|
95
|
-
type: '
|
|
96
|
-
description: '
|
|
94
|
+
component_id: {
|
|
95
|
+
type: 'integer',
|
|
96
|
+
description: 'Jira component ID (optional, Cloud only — use the numeric component ID, not the name)',
|
|
97
97
|
},
|
|
98
|
-
|
|
98
|
+
owner_id: {
|
|
99
99
|
type: 'string',
|
|
100
|
-
description: 'Test case owner (optional)',
|
|
100
|
+
description: 'Test case owner Jira Account ID (optional, Cloud only — e.g. "5b10a2844c20165700ede21g")',
|
|
101
101
|
},
|
|
102
102
|
estimated_time: {
|
|
103
103
|
type: 'number',
|
|
@@ -110,7 +110,7 @@ export const toolSchemas = [
|
|
|
110
110
|
},
|
|
111
111
|
issue_links: {
|
|
112
112
|
type: 'array',
|
|
113
|
-
description: 'Array of issue
|
|
113
|
+
description: 'Array of Jira issue keys to link to this test case (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric Jira issue ID via the Jira REST API, then linked via POST /testcases/{key}/links/issues — failures are reported as warnings but do not fail the tool call. On Data Center, sent directly in the create payload.',
|
|
114
114
|
items: { type: 'string' }
|
|
115
115
|
},
|
|
116
116
|
custom_fields: {
|
|
@@ -191,7 +191,7 @@ export const toolSchemas = [
|
|
|
191
191
|
folder_type: {
|
|
192
192
|
type: 'string',
|
|
193
193
|
description: 'Type of folder',
|
|
194
|
-
enum: ['TEST_CASE', 'TEST_PLAN', '
|
|
194
|
+
enum: ['TEST_CASE', 'TEST_PLAN', 'TEST_CYCLE'],
|
|
195
195
|
default: 'TEST_CASE',
|
|
196
196
|
},
|
|
197
197
|
},
|
package/src/types.ts
CHANGED
|
@@ -31,8 +31,10 @@ export interface TestCaseArgs {
|
|
|
31
31
|
priority?: 'High' | 'Normal' | 'Low';
|
|
32
32
|
precondition?: string;
|
|
33
33
|
objective?: string;
|
|
34
|
-
component?: string;
|
|
35
|
-
owner?: string;
|
|
34
|
+
component?: string; // Data Center: component name
|
|
35
|
+
owner?: string; // Data Center: owner name
|
|
36
|
+
component_id?: number; // Cloud: Jira component ID (integer)
|
|
37
|
+
owner_id?: string; // Cloud: Jira Account ID
|
|
36
38
|
estimated_time?: number;
|
|
37
39
|
labels?: string[];
|
|
38
40
|
issue_links?: string[];
|
|
@@ -49,7 +51,7 @@ export interface UpdateBddArgs {
|
|
|
49
51
|
export interface FolderArgs {
|
|
50
52
|
project_key: string;
|
|
51
53
|
name: string; // Full folder path including parent folders (e.g., "/folder/subfolder")
|
|
52
|
-
folder_type?: 'TEST_CASE' | 'TEST_PLAN' | '
|
|
54
|
+
folder_type?: 'TEST_CASE' | 'TEST_PLAN' | 'TEST_CYCLE';
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export interface TestRunArgs {
|
|
@@ -104,6 +106,7 @@ export interface ApiEndpoints {
|
|
|
104
106
|
export interface JiraConfig {
|
|
105
107
|
type: JiraType;
|
|
106
108
|
baseUrl: string;
|
|
109
|
+
jiraBaseUrl: string;
|
|
107
110
|
authHeaders: Record<string, string>;
|
|
108
111
|
apiEndpoints: ApiEndpoints;
|
|
109
112
|
}
|