@uarlouski/testrail-mcp-server 1.3.0 → 1.4.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/README.md +178 -48
- package/dist/client/testrail.js +184 -0
- package/{src/index.ts → dist/index.js} +25 -35
- package/{src/tools/add_attachment_to_run.ts → dist/tools/add_attachment_to_run.js} +10 -20
- package/{src/tools/add_results.ts → dist/tools/add_results.js} +3 -7
- package/{src/tools/add_run.ts → dist/tools/add_run.js} +5 -10
- package/{src/tools/create_case.ts → dist/tools/create_case.js} +2 -8
- package/{src/tools/get_case.ts → dist/tools/get_case.js} +7 -14
- 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} +18 -22
- package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +1 -6
- package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +2 -7
- package/{src/tools/get_statuses.ts → dist/tools/get_statuses.js} +2 -7
- package/{src/tools/get_templates.ts → dist/tools/get_templates.js} +2 -7
- package/{src/tools/get_tests.ts → dist/tools/get_tests.js} +5 -10
- package/{src/tools/update_case.ts → dist/tools/update_case.js} +3 -8
- package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +2 -7
- package/dist/types/custom.js +1 -0
- package/{src/types/testrail.ts → dist/types/testrail.js} +6 -43
- 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 +8 -2
- package/.env.example +0 -3
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -35
- package/CHANGELOG.md +0 -38
- package/jest.config.js +0 -24
- package/src/client/testrail.ts +0 -227
- package/src/types/custom.ts +0 -19
- package/test/client/testrail.test.ts +0 -545
- package/test/tools/add_results.test.ts +0 -94
- package/test/tools/add_run.test.ts +0 -83
- 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_statuses.test.ts +0 -56
- package/test/tools/get_templates.test.ts +0 -38
- package/test/tools/get_tests.test.ts +0 -47
- package/test/tools/update_case.test.ts +0 -102
- package/test/tools/update_cases.test.ts +0 -89
- 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,41 +1,103 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">TestRail MCP Server</h1>
|
|
3
|
+
<p align="center">
|
|
4
|
+
A Model Context Protocol (MCP) server that connects AI assistants to TestRail test management — search, create, update, and execute test cases using natural language.
|
|
5
|
+
</p>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://www.npmjs.com/package/@uarlouski/testrail-mcp-server"><img src="https://badge.fury.io/js/@uarlouski%2Ftestrail-mcp-server.svg" alt="npm version"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@uarlouski/testrail-mcp-server"><img src="https://img.shields.io/npm/dm/@uarlouski/testrail-mcp-server.svg" alt="npm downloads"></a>
|
|
11
|
+
<a href="https://github.com/uarlouski/testrail-mcp-server/actions/workflows/ci.yml"><img src="https://github.com/uarlouski/testrail-mcp-server/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
12
|
+
<a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License"></a>
|
|
13
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.9-blue.svg" alt="TypeScript"></a>
|
|
14
|
+
<a href="https://github.com/uarlouski/testrail-mcp-server"><img src="https://img.shields.io/github/stars/uarlouski/testrail-mcp-server.svg?style=social&label=Star" alt="GitHub stars"></a>
|
|
15
|
+
</p>
|
|
2
16
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why TestRail MCP Server?
|
|
20
|
+
|
|
21
|
+
Managing test cases manually is tedious. With the TestRail MCP Server, your AI assistant (Claude, Cursor, Windsurf, or any MCP-compliant client) talks directly to TestRail. Ask it to find test cases, create new ones, kick off test runs, or report results — all through natural conversation.
|
|
22
|
+
|
|
23
|
+
**No context switching. No copy-pasting. Just ask.**
|
|
24
|
+
|
|
25
|
+
## ✨ Features
|
|
9
26
|
|
|
27
|
+
| Category | Capabilities |
|
|
28
|
+
|----------|-------------|
|
|
29
|
+
| **🔍 Discovery** | Browse projects, suites, and sections to map your test organization |
|
|
30
|
+
| **📋 Case Management** | Fetch, create, update, and bulk-edit test cases with full custom field support |
|
|
31
|
+
| **▶️ Test Execution** | Create runs, record results, attach files, and track test statuses |
|
|
32
|
+
| **🧠 Context-Aware** | Exposes templates, field definitions, priorities, and case types so LLMs generate valid, well-structured test cases |
|
|
10
33
|
|
|
11
|
-
|
|
34
|
+
## 🚀 Quick Start
|
|
12
35
|
|
|
13
|
-
|
|
36
|
+
### 1. Get Your TestRail API Key
|
|
14
37
|
|
|
15
|
-
|
|
38
|
+
Navigate to **My Settings → API Keys** in your TestRail instance and generate a new key.
|
|
16
39
|
|
|
17
|
-
|
|
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.
|
|
40
|
+
### 2. Configure Your MCP Client
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
Add the server to your MCP client configuration. Below are examples for popular clients.
|
|
43
|
+
|
|
44
|
+
<details>
|
|
45
|
+
<summary><strong>Claude Desktop</strong></summary>
|
|
46
|
+
|
|
47
|
+
Edit your `claude_desktop_config.json`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"testrail": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "@uarlouski/testrail-mcp-server@latest"],
|
|
55
|
+
"env": {
|
|
56
|
+
"TESTRAIL_INSTANCE_URL": "https://your-instance.testrail.io",
|
|
57
|
+
"TESTRAIL_USERNAME": "your@email.com",
|
|
58
|
+
"TESTRAIL_API_KEY": "your-api-key"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary><strong>Cursor</strong></summary>
|
|
69
|
+
|
|
70
|
+
Open **Settings → MCP** and add a new server:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"testrail": {
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["-y", "@uarlouski/testrail-mcp-server@latest"],
|
|
78
|
+
"env": {
|
|
79
|
+
"TESTRAIL_INSTANCE_URL": "https://your-instance.testrail.io",
|
|
80
|
+
"TESTRAIL_USERNAME": "your@email.com",
|
|
81
|
+
"TESTRAIL_API_KEY": "your-api-key"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
23
87
|
|
|
24
|
-
|
|
88
|
+
</details>
|
|
25
89
|
|
|
26
|
-
|
|
90
|
+
<details>
|
|
91
|
+
<summary><strong>Windsurf</strong></summary>
|
|
27
92
|
|
|
28
|
-
Add
|
|
93
|
+
Add to your Windsurf MCP configuration:
|
|
29
94
|
|
|
30
95
|
```json
|
|
31
96
|
{
|
|
32
97
|
"mcpServers": {
|
|
33
98
|
"testrail": {
|
|
34
99
|
"command": "npx",
|
|
35
|
-
"args": [
|
|
36
|
-
"-y",
|
|
37
|
-
"@uarlouski/testrail-mcp-server@latest"
|
|
38
|
-
],
|
|
100
|
+
"args": ["-y", "@uarlouski/testrail-mcp-server@latest"],
|
|
39
101
|
"env": {
|
|
40
102
|
"TESTRAIL_INSTANCE_URL": "https://your-instance.testrail.io",
|
|
41
103
|
"TESTRAIL_USERNAME": "your@email.com",
|
|
@@ -46,42 +108,110 @@ Add the following to your configuration file:
|
|
|
46
108
|
}
|
|
47
109
|
```
|
|
48
110
|
|
|
49
|
-
|
|
111
|
+
</details>
|
|
112
|
+
|
|
113
|
+
<details>
|
|
114
|
+
<summary><strong>Other MCP Clients</strong></summary>
|
|
115
|
+
|
|
116
|
+
Any MCP-compliant client can use this server. The configuration pattern is the same — point it at the `npx` command with the required environment variables.
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
### 3. Start Using It
|
|
121
|
+
|
|
122
|
+
Once configured, ask your AI assistant things like:
|
|
123
|
+
|
|
124
|
+
- *"List all projects in TestRail"*
|
|
125
|
+
- *"Show me the test cases in suite 1 of project 3"*
|
|
126
|
+
- *"Create a test case for login validation with steps"*
|
|
127
|
+
- *"Start a new test run with cases from section 5"*
|
|
128
|
+
- *"Mark test 42 as passed with a comment"*
|
|
129
|
+
|
|
130
|
+
## ⚙️ Configuration
|
|
50
131
|
|
|
51
|
-
| Variable | Description | Required |
|
|
52
|
-
|
|
53
|
-
| `TESTRAIL_INSTANCE_URL` |
|
|
54
|
-
| `TESTRAIL_USERNAME` | Your TestRail email address |
|
|
55
|
-
| `TESTRAIL_API_KEY` | Your TestRail API key (
|
|
132
|
+
| Environment Variable | Description | Required |
|
|
133
|
+
|---------------------|-------------|:--------:|
|
|
134
|
+
| `TESTRAIL_INSTANCE_URL` | Your TestRail instance URL (e.g., `https://example.testrail.io`) | ✅ |
|
|
135
|
+
| `TESTRAIL_USERNAME` | Your TestRail email address | ✅ |
|
|
136
|
+
| `TESTRAIL_API_KEY` | Your TestRail API key ([how to get one](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)) | ✅ |
|
|
56
137
|
|
|
57
138
|
## 🛠️ Available Tools
|
|
58
139
|
|
|
59
|
-
|
|
140
|
+
### Discovery
|
|
60
141
|
|
|
61
142
|
| Tool | Description |
|
|
62
143
|
|------|-------------|
|
|
63
|
-
| `get_projects` |
|
|
64
|
-
| `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
|
79
|
-
|
|
144
|
+
| `get_projects` | List all available projects in your TestRail instance |
|
|
145
|
+
| `get_sections` | Navigate the folder/section hierarchy of a test suite |
|
|
146
|
+
|
|
147
|
+
### Test Case Management
|
|
148
|
+
|
|
149
|
+
| Tool | Description |
|
|
150
|
+
|------|-------------|
|
|
151
|
+
| `get_cases` | Query test cases with filtering by priority, template, type, and more — supports pagination and recursive section fetching |
|
|
152
|
+
| `get_case` | Fetch complete details of a specific test case including steps and custom fields |
|
|
153
|
+
| `create_case` | Create a new test case with full custom field support |
|
|
154
|
+
| `update_case` | Modify an existing test case's fields, title, or steps |
|
|
155
|
+
| `update_cases` | Bulk-update multiple test cases simultaneously |
|
|
156
|
+
|
|
157
|
+
### Test Execution
|
|
158
|
+
|
|
159
|
+
| Tool | Description |
|
|
160
|
+
|------|-------------|
|
|
161
|
+
| `add_run` | Create a new test run in TestRail |
|
|
162
|
+
| `get_tests` | Get tests for a run, optionally filtered by status |
|
|
163
|
+
| `add_results` | Submit one or more test results to a run |
|
|
164
|
+
| `add_attachment_to_run` | Attach a file or zipped folder to a test run |
|
|
165
|
+
|
|
166
|
+
### Metadata
|
|
167
|
+
|
|
168
|
+
| Tool | Description |
|
|
169
|
+
|------|-------------|
|
|
170
|
+
| `get_statuses` | List all available test statuses (Passed, Failed, Blocked, etc.) |
|
|
171
|
+
| `get_case_fields` | Retrieve custom field definitions and dropdown options |
|
|
172
|
+
| `get_templates` | List available case templates for correct case structure |
|
|
173
|
+
|
|
174
|
+
## 🏗️ Development
|
|
175
|
+
|
|
176
|
+
### Prerequisites
|
|
177
|
+
|
|
178
|
+
- [Node.js](https://nodejs.org/) 18+
|
|
179
|
+
- npm
|
|
180
|
+
|
|
181
|
+
### Setup
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git clone https://github.com/uarlouski/testrail-mcp-server.git
|
|
185
|
+
cd testrail-mcp-server
|
|
186
|
+
npm install
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Build
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm run build
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Run Tests
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm test
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 🤝 Contributing
|
|
202
|
+
|
|
203
|
+
Contributions are welcome! Please feel free to open an [issue](https://github.com/uarlouski/testrail-mcp-server/issues) or submit a pull request.
|
|
80
204
|
|
|
81
205
|
## 📜 License
|
|
82
206
|
|
|
83
|
-
This project is licensed under the Apache 2.0
|
|
207
|
+
This project is licensed under the [Apache License 2.0](LICENSE).
|
|
84
208
|
|
|
85
209
|
---
|
|
86
210
|
|
|
87
|
-
|
|
211
|
+
<p align="center">
|
|
212
|
+
<b>TestRail MCP Server</b> · Built with the <a href="https://modelcontextprotocol.io">Model Context Protocol</a>
|
|
213
|
+
</p>
|
|
214
|
+
|
|
215
|
+
<p align="center">
|
|
216
|
+
<sub>Keywords: TestRail MCP Server, Model Context Protocol, TestRail API integration, AI test management, LLM testing tools, Claude TestRail, Cursor TestRail, Windsurf TestRail, test case automation, QA AI assistant, MCP server for testing, TestRail plugin, test run management, AI-powered QA</sub>
|
|
217
|
+
</p>
|
|
@@ -0,0 +1,184 @@
|
|
|
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 getCasesRecursively(projectId, sectionId, filter, excludedSectionNames) {
|
|
27
|
+
const sections = await this.getSections(projectId);
|
|
28
|
+
const sectionIds = this.getSubSectionIds(sections, sectionId, excludedSectionNames);
|
|
29
|
+
sectionIds.push(sectionId);
|
|
30
|
+
const promises = sectionIds.map(id => this.getCases(projectId, id, filter));
|
|
31
|
+
const results = await Promise.all(promises);
|
|
32
|
+
return results.flat();
|
|
33
|
+
}
|
|
34
|
+
getSubSectionIds(sections, parentId, excludedSectionNames) {
|
|
35
|
+
const children = sections.filter(s => s.parent_id === parentId);
|
|
36
|
+
const validChildren = children.filter(s => !excludedSectionNames?.includes(s.name));
|
|
37
|
+
let ids = validChildren.map(s => s.id);
|
|
38
|
+
for (const child of validChildren) {
|
|
39
|
+
ids = ids.concat(this.getSubSectionIds(sections, child.id, excludedSectionNames));
|
|
40
|
+
}
|
|
41
|
+
return ids;
|
|
42
|
+
}
|
|
43
|
+
async getCases(projectId, sectionId, filter) {
|
|
44
|
+
let url = `${API_BASE_V2}/get_cases/${projectId}`;
|
|
45
|
+
if (sectionId) {
|
|
46
|
+
url += `§ion_id=${sectionId}`;
|
|
47
|
+
}
|
|
48
|
+
if (filter) {
|
|
49
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
50
|
+
url += `&${key}=${encodeURIComponent(value)}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const allCases = [];
|
|
54
|
+
let nextUrl = url;
|
|
55
|
+
while (nextUrl) {
|
|
56
|
+
const response = await this.get(nextUrl);
|
|
57
|
+
allCases.push(...response.cases);
|
|
58
|
+
nextUrl = response._links?.next;
|
|
59
|
+
if (nextUrl) {
|
|
60
|
+
nextUrl = `${API_INDEX}${nextUrl}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return allCases;
|
|
64
|
+
}
|
|
65
|
+
async getSection(sectionId) {
|
|
66
|
+
return this.get(`${API_BASE_V2}/get_section/${sectionId}`);
|
|
67
|
+
}
|
|
68
|
+
async getPriorities() {
|
|
69
|
+
if (!this.prioritiesPromise) {
|
|
70
|
+
this.prioritiesPromise = this.get(`${API_BASE_V2}/get_priorities`);
|
|
71
|
+
}
|
|
72
|
+
return this.prioritiesPromise;
|
|
73
|
+
}
|
|
74
|
+
async getCaseTypes() {
|
|
75
|
+
if (!this.caseTypesPromise) {
|
|
76
|
+
this.caseTypesPromise = this.get(`${API_BASE_V2}/get_case_types`);
|
|
77
|
+
}
|
|
78
|
+
return this.caseTypesPromise;
|
|
79
|
+
}
|
|
80
|
+
async getCaseFields() {
|
|
81
|
+
if (!this.caseFieldsPromise) {
|
|
82
|
+
this.caseFieldsPromise = this.get(`${API_BASE_V2}/get_case_fields`);
|
|
83
|
+
}
|
|
84
|
+
return this.caseFieldsPromise;
|
|
85
|
+
}
|
|
86
|
+
async getStatuses() {
|
|
87
|
+
if (!this.statusesPromise) {
|
|
88
|
+
this.statusesPromise = this.get(`${API_BASE_V2}/get_statuses`);
|
|
89
|
+
}
|
|
90
|
+
return this.statusesPromise;
|
|
91
|
+
}
|
|
92
|
+
async getTemplates(projectId) {
|
|
93
|
+
if (!this.templatesPromiseMap.has(projectId.toString())) {
|
|
94
|
+
this.templatesPromiseMap.set(projectId.toString(), this.get(`${API_BASE_V2}/get_templates/${projectId}`));
|
|
95
|
+
}
|
|
96
|
+
return this.templatesPromiseMap.get(projectId.toString());
|
|
97
|
+
}
|
|
98
|
+
async updateCase(caseId, fields) {
|
|
99
|
+
return this.post(`${API_BASE_V2}/update_case/${caseId}`, fields);
|
|
100
|
+
}
|
|
101
|
+
async updateCases(suiteId, caseIds, fields) {
|
|
102
|
+
const response = await this.post(`${API_BASE_V2}/update_cases/${suiteId}`, {
|
|
103
|
+
case_ids: caseIds,
|
|
104
|
+
...fields,
|
|
105
|
+
});
|
|
106
|
+
return response.updated_cases;
|
|
107
|
+
}
|
|
108
|
+
async createCase(sectionId, fields) {
|
|
109
|
+
return this.post(`${API_BASE_V2}/add_case/${sectionId}`, fields);
|
|
110
|
+
}
|
|
111
|
+
async addRun(projectId, fields) {
|
|
112
|
+
return this.post(`${API_BASE_V2}/add_run/${projectId}`, fields);
|
|
113
|
+
}
|
|
114
|
+
async getSections(projectId) {
|
|
115
|
+
const response = await this.get(`${API_BASE_V2}/get_sections/${projectId}`);
|
|
116
|
+
return response.sections;
|
|
117
|
+
}
|
|
118
|
+
async getProjects() {
|
|
119
|
+
if (!this.projectsPromise) {
|
|
120
|
+
this.projectsPromise = this.get(`${API_BASE_V2}/get_projects`)
|
|
121
|
+
.then(response => response.projects);
|
|
122
|
+
}
|
|
123
|
+
return this.projectsPromise;
|
|
124
|
+
}
|
|
125
|
+
async getTests(runId, statusId) {
|
|
126
|
+
let url = `${API_BASE_V2}/get_tests/${runId}`;
|
|
127
|
+
if (statusId && statusId.length > 0) {
|
|
128
|
+
url += `&status_id=${statusId.join(',')}`;
|
|
129
|
+
}
|
|
130
|
+
const allTests = [];
|
|
131
|
+
let nextUrl = url;
|
|
132
|
+
while (nextUrl) {
|
|
133
|
+
const response = await this.get(nextUrl);
|
|
134
|
+
allTests.push(...response.tests);
|
|
135
|
+
nextUrl = response._links?.next;
|
|
136
|
+
if (nextUrl) {
|
|
137
|
+
nextUrl = `${API_INDEX}${nextUrl}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return allTests;
|
|
141
|
+
}
|
|
142
|
+
async addResults(runId, results) {
|
|
143
|
+
return this.post(`${API_BASE_V2}/add_results/${runId}`, { results });
|
|
144
|
+
}
|
|
145
|
+
async addAttachmentToRun(runId, filePath, filename) {
|
|
146
|
+
const fileBuffer = await fs.promises.readFile(filePath);
|
|
147
|
+
const blob = new Blob([fileBuffer]);
|
|
148
|
+
const formData = new FormData();
|
|
149
|
+
formData.append('attachment', blob, filename);
|
|
150
|
+
const headers = {
|
|
151
|
+
'Authorization': this.auth,
|
|
152
|
+
};
|
|
153
|
+
return this._executeRequest('POST', `${API_BASE_V2}/add_attachment_to_run/${runId}`, headers, formData);
|
|
154
|
+
}
|
|
155
|
+
async get(endpoint) {
|
|
156
|
+
return this.executeRequest('GET', endpoint);
|
|
157
|
+
}
|
|
158
|
+
async post(endpoint, data) {
|
|
159
|
+
return this.executeRequest('POST', endpoint, data);
|
|
160
|
+
}
|
|
161
|
+
async executeRequest(method, endpoint, data) {
|
|
162
|
+
let jsonData = undefined;
|
|
163
|
+
if (data) {
|
|
164
|
+
jsonData = JSON.stringify(data);
|
|
165
|
+
}
|
|
166
|
+
return this._executeRequest(method, endpoint, this.headers, jsonData);
|
|
167
|
+
}
|
|
168
|
+
async _executeRequest(method, endpoint, headers, body) {
|
|
169
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
170
|
+
const params = {
|
|
171
|
+
method,
|
|
172
|
+
headers,
|
|
173
|
+
body,
|
|
174
|
+
};
|
|
175
|
+
const response = await fetch(url, params);
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
let errorMessage = `TestRail: ${response.status} ${response.statusText}`;
|
|
178
|
+
const errorText = await response.text();
|
|
179
|
+
errorMessage += ` - ${errorText}`;
|
|
180
|
+
throw new Error(errorMessage);
|
|
181
|
+
}
|
|
182
|
+
return await response.json();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -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";
|
|
@@ -17,23 +18,18 @@ import { getTestsTool } from "./tools/get_tests.js";
|
|
|
17
18
|
import { addResultsTool } from "./tools/add_results.js";
|
|
18
19
|
import { addAttachmentToRunTool } from "./tools/add_attachment_to_run.js";
|
|
19
20
|
import { removeNullish } from "./utils/sanitizer.js";
|
|
20
|
-
|
|
21
21
|
const TESTRAIL_INSTANCE_URL = process.env.TESTRAIL_INSTANCE_URL;
|
|
22
22
|
const TESTRAIL_USERNAME = process.env.TESTRAIL_USERNAME;
|
|
23
23
|
const TESTRAIL_API_KEY = process.env.TESTRAIL_API_KEY;
|
|
24
|
-
|
|
25
24
|
if (!TESTRAIL_INSTANCE_URL || !TESTRAIL_USERNAME || !TESTRAIL_API_KEY) {
|
|
26
25
|
console.error("Error: TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, and TESTRAIL_API_KEY environment variables are required.");
|
|
27
26
|
process.exit(1);
|
|
28
27
|
}
|
|
29
|
-
|
|
30
28
|
const server = new McpServer({
|
|
31
29
|
name: "TestRail MCP Server",
|
|
32
|
-
version: "1.
|
|
30
|
+
version: "1.4.0",
|
|
33
31
|
});
|
|
34
|
-
|
|
35
32
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
36
|
-
|
|
37
33
|
const tools = [
|
|
38
34
|
getProjectsTool,
|
|
39
35
|
getCaseTool,
|
|
@@ -49,37 +45,31 @@ const tools = [
|
|
|
49
45
|
getTestsTool,
|
|
50
46
|
addResultsTool,
|
|
51
47
|
addAttachmentToRunTool,
|
|
52
|
-
]
|
|
53
|
-
|
|
48
|
+
];
|
|
54
49
|
for (const tool of tools) {
|
|
55
|
-
server.registerTool(
|
|
56
|
-
tool.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
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
|
+
};
|
|
80
65
|
}
|
|
81
|
-
|
|
66
|
+
catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
82
73
|
}
|
|
83
|
-
|
|
84
74
|
const transport = new StdioServerTransport();
|
|
85
75
|
await server.connect(transport);
|
|
@@ -1,59 +1,49 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { AttachmentSchema } from "../types/testrail.js";
|
|
4
3
|
import * as fs from "fs";
|
|
5
4
|
import * as path from "path";
|
|
6
5
|
import * as os from "os";
|
|
7
6
|
import archiver from "archiver";
|
|
8
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
9
|
-
|
|
10
7
|
const parameters = {
|
|
11
|
-
run_id: z.
|
|
8
|
+
run_id: z.number().describe("The ID of the test run to attach the file to"),
|
|
12
9
|
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> = {
|
|
10
|
+
};
|
|
11
|
+
export const addAttachmentToRunTool = {
|
|
16
12
|
name: "add_attachment_to_run",
|
|
17
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.",
|
|
18
14
|
parameters,
|
|
19
|
-
handler: async ({ run_id, file_path }, client
|
|
15
|
+
handler: async ({ run_id, file_path }, client) => {
|
|
20
16
|
if (!fs.existsSync(file_path)) {
|
|
21
17
|
throw new Error(`File or directory not found: ${file_path}`);
|
|
22
18
|
}
|
|
23
|
-
|
|
24
19
|
const stats = fs.statSync(file_path);
|
|
25
|
-
let uploadPath
|
|
26
|
-
let filename
|
|
20
|
+
let uploadPath = file_path;
|
|
21
|
+
let filename = path.basename(file_path);
|
|
27
22
|
let isTemporary = false;
|
|
28
|
-
|
|
29
23
|
if (stats.isDirectory()) {
|
|
30
24
|
const basename = path.basename(file_path);
|
|
31
25
|
const tempDir = os.tmpdir();
|
|
32
26
|
const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
|
|
33
|
-
|
|
34
|
-
await new Promise<void>((resolve, reject) => {
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
35
28
|
const output = fs.createWriteStream(zipPath);
|
|
36
29
|
const archive = archiver('zip', {
|
|
37
30
|
zlib: { level: 9 }
|
|
38
31
|
});
|
|
39
|
-
|
|
40
32
|
output.on('close', () => resolve());
|
|
41
33
|
archive.on('error', (err) => reject(err));
|
|
42
|
-
|
|
43
34
|
archive.pipe(output);
|
|
44
35
|
archive.directory(file_path, basename);
|
|
45
36
|
archive.finalize();
|
|
46
37
|
});
|
|
47
|
-
|
|
48
38
|
uploadPath = zipPath;
|
|
49
39
|
filename = `${basename}.zip`;
|
|
50
40
|
isTemporary = true;
|
|
51
41
|
}
|
|
52
|
-
|
|
53
42
|
try {
|
|
54
|
-
const result = await client.addAttachmentToRun(
|
|
43
|
+
const result = await client.addAttachmentToRun(run_id, uploadPath, filename);
|
|
55
44
|
return AttachmentSchema.parse(result);
|
|
56
|
-
}
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
57
47
|
if (isTemporary && fs.existsSync(uploadPath)) {
|
|
58
48
|
fs.unlinkSync(uploadPath);
|
|
59
49
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { ResultSchema } from "../types/testrail.js";
|
|
4
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
-
|
|
6
3
|
const parameters = {
|
|
7
4
|
run_id: z.number().describe("The ID of the test run"),
|
|
8
5
|
results: z.array(z.object({
|
|
@@ -11,13 +8,12 @@ const parameters = {
|
|
|
11
8
|
comment: z.string().optional().describe("Optional comment/description for the result"),
|
|
12
9
|
defects: z.string().optional().describe("Optional comma-separated list of defect IDs"),
|
|
13
10
|
})).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> = {
|
|
11
|
+
};
|
|
12
|
+
export const addResultsTool = {
|
|
17
13
|
name: "add_results",
|
|
18
14
|
description: "Add one or more test results to a test run",
|
|
19
15
|
parameters,
|
|
20
|
-
handler: async ({ run_id, results }, client
|
|
16
|
+
handler: async ({ run_id, results }, client) => {
|
|
21
17
|
const response = await client.addResults(run_id, results);
|
|
22
18
|
return {
|
|
23
19
|
success: true,
|
|
@@ -1,24 +1,19 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { RunSchema } from "../types/testrail.js";
|
|
4
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
-
|
|
6
3
|
const parameters = {
|
|
7
|
-
project_id: z.
|
|
4
|
+
project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
5
|
name: z.string().optional().describe("The name of the test run"),
|
|
9
6
|
description: z.string().optional().describe("The description of the test run"),
|
|
10
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")
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const addRunTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
8
|
+
};
|
|
9
|
+
export const addRunTool = {
|
|
14
10
|
name: "add_run",
|
|
15
11
|
description: "Create a new test run in TestRail",
|
|
16
12
|
parameters,
|
|
17
|
-
handler: async ({ project_id, ...fields }, client
|
|
18
|
-
const caseData = await client.getCase(
|
|
13
|
+
handler: async ({ project_id, ...fields }, client) => {
|
|
14
|
+
const caseData = await client.getCase(fields.case_ids[0]);
|
|
19
15
|
const params = { suite_id: caseData.suite_id, include_all: false, ...fields };
|
|
20
16
|
const run = await client.addRun(project_id, params);
|
|
21
|
-
|
|
22
17
|
return RunSchema.parse(run);
|
|
23
18
|
},
|
|
24
19
|
};
|