@uarlouski/testrail-mcp-server 1.2.0 → 1.3.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/README.md +12 -2
- package/dist/client/testrail.js +167 -0
- package/{src/index.ts → dist/index.js} +35 -35
- package/dist/tools/add_attachment_to_run.js +52 -0
- package/dist/tools/add_results.js +24 -0
- package/dist/tools/add_run.js +19 -0
- package/dist/tools/create_case.js +23 -0
- package/{src/tools/get_case.ts → dist/tools/get_case.js} +3 -12
- package/{src/tools/get_case_fields.ts → dist/tools/get_case_fields.js} +8 -37
- package/{src/tools/get_cases.ts → dist/tools/get_cases.js} +10 -24
- package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +3 -10
- package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +4 -11
- package/dist/tools/get_statuses.js +13 -0
- package/dist/tools/get_templates.js +16 -0
- package/dist/tools/get_tests.js +17 -0
- package/{src/tools/update_case.ts → dist/tools/update_case.js} +2 -8
- package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +4 -8
- package/dist/types/custom.js +1 -0
- package/{src/types/testrail.ts → dist/types/testrail.js} +37 -24
- package/{src/utils/mapper.ts → dist/utils/mapper.js} +16 -36
- package/{src/utils/sanitizer.ts → dist/utils/sanitizer.js} +4 -8
- package/package.json +10 -2
- package/.env.example +0 -3
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -35
- package/CHANGELOG.md +0 -27
- package/jest.config.js +0 -24
- package/src/client/testrail.ts +0 -161
- package/src/tools/create_case.ts +0 -29
- package/src/tools/get_templates.ts +0 -23
- package/src/types/custom.ts +0 -19
- package/test/client/testrail.test.ts +0 -430
- package/test/tools/create_case.test.ts +0 -101
- package/test/tools/get_case.test.ts +0 -185
- package/test/tools/get_case_fields.test.ts +0 -132
- package/test/tools/get_cases.test.ts +0 -104
- package/test/tools/get_projects.test.ts +0 -38
- package/test/tools/get_sections.test.ts +0 -39
- package/test/tools/get_templates.test.ts +0 -38
- package/test/tools/update_case.test.ts +0 -102
- package/test/tools/update_cases.test.ts +0 -61
- package/test/utils/mapper.test.ts +0 -112
- package/test/utils/sanitizer.test.ts +0 -143
- package/tsconfig.json +0 -24
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# TestRail MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@uarlouski/testrail-mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/@uarlouski/testrail-mcp-server)
|
|
5
|
+
[](https://github.com/uarlouski/testrail-mcp-server/actions/workflows/ci.yml)
|
|
6
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://github.com/uarlouski/testrail-mcp-server)
|
|
9
|
+
|
|
5
10
|
|
|
6
11
|
A powerful **Model Context Protocol (MCP)** server for seamless integration between **Large Language Models (LLMs)** and **TestRail** test case management.
|
|
7
12
|
|
|
@@ -63,6 +68,11 @@ This MCP server exposes the following tools to the LLM:
|
|
|
63
68
|
| `create_case` | **Authoring**: Create new test cases. *Tip: The model should check templates and fields first to ensure validity.* |
|
|
64
69
|
| `update_case` | **Maintenance**: Modify existing test case fields, title, or steps. |
|
|
65
70
|
| `update_cases` | **Bulk Edit**: Update multiple cases simultaneously for efficient refactoring. |
|
|
71
|
+
| `add_run` | **Test Execution**: Create a new test run in TestRail. |
|
|
72
|
+
| `get_tests` | **Test Execution**: Get tests for a test run, optionally filtered by status. |
|
|
73
|
+
| `add_results` | **Test Execution**: Add one or more test results to a test run. |
|
|
74
|
+
| `add_attachment_to_run` | **Test Execution**: Add an attachment (file or zipped folder) to a test run. |
|
|
75
|
+
| `get_statuses` | **Metadata**: Get all available test statuses (e.g. Passed, Failed, Blocked). |
|
|
66
76
|
| `get_case_fields` | **Metadata**: Retrieve definitions of custom fields to understand available inputs. |
|
|
67
77
|
| `get_templates` | **Metadata**: List available case templates to ensure correct case creation structure. |
|
|
68
78
|
| `get_priorities` | **Metadata**: List available priority levels. |
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
const API_INDEX = '/index.php?';
|
|
3
|
+
const API_BASE_V2 = `${API_INDEX}/api/v2`;
|
|
4
|
+
export class TestRailClient {
|
|
5
|
+
baseUrl;
|
|
6
|
+
headers;
|
|
7
|
+
auth;
|
|
8
|
+
prioritiesPromise = null;
|
|
9
|
+
caseTypesPromise = null;
|
|
10
|
+
caseFieldsPromise = null;
|
|
11
|
+
statusesPromise = null;
|
|
12
|
+
projectsPromise = null;
|
|
13
|
+
templatesPromiseMap = new Map();
|
|
14
|
+
constructor(baseUrl, email, apiKey) {
|
|
15
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
16
|
+
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
|
17
|
+
this.auth = `Basic ${auth}`;
|
|
18
|
+
this.headers = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Authorization": this.auth,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async getCase(caseId) {
|
|
24
|
+
return this.get(`${API_BASE_V2}/get_case/${caseId}`);
|
|
25
|
+
}
|
|
26
|
+
async getCases(projectId, sectionId, filter) {
|
|
27
|
+
let url = `${API_BASE_V2}/get_cases/${projectId}`;
|
|
28
|
+
if (sectionId) {
|
|
29
|
+
url += `§ion_id=${sectionId}`;
|
|
30
|
+
}
|
|
31
|
+
if (filter) {
|
|
32
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
33
|
+
url += `&${key}=${encodeURIComponent(value)}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const allCases = [];
|
|
37
|
+
let nextUrl = url;
|
|
38
|
+
while (nextUrl) {
|
|
39
|
+
const response = await this.get(nextUrl);
|
|
40
|
+
allCases.push(...response.cases);
|
|
41
|
+
nextUrl = response._links?.next;
|
|
42
|
+
if (nextUrl) {
|
|
43
|
+
nextUrl = `${API_INDEX}${nextUrl}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return allCases;
|
|
47
|
+
}
|
|
48
|
+
async getSection(sectionId) {
|
|
49
|
+
return this.get(`${API_BASE_V2}/get_section/${sectionId}`);
|
|
50
|
+
}
|
|
51
|
+
async getPriorities() {
|
|
52
|
+
if (!this.prioritiesPromise) {
|
|
53
|
+
this.prioritiesPromise = this.get(`${API_BASE_V2}/get_priorities`);
|
|
54
|
+
}
|
|
55
|
+
return this.prioritiesPromise;
|
|
56
|
+
}
|
|
57
|
+
async getCaseTypes() {
|
|
58
|
+
if (!this.caseTypesPromise) {
|
|
59
|
+
this.caseTypesPromise = this.get(`${API_BASE_V2}/get_case_types`);
|
|
60
|
+
}
|
|
61
|
+
return this.caseTypesPromise;
|
|
62
|
+
}
|
|
63
|
+
async getCaseFields() {
|
|
64
|
+
if (!this.caseFieldsPromise) {
|
|
65
|
+
this.caseFieldsPromise = this.get(`${API_BASE_V2}/get_case_fields`);
|
|
66
|
+
}
|
|
67
|
+
return this.caseFieldsPromise;
|
|
68
|
+
}
|
|
69
|
+
async getStatuses() {
|
|
70
|
+
if (!this.statusesPromise) {
|
|
71
|
+
this.statusesPromise = this.get(`${API_BASE_V2}/get_statuses`);
|
|
72
|
+
}
|
|
73
|
+
return this.statusesPromise;
|
|
74
|
+
}
|
|
75
|
+
async getTemplates(projectId) {
|
|
76
|
+
if (!this.templatesPromiseMap.has(projectId)) {
|
|
77
|
+
this.templatesPromiseMap.set(projectId, this.get(`${API_BASE_V2}/get_templates/${projectId}`));
|
|
78
|
+
}
|
|
79
|
+
return this.templatesPromiseMap.get(projectId);
|
|
80
|
+
}
|
|
81
|
+
async updateCase(caseId, fields) {
|
|
82
|
+
return this.post(`${API_BASE_V2}/update_case/${caseId}`, fields);
|
|
83
|
+
}
|
|
84
|
+
async updateCases(suiteId, caseIds, fields) {
|
|
85
|
+
const response = await this.post(`${API_BASE_V2}/update_cases/${suiteId}`, {
|
|
86
|
+
case_ids: caseIds,
|
|
87
|
+
...fields,
|
|
88
|
+
});
|
|
89
|
+
return response.updated_cases;
|
|
90
|
+
}
|
|
91
|
+
async createCase(sectionId, fields) {
|
|
92
|
+
return this.post(`${API_BASE_V2}/add_case/${sectionId}`, fields);
|
|
93
|
+
}
|
|
94
|
+
async addRun(projectId, fields) {
|
|
95
|
+
return this.post(`${API_BASE_V2}/add_run/${projectId}`, fields);
|
|
96
|
+
}
|
|
97
|
+
async getSections(projectId) {
|
|
98
|
+
const response = await this.get(`${API_BASE_V2}/get_sections/${projectId}`);
|
|
99
|
+
return response.sections;
|
|
100
|
+
}
|
|
101
|
+
async getProjects() {
|
|
102
|
+
if (!this.projectsPromise) {
|
|
103
|
+
this.projectsPromise = this.get(`${API_BASE_V2}/get_projects`)
|
|
104
|
+
.then(response => response.projects);
|
|
105
|
+
}
|
|
106
|
+
return this.projectsPromise;
|
|
107
|
+
}
|
|
108
|
+
async getTests(runId, statusId) {
|
|
109
|
+
let url = `${API_BASE_V2}/get_tests/${runId}`;
|
|
110
|
+
if (statusId && statusId.length > 0) {
|
|
111
|
+
url += `&status_id=${statusId.join(',')}`;
|
|
112
|
+
}
|
|
113
|
+
const allTests = [];
|
|
114
|
+
let nextUrl = url;
|
|
115
|
+
while (nextUrl) {
|
|
116
|
+
const response = await this.get(nextUrl);
|
|
117
|
+
allTests.push(...response.tests);
|
|
118
|
+
nextUrl = response._links?.next;
|
|
119
|
+
if (nextUrl) {
|
|
120
|
+
nextUrl = `${API_INDEX}${nextUrl}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return allTests;
|
|
124
|
+
}
|
|
125
|
+
async addResults(runId, results) {
|
|
126
|
+
return this.post(`${API_BASE_V2}/add_results/${runId}`, { results });
|
|
127
|
+
}
|
|
128
|
+
async addAttachmentToRun(runId, filePath, filename) {
|
|
129
|
+
const fileBuffer = await fs.promises.readFile(filePath);
|
|
130
|
+
const blob = new Blob([fileBuffer]);
|
|
131
|
+
const formData = new FormData();
|
|
132
|
+
formData.append('attachment', blob, filename);
|
|
133
|
+
const headers = {
|
|
134
|
+
'Authorization': this.auth,
|
|
135
|
+
};
|
|
136
|
+
return this._executeRequest('POST', `${API_BASE_V2}/add_attachment_to_run/${runId}`, headers, formData);
|
|
137
|
+
}
|
|
138
|
+
async get(endpoint) {
|
|
139
|
+
return this.executeRequest('GET', endpoint);
|
|
140
|
+
}
|
|
141
|
+
async post(endpoint, data) {
|
|
142
|
+
return this.executeRequest('POST', endpoint, data);
|
|
143
|
+
}
|
|
144
|
+
async executeRequest(method, endpoint, data) {
|
|
145
|
+
let jsonData = undefined;
|
|
146
|
+
if (data) {
|
|
147
|
+
jsonData = JSON.stringify(data);
|
|
148
|
+
}
|
|
149
|
+
return this._executeRequest(method, endpoint, this.headers, jsonData);
|
|
150
|
+
}
|
|
151
|
+
async _executeRequest(method, endpoint, headers, body) {
|
|
152
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
153
|
+
const params = {
|
|
154
|
+
method,
|
|
155
|
+
headers,
|
|
156
|
+
body,
|
|
157
|
+
};
|
|
158
|
+
const response = await fetch(url, params);
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
let errorMessage = `TestRail: ${response.status} ${response.statusText}`;
|
|
161
|
+
const errorText = await response.text();
|
|
162
|
+
errorMessage += ` - ${errorText}`;
|
|
163
|
+
throw new Error(errorMessage);
|
|
164
|
+
}
|
|
165
|
+
return await response.json();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import "dotenv/config";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -11,24 +12,24 @@ import { updateCasesTool } from "./tools/update_cases.js";
|
|
|
11
12
|
import { createCaseTool } from "./tools/create_case.js";
|
|
12
13
|
import { getSectionsTool } from "./tools/get_sections.js";
|
|
13
14
|
import { getProjectsTool } from "./tools/get_projects.js";
|
|
15
|
+
import { addRunTool } from "./tools/add_run.js";
|
|
16
|
+
import { getStatusesTool } from "./tools/get_statuses.js";
|
|
17
|
+
import { getTestsTool } from "./tools/get_tests.js";
|
|
18
|
+
import { addResultsTool } from "./tools/add_results.js";
|
|
19
|
+
import { addAttachmentToRunTool } from "./tools/add_attachment_to_run.js";
|
|
14
20
|
import { removeNullish } from "./utils/sanitizer.js";
|
|
15
|
-
|
|
16
21
|
const TESTRAIL_INSTANCE_URL = process.env.TESTRAIL_INSTANCE_URL;
|
|
17
22
|
const TESTRAIL_USERNAME = process.env.TESTRAIL_USERNAME;
|
|
18
23
|
const TESTRAIL_API_KEY = process.env.TESTRAIL_API_KEY;
|
|
19
|
-
|
|
20
24
|
if (!TESTRAIL_INSTANCE_URL || !TESTRAIL_USERNAME || !TESTRAIL_API_KEY) {
|
|
21
25
|
console.error("Error: TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, and TESTRAIL_API_KEY environment variables are required.");
|
|
22
26
|
process.exit(1);
|
|
23
27
|
}
|
|
24
|
-
|
|
25
28
|
const server = new McpServer({
|
|
26
29
|
name: "TestRail MCP Server",
|
|
27
|
-
version: "1.
|
|
30
|
+
version: "1.3.1",
|
|
28
31
|
});
|
|
29
|
-
|
|
30
32
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
31
|
-
|
|
32
33
|
const tools = [
|
|
33
34
|
getProjectsTool,
|
|
34
35
|
getCaseTool,
|
|
@@ -39,37 +40,36 @@ const tools = [
|
|
|
39
40
|
updateCaseTool,
|
|
40
41
|
updateCasesTool,
|
|
41
42
|
createCaseTool,
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
addRunTool,
|
|
44
|
+
getStatusesTool,
|
|
45
|
+
getTestsTool,
|
|
46
|
+
addResultsTool,
|
|
47
|
+
addAttachmentToRunTool,
|
|
48
|
+
];
|
|
44
49
|
for (const tool of tools) {
|
|
45
|
-
server.registerTool(
|
|
46
|
-
tool.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
text: JSON.stringify(sanitized, null, 2),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
} as any;
|
|
64
|
-
} catch (error: any) {
|
|
65
|
-
return {
|
|
66
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
67
|
-
isError: true,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
50
|
+
server.registerTool(tool.name, {
|
|
51
|
+
description: tool.description,
|
|
52
|
+
inputSchema: tool.parameters,
|
|
53
|
+
}, async (args) => {
|
|
54
|
+
try {
|
|
55
|
+
const output = await tool.handler(args, client);
|
|
56
|
+
const sanitized = removeNullish(output);
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: JSON.stringify(sanitized),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
70
65
|
}
|
|
71
|
-
|
|
66
|
+
catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
72
73
|
}
|
|
73
|
-
|
|
74
74
|
const transport = new StdioServerTransport();
|
|
75
75
|
await server.connect(transport);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AttachmentSchema } from "../types/testrail.js";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import archiver from "archiver";
|
|
7
|
+
const parameters = {
|
|
8
|
+
run_id: z.string().describe("The ID of the test run to attach the file to"),
|
|
9
|
+
file_path: z.string().describe("The path to the file or directory to attach. Directories will be automatically zipped."),
|
|
10
|
+
};
|
|
11
|
+
export const addAttachmentToRunTool = {
|
|
12
|
+
name: "add_attachment_to_run",
|
|
13
|
+
description: "Add an attachment to a test run in TestRail. If the file_path points to a directory, it will be automatically zipped before uploading. Maximum upload size is 256MB.",
|
|
14
|
+
parameters,
|
|
15
|
+
handler: async ({ run_id, file_path }, client) => {
|
|
16
|
+
if (!fs.existsSync(file_path)) {
|
|
17
|
+
throw new Error(`File or directory not found: ${file_path}`);
|
|
18
|
+
}
|
|
19
|
+
const stats = fs.statSync(file_path);
|
|
20
|
+
let uploadPath = file_path;
|
|
21
|
+
let filename = path.basename(file_path);
|
|
22
|
+
let isTemporary = false;
|
|
23
|
+
if (stats.isDirectory()) {
|
|
24
|
+
const basename = path.basename(file_path);
|
|
25
|
+
const tempDir = os.tmpdir();
|
|
26
|
+
const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
const output = fs.createWriteStream(zipPath);
|
|
29
|
+
const archive = archiver('zip', {
|
|
30
|
+
zlib: { level: 9 }
|
|
31
|
+
});
|
|
32
|
+
output.on('close', () => resolve());
|
|
33
|
+
archive.on('error', (err) => reject(err));
|
|
34
|
+
archive.pipe(output);
|
|
35
|
+
archive.directory(file_path, basename);
|
|
36
|
+
archive.finalize();
|
|
37
|
+
});
|
|
38
|
+
uploadPath = zipPath;
|
|
39
|
+
filename = `${basename}.zip`;
|
|
40
|
+
isTemporary = true;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const result = await client.addAttachmentToRun(Number(run_id), uploadPath, filename);
|
|
44
|
+
return AttachmentSchema.parse(result);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
if (isTemporary && fs.existsSync(uploadPath)) {
|
|
48
|
+
fs.unlinkSync(uploadPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ResultSchema } from "../types/testrail.js";
|
|
3
|
+
const parameters = {
|
|
4
|
+
run_id: z.number().describe("The ID of the test run"),
|
|
5
|
+
results: z.array(z.object({
|
|
6
|
+
test_id: z.number().describe("The ID of the test. Use get_tests with a run_id to retrieve available test IDs"),
|
|
7
|
+
status_id: z.number().describe("The ID of the test status (e.g. Passed, Failed). Use get_statuses to retrieve available status IDs"),
|
|
8
|
+
comment: z.string().optional().describe("Optional comment/description for the result"),
|
|
9
|
+
defects: z.string().optional().describe("Optional comma-separated list of defect IDs"),
|
|
10
|
+
})).describe("Array of results to add. Each result must have test_id and status_id"),
|
|
11
|
+
};
|
|
12
|
+
export const addResultsTool = {
|
|
13
|
+
name: "add_results",
|
|
14
|
+
description: "Add one or more test results to a test run",
|
|
15
|
+
parameters,
|
|
16
|
+
handler: async ({ run_id, results }, client) => {
|
|
17
|
+
const response = await client.addResults(run_id, results);
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
added_count: response.length,
|
|
21
|
+
results: response.map(r => ResultSchema.parse(r)),
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RunSchema } from "../types/testrail.js";
|
|
3
|
+
const parameters = {
|
|
4
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
5
|
+
name: z.string().optional().describe("The name of the test run"),
|
|
6
|
+
description: z.string().optional().describe("The description of the test run"),
|
|
7
|
+
case_ids: z.array(z.number()).describe("Array of case IDs to include in the run. After creating the run, use get_tests with the returned run_id to retrieve test IDs for result submission")
|
|
8
|
+
};
|
|
9
|
+
export const addRunTool = {
|
|
10
|
+
name: "add_run",
|
|
11
|
+
description: "Create a new test run in TestRail",
|
|
12
|
+
parameters,
|
|
13
|
+
handler: async ({ project_id, ...fields }, client) => {
|
|
14
|
+
const caseData = await client.getCase(String(fields.case_ids[0]));
|
|
15
|
+
const params = { suite_id: caseData.suite_id, include_all: false, ...fields };
|
|
16
|
+
const run = await client.addRun(project_id, params);
|
|
17
|
+
return RunSchema.parse(run);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const parameters = {
|
|
3
|
+
section_id: z.string().describe("The ID of the section where the case should be created. Use get_sections to find available sections"),
|
|
4
|
+
title: z.string().describe("The title of the test case"),
|
|
5
|
+
fields: z.record(z.string(), z.any()).optional().describe("Optional fields for the test case. First use get_templates to discover available templates, then get_case_fields to see available fields for your template. Example: {\"priority_id\": 2, \"template_id\": 1, \"custom_automation_priority\": 1}"),
|
|
6
|
+
};
|
|
7
|
+
export const createCaseTool = {
|
|
8
|
+
name: "create_case",
|
|
9
|
+
description: "Create a new test case in TestRail",
|
|
10
|
+
parameters,
|
|
11
|
+
handler: async ({ section_id, title, fields }, client) => {
|
|
12
|
+
const caseData = {
|
|
13
|
+
title,
|
|
14
|
+
...fields,
|
|
15
|
+
};
|
|
16
|
+
const createdCase = await client.createCase(section_id, caseData);
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
case_id: createdCase.id,
|
|
20
|
+
message: `Test case C${createdCase.id} created successfully`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { processCustomFields } from "../utils/mapper.js";
|
|
3
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
4
|
-
import { TestCaseResponse, ToolDefinition } from "../types/custom.js";
|
|
5
|
-
|
|
6
3
|
const parameters = {
|
|
7
4
|
case_id: z.string().describe("The ID of the test case (e.g. '123' or 'C123')"),
|
|
8
5
|
};
|
|
9
|
-
|
|
10
|
-
export const getCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
6
|
+
export const getCaseTool = {
|
|
11
7
|
name: "get_case",
|
|
12
|
-
description: "Get a test case
|
|
8
|
+
description: "Get detailed information about a test case including its custom fields",
|
|
13
9
|
parameters,
|
|
14
10
|
handler: async ({ case_id }, client) => {
|
|
15
11
|
const id = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
|
|
16
12
|
const testCase = await client.getCase(id);
|
|
17
|
-
|
|
18
13
|
const [section, caseTypes, priorities, caseFields] = await Promise.all([
|
|
19
14
|
client.getSection(testCase.section_id.toString()),
|
|
20
15
|
client.getCaseTypes(),
|
|
21
16
|
client.getPriorities(),
|
|
22
17
|
client.getCaseFields()
|
|
23
18
|
]);
|
|
24
|
-
|
|
25
19
|
const caseType = caseTypes.find(t => t.id === testCase.type_id);
|
|
26
20
|
const priority = priorities.find(p => p.id === testCase.priority_id);
|
|
27
|
-
|
|
28
21
|
const customFields = processCustomFields(testCase, caseFields);
|
|
29
|
-
|
|
30
|
-
const response: TestCaseResponse = {
|
|
22
|
+
const response = {
|
|
31
23
|
id: testCase.id,
|
|
32
24
|
title: testCase.title,
|
|
33
25
|
section: section.name,
|
|
@@ -38,7 +30,6 @@ export const getCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
|
38
30
|
updated_on: testCase.updated_on,
|
|
39
31
|
...customFields
|
|
40
32
|
};
|
|
41
|
-
|
|
42
33
|
return response;
|
|
43
34
|
}
|
|
44
35
|
};
|
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
3
|
-
import { CaseField } from "../types/testrail.js";
|
|
4
1
|
import { parseDropdownOptions } from "../utils/mapper.js";
|
|
5
|
-
|
|
6
2
|
const parameters = {};
|
|
7
|
-
|
|
8
|
-
interface FieldSchema {
|
|
9
|
-
system_name: string;
|
|
10
|
-
label: string;
|
|
11
|
-
type: string;
|
|
12
|
-
is_required: boolean;
|
|
13
|
-
template_ids?: number[];
|
|
14
|
-
options?: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface CaseFieldsResponse {
|
|
18
|
-
fields: FieldSchema[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const FIELD_TYPE_NAMES: Record<number, string> = {
|
|
3
|
+
const FIELD_TYPE_NAMES = {
|
|
22
4
|
1: "String",
|
|
23
5
|
2: "Integer",
|
|
24
6
|
3: "Text",
|
|
@@ -33,35 +15,28 @@ const FIELD_TYPE_NAMES: Record<number, string> = {
|
|
|
33
15
|
12: "Multi-select",
|
|
34
16
|
13: "Scenarios",
|
|
35
17
|
};
|
|
36
|
-
|
|
37
|
-
function getFieldTypeName(typeId: number): string {
|
|
18
|
+
function getFieldTypeName(typeId) {
|
|
38
19
|
return FIELD_TYPE_NAMES[typeId] || `Unknown (${typeId})`;
|
|
39
20
|
}
|
|
40
|
-
|
|
41
|
-
function isFieldRequired(field: CaseField): boolean {
|
|
21
|
+
function isFieldRequired(field) {
|
|
42
22
|
return field.configs.some(config => config.options?.is_required === true);
|
|
43
23
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const schema: FieldSchema = {
|
|
24
|
+
function mapToFieldSchema(field) {
|
|
25
|
+
const schema = {
|
|
47
26
|
system_name: field.system_name,
|
|
48
27
|
label: field.label,
|
|
49
28
|
type: getFieldTypeName(field.type_id),
|
|
50
29
|
is_required: isFieldRequired(field),
|
|
51
30
|
};
|
|
52
|
-
|
|
53
31
|
if (!field.include_all && field.template_ids.length > 0) {
|
|
54
32
|
schema.template_ids = field.template_ids;
|
|
55
33
|
}
|
|
56
|
-
|
|
57
34
|
if (field.type_id === 6 || field.type_id === 12) {
|
|
58
35
|
schema.options = Array.from(parseDropdownOptions(field).values());
|
|
59
36
|
}
|
|
60
|
-
|
|
61
37
|
return schema;
|
|
62
38
|
}
|
|
63
|
-
|
|
64
|
-
const SYSTEM_FIELDS: FieldSchema[] = [
|
|
39
|
+
const SYSTEM_FIELDS = [
|
|
65
40
|
{ system_name: "title", label: "Title", type: "String", is_required: true },
|
|
66
41
|
{ system_name: "section_id", label: "Section", type: "Integer", is_required: true },
|
|
67
42
|
{ system_name: "template_id", label: "Template", type: "Integer", is_required: false },
|
|
@@ -71,20 +46,16 @@ const SYSTEM_FIELDS: FieldSchema[] = [
|
|
|
71
46
|
{ system_name: "milestone_id", label: "Milestone", type: "Integer", is_required: false },
|
|
72
47
|
{ system_name: "refs", label: "References", type: "String", is_required: false },
|
|
73
48
|
];
|
|
74
|
-
|
|
75
|
-
export const getCaseFieldsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
49
|
+
export const getCaseFieldsTool = {
|
|
76
50
|
name: "get_case_fields",
|
|
77
51
|
description: "Get the field schema for test cases. Returns all available fields with their types, descriptions, and options (for dropdown fields). Use this to understand what fields can be set when creating or updating test cases.",
|
|
78
52
|
parameters,
|
|
79
53
|
handler: async (_args, client) => {
|
|
80
54
|
const caseFields = await client.getCaseFields();
|
|
81
|
-
|
|
82
55
|
const customFieldSchemas = caseFields.filter(field => field.is_active).map(mapToFieldSchema);
|
|
83
|
-
|
|
84
|
-
const response: CaseFieldsResponse = {
|
|
56
|
+
const response = {
|
|
85
57
|
fields: [...SYSTEM_FIELDS, ...customFieldSchemas],
|
|
86
58
|
};
|
|
87
|
-
|
|
88
59
|
return response;
|
|
89
60
|
}
|
|
90
61
|
};
|
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
|
-
import { Case } from "../types/testrail.js";
|
|
5
|
-
|
|
6
2
|
const parameters = {
|
|
7
|
-
project_id: z.string().describe("The ID of the project"),
|
|
8
|
-
section_id: z.string().optional().describe("Optional section ID to filter cases by section"),
|
|
9
|
-
filter: z.record(z.string(), z.string()).optional().describe("Optional API
|
|
10
|
-
where: z.record(z.string(), z.any()).optional().describe("Optional client-side filter for any field including custom fields
|
|
11
|
-
fields: z.array(z.string()).optional().describe("
|
|
3
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
4
|
+
section_id: z.string().optional().describe("Optional section ID to filter cases by section. Use get_sections to find available sections"),
|
|
5
|
+
filter: z.record(z.string(), z.string()).optional().describe("Optional API-side filters (more efficient for large datasets). Supported: priority_id, type_id, created_by, updated_by, milestone_id, refs, created_after, created_before, updated_after, updated_before. Use comma-separated values for IDs. Example: {\"priority_id\": \"1,2\", \"type_id\": \"3\"}"),
|
|
6
|
+
where: z.record(z.string(), z.any()).optional().describe("Optional client-side filter for any field including custom fields (filters after fetching all cases). Supports exact value matching. Example: {\"custom_automation_status\": 1, \"priority_id\": 2}"),
|
|
7
|
+
fields: z.array(z.string()).optional().describe("Additional fields to include in response beyond id, title, and suite_id. Use get_case_fields to see available fields. Example: [\"priority_id\", \"type_id\", \"custom_automation_status\"]"),
|
|
12
8
|
};
|
|
13
|
-
|
|
14
|
-
interface CasesResponse {
|
|
15
|
-
cases: Record<string, any>[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
9
|
+
export const getCasesTool = {
|
|
19
10
|
name: "get_cases",
|
|
20
11
|
description: "Get all test cases for a project. Filter by section, API params (priority, type), or any field including custom fields via 'where'. Returns case IDs, titles, and any additional requested fields.",
|
|
21
12
|
parameters,
|
|
22
13
|
handler: async ({ project_id, section_id, filter, where, fields }, client) => {
|
|
23
|
-
let cases
|
|
24
|
-
|
|
14
|
+
let cases = await client.getCases(project_id, section_id, filter);
|
|
25
15
|
// Client-side filtering for custom fields and other unsupported API filters
|
|
26
16
|
if (where) {
|
|
27
17
|
cases = cases.filter(c => {
|
|
28
18
|
for (const [key, value] of Object.entries(where)) {
|
|
29
|
-
const caseValue =
|
|
19
|
+
const caseValue = c[key];
|
|
30
20
|
if (caseValue !== value) {
|
|
31
21
|
return false;
|
|
32
22
|
}
|
|
@@ -34,15 +24,13 @@ export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
|
34
24
|
return true;
|
|
35
25
|
});
|
|
36
26
|
}
|
|
37
|
-
|
|
38
|
-
const response: CasesResponse = {
|
|
27
|
+
const response = {
|
|
39
28
|
cases: cases.map(c => {
|
|
40
|
-
const result
|
|
29
|
+
const result = {
|
|
41
30
|
id: c.id,
|
|
42
31
|
title: c.title,
|
|
43
32
|
suite_id: c.suite_id,
|
|
44
33
|
};
|
|
45
|
-
|
|
46
34
|
if (fields) {
|
|
47
35
|
for (const field of fields) {
|
|
48
36
|
if (field in c) {
|
|
@@ -50,11 +38,9 @@ export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
|
50
38
|
}
|
|
51
39
|
}
|
|
52
40
|
}
|
|
53
|
-
|
|
54
41
|
return result;
|
|
55
42
|
}),
|
|
56
43
|
};
|
|
57
|
-
|
|
58
44
|
return response;
|
|
59
45
|
}
|
|
60
46
|
};
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
3
1
|
import { ProjectSchema } from "../types/testrail.js";
|
|
4
|
-
|
|
5
2
|
const parameters = {};
|
|
6
|
-
|
|
7
|
-
export const getProjectsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
3
|
+
export const getProjectsTool = {
|
|
8
4
|
name: "get_projects",
|
|
9
|
-
description: "Get all available projects in TestRail. Returns project IDs and names
|
|
5
|
+
description: "Get all available projects in TestRail. Returns project IDs and names that can be used with get_sections, get_templates, get_cases, and add_run",
|
|
10
6
|
parameters,
|
|
11
7
|
handler: async (_args, client) => {
|
|
12
8
|
const projects = await client.getProjects();
|
|
13
|
-
|
|
14
|
-
const response = {
|
|
9
|
+
return {
|
|
15
10
|
projects: projects
|
|
16
11
|
.filter(p => !p.is_completed)
|
|
17
12
|
.map(p => ProjectSchema.parse(p)),
|
|
18
13
|
};
|
|
19
|
-
|
|
20
|
-
return response;
|
|
21
14
|
}
|
|
22
15
|
};
|