@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +62 -16
  3. package/package.json +3 -2
  4. package/src/client/testrail.ts +111 -56
  5. package/src/index.ts +32 -2
  6. package/src/tools/add_attachment_to_run.ts +62 -0
  7. package/src/tools/add_results.ts +28 -0
  8. package/src/tools/add_run.ts +24 -0
  9. package/src/tools/create_case.ts +10 -17
  10. package/src/tools/get_case.ts +5 -12
  11. package/src/tools/get_case_fields.ts +4 -11
  12. package/src/tools/get_cases.ts +9 -16
  13. package/src/tools/get_projects.ts +7 -27
  14. package/src/tools/get_sections.ts +7 -27
  15. package/src/tools/get_statuses.ts +18 -0
  16. package/src/tools/get_templates.ts +7 -27
  17. package/src/tools/get_tests.ts +22 -0
  18. package/src/tools/update_case.ts +7 -14
  19. package/src/tools/update_cases.ts +10 -16
  20. package/src/types/custom.ts +1 -16
  21. package/src/types/testrail.ts +140 -87
  22. package/src/utils/sanitizer.ts +29 -0
  23. package/test/client/testrail.test.ts +329 -141
  24. package/test/tools/add_results.test.ts +94 -0
  25. package/test/tools/add_run.test.ts +83 -0
  26. package/test/tools/create_case.test.ts +10 -16
  27. package/test/tools/get_case.test.ts +47 -62
  28. package/test/tools/get_case_fields.test.ts +7 -80
  29. package/test/tools/get_cases.test.ts +19 -108
  30. package/test/tools/get_projects.test.ts +4 -41
  31. package/test/tools/get_sections.test.ts +7 -36
  32. package/test/tools/get_statuses.test.ts +56 -0
  33. package/test/tools/get_templates.test.ts +1 -30
  34. package/test/tools/get_tests.test.ts +47 -0
  35. package/test/tools/update_case.test.ts +10 -16
  36. package/test/tools/update_cases.test.ts +29 -28
  37. package/test/utils/mapper.test.ts +13 -13
  38. 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
- A Model Context Protocol (MCP) server for integrating LLMs with TestRail test case management.
3
+ [![npm version](https://badge.fury.io/js/@uarlouski%2Ftestrail-mcp-server.svg)](https://www.npmjs.com/package/@uarlouski/testrail-mcp-server)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@uarlouski/testrail-mcp-server.svg)](https://www.npmjs.com/package/@uarlouski/testrail-mcp-server)
5
+ [![CI](https://github.com/uarlouski/testrail-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/uarlouski/testrail-mcp-server/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9.3-blue.svg)](https://www.typescriptlang.org/)
8
+ [![GitHub stars](https://img.shields.io/github/stars/uarlouski/testrail-mcp-server.svg?style=social&label=Star)](https://github.com/uarlouski/testrail-mcp-server)
4
9
 
5
- ## Usage
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
- ## Tools
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` | Discover available projects in your TestRail instance |
30
- | `get_case` | Fetch detailed test case info including custom fields |
31
- | `get_cases` | Query cases with filtering and pagination support |
32
- | `get_case_fields` | Explore available fields and dropdown options |
33
- | `get_templates` | List project templates to understand case structures |
34
- | `get_sections` | Navigate the test case hierarchy |
35
- | `create_case` | Create new test cases with full custom field support |
36
- | `update_case` | Modify a single test case |
37
- | `update_cases` | Bulk update multiple cases at once |
38
-
39
- ## License
40
-
41
- Apache 2.0
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.1.0",
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
- "axios": "^1.13.2",
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",
@@ -1,42 +1,43 @@
1
- import axios, { AxiosInstance, AxiosError } from "axios";
2
- import { Case, Section, Priority, CaseType, CaseField, Template, Project } from "../types/testrail.js";
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 client: AxiosInstance;
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
- const cleanedBaseUrl = baseUrl.replace(/\/$/, "");
22
-
23
- this.client = axios.create({
24
- baseURL: cleanedBaseUrl,
25
- headers: {
26
- "Content-Type": "application/json",
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.makeRequest<Case>(`${API_BASE_V2}/get_case/${caseId}`);
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.makeRequest<PaginatedCasesResponse>(nextUrl);
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.makeRequest<Section>(`${API_BASE_V2}/get_section/${sectionId}`);
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.makeRequest<Priority[]>(`${API_BASE_V2}/get_priorities`);
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.makeRequest<CaseType[]>(`${API_BASE_V2}/get_case_types`);
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.makeRequest<CaseField[]>(`${API_BASE_V2}/get_case_fields`);
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.makeRequest<Template[]>(`${API_BASE_V2}/get_templates/${projectId}`)
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.postRequest<Case>(`${API_BASE_V2}/update_case/${caseId}`, fields);
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
- // Assume the project is operating in single suite mode, get suite_id from the first case
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.postRequest<Case>(`${API_BASE_V2}/add_case/${sectionId}`, fields);
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.makeRequest<{ sections: Section[] }>(`${API_BASE_V2}/get_sections/${projectId}`);
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.makeRequest<{ projects: Project[] }>(`${API_BASE_V2}/get_projects`)
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
- private async makeRequest<T>(url: string): Promise<T> {
141
- try {
142
- const response = await this.client.get<T>(url);
143
- return response.data;
144
- } catch (error: any) {
145
- throw this.handleError(error);
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
- private async postRequest<T>(url: string, data: Record<string, any>): Promise<T> {
150
- try {
151
- const response = await this.client.post<T>(url, data);
152
- return response.data;
153
- } catch (error: any) {
154
- throw this.handleError(error);
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 handleError(error: any): Error {
159
- let errorMessage = `TestRail API error: ${error.message}`;
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
- if (axios.isAxiosError(error)) {
162
- const axiosError = error as AxiosError;
163
- if (axiosError.response) {
164
- errorMessage = `TestRail API error: ${axiosError.response.status} ${axiosError.response.statusText} - ${JSON.stringify(axiosError.response.data)}`;
165
- } else if (axiosError.request) {
166
- errorMessage = `TestRail API error: No response received. ${axiosError.message}`;
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 new Error(errorMessage);
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.1.0",
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) => tool.handler(args, client)
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
+ };
@@ -1,18 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition, withErrorHandling } from "../types/custom.js";
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 (required)"),
8
- fields: z.record(z.string(), z.any()).optional().describe("Optional object containing additional fields. Use get_case_fields to see available fields. Example: {\"priority_id\": 2, \"template_id\": 1, \"custom_automation_priority\": 1}"),
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. IMPORTANT: Before creating a case, first call get_templates to discover available templates for the project, then use get_case_fields to see which fields are available for the chosen template. Requires section_id and title.",
13
+ description: "Create a new test case in TestRail",
14
14
  parameters,
15
- handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ section_id, title, fields }, client) => {
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
- content: [
25
- {
26
- type: "text" as const,
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
  };