@uarlouski/testrail-mcp-server 1.3.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 (45) hide show
  1. package/dist/client/testrail.js +167 -0
  2. package/{src/index.ts → dist/index.js} +25 -35
  3. package/{src/tools/add_attachment_to_run.ts → dist/tools/add_attachment_to_run.js} +8 -18
  4. package/{src/tools/add_results.ts → dist/tools/add_results.js} +3 -7
  5. package/{src/tools/add_run.ts → dist/tools/add_run.js} +3 -8
  6. package/{src/tools/create_case.ts → dist/tools/create_case.js} +1 -7
  7. package/{src/tools/get_case.ts → dist/tools/get_case.js} +2 -11
  8. package/{src/tools/get_case_fields.ts → dist/tools/get_case_fields.js} +8 -37
  9. package/{src/tools/get_cases.ts → dist/tools/get_cases.js} +5 -19
  10. package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +1 -6
  11. package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +1 -6
  12. package/{src/tools/get_statuses.ts → dist/tools/get_statuses.js} +2 -7
  13. package/{src/tools/get_templates.ts → dist/tools/get_templates.js} +1 -6
  14. package/{src/tools/get_tests.ts → dist/tools/get_tests.js} +5 -10
  15. package/{src/tools/update_case.ts → dist/tools/update_case.js} +1 -7
  16. package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +1 -6
  17. package/dist/types/custom.js +1 -0
  18. package/{src/types/testrail.ts → dist/types/testrail.js} +2 -42
  19. package/{src/utils/mapper.ts → dist/utils/mapper.js} +16 -36
  20. package/{src/utils/sanitizer.ts → dist/utils/sanitizer.js} +4 -8
  21. package/package.json +8 -2
  22. package/.env.example +0 -3
  23. package/.github/workflows/ci.yml +0 -46
  24. package/.github/workflows/publish.yml +0 -35
  25. package/CHANGELOG.md +0 -38
  26. package/jest.config.js +0 -24
  27. package/src/client/testrail.ts +0 -227
  28. package/src/types/custom.ts +0 -19
  29. package/test/client/testrail.test.ts +0 -545
  30. package/test/tools/add_results.test.ts +0 -94
  31. package/test/tools/add_run.test.ts +0 -83
  32. package/test/tools/create_case.test.ts +0 -101
  33. package/test/tools/get_case.test.ts +0 -185
  34. package/test/tools/get_case_fields.test.ts +0 -132
  35. package/test/tools/get_cases.test.ts +0 -104
  36. package/test/tools/get_projects.test.ts +0 -38
  37. package/test/tools/get_sections.test.ts +0 -39
  38. package/test/tools/get_statuses.test.ts +0 -56
  39. package/test/tools/get_templates.test.ts +0 -38
  40. package/test/tools/get_tests.test.ts +0 -47
  41. package/test/tools/update_case.test.ts +0 -102
  42. package/test/tools/update_cases.test.ts +0 -89
  43. package/test/utils/mapper.test.ts +0 -112
  44. package/test/utils/sanitizer.test.ts +0 -143
  45. package/tsconfig.json +0 -24
@@ -1,19 +1,14 @@
1
1
  import { z } from "zod";
2
- import { TestRailClient } from "../client/testrail.js";
3
- import { ToolDefinition } from "../types/custom.js";
4
2
  import { TemplateSchema } from "../types/testrail.js";
5
-
6
3
  const parameters = {
7
4
  project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
8
5
  };
9
-
10
- export const getTemplatesTool: ToolDefinition<typeof parameters, TestRailClient> = {
6
+ export const getTemplatesTool = {
11
7
  name: "get_templates",
12
8
  description: "Get available test case templates for a project. Template IDs determine which fields are available when creating or updating test cases",
13
9
  parameters,
14
10
  handler: async ({ project_id }, client) => {
15
11
  const templates = await client.getTemplates(project_id);
16
-
17
12
  return {
18
13
  templates: templates.map(t => TemplateSchema.parse(t)),
19
14
  };
@@ -1,20 +1,15 @@
1
- import { TestRailClient } from "../client/testrail.js";
2
1
  import { z } from "zod";
3
- import { Test, TestSchema } from "../types/testrail.js";
4
- import { ToolDefinition } from "../types/custom.js";
5
-
2
+ import { TestSchema } from "../types/testrail.js";
6
3
  const parameters = {
7
4
  run_id: z.number().describe("The ID of the test run"),
8
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"),
9
- }
10
-
11
- export const getTestsTool: ToolDefinition<typeof parameters, TestRailClient> = {
6
+ };
7
+ export const getTestsTool = {
12
8
  name: "get_tests",
13
9
  description: "Get tests for a test run, optionally filtered by status",
14
10
  parameters,
15
- handler: async ({ run_id, status_id }, client: TestRailClient) => {
16
- const tests: Test[] = await client.getTests(run_id, status_id);
17
-
11
+ handler: async ({ run_id, status_id }, client) => {
12
+ const tests = await client.getTests(run_id, status_id);
18
13
  return {
19
14
  tests: tests.map(test => TestSchema.parse(test)),
20
15
  };
@@ -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
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,20 +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
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
11
  const caseData = await client.getCase(String(case_ids[0]));
16
12
  const updatedCases = await client.updateCases(caseData.suite_id, case_ids, fields);
17
-
18
13
  return {
19
14
  success: true,
20
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,42 +40,27 @@ 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>;
88
-
89
64
  export const RunSchema = z.object({
90
65
  id: z.number(),
91
66
  name: z.string(),
@@ -100,29 +75,19 @@ export const RunSchema = z.object({
100
75
  failed_count: z.number(),
101
76
  url: z.string(),
102
77
  });
103
-
104
- export type Run = z.infer<typeof RunSchema>;
105
-
106
78
  export const StatusSchema = z.object({
107
79
  id: z.number(),
108
80
  name: z.string(),
109
81
  label: z.string(),
110
82
  });
111
-
112
- export type Status = z.infer<typeof StatusSchema>;
113
-
114
83
  export const TestSchema = z.object({
115
84
  id: z.number(),
116
85
  case_id: z.number(),
117
86
  status_id: z.number(),
118
87
  title: z.string(),
119
88
  run_id: z.number(),
120
- })
121
-
122
- export const TestsSchema = z.array(TestSchema)
123
-
124
- export type Test = z.infer<typeof TestSchema>
125
-
89
+ });
90
+ export const TestsSchema = z.array(TestSchema);
126
91
  export const ResultSchema = z.object({
127
92
  id: z.number(),
128
93
  test_id: z.number(),
@@ -130,11 +95,6 @@ export const ResultSchema = z.object({
130
95
  comment: z.string().nullable(),
131
96
  defects: z.string().nullable(),
132
97
  });
133
-
134
- export type Result = z.infer<typeof ResultSchema>
135
-
136
98
  export const AttachmentSchema = z.object({
137
99
  attachment_id: z.number(),
138
100
  });
139
-
140
- export type Attachment = z.infer<typeof AttachmentSchema>
@@ -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.3.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",
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
- };