@uarlouski/testrail-mcp-server 1.1.0 → 1.3.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/CHANGELOG.md +18 -0
- package/README.md +62 -16
- package/package.json +3 -2
- package/src/client/testrail.ts +111 -56
- package/src/index.ts +32 -2
- package/src/tools/add_attachment_to_run.ts +62 -0
- package/src/tools/add_results.ts +28 -0
- package/src/tools/add_run.ts +24 -0
- package/src/tools/create_case.ts +10 -17
- package/src/tools/get_case.ts +5 -12
- package/src/tools/get_case_fields.ts +4 -11
- package/src/tools/get_cases.ts +9 -16
- package/src/tools/get_projects.ts +7 -27
- package/src/tools/get_sections.ts +7 -27
- package/src/tools/get_statuses.ts +18 -0
- package/src/tools/get_templates.ts +7 -27
- package/src/tools/get_tests.ts +22 -0
- package/src/tools/update_case.ts +7 -14
- package/src/tools/update_cases.ts +10 -16
- package/src/types/custom.ts +1 -16
- package/src/types/testrail.ts +140 -87
- package/src/utils/sanitizer.ts +29 -0
- package/test/client/testrail.test.ts +329 -141
- package/test/tools/add_results.test.ts +94 -0
- package/test/tools/add_run.test.ts +83 -0
- package/test/tools/create_case.test.ts +10 -16
- package/test/tools/get_case.test.ts +47 -62
- package/test/tools/get_case_fields.test.ts +7 -80
- package/test/tools/get_cases.test.ts +19 -108
- package/test/tools/get_projects.test.ts +4 -41
- package/test/tools/get_sections.test.ts +7 -36
- package/test/tools/get_statuses.test.ts +56 -0
- package/test/tools/get_templates.test.ts +1 -30
- package/test/tools/get_tests.test.ts +47 -0
- package/test/tools/update_case.test.ts +10 -16
- package/test/tools/update_cases.test.ts +29 -28
- package/test/utils/mapper.test.ts +13 -13
- package/test/utils/sanitizer.test.ts +85 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2026-02-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Test run and result management tools**
|
|
8
|
+
- `add_run` - Create a new test run in TestRail
|
|
9
|
+
- `get_tests` - Get tests for a test run, optionally filtered by status
|
|
10
|
+
- `add_results` - Add one or more test results to a test run
|
|
11
|
+
- `get_statuses` - Get all available test statuses (e.g. Passed, Failed, Blocked)
|
|
12
|
+
- Add `add_attachment_to_run` tool to attach files to test runs
|
|
13
|
+
|
|
14
|
+
## [1.2.0] - 2026-02-14
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Optimize MCP call responses by removing non-essential fields and nullish values
|
|
19
|
+
- Migrated from `axios` to native `fetch` API for reduced dependency footprint
|
|
20
|
+
|
|
3
21
|
## [1.1.0] - 2026-02-09
|
|
4
22
|
|
|
5
23
|
### Added
|
package/README.md
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
# TestRail MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
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)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
|
|
11
|
+
A powerful **Model Context Protocol (MCP)** server for seamless integration between **Large Language Models (LLMs)** and **TestRail** test case management.
|
|
12
|
+
|
|
13
|
+
This MCP server enables AI assistants like **Claude**, **Cursor**, **Windsurf**, and other MCP-compliant clients to interact directly with your TestRail instance. Streamline your QA workflows by allowing your AI to search, retrieve, create, and update test cases, plans, and runs.
|
|
14
|
+
|
|
15
|
+
## 🚀 Features
|
|
16
|
+
|
|
17
|
+
- **AI-Powered Test Management**: Bridge the gap between AI and your QA process.
|
|
18
|
+
- **Smart Discovery**: List projects, suites, and sections to help LLMs understand your test organization.
|
|
19
|
+
- **Comprehensive Case Operations**: Fetch, create, and update test cases with full support for custom fields.
|
|
20
|
+
- **Context-Aware**: Designed to provide LLMs with the right context (templates, field options) for accurate test generation.
|
|
21
|
+
|
|
22
|
+
## ⚙️ Usage
|
|
23
|
+
|
|
24
|
+
Configure the server in your MCP client settings (e.g., `claude_desktop_config.json` or Cursor settings).
|
|
25
|
+
|
|
26
|
+
### Claude Desktop / General MCP Clients
|
|
27
|
+
|
|
28
|
+
Add the following to your configuration file:
|
|
6
29
|
|
|
7
30
|
```json
|
|
8
31
|
{
|
|
@@ -10,6 +33,7 @@ A Model Context Protocol (MCP) server for integrating LLMs with TestRail test ca
|
|
|
10
33
|
"testrail": {
|
|
11
34
|
"command": "npx",
|
|
12
35
|
"args": [
|
|
36
|
+
"-y",
|
|
13
37
|
"@uarlouski/testrail-mcp-server@latest"
|
|
14
38
|
],
|
|
15
39
|
"env": {
|
|
@@ -22,20 +46,42 @@ A Model Context Protocol (MCP) server for integrating LLMs with TestRail test ca
|
|
|
22
46
|
}
|
|
23
47
|
```
|
|
24
48
|
|
|
25
|
-
|
|
49
|
+
### Environment Variables
|
|
50
|
+
|
|
51
|
+
| Variable | Description | Required |
|
|
52
|
+
|----------|-------------|:--------:|
|
|
53
|
+
| `TESTRAIL_INSTANCE_URL` | The URL of your TestRail instance (e.g., `https://example.testrail.io`) | Yes |
|
|
54
|
+
| `TESTRAIL_USERNAME` | Your TestRail email address | Yes |
|
|
55
|
+
| `TESTRAIL_API_KEY` | Your TestRail API key (generate in My Settings > API Keys) | Yes |
|
|
56
|
+
|
|
57
|
+
## 🛠️ Available Tools
|
|
58
|
+
|
|
59
|
+
This MCP server exposes the following tools to the LLM:
|
|
26
60
|
|
|
27
61
|
| Tool | Description |
|
|
28
62
|
|------|-------------|
|
|
29
|
-
| `get_projects` |
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
| `get_projects` | **Discovery**: List all available projects in the TestRail instance to establish context. |
|
|
64
|
+
| `get_suites` | **Discovery**: Retrieve test suites for a given project (if not in single-suite mode). |
|
|
65
|
+
| `get_sections` | **Structure**: navigate the folder/section hierarchy of a test suite. |
|
|
66
|
+
| `get_cases` | **Search**: Query test cases with filtering (priority, template, type, etc.) and pagination. |
|
|
67
|
+
| `get_case` | **Detail**: Fetch comprehensive details of a specific test case, including steps and custom fields. |
|
|
68
|
+
| `create_case` | **Authoring**: Create new test cases. *Tip: The model should check templates and fields first to ensure validity.* |
|
|
69
|
+
| `update_case` | **Maintenance**: Modify existing test case fields, title, or steps. |
|
|
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). |
|
|
76
|
+
| `get_case_fields` | **Metadata**: Retrieve definitions of custom fields to understand available inputs. |
|
|
77
|
+
| `get_templates` | **Metadata**: List available case templates to ensure correct case creation structure. |
|
|
78
|
+
| `get_priorities` | **Metadata**: List available priority levels. |
|
|
79
|
+
| `get_case_types` | **Metadata**: List available case types (e.g., Automated, Manual). |
|
|
80
|
+
|
|
81
|
+
## 📜 License
|
|
82
|
+
|
|
83
|
+
This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
*Keywords: TestRail, MCP, Model Context Protocol, LLM, AI, Claude, Cursor, Windsurf, Test Management, QA, Automation, Testing, API*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uarlouski/testrail-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for TestRail integration, enabling LLMs to fetch test cases and metadata.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"license": "Apache-2.0",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
37
|
-
"
|
|
37
|
+
"archiver": "^7.0.1",
|
|
38
38
|
"dotenv": "^17.2.3",
|
|
39
39
|
"zod": "^4.3.5"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
+
"@types/archiver": "^7.0.0",
|
|
42
43
|
"@types/jest": "^30.0.0",
|
|
43
44
|
"@types/node": "^25.0.9",
|
|
44
45
|
"jest": "^30.2.0",
|
package/src/client/testrail.ts
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { Case, Section, Priority, CaseType, CaseField, Template, Project, Run, Status, Test, Result, Attachment } from "../types/testrail.js";
|
|
2
|
+
|
|
3
|
+
import * as fs from "fs";
|
|
3
4
|
|
|
4
5
|
interface PaginatedCasesResponse {
|
|
5
6
|
cases: Case[];
|
|
6
7
|
_links: { next: string | null };
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
interface PaginatedTestsResponse {
|
|
11
|
+
tests: Test[];
|
|
12
|
+
_links: { next: string | null };
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
const API_INDEX = '/index.php?';
|
|
10
16
|
const API_BASE_V2 = `${API_INDEX}/api/v2`;
|
|
11
17
|
|
|
12
18
|
export class TestRailClient {
|
|
13
|
-
private
|
|
19
|
+
private baseUrl: string;
|
|
20
|
+
private headers: HeadersInit;
|
|
21
|
+
private auth: string;
|
|
14
22
|
private prioritiesPromise: Promise<Priority[]> | null = null;
|
|
15
23
|
private caseTypesPromise: Promise<CaseType[]> | null = null;
|
|
16
24
|
private caseFieldsPromise: Promise<CaseField[]> | null = null;
|
|
25
|
+
private statusesPromise: Promise<Status[]> | null = null;
|
|
17
26
|
private projectsPromise: Promise<Project[]> | null = null;
|
|
18
27
|
private templatesPromiseMap: Map<string, Promise<Template[]>> = new Map();
|
|
19
28
|
|
|
20
29
|
constructor(baseUrl: string, email: string, apiKey: string) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
auth: {
|
|
29
|
-
username: email,
|
|
30
|
-
password: apiKey,
|
|
31
|
-
},
|
|
32
|
-
validateStatus: (status: number) => {
|
|
33
|
-
return status >= 200 && status < 300;
|
|
34
|
-
}
|
|
35
|
-
});
|
|
30
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
31
|
+
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
|
32
|
+
this.auth = `Basic ${auth}`;
|
|
33
|
+
this.headers = {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
"Authorization": this.auth,
|
|
36
|
+
};
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
async getCase(caseId: string): Promise<Case> {
|
|
39
|
-
return this.
|
|
40
|
+
return this.get<Case>(`${API_BASE_V2}/get_case/${caseId}`);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async getCases(projectId: string, sectionId?: string, filter?: Record<string, string>): Promise<Case[]> {
|
|
@@ -56,7 +57,7 @@ export class TestRailClient {
|
|
|
56
57
|
let nextUrl: string | null = url;
|
|
57
58
|
|
|
58
59
|
while (nextUrl) {
|
|
59
|
-
const response: PaginatedCasesResponse = await this.
|
|
60
|
+
const response: PaginatedCasesResponse = await this.get<PaginatedCasesResponse>(nextUrl);
|
|
60
61
|
|
|
61
62
|
allCases.push(...response.cases);
|
|
62
63
|
|
|
@@ -70,49 +71,53 @@ export class TestRailClient {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
async getSection(sectionId: string): Promise<Section> {
|
|
73
|
-
return this.
|
|
74
|
+
return this.get<Section>(`${API_BASE_V2}/get_section/${sectionId}`);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
async getPriorities(): Promise<Priority[]> {
|
|
77
78
|
if (!this.prioritiesPromise) {
|
|
78
|
-
this.prioritiesPromise = this.
|
|
79
|
+
this.prioritiesPromise = this.get<Priority[]>(`${API_BASE_V2}/get_priorities`);
|
|
79
80
|
}
|
|
80
81
|
return this.prioritiesPromise;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
async getCaseTypes(): Promise<CaseType[]> {
|
|
84
85
|
if (!this.caseTypesPromise) {
|
|
85
|
-
this.caseTypesPromise = this.
|
|
86
|
+
this.caseTypesPromise = this.get<CaseType[]>(`${API_BASE_V2}/get_case_types`);
|
|
86
87
|
}
|
|
87
88
|
return this.caseTypesPromise;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async getCaseFields(): Promise<CaseField[]> {
|
|
91
92
|
if (!this.caseFieldsPromise) {
|
|
92
|
-
this.caseFieldsPromise = this.
|
|
93
|
+
this.caseFieldsPromise = this.get<CaseField[]>(`${API_BASE_V2}/get_case_fields`);
|
|
93
94
|
}
|
|
94
95
|
return this.caseFieldsPromise;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
async getStatuses(): Promise<Status[]> {
|
|
99
|
+
if (!this.statusesPromise) {
|
|
100
|
+
this.statusesPromise = this.get<Status[]>(`${API_BASE_V2}/get_statuses`);
|
|
101
|
+
}
|
|
102
|
+
return this.statusesPromise;
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
async getTemplates(projectId: string): Promise<Template[]> {
|
|
98
106
|
if (!this.templatesPromiseMap.has(projectId)) {
|
|
99
107
|
this.templatesPromiseMap.set(
|
|
100
108
|
projectId,
|
|
101
|
-
this.
|
|
109
|
+
this.get<Template[]>(`${API_BASE_V2}/get_templates/${projectId}`)
|
|
102
110
|
);
|
|
103
111
|
}
|
|
104
112
|
return this.templatesPromiseMap.get(projectId)!;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
async updateCase(caseId: string, fields: Record<string, any>): Promise<Case> {
|
|
108
|
-
return this.
|
|
116
|
+
return this.post<Case>(`${API_BASE_V2}/update_case/${caseId}`, fields);
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
async updateCases(caseIds: number[], fields: Record<string, any>): Promise<Case[]> {
|
|
112
|
-
|
|
113
|
-
const caseData = await this.getCase(String(caseIds[0]));
|
|
114
|
-
|
|
115
|
-
const response = await this.postRequest<{ updated_cases: Case[] }>(`${API_BASE_V2}/update_cases/${caseData.suite_id}`, {
|
|
119
|
+
async updateCases(suiteId: number, caseIds: number[], fields: Record<string, any>): Promise<Case[]> {
|
|
120
|
+
const response = await this.post<{ updated_cases: Case[] }>(`${API_BASE_V2}/update_cases/${suiteId}`, {
|
|
116
121
|
case_ids: caseIds,
|
|
117
122
|
...fields,
|
|
118
123
|
});
|
|
@@ -121,52 +126,102 @@ export class TestRailClient {
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
async createCase(sectionId: string, fields: Record<string, any>): Promise<Case> {
|
|
124
|
-
return this.
|
|
129
|
+
return this.post<Case>(`${API_BASE_V2}/add_case/${sectionId}`, fields);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async addRun(projectId: string, fields: Record<string, any>): Promise<Run> {
|
|
133
|
+
return this.post<Run>(`${API_BASE_V2}/add_run/${projectId}`, fields);
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
async getSections(projectId: string): Promise<Section[]> {
|
|
128
|
-
const response = await this.
|
|
137
|
+
const response = await this.get<{ sections: Section[] }>(`${API_BASE_V2}/get_sections/${projectId}`);
|
|
129
138
|
return response.sections;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
async getProjects(): Promise<Project[]> {
|
|
133
142
|
if (!this.projectsPromise) {
|
|
134
|
-
this.projectsPromise = this.
|
|
143
|
+
this.projectsPromise = this.get<{ projects: Project[] }>(`${API_BASE_V2}/get_projects`)
|
|
135
144
|
.then(response => response.projects);
|
|
136
145
|
}
|
|
137
146
|
return this.projectsPromise;
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
|
|
150
|
+
async getTests(runId: number, statusId?: number[]): Promise<Test[]> {
|
|
151
|
+
let url = `${API_BASE_V2}/get_tests/${runId}`;
|
|
152
|
+
|
|
153
|
+
if (statusId && statusId.length > 0) {
|
|
154
|
+
url += `&status_id=${statusId.join(',')}`;
|
|
146
155
|
}
|
|
156
|
+
|
|
157
|
+
const allTests: Test[] = [];
|
|
158
|
+
let nextUrl: string | null = url;
|
|
159
|
+
|
|
160
|
+
while (nextUrl) {
|
|
161
|
+
const response: PaginatedTestsResponse = await this.get<PaginatedTestsResponse>(nextUrl);
|
|
162
|
+
allTests.push(...response.tests);
|
|
163
|
+
|
|
164
|
+
nextUrl = response._links?.next;
|
|
165
|
+
if (nextUrl) {
|
|
166
|
+
nextUrl = `${API_INDEX}${nextUrl}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return allTests;
|
|
147
171
|
}
|
|
148
172
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
|
|
174
|
+
async addResults(runId: number, results: Array<Record<string, any>>): Promise<Result[]> {
|
|
175
|
+
return this.post<Result[]>(`${API_BASE_V2}/add_results/${runId}`, { results });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async addAttachmentToRun(runId: number, filePath: string, filename: string): Promise<Attachment> {
|
|
179
|
+
const fileBuffer = await fs.promises.readFile(filePath);
|
|
180
|
+
const blob = new Blob([fileBuffer]);
|
|
181
|
+
const formData = new FormData();
|
|
182
|
+
formData.append('attachment', blob, filename);
|
|
183
|
+
|
|
184
|
+
const headers: HeadersInit = {
|
|
185
|
+
'Authorization': this.auth,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return this._executeRequest<Attachment>('POST', `${API_BASE_V2}/add_attachment_to_run/${runId}`, headers, formData);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async get<T>(endpoint: string): Promise<T> {
|
|
192
|
+
return this.executeRequest<T>('GET', endpoint);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async post<T>(endpoint: string, data: Record<string, any>): Promise<T> {
|
|
196
|
+
return this.executeRequest<T>('POST', endpoint, data);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async executeRequest<T>(method: 'GET' | 'POST', endpoint: string, data?: Record<string, any>): Promise<T> {
|
|
200
|
+
let jsonData = undefined;
|
|
201
|
+
if (data) {
|
|
202
|
+
jsonData = JSON.stringify(data);
|
|
155
203
|
}
|
|
204
|
+
|
|
205
|
+
return this._executeRequest<T>(method, endpoint, this.headers, jsonData);
|
|
156
206
|
}
|
|
157
207
|
|
|
158
|
-
private
|
|
159
|
-
|
|
208
|
+
private async _executeRequest<T>(method: 'GET' | 'POST', endpoint: string, headers: HeadersInit, body?: any): Promise<T> {
|
|
209
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
210
|
+
const params: RequestInit = {
|
|
211
|
+
method,
|
|
212
|
+
headers,
|
|
213
|
+
body,
|
|
214
|
+
};
|
|
160
215
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
const response = await fetch(url, params);
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
let errorMessage = `TestRail: ${response.status} ${response.statusText}`;
|
|
220
|
+
const errorText = await response.text();
|
|
221
|
+
errorMessage += ` - ${errorText}`;
|
|
222
|
+
throw new Error(errorMessage);
|
|
168
223
|
}
|
|
169
224
|
|
|
170
|
-
return
|
|
225
|
+
return await response.json() as T;
|
|
171
226
|
}
|
|
172
227
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { updateCasesTool } from "./tools/update_cases.js";
|
|
|
11
11
|
import { createCaseTool } from "./tools/create_case.js";
|
|
12
12
|
import { getSectionsTool } from "./tools/get_sections.js";
|
|
13
13
|
import { getProjectsTool } from "./tools/get_projects.js";
|
|
14
|
+
import { addRunTool } from "./tools/add_run.js";
|
|
15
|
+
import { getStatusesTool } from "./tools/get_statuses.js";
|
|
16
|
+
import { getTestsTool } from "./tools/get_tests.js";
|
|
17
|
+
import { addResultsTool } from "./tools/add_results.js";
|
|
18
|
+
import { addAttachmentToRunTool } from "./tools/add_attachment_to_run.js";
|
|
19
|
+
import { removeNullish } from "./utils/sanitizer.js";
|
|
14
20
|
|
|
15
21
|
const TESTRAIL_INSTANCE_URL = process.env.TESTRAIL_INSTANCE_URL;
|
|
16
22
|
const TESTRAIL_USERNAME = process.env.TESTRAIL_USERNAME;
|
|
@@ -23,7 +29,7 @@ if (!TESTRAIL_INSTANCE_URL || !TESTRAIL_USERNAME || !TESTRAIL_API_KEY) {
|
|
|
23
29
|
|
|
24
30
|
const server = new McpServer({
|
|
25
31
|
name: "TestRail MCP Server",
|
|
26
|
-
version: "1.
|
|
32
|
+
version: "1.3.0",
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
@@ -38,6 +44,11 @@ const tools = [
|
|
|
38
44
|
updateCaseTool,
|
|
39
45
|
updateCasesTool,
|
|
40
46
|
createCaseTool,
|
|
47
|
+
addRunTool,
|
|
48
|
+
getStatusesTool,
|
|
49
|
+
getTestsTool,
|
|
50
|
+
addResultsTool,
|
|
51
|
+
addAttachmentToRunTool,
|
|
41
52
|
]
|
|
42
53
|
|
|
43
54
|
for (const tool of tools) {
|
|
@@ -47,7 +58,26 @@ for (const tool of tools) {
|
|
|
47
58
|
description: tool.description,
|
|
48
59
|
inputSchema: tool.parameters,
|
|
49
60
|
},
|
|
50
|
-
(args: any) =>
|
|
61
|
+
async (args: any) => {
|
|
62
|
+
try {
|
|
63
|
+
const output: Record<string, any> = await tool.handler(args, client);
|
|
64
|
+
const sanitized = removeNullish(output);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text" as const,
|
|
70
|
+
text: JSON.stringify(sanitized),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
} as any;
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
51
81
|
);
|
|
52
82
|
}
|
|
53
83
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { AttachmentSchema } from "../types/testrail.js";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import archiver from "archiver";
|
|
8
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
9
|
+
|
|
10
|
+
const parameters = {
|
|
11
|
+
run_id: z.string().describe("The ID of the test run to attach the file to"),
|
|
12
|
+
file_path: z.string().describe("The path to the file or directory to attach. Directories will be automatically zipped."),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const addAttachmentToRunTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
16
|
+
name: "add_attachment_to_run",
|
|
17
|
+
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.",
|
|
18
|
+
parameters,
|
|
19
|
+
handler: async ({ run_id, file_path }, client: TestRailClient) => {
|
|
20
|
+
if (!fs.existsSync(file_path)) {
|
|
21
|
+
throw new Error(`File or directory not found: ${file_path}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const stats = fs.statSync(file_path);
|
|
25
|
+
let uploadPath: string = file_path;
|
|
26
|
+
let filename: string = path.basename(file_path);
|
|
27
|
+
let isTemporary = false;
|
|
28
|
+
|
|
29
|
+
if (stats.isDirectory()) {
|
|
30
|
+
const basename = path.basename(file_path);
|
|
31
|
+
const tempDir = os.tmpdir();
|
|
32
|
+
const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
|
|
33
|
+
|
|
34
|
+
await new Promise<void>((resolve, reject) => {
|
|
35
|
+
const output = fs.createWriteStream(zipPath);
|
|
36
|
+
const archive = archiver('zip', {
|
|
37
|
+
zlib: { level: 9 }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
output.on('close', () => resolve());
|
|
41
|
+
archive.on('error', (err) => reject(err));
|
|
42
|
+
|
|
43
|
+
archive.pipe(output);
|
|
44
|
+
archive.directory(file_path, basename);
|
|
45
|
+
archive.finalize();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
uploadPath = zipPath;
|
|
49
|
+
filename = `${basename}.zip`;
|
|
50
|
+
isTemporary = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await client.addAttachmentToRun(Number(run_id), uploadPath, filename);
|
|
55
|
+
return AttachmentSchema.parse(result);
|
|
56
|
+
} finally {
|
|
57
|
+
if (isTemporary && fs.existsSync(uploadPath)) {
|
|
58
|
+
fs.unlinkSync(uploadPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ResultSchema } from "../types/testrail.js";
|
|
4
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
+
|
|
6
|
+
const parameters = {
|
|
7
|
+
run_id: z.number().describe("The ID of the test run"),
|
|
8
|
+
results: z.array(z.object({
|
|
9
|
+
test_id: z.number().describe("The ID of the test. Use get_tests with a run_id to retrieve available test IDs"),
|
|
10
|
+
status_id: z.number().describe("The ID of the test status (e.g. Passed, Failed). Use get_statuses to retrieve available status IDs"),
|
|
11
|
+
comment: z.string().optional().describe("Optional comment/description for the result"),
|
|
12
|
+
defects: z.string().optional().describe("Optional comma-separated list of defect IDs"),
|
|
13
|
+
})).describe("Array of results to add. Each result must have test_id and status_id"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const addResultsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
17
|
+
name: "add_results",
|
|
18
|
+
description: "Add one or more test results to a test run",
|
|
19
|
+
parameters,
|
|
20
|
+
handler: async ({ run_id, results }, client: TestRailClient) => {
|
|
21
|
+
const response = await client.addResults(run_id, results);
|
|
22
|
+
return {
|
|
23
|
+
success: true,
|
|
24
|
+
added_count: response.length,
|
|
25
|
+
results: response.map(r => ResultSchema.parse(r)),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { RunSchema } from "../types/testrail.js";
|
|
4
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
+
|
|
6
|
+
const parameters = {
|
|
7
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
|
+
name: z.string().optional().describe("The name of the test run"),
|
|
9
|
+
description: z.string().optional().describe("The description of the test run"),
|
|
10
|
+
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")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const addRunTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
14
|
+
name: "add_run",
|
|
15
|
+
description: "Create a new test run in TestRail",
|
|
16
|
+
parameters,
|
|
17
|
+
handler: async ({ project_id, ...fields }, client: TestRailClient) => {
|
|
18
|
+
const caseData = await client.getCase(String(fields.case_ids[0]));
|
|
19
|
+
const params = { suite_id: caseData.suite_id, include_all: false, ...fields };
|
|
20
|
+
const run = await client.addRun(project_id, params);
|
|
21
|
+
|
|
22
|
+
return RunSchema.parse(run);
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/tools/create_case.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition
|
|
3
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
4
4
|
|
|
5
5
|
const parameters = {
|
|
6
|
-
section_id: z.string().describe("The ID of the section where the case should be created"),
|
|
7
|
-
title: z.string().describe("The title of the test case
|
|
8
|
-
fields: z.record(z.string(), z.any()).optional().describe("Optional
|
|
6
|
+
section_id: z.string().describe("The ID of the section where the case should be created. Use get_sections to find available sections"),
|
|
7
|
+
title: z.string().describe("The title of the test case"),
|
|
8
|
+
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}"),
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
12
12
|
name: "create_case",
|
|
13
|
-
description: "Create a new test case in TestRail
|
|
13
|
+
description: "Create a new test case in TestRail",
|
|
14
14
|
parameters,
|
|
15
|
-
handler:
|
|
15
|
+
handler: async ({ section_id, title, fields }, client) => {
|
|
16
16
|
const caseData = {
|
|
17
17
|
title,
|
|
18
18
|
...fields,
|
|
@@ -21,16 +21,9 @@ export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> =
|
|
|
21
21
|
const createdCase = await client.createCase(section_id, caseData);
|
|
22
22
|
|
|
23
23
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
text: JSON.stringify({
|
|
28
|
-
success: true,
|
|
29
|
-
case_id: createdCase.id,
|
|
30
|
-
message: `Test case C${createdCase.id} created successfully`,
|
|
31
|
-
}, null, 2),
|
|
32
|
-
},
|
|
33
|
-
],
|
|
24
|
+
success: true,
|
|
25
|
+
case_id: createdCase.id,
|
|
26
|
+
message: `Test case C${createdCase.id} created successfully`,
|
|
34
27
|
};
|
|
35
|
-
}
|
|
28
|
+
}
|
|
36
29
|
};
|