@uarlouski/testrail-mcp-server 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +12 -2
  2. package/dist/client/testrail.js +167 -0
  3. package/{src/index.ts → dist/index.js} +35 -35
  4. package/dist/tools/add_attachment_to_run.js +52 -0
  5. package/dist/tools/add_results.js +24 -0
  6. package/dist/tools/add_run.js +19 -0
  7. package/dist/tools/create_case.js +23 -0
  8. package/{src/tools/get_case.ts → dist/tools/get_case.js} +3 -12
  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} +10 -24
  11. package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +3 -10
  12. package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +4 -11
  13. package/dist/tools/get_statuses.js +13 -0
  14. package/dist/tools/get_templates.js +16 -0
  15. package/dist/tools/get_tests.js +17 -0
  16. package/{src/tools/update_case.ts → dist/tools/update_case.js} +2 -8
  17. package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +4 -8
  18. package/dist/types/custom.js +1 -0
  19. package/{src/types/testrail.ts → dist/types/testrail.js} +37 -24
  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 +10 -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 -27
  27. package/jest.config.js +0 -24
  28. package/src/client/testrail.ts +0 -161
  29. package/src/tools/create_case.ts +0 -29
  30. package/src/tools/get_templates.ts +0 -23
  31. package/src/types/custom.ts +0 -19
  32. package/test/client/testrail.test.ts +0 -430
  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_templates.test.ts +0 -38
  40. package/test/tools/update_case.test.ts +0 -102
  41. package/test/tools/update_cases.test.ts +0 -61
  42. package/test/utils/mapper.test.ts +0 -112
  43. package/test/utils/sanitizer.test.ts +0 -143
  44. package/tsconfig.json +0 -24
@@ -1,23 +1,16 @@
1
1
  import { z } from "zod";
2
- import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition } from "../types/custom.js";
4
2
  import { SectionSchema } from "../types/testrail.js";
5
-
6
3
  const parameters = {
7
- project_id: z.string().describe("The ID of the project"),
4
+ project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
8
5
  };
9
-
10
- export const getSectionsTool: ToolDefinition<typeof parameters, TestRailClient> = {
6
+ export const getSectionsTool = {
11
7
  name: "get_sections",
12
- description: "Get all sections for a project. Returns section IDs and names. Use this to find the section_id needed for create_case.",
8
+ description: "Get all sections for a project. Returns section IDs and names that can be used with create_case",
13
9
  parameters,
14
10
  handler: async ({ project_id }, client) => {
15
11
  const sections = await client.getSections(project_id);
16
-
17
- const response = {
12
+ return {
18
13
  sections: sections.map(s => SectionSchema.parse(s)),
19
14
  };
20
-
21
- return response;
22
15
  }
23
16
  };
@@ -0,0 +1,13 @@
1
+ import { StatusSchema } from "../types/testrail.js";
2
+ const parameters = {};
3
+ export const getStatusesTool = {
4
+ name: "get_statuses",
5
+ description: "Get all available test statuses (e.g. Passed, Failed, Blocked). Returns status IDs and names that can be used with add_result and get_tests",
6
+ parameters,
7
+ handler: async (args, client) => {
8
+ const statuses = await client.getStatuses();
9
+ return {
10
+ statuses: statuses.map(s => StatusSchema.parse(s)),
11
+ };
12
+ },
13
+ };
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { TemplateSchema } from "../types/testrail.js";
3
+ const parameters = {
4
+ project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
5
+ };
6
+ export const getTemplatesTool = {
7
+ name: "get_templates",
8
+ description: "Get available test case templates for a project. Template IDs determine which fields are available when creating or updating test cases",
9
+ parameters,
10
+ handler: async ({ project_id }, client) => {
11
+ const templates = await client.getTemplates(project_id);
12
+ return {
13
+ templates: templates.map(t => TemplateSchema.parse(t)),
14
+ };
15
+ }
16
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { TestSchema } from "../types/testrail.js";
3
+ const parameters = {
4
+ run_id: z.number().describe("The ID of the test run"),
5
+ status_id: z.array(z.number()).optional().describe("Optional array of status IDs to filter by. Use get_statuses to retrieve available status IDs"),
6
+ };
7
+ export const getTestsTool = {
8
+ name: "get_tests",
9
+ description: "Get tests for a test run, optionally filtered by status",
10
+ parameters,
11
+ handler: async ({ run_id, status_id }, client) => {
12
+ const tests = await client.getTests(run_id, status_id);
13
+ return {
14
+ tests: tests.map(test => TestSchema.parse(test)),
15
+ };
16
+ },
17
+ };
@@ -1,21 +1,15 @@
1
1
  import { z } from "zod";
2
- import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition } from "../types/custom.js";
4
-
5
2
  const parameters = {
6
3
  case_id: z.string().describe("The ID of the test case to update (e.g. '123' or 'C123')"),
7
4
  fields: z.record(z.string(), z.any()).describe("Object containing fields to update. Use get_case_fields to see available fields. Example: {\"title\": \"New title\", \"priority_id\": 2, \"custom_automation_priority\": 1}"),
8
5
  };
9
-
10
- export const updateCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
6
+ export const updateCaseTool = {
11
7
  name: "update_case",
12
- description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change. Use get_case_fields to see available fields and their types.",
8
+ description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change",
13
9
  parameters,
14
10
  handler: async ({ case_id, fields }, client) => {
15
11
  const id = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
16
-
17
12
  const updatedCase = await client.updateCase(id, fields);
18
-
19
13
  return {
20
14
  success: true,
21
15
  case_id: updatedCase.id,
@@ -1,19 +1,15 @@
1
1
  import { z } from "zod";
2
- import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition } from "../types/custom.js";
4
-
5
2
  const parameters = {
6
3
  case_ids: z.array(z.number()).describe("Array of case IDs to update (e.g. [123, 456, 789])"),
7
4
  fields: z.record(z.string(), z.any()).describe("Object containing fields to update for ALL specified cases. Use get_case_fields to see available fields. Example: {\"priority_id\": 2, \"type_id\": 1}"),
8
5
  };
9
-
10
- export const updateCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
6
+ export const updateCasesTool = {
11
7
  name: "update_cases",
12
- description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times. All cases will receive the same field values.",
8
+ description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times",
13
9
  parameters,
14
10
  handler: async ({ case_ids, fields }, client) => {
15
- const updatedCases = await client.updateCases(case_ids, fields);
16
-
11
+ const caseData = await client.getCase(String(case_ids[0]));
12
+ const updatedCases = await client.updateCases(caseData.suite_id, case_ids, fields);
17
13
  return {
18
14
  success: true,
19
15
  updated_count: updatedCases.length,
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
-
3
2
  export const CaseFieldConfigSchema = z.object({
4
3
  options: z.object({
5
4
  default_value: z.string().optional(),
@@ -7,9 +6,6 @@ export const CaseFieldConfigSchema = z.object({
7
6
  items: z.string().optional(),
8
7
  }),
9
8
  });
10
-
11
- export type CaseFieldConfig = z.infer<typeof CaseFieldConfigSchema>;
12
-
13
9
  export const CaseFieldSchema = z.object({
14
10
  id: z.number(),
15
11
  name: z.string(),
@@ -22,9 +18,6 @@ export const CaseFieldSchema = z.object({
22
18
  include_all: z.boolean(),
23
19
  configs: z.array(CaseFieldConfigSchema),
24
20
  });
25
-
26
- export type CaseField = z.infer<typeof CaseFieldSchema>;
27
-
28
21
  export const CaseSchema = z.object({
29
22
  id: z.number(),
30
23
  title: z.string(),
@@ -40,9 +33,6 @@ export const CaseSchema = z.object({
40
33
  suite_id: z.number(),
41
34
  labels: z.array(z.any()),
42
35
  }).loose();
43
-
44
- export type Case = z.infer<typeof CaseSchema>;
45
-
46
36
  export const SectionSchema = z.object({
47
37
  id: z.number(),
48
38
  name: z.string(),
@@ -50,38 +40,61 @@ export const SectionSchema = z.object({
50
40
  parent_id: z.number().nullish(),
51
41
  suite_id: z.number(),
52
42
  });
53
-
54
- export type Section = z.infer<typeof SectionSchema>;
55
-
56
43
  export const PrioritySchema = z.object({
57
44
  id: z.number(),
58
45
  is_default: z.boolean(),
59
46
  name: z.string(),
60
47
  });
61
-
62
- export type Priority = z.infer<typeof PrioritySchema>;
63
-
64
48
  export const CaseTypeSchema = z.object({
65
49
  id: z.number(),
66
50
  is_default: z.boolean(),
67
51
  name: z.string(),
68
52
  });
69
-
70
- export type CaseType = z.infer<typeof CaseTypeSchema>;
71
-
72
53
  export const TemplateSchema = z.object({
73
54
  id: z.number(),
74
55
  name: z.string(),
75
56
  is_default: z.boolean(),
76
57
  });
77
-
78
- export type Template = z.infer<typeof TemplateSchema>;
79
-
80
58
  export const ProjectSchema = z.object({
81
59
  id: z.number(),
82
60
  name: z.string(),
83
61
  is_completed: z.boolean(),
84
62
  suite_mode: z.number(),
85
63
  });
86
-
87
- export type Project = z.infer<typeof ProjectSchema>;
64
+ export const RunSchema = z.object({
65
+ id: z.number(),
66
+ name: z.string(),
67
+ description: z.string().nullable(),
68
+ suite_id: z.number().nullable(),
69
+ project_id: z.number(),
70
+ is_completed: z.boolean(),
71
+ passed_count: z.number(),
72
+ blocked_count: z.number(),
73
+ untested_count: z.number(),
74
+ retest_count: z.number(),
75
+ failed_count: z.number(),
76
+ url: z.string(),
77
+ });
78
+ export const StatusSchema = z.object({
79
+ id: z.number(),
80
+ name: z.string(),
81
+ label: z.string(),
82
+ });
83
+ export const TestSchema = z.object({
84
+ id: z.number(),
85
+ case_id: z.number(),
86
+ status_id: z.number(),
87
+ title: z.string(),
88
+ run_id: z.number(),
89
+ });
90
+ export const TestsSchema = z.array(TestSchema);
91
+ export const ResultSchema = z.object({
92
+ id: z.number(),
93
+ test_id: z.number(),
94
+ status_id: z.number(),
95
+ comment: z.string().nullable(),
96
+ defects: z.string().nullable(),
97
+ });
98
+ export const AttachmentSchema = z.object({
99
+ attachment_id: z.number(),
100
+ });
@@ -1,73 +1,55 @@
1
- import { Case, CaseField } from "../types/testrail.js";
2
1
  import { sanitizeValue } from "./sanitizer.js";
3
-
4
2
  /**
5
3
  * Processes custom fields from a test case, converting system names to human-readable labels
6
4
  * and mapping dropdown values to their display text.
7
5
  */
8
- export function processCustomFields(
9
- testCase: Case,
10
- caseFields: CaseField[]
11
- ): Record<string, any> {
6
+ export function processCustomFields(testCase, caseFields) {
12
7
  if (!testCase) {
13
8
  throw new Error("Test case is undefined or null");
14
9
  }
15
-
16
10
  const templateId = testCase.template_id;
17
- const result: Record<string, any> = {};
18
-
19
- const applicableFields = caseFields.filter(field =>
20
- field.include_all || field.template_ids.includes(templateId)
21
- );
22
-
23
- const fieldNameMap = new Map<string, string>();
24
- const dropdownOptionsMap = new Map<string, Map<string, string>>();
25
-
11
+ const result = {};
12
+ const applicableFields = caseFields.filter(field => field.include_all || field.template_ids.includes(templateId));
13
+ const fieldNameMap = new Map();
14
+ const dropdownOptionsMap = new Map();
26
15
  for (const field of applicableFields) {
27
16
  fieldNameMap.set(field.system_name, toSnakeCase(field.label));
28
-
29
17
  const optionsMap = parseDropdownOptions(field);
30
18
  if (optionsMap.size > 0) {
31
19
  dropdownOptionsMap.set(field.system_name, optionsMap);
32
20
  }
33
21
  }
34
-
35
22
  for (const [key, value] of Object.entries(testCase)) {
36
- if (!key.startsWith("custom_")) continue;
37
- if (value === null || value === undefined) continue;
38
-
23
+ if (!key.startsWith("custom_"))
24
+ continue;
25
+ if (value === null || value === undefined)
26
+ continue;
39
27
  if (!fieldNameMap.has(key)) {
40
28
  console.error(`No field mapping found for: ${key}`);
41
29
  result[key] = value;
42
30
  continue;
43
31
  }
44
-
45
- const outputKey = fieldNameMap.get(key)!;
32
+ const outputKey = fieldNameMap.get(key);
46
33
  let finalValue = value;
47
-
48
34
  if (dropdownOptionsMap.has(key)) {
49
- const options = dropdownOptionsMap.get(key)!;
35
+ const options = dropdownOptionsMap.get(key);
50
36
  const optionKey = String(value);
51
37
  finalValue = options.get(optionKey) || value;
52
38
  }
53
-
54
39
  result[outputKey] = sanitizeValue(finalValue);
55
40
  }
56
-
57
41
  return result;
58
42
  }
59
-
60
43
  /**
61
44
  * Parse dropdown options from field configs.
62
45
  * Format: "1, First\n2, Second\n3, Third"
63
46
  */
64
- export function parseDropdownOptions(field: CaseField): Map<string, string> {
65
- const options = new Map<string, string>();
66
-
47
+ export function parseDropdownOptions(field) {
48
+ const options = new Map();
67
49
  for (const config of field.configs) {
68
50
  const items = config.options?.items;
69
- if (!items) continue;
70
-
51
+ if (!items)
52
+ continue;
71
53
  const lines = items.split('\n');
72
54
  for (const line of lines) {
73
55
  const commaIndex = line.indexOf(',');
@@ -78,11 +60,9 @@ export function parseDropdownOptions(field: CaseField): Map<string, string> {
78
60
  }
79
61
  }
80
62
  }
81
-
82
63
  return options;
83
64
  }
84
-
85
- function toSnakeCase(str: string): string {
65
+ function toSnakeCase(str) {
86
66
  return str
87
67
  .toLowerCase()
88
68
  .replace(/[^a-z0-9]+/g, '_')
@@ -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.2.0",
3
+ "version": "1.3.1",
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",
@@ -34,10 +40,12 @@
34
40
  "license": "Apache-2.0",
35
41
  "dependencies": {
36
42
  "@modelcontextprotocol/sdk": "^1.25.3",
43
+ "archiver": "^7.0.1",
37
44
  "dotenv": "^17.2.3",
38
45
  "zod": "^4.3.5"
39
46
  },
40
47
  "devDependencies": {
48
+ "@types/archiver": "^7.0.0",
41
49
  "@types/jest": "^30.0.0",
42
50
  "@types/node": "^25.0.9",
43
51
  "jest": "^30.2.0",
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,27 +0,0 @@
1
- # Changelog
2
-
3
- ## [1.2.0] - 2026-02-14
4
-
5
- ### Changed
6
-
7
- - Optimize MCP call responses by removing non-essential fields and nullish values
8
- - Migrated from `axios` to native `fetch` API for reduced dependency footprint
9
-
10
- ## [1.1.0] - 2026-02-09
11
-
12
- ### Added
13
-
14
- - **Configuration via environment variables**
15
- - `TESTRAIL_INSTANCE_URL` - Your TestRail instance URL
16
- - `TESTRAIL_USERNAME` - Your TestRail username
17
- - `TESTRAIL_API_KEY` - [How to get your API key](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)
18
-
19
- - **Test case management tools**
20
- - `get_projects` - Discover available projects in your TestRail instance
21
- - `get_case` - Fetch detailed test case info including custom fields
22
- - `get_cases` - Query cases with filtering and pagination support
23
- - `get_case_fields` - Explore available fields and dropdown options
24
- - `get_templates` - List project templates to understand case structures
25
- - `get_sections` - Navigate the test case hierarchy
26
- - `create_case` - Create new test cases with full custom field support
27
- - `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
- };