@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.
Files changed (46) hide show
  1. package/README.md +178 -48
  2. package/dist/client/testrail.js +184 -0
  3. package/{src/index.ts → dist/index.js} +25 -35
  4. package/{src/tools/add_attachment_to_run.ts → dist/tools/add_attachment_to_run.js} +10 -20
  5. package/{src/tools/add_results.ts → dist/tools/add_results.js} +3 -7
  6. package/{src/tools/add_run.ts → dist/tools/add_run.js} +5 -10
  7. package/{src/tools/create_case.ts → dist/tools/create_case.js} +2 -8
  8. package/{src/tools/get_case.ts → dist/tools/get_case.js} +7 -14
  9. package/{src/tools/get_case_fields.ts → dist/tools/get_case_fields.js} +8 -37
  10. package/{src/tools/get_cases.ts → dist/tools/get_cases.js} +18 -22
  11. package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +1 -6
  12. package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +2 -7
  13. package/{src/tools/get_statuses.ts → dist/tools/get_statuses.js} +2 -7
  14. package/{src/tools/get_templates.ts → dist/tools/get_templates.js} +2 -7
  15. package/{src/tools/get_tests.ts → dist/tools/get_tests.js} +5 -10
  16. package/{src/tools/update_case.ts → dist/tools/update_case.js} +3 -8
  17. package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +2 -7
  18. package/dist/types/custom.js +1 -0
  19. package/{src/types/testrail.ts → dist/types/testrail.js} +6 -43
  20. package/{src/utils/mapper.ts → dist/utils/mapper.js} +16 -36
  21. package/{src/utils/sanitizer.ts → dist/utils/sanitizer.js} +4 -8
  22. package/package.json +8 -2
  23. package/.env.example +0 -3
  24. package/.github/workflows/ci.yml +0 -46
  25. package/.github/workflows/publish.yml +0 -35
  26. package/CHANGELOG.md +0 -38
  27. package/jest.config.js +0 -24
  28. package/src/client/testrail.ts +0 -227
  29. package/src/types/custom.ts +0 -19
  30. package/test/client/testrail.test.ts +0 -545
  31. package/test/tools/add_results.test.ts +0 -94
  32. package/test/tools/add_run.test.ts +0 -83
  33. package/test/tools/create_case.test.ts +0 -101
  34. package/test/tools/get_case.test.ts +0 -185
  35. package/test/tools/get_case_fields.test.ts +0 -132
  36. package/test/tools/get_cases.test.ts +0 -104
  37. package/test/tools/get_projects.test.ts +0 -38
  38. package/test/tools/get_sections.test.ts +0 -39
  39. package/test/tools/get_statuses.test.ts +0 -56
  40. package/test/tools/get_templates.test.ts +0 -38
  41. package/test/tools/get_tests.test.ts +0 -47
  42. package/test/tools/update_case.test.ts +0 -102
  43. package/test/tools/update_cases.test.ts +0 -89
  44. package/test/utils/mapper.test.ts +0 -112
  45. package/test/utils/sanitizer.test.ts +0 -143
  46. package/tsconfig.json +0 -24
@@ -3,7 +3,7 @@
3
3
  * in an object or array. Useful for cleaning up TestRail markdown content before sending
4
4
  * to LLMs.
5
5
  */
6
- export function sanitizeValue(value: any): any {
6
+ export function sanitizeValue(value) {
7
7
  if (typeof value === 'string') {
8
8
  return value.replace(/\s*style\s*=\s*["'][^"']*["']/gi, '');
9
9
  }
@@ -11,7 +11,7 @@ export function sanitizeValue(value: any): any {
11
11
  return value.map(sanitizeValue);
12
12
  }
13
13
  if (value !== null && typeof value === 'object') {
14
- const result: Record<string, any> = {};
14
+ const result = {};
15
15
  for (const [key, val] of Object.entries(value)) {
16
16
  result[key] = sanitizeValue(val);
17
17
  }
@@ -19,24 +19,21 @@ export function sanitizeValue(value: any): any {
19
19
  }
20
20
  return value;
21
21
  }
22
-
23
22
  /**
24
23
  * Recursively removes null and undefined values from objects and arrays.
25
24
  * For arrays, filters nested object items as well.
26
25
  */
27
- export function removeNullish(value: any): any {
26
+ export function removeNullish(value) {
28
27
  if (value == null) {
29
28
  return value;
30
29
  }
31
-
32
30
  if (Array.isArray(value)) {
33
31
  return value
34
32
  .map(item => removeNullish(item))
35
33
  .filter(item => item != null);
36
34
  }
37
-
38
35
  if (typeof value === 'object') {
39
- const result: Record<string, any> = {};
36
+ const result = {};
40
37
  for (const [key, val] of Object.entries(value)) {
41
38
  const cleanedVal = removeNullish(val);
42
39
  if (cleanedVal != null) {
@@ -45,6 +42,5 @@ export function removeNullish(value: any): any {
45
42
  }
46
43
  return result;
47
44
  }
48
-
49
45
  return value;
50
46
  }
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@uarlouski/testrail-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Model Context Protocol (MCP) server for TestRail integration, enabling LLMs to fetch test cases and metadata.",
5
- "main": "index.js",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "testrail-mcp-server": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
6
12
  "type": "module",
7
13
  "scripts": {
8
14
  "build": "rimraf dist && tsc",
package/.env.example DELETED
@@ -1,3 +0,0 @@
1
- TESTRAIL_INSTANCE_URL=https://mock.testrail.io
2
- TESTRAIL_USERNAME=mock@example.com
3
- TESTRAIL_API_KEY=mock_api_key
@@ -1,46 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: ["main"]
6
- pull_request:
7
- branches: ["main"]
8
-
9
- jobs:
10
- build-and-test:
11
- name: Build and Test (Node ${{ matrix.node-version }})
12
- runs-on: ubuntu-latest
13
-
14
- strategy:
15
- matrix:
16
- node-version: [18.x, 20.x, 22.x]
17
-
18
- steps:
19
- - name: Checkout code
20
- uses: actions/checkout@v4
21
-
22
- - name: Setup Node.js ${{ matrix.node-version }}
23
- uses: actions/setup-node@v4
24
- with:
25
- node-version: ${{ matrix.node-version }}
26
- cache: "npm"
27
-
28
- - name: Install dependencies
29
- run: npm ci
30
-
31
- - name: Run tests
32
- run: npm test
33
-
34
- - name: Run tests with coverage
35
- run: npm run test:coverage
36
-
37
- - name: Build project
38
- run: npm run build
39
-
40
- - name: Upload coverage reports
41
- uses: actions/upload-artifact@v4
42
- if: matrix.node-version == '20.x'
43
- with:
44
- name: coverage-report
45
- path: coverage/
46
- retention-days: 7
@@ -1,35 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- jobs:
8
- publish:
9
- runs-on: ubuntu-latest
10
- permissions:
11
- contents: read
12
- id-token: write
13
- steps:
14
- - name: Checkout code
15
- uses: actions/checkout@v4
16
-
17
- - name: Setup Node.js
18
- uses: actions/setup-node@v4
19
- with:
20
- node-version: "20.x"
21
- registry-url: "https://registry.npmjs.org"
22
-
23
- - name: Install dependencies
24
- run: npm ci
25
-
26
- - name: Run tests
27
- run: npm test
28
-
29
- - name: Build package
30
- run: npm run build
31
-
32
- - name: Publish to NPM
33
- run: npm publish --provenance --access public
34
- env:
35
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md DELETED
@@ -1,38 +0,0 @@
1
- # Changelog
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
-
21
- ## [1.1.0] - 2026-02-09
22
-
23
- ### Added
24
-
25
- - **Configuration via environment variables**
26
- - `TESTRAIL_INSTANCE_URL` - Your TestRail instance URL
27
- - `TESTRAIL_USERNAME` - Your TestRail username
28
- - `TESTRAIL_API_KEY` - [How to get your API key](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)
29
-
30
- - **Test case management tools**
31
- - `get_projects` - Discover available projects in your TestRail instance
32
- - `get_case` - Fetch detailed test case info including custom fields
33
- - `get_cases` - Query cases with filtering and pagination support
34
- - `get_case_fields` - Explore available fields and dropdown options
35
- - `get_templates` - List project templates to understand case structures
36
- - `get_sections` - Navigate the test case hierarchy
37
- - `create_case` - Create new test cases with full custom field support
38
- - `update_case` / `update_cases` - Modify single or bulk update cases
package/jest.config.js DELETED
@@ -1,24 +0,0 @@
1
- export default {
2
- preset: 'ts-jest/presets/default-esm',
3
- testEnvironment: 'node',
4
- extensionsToTreatAsEsm: ['.ts'],
5
- moduleNameMapper: {
6
- '^(\\.{1,2}/.*)\\.js$': '$1',
7
- },
8
- transform: {
9
- '^.+\\.tsx?$': [
10
- 'ts-jest',
11
- {
12
- useESM: true,
13
- },
14
- ],
15
- },
16
- coverageThreshold: {
17
- global: {
18
- branches: 95,
19
- functions: 95,
20
- lines: 95,
21
- statements: 95,
22
- },
23
- },
24
- };
@@ -1,227 +0,0 @@
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";
4
-
5
- interface PaginatedCasesResponse {
6
- cases: Case[];
7
- _links: { next: string | null };
8
- }
9
-
10
- interface PaginatedTestsResponse {
11
- tests: Test[];
12
- _links: { next: string | null };
13
- }
14
-
15
- const API_INDEX = '/index.php?';
16
- const API_BASE_V2 = `${API_INDEX}/api/v2`;
17
-
18
- export class TestRailClient {
19
- private baseUrl: string;
20
- private headers: HeadersInit;
21
- private auth: string;
22
- private prioritiesPromise: Promise<Priority[]> | null = null;
23
- private caseTypesPromise: Promise<CaseType[]> | null = null;
24
- private caseFieldsPromise: Promise<CaseField[]> | null = null;
25
- private statusesPromise: Promise<Status[]> | null = null;
26
- private projectsPromise: Promise<Project[]> | null = null;
27
- private templatesPromiseMap: Map<string, Promise<Template[]>> = new Map();
28
-
29
- constructor(baseUrl: string, email: string, apiKey: string) {
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
- };
37
- }
38
-
39
- async getCase(caseId: string): Promise<Case> {
40
- return this.get<Case>(`${API_BASE_V2}/get_case/${caseId}`);
41
- }
42
-
43
- async getCases(projectId: string, sectionId?: string, filter?: Record<string, string>): Promise<Case[]> {
44
- let url = `${API_BASE_V2}/get_cases/${projectId}`;
45
-
46
- if (sectionId) {
47
- url += `&section_id=${sectionId}`;
48
- }
49
-
50
- if (filter) {
51
- for (const [key, value] of Object.entries(filter)) {
52
- url += `&${key}=${encodeURIComponent(value)}`;
53
- }
54
- }
55
-
56
- const allCases: Case[] = [];
57
- let nextUrl: string | null = url;
58
-
59
- while (nextUrl) {
60
- const response: PaginatedCasesResponse = await this.get<PaginatedCasesResponse>(nextUrl);
61
-
62
- allCases.push(...response.cases);
63
-
64
- nextUrl = response._links?.next;
65
- if (nextUrl) {
66
- nextUrl = `${API_INDEX}${nextUrl}`;
67
- }
68
- }
69
-
70
- return allCases;
71
- }
72
-
73
- async getSection(sectionId: string): Promise<Section> {
74
- return this.get<Section>(`${API_BASE_V2}/get_section/${sectionId}`);
75
- }
76
-
77
- async getPriorities(): Promise<Priority[]> {
78
- if (!this.prioritiesPromise) {
79
- this.prioritiesPromise = this.get<Priority[]>(`${API_BASE_V2}/get_priorities`);
80
- }
81
- return this.prioritiesPromise;
82
- }
83
-
84
- async getCaseTypes(): Promise<CaseType[]> {
85
- if (!this.caseTypesPromise) {
86
- this.caseTypesPromise = this.get<CaseType[]>(`${API_BASE_V2}/get_case_types`);
87
- }
88
- return this.caseTypesPromise;
89
- }
90
-
91
- async getCaseFields(): Promise<CaseField[]> {
92
- if (!this.caseFieldsPromise) {
93
- this.caseFieldsPromise = this.get<CaseField[]>(`${API_BASE_V2}/get_case_fields`);
94
- }
95
- return this.caseFieldsPromise;
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
-
105
- async getTemplates(projectId: string): Promise<Template[]> {
106
- if (!this.templatesPromiseMap.has(projectId)) {
107
- this.templatesPromiseMap.set(
108
- projectId,
109
- this.get<Template[]>(`${API_BASE_V2}/get_templates/${projectId}`)
110
- );
111
- }
112
- return this.templatesPromiseMap.get(projectId)!;
113
- }
114
-
115
- async updateCase(caseId: string, fields: Record<string, any>): Promise<Case> {
116
- return this.post<Case>(`${API_BASE_V2}/update_case/${caseId}`, fields);
117
- }
118
-
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}`, {
121
- case_ids: caseIds,
122
- ...fields,
123
- });
124
-
125
- return response.updated_cases;
126
- }
127
-
128
- async createCase(sectionId: string, fields: Record<string, any>): Promise<Case> {
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);
134
- }
135
-
136
- async getSections(projectId: string): Promise<Section[]> {
137
- const response = await this.get<{ sections: Section[] }>(`${API_BASE_V2}/get_sections/${projectId}`);
138
- return response.sections;
139
- }
140
-
141
- async getProjects(): Promise<Project[]> {
142
- if (!this.projectsPromise) {
143
- this.projectsPromise = this.get<{ projects: Project[] }>(`${API_BASE_V2}/get_projects`)
144
- .then(response => response.projects);
145
- }
146
- return this.projectsPromise;
147
- }
148
-
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(',')}`;
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;
171
- }
172
-
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);
203
- }
204
-
205
- return this._executeRequest<T>(method, endpoint, this.headers, jsonData);
206
- }
207
-
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
- };
215
-
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);
223
- }
224
-
225
- return await response.json() as T;
226
- }
227
- }
@@ -1,19 +0,0 @@
1
- import { z } from "zod";
2
-
3
- export interface TestCaseResponse {
4
- id: number;
5
- title: string;
6
- section: string;
7
- type: string;
8
- priority: string;
9
- labels: string[];
10
- references: string | null;
11
- [key: string]: any;
12
- }
13
-
14
- export interface ToolDefinition<T extends z.ZodRawShape, Context = any> {
15
- name: string;
16
- description: string;
17
- parameters: T;
18
- handler: (args: z.infer<z.ZodObject<T>>, context: Context) => Promise<Record<string, any>>;
19
- }