imperial-mcp-qase 1.0.0
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/.claude/settings.local.json +15 -0
- package/README.md +208 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14815 -0
- package/package.json +36 -0
- package/src/client.ts +331 -0
- package/src/index.ts +57 -0
- package/src/tools/cases.ts +219 -0
- package/src/tools/defects.ts +218 -0
- package/src/tools/projects.ts +49 -0
- package/src/tools/results.ts +204 -0
- package/src/tools/runs.ts +182 -0
- package/src/types/qase.ts +343 -0
- package/tsconfig.json +24 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imperial-mcp-qase",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Qase.io test management - Built by Imperial Healthtech",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"imperial-mcp-qase": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
12
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"qase",
|
|
19
|
+
"test-management",
|
|
20
|
+
"claude",
|
|
21
|
+
"anthropic"
|
|
22
|
+
],
|
|
23
|
+
"author": "Imperial Healthtech",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
QaseResponse,
|
|
3
|
+
QaseListResponse,
|
|
4
|
+
TestCase,
|
|
5
|
+
CreateTestCaseInput,
|
|
6
|
+
UpdateTestCaseInput,
|
|
7
|
+
ListCasesFilters,
|
|
8
|
+
Defect,
|
|
9
|
+
CreateDefectInput,
|
|
10
|
+
UpdateDefectInput,
|
|
11
|
+
ListDefectsFilters,
|
|
12
|
+
TestRun,
|
|
13
|
+
CreateTestRunInput,
|
|
14
|
+
ListRunsFilters,
|
|
15
|
+
TestResult,
|
|
16
|
+
CreateTestResultInput,
|
|
17
|
+
Project,
|
|
18
|
+
ListProjectsFilters,
|
|
19
|
+
} from "./types/qase.js";
|
|
20
|
+
|
|
21
|
+
const QASE_API_BASE = "https://api.qase.io/v1";
|
|
22
|
+
|
|
23
|
+
export class QaseApiError extends Error {
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
public statusCode: number,
|
|
27
|
+
public errorFields?: Record<string, string[]>
|
|
28
|
+
) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "QaseApiError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class QaseClient {
|
|
35
|
+
private apiToken: string;
|
|
36
|
+
|
|
37
|
+
constructor(apiToken?: string) {
|
|
38
|
+
const token = apiToken ?? process.env.QASE_API_TOKEN;
|
|
39
|
+
if (!token) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"QASE_API_TOKEN is required. Set it via environment variable or pass it to the constructor."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
this.apiToken = token;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async request<T>(
|
|
48
|
+
method: string,
|
|
49
|
+
endpoint: string,
|
|
50
|
+
body?: unknown
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
const url = `${QASE_API_BASE}${endpoint}`;
|
|
53
|
+
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
method,
|
|
56
|
+
headers: {
|
|
57
|
+
"Token": this.apiToken,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"Accept": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
|
|
66
|
+
if (!response.ok || data.status === false) {
|
|
67
|
+
throw new QaseApiError(
|
|
68
|
+
data.errorMessage ?? `API request failed with status ${response.status}`,
|
|
69
|
+
response.status,
|
|
70
|
+
data.errorFields
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return data as T;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Project Methods
|
|
78
|
+
async listProjects(
|
|
79
|
+
filters?: ListProjectsFilters
|
|
80
|
+
): Promise<QaseListResponse<Project>> {
|
|
81
|
+
const query = this.buildQueryString(filters ?? {});
|
|
82
|
+
return this.request<QaseListResponse<Project>>("GET", `/project${query}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private buildQueryString(params: Record<string, unknown>): string {
|
|
86
|
+
const searchParams = new URLSearchParams();
|
|
87
|
+
for (const [key, value] of Object.entries(params)) {
|
|
88
|
+
if (value !== undefined && value !== null) {
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
value.forEach((v) => searchParams.append(`${key}[]`, String(v)));
|
|
91
|
+
} else {
|
|
92
|
+
searchParams.append(key, String(value));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const queryString = searchParams.toString();
|
|
97
|
+
return queryString ? `?${queryString}` : "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Test Case Methods
|
|
101
|
+
async listCases(
|
|
102
|
+
projectCode: string,
|
|
103
|
+
filters?: ListCasesFilters
|
|
104
|
+
): Promise<QaseListResponse<TestCase>> {
|
|
105
|
+
const query = this.buildQueryString(filters ?? {});
|
|
106
|
+
return this.request<QaseListResponse<TestCase>>(
|
|
107
|
+
"GET",
|
|
108
|
+
`/case/${projectCode}${query}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getCase(
|
|
113
|
+
projectCode: string,
|
|
114
|
+
caseId: number
|
|
115
|
+
): Promise<QaseResponse<TestCase>> {
|
|
116
|
+
return this.request<QaseResponse<TestCase>>(
|
|
117
|
+
"GET",
|
|
118
|
+
`/case/${projectCode}/${caseId}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async createCase(
|
|
123
|
+
projectCode: string,
|
|
124
|
+
data: CreateTestCaseInput
|
|
125
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
126
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
127
|
+
"POST",
|
|
128
|
+
`/case/${projectCode}`,
|
|
129
|
+
data
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async updateCase(
|
|
134
|
+
projectCode: string,
|
|
135
|
+
caseId: number,
|
|
136
|
+
data: UpdateTestCaseInput
|
|
137
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
138
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
139
|
+
"PATCH",
|
|
140
|
+
`/case/${projectCode}/${caseId}`,
|
|
141
|
+
data
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async deleteCase(
|
|
146
|
+
projectCode: string,
|
|
147
|
+
caseId: number
|
|
148
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
149
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
150
|
+
"DELETE",
|
|
151
|
+
`/case/${projectCode}/${caseId}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Defect Methods
|
|
156
|
+
async listDefects(
|
|
157
|
+
projectCode: string,
|
|
158
|
+
filters?: ListDefectsFilters
|
|
159
|
+
): Promise<QaseListResponse<Defect>> {
|
|
160
|
+
const query = this.buildQueryString(filters ?? {});
|
|
161
|
+
return this.request<QaseListResponse<Defect>>(
|
|
162
|
+
"GET",
|
|
163
|
+
`/defect/${projectCode}${query}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getDefect(
|
|
168
|
+
projectCode: string,
|
|
169
|
+
defectId: number
|
|
170
|
+
): Promise<QaseResponse<Defect>> {
|
|
171
|
+
return this.request<QaseResponse<Defect>>(
|
|
172
|
+
"GET",
|
|
173
|
+
`/defect/${projectCode}/${defectId}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async createDefect(
|
|
178
|
+
projectCode: string,
|
|
179
|
+
data: CreateDefectInput
|
|
180
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
181
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
182
|
+
"POST",
|
|
183
|
+
`/defect/${projectCode}`,
|
|
184
|
+
data
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async updateDefect(
|
|
189
|
+
projectCode: string,
|
|
190
|
+
defectId: number,
|
|
191
|
+
data: UpdateDefectInput
|
|
192
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
193
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
194
|
+
"PATCH",
|
|
195
|
+
`/defect/${projectCode}/${defectId}`,
|
|
196
|
+
data
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async resolveDefect(
|
|
201
|
+
projectCode: string,
|
|
202
|
+
defectId: number
|
|
203
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
204
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
205
|
+
"PATCH",
|
|
206
|
+
`/defect/${projectCode}/resolve/${defectId}`,
|
|
207
|
+
{}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async deleteDefect(
|
|
212
|
+
projectCode: string,
|
|
213
|
+
defectId: number
|
|
214
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
215
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
216
|
+
"DELETE",
|
|
217
|
+
`/defect/${projectCode}/${defectId}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Test Run Methods
|
|
222
|
+
async listRuns(
|
|
223
|
+
projectCode: string,
|
|
224
|
+
filters?: ListRunsFilters
|
|
225
|
+
): Promise<QaseListResponse<TestRun>> {
|
|
226
|
+
const query = this.buildQueryString(filters ?? {});
|
|
227
|
+
return this.request<QaseListResponse<TestRun>>(
|
|
228
|
+
"GET",
|
|
229
|
+
`/run/${projectCode}${query}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getRun(
|
|
234
|
+
projectCode: string,
|
|
235
|
+
runId: number
|
|
236
|
+
): Promise<QaseResponse<TestRun>> {
|
|
237
|
+
return this.request<QaseResponse<TestRun>>(
|
|
238
|
+
"GET",
|
|
239
|
+
`/run/${projectCode}/${runId}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async createRun(
|
|
244
|
+
projectCode: string,
|
|
245
|
+
data: CreateTestRunInput
|
|
246
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
247
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
248
|
+
"POST",
|
|
249
|
+
`/run/${projectCode}`,
|
|
250
|
+
data
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async completeRun(
|
|
255
|
+
projectCode: string,
|
|
256
|
+
runId: number
|
|
257
|
+
): Promise<QaseResponse<boolean>> {
|
|
258
|
+
return this.request<QaseResponse<boolean>>(
|
|
259
|
+
"POST",
|
|
260
|
+
`/run/${projectCode}/${runId}/complete`,
|
|
261
|
+
{}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async deleteRun(
|
|
266
|
+
projectCode: string,
|
|
267
|
+
runId: number
|
|
268
|
+
): Promise<QaseResponse<{ id: number }>> {
|
|
269
|
+
return this.request<QaseResponse<{ id: number }>>(
|
|
270
|
+
"DELETE",
|
|
271
|
+
`/run/${projectCode}/${runId}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Test Result Methods
|
|
276
|
+
async listResults(
|
|
277
|
+
projectCode: string,
|
|
278
|
+
runId: number
|
|
279
|
+
): Promise<QaseListResponse<TestResult>> {
|
|
280
|
+
return this.request<QaseListResponse<TestResult>>(
|
|
281
|
+
"GET",
|
|
282
|
+
`/result/${projectCode}/${runId}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async getResult(
|
|
287
|
+
projectCode: string,
|
|
288
|
+
runId: number,
|
|
289
|
+
hash: string
|
|
290
|
+
): Promise<QaseResponse<TestResult>> {
|
|
291
|
+
return this.request<QaseResponse<TestResult>>(
|
|
292
|
+
"GET",
|
|
293
|
+
`/result/${projectCode}/${runId}/${hash}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async createResult(
|
|
298
|
+
projectCode: string,
|
|
299
|
+
runId: number,
|
|
300
|
+
data: CreateTestResultInput
|
|
301
|
+
): Promise<QaseResponse<{ hash: string }>> {
|
|
302
|
+
return this.request<QaseResponse<{ hash: string }>>(
|
|
303
|
+
"POST",
|
|
304
|
+
`/result/${projectCode}/${runId}`,
|
|
305
|
+
data
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async createResultBulk(
|
|
310
|
+
projectCode: string,
|
|
311
|
+
runId: number,
|
|
312
|
+
results: CreateTestResultInput[]
|
|
313
|
+
): Promise<QaseResponse<{ hash: string }[]>> {
|
|
314
|
+
return this.request<QaseResponse<{ hash: string }[]>>(
|
|
315
|
+
"POST",
|
|
316
|
+
`/result/${projectCode}/${runId}/bulk`,
|
|
317
|
+
{ results }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async deleteResult(
|
|
322
|
+
projectCode: string,
|
|
323
|
+
runId: number,
|
|
324
|
+
hash: string
|
|
325
|
+
): Promise<QaseResponse<{ hash: string }>> {
|
|
326
|
+
return this.request<QaseResponse<{ hash: string }>>(
|
|
327
|
+
"DELETE",
|
|
328
|
+
`/result/${projectCode}/${runId}/${hash}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { QaseClient } from "./client.js";
|
|
6
|
+
import { registerCaseTools } from "./tools/cases.js";
|
|
7
|
+
import { registerDefectTools } from "./tools/defects.js";
|
|
8
|
+
import { registerRunTools } from "./tools/runs.js";
|
|
9
|
+
import { registerResultTools } from "./tools/results.js";
|
|
10
|
+
import { registerProjectTools } from "./tools/projects.js";
|
|
11
|
+
|
|
12
|
+
async function main(): Promise<void> {
|
|
13
|
+
// Initialize the MCP server
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "imperial-mcp-qase",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Initialize Qase client (will use QASE_API_TOKEN from environment)
|
|
20
|
+
let client: QaseClient;
|
|
21
|
+
try {
|
|
22
|
+
client = new QaseClient();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(
|
|
25
|
+
"Failed to initialize Qase client. Ensure QASE_API_TOKEN environment variable is set."
|
|
26
|
+
);
|
|
27
|
+
console.error(error instanceof Error ? error.message : error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Register all tool categories
|
|
32
|
+
registerProjectTools(server, client);
|
|
33
|
+
registerCaseTools(server, client);
|
|
34
|
+
registerDefectTools(server, client);
|
|
35
|
+
registerRunTools(server, client);
|
|
36
|
+
registerResultTools(server, client);
|
|
37
|
+
|
|
38
|
+
// Connect to stdio transport
|
|
39
|
+
const transport = new StdioServerTransport();
|
|
40
|
+
await server.connect(transport);
|
|
41
|
+
|
|
42
|
+
// Handle graceful shutdown
|
|
43
|
+
process.on("SIGINT", async () => {
|
|
44
|
+
await server.close();
|
|
45
|
+
process.exit(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
process.on("SIGTERM", async () => {
|
|
49
|
+
await server.close();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main().catch((error) => {
|
|
55
|
+
console.error("Fatal error:", error);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { QaseClient, QaseApiError } from "../client.js";
|
|
4
|
+
|
|
5
|
+
// Zod schemas for validation
|
|
6
|
+
const ListCasesSchema = {
|
|
7
|
+
project_code: z.string().describe("Qase project code (e.g., 'DEMO', 'BPJS')"),
|
|
8
|
+
limit: z.number().optional().describe("Maximum number of results (default: 100)"),
|
|
9
|
+
offset: z.number().optional().describe("Offset for pagination"),
|
|
10
|
+
search: z.string().optional().describe("Search query to filter cases by title"),
|
|
11
|
+
suite_id: z.number().optional().describe("Filter by test suite ID"),
|
|
12
|
+
milestone_id: z.number().optional().describe("Filter by milestone ID"),
|
|
13
|
+
severity: z.array(z.number()).optional().describe("Filter by severity (0=not set, 1=blocker, 2=critical, 3=major, 4=normal, 5=minor, 6=trivial)"),
|
|
14
|
+
priority: z.array(z.number()).optional().describe("Filter by priority (0=not set, 1=high, 2=medium, 3=low)"),
|
|
15
|
+
type: z.array(z.number()).optional().describe("Filter by type (0=other, 1=functional, 2=smoke, 3=regression, etc.)"),
|
|
16
|
+
automation: z.array(z.number()).optional().describe("Filter by automation status (0=not automated, 1=to be automated, 2=automated)"),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const GetCaseSchema = {
|
|
20
|
+
project_code: z.string().describe("Qase project code (e.g., 'DEMO', 'BPJS')"),
|
|
21
|
+
case_id: z.number().describe("Test case ID"),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const TestStepSchema = z.object({
|
|
25
|
+
action: z.string().describe("Step action/description"),
|
|
26
|
+
expected_result: z.string().optional().describe("Expected result for the step"),
|
|
27
|
+
data: z.string().optional().describe("Test data for the step"),
|
|
28
|
+
position: z.number().optional().describe("Step position (order)"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CreateCaseSchema = {
|
|
32
|
+
project_code: z.string().describe("Qase project code (e.g., 'DEMO', 'BPJS')"),
|
|
33
|
+
title: z.string().describe("Test case title"),
|
|
34
|
+
description: z.string().optional().describe("Test case description"),
|
|
35
|
+
preconditions: z.string().optional().describe("Test preconditions"),
|
|
36
|
+
postconditions: z.string().optional().describe("Test postconditions"),
|
|
37
|
+
severity: z.number().optional().describe("Severity (0=not set, 1=blocker, 2=critical, 3=major, 4=normal, 5=minor, 6=trivial)"),
|
|
38
|
+
priority: z.number().optional().describe("Priority (0=not set, 1=high, 2=medium, 3=low)"),
|
|
39
|
+
type: z.number().optional().describe("Type (0=other, 1=functional, 2=smoke, 3=regression, etc.)"),
|
|
40
|
+
layer: z.number().optional().describe("Layer (0=unknown, 1=e2e, 2=api, 3=unit)"),
|
|
41
|
+
is_flaky: z.number().optional().describe("Is flaky (0=no, 1=yes)"),
|
|
42
|
+
behavior: z.number().optional().describe("Behavior (0=not set, 1=positive, 2=negative, 3=destructive)"),
|
|
43
|
+
automation: z.number().optional().describe("Automation status (0=not automated, 1=to be automated, 2=automated)"),
|
|
44
|
+
status: z.number().optional().describe("Status (0=actual, 1=draft, 2=deprecated)"),
|
|
45
|
+
suite_id: z.number().optional().describe("Parent test suite ID"),
|
|
46
|
+
milestone_id: z.number().optional().describe("Associated milestone ID"),
|
|
47
|
+
steps: z.array(TestStepSchema).optional().describe("Test steps"),
|
|
48
|
+
tags: z.array(z.string()).optional().describe("Tags for the test case"),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const UpdateCaseSchema = {
|
|
52
|
+
project_code: z.string().describe("Qase project code (e.g., 'DEMO', 'BPJS')"),
|
|
53
|
+
case_id: z.number().describe("Test case ID to update"),
|
|
54
|
+
title: z.string().optional().describe("Updated test case title"),
|
|
55
|
+
description: z.string().optional().describe("Updated description"),
|
|
56
|
+
preconditions: z.string().optional().describe("Updated preconditions"),
|
|
57
|
+
postconditions: z.string().optional().describe("Updated postconditions"),
|
|
58
|
+
severity: z.number().optional().describe("Updated severity"),
|
|
59
|
+
priority: z.number().optional().describe("Updated priority"),
|
|
60
|
+
type: z.number().optional().describe("Updated type"),
|
|
61
|
+
layer: z.number().optional().describe("Updated layer"),
|
|
62
|
+
is_flaky: z.number().optional().describe("Updated flaky status"),
|
|
63
|
+
behavior: z.number().optional().describe("Updated behavior"),
|
|
64
|
+
automation: z.number().optional().describe("Updated automation status"),
|
|
65
|
+
status: z.number().optional().describe("Updated status"),
|
|
66
|
+
suite_id: z.number().optional().describe("Updated suite ID"),
|
|
67
|
+
milestone_id: z.number().optional().describe("Updated milestone ID"),
|
|
68
|
+
steps: z.array(TestStepSchema).optional().describe("Updated test steps"),
|
|
69
|
+
tags: z.array(z.string()).optional().describe("Updated tags"),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const DeleteCaseSchema = {
|
|
73
|
+
project_code: z.string().describe("Qase project code (e.g., 'DEMO', 'BPJS')"),
|
|
74
|
+
case_id: z.number().describe("Test case ID to delete"),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function formatError(error: unknown): string {
|
|
78
|
+
if (error instanceof QaseApiError) {
|
|
79
|
+
let message = `Qase API Error (${error.statusCode}): ${error.message}`;
|
|
80
|
+
if (error.errorFields) {
|
|
81
|
+
message += `\nField errors: ${JSON.stringify(error.errorFields, null, 2)}`;
|
|
82
|
+
}
|
|
83
|
+
return message;
|
|
84
|
+
}
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
return `Error: ${error.message}`;
|
|
87
|
+
}
|
|
88
|
+
return `Unknown error: ${String(error)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function registerCaseTools(server: McpServer, client: QaseClient): void {
|
|
92
|
+
// List test cases
|
|
93
|
+
server.tool(
|
|
94
|
+
"qase_list_cases",
|
|
95
|
+
"List test cases in a Qase project with optional filtering",
|
|
96
|
+
ListCasesSchema,
|
|
97
|
+
async (args) => {
|
|
98
|
+
try {
|
|
99
|
+
const { project_code, ...filters } = args;
|
|
100
|
+
const response = await client.listCases(project_code, filters);
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: JSON.stringify(response, null, 2),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: formatError(error) }],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Get single test case
|
|
119
|
+
server.tool(
|
|
120
|
+
"qase_get_case",
|
|
121
|
+
"Get a single test case by ID from a Qase project",
|
|
122
|
+
GetCaseSchema,
|
|
123
|
+
async (args) => {
|
|
124
|
+
try {
|
|
125
|
+
const response = await client.getCase(args.project_code, args.case_id);
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: JSON.stringify(response, null, 2),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: formatError(error) }],
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Create test case
|
|
144
|
+
server.tool(
|
|
145
|
+
"qase_create_case",
|
|
146
|
+
"Create a new test case in a Qase project",
|
|
147
|
+
CreateCaseSchema,
|
|
148
|
+
async (args) => {
|
|
149
|
+
try {
|
|
150
|
+
const { project_code, ...caseData } = args;
|
|
151
|
+
const response = await client.createCase(project_code, caseData);
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: JSON.stringify(response, null, 2),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: formatError(error) }],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Update test case
|
|
170
|
+
server.tool(
|
|
171
|
+
"qase_update_case",
|
|
172
|
+
"Update an existing test case in a Qase project",
|
|
173
|
+
UpdateCaseSchema,
|
|
174
|
+
async (args) => {
|
|
175
|
+
try {
|
|
176
|
+
const { project_code, case_id, ...updateData } = args;
|
|
177
|
+
const response = await client.updateCase(project_code, case_id, updateData);
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: JSON.stringify(response, null, 2),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: formatError(error) }],
|
|
189
|
+
isError: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Delete test case
|
|
196
|
+
server.tool(
|
|
197
|
+
"qase_delete_case",
|
|
198
|
+
"Delete a test case from a Qase project",
|
|
199
|
+
DeleteCaseSchema,
|
|
200
|
+
async (args) => {
|
|
201
|
+
try {
|
|
202
|
+
const response = await client.deleteCase(args.project_code, args.case_id);
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: JSON.stringify(response, null, 2),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: formatError(error) }],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
}
|