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/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
+ }