@uarlouski/testrail-mcp-server 1.10.0 → 2.0.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 (59) hide show
  1. package/README.md +28 -12
  2. package/dist/client/testrail.js +55 -0
  3. package/dist/index.js +14 -53
  4. package/dist/tools/{add_case.js → cases/add_case.js} +2 -1
  5. package/dist/tools/{get_case.js → cases/get_case.js} +3 -2
  6. package/dist/tools/{get_case_fields.js → cases/get_case_fields.js} +6 -3
  7. package/dist/tools/{get_cases.js → cases/get_cases.js} +2 -1
  8. package/dist/tools/cases/index.js +14 -0
  9. package/dist/tools/cases/types.js +40 -0
  10. package/dist/tools/{update_case.js → cases/update_case.js} +2 -1
  11. package/dist/tools/{update_cases.js → cases/update_cases.js} +2 -1
  12. package/dist/tools/commons/delete_entity.js +24 -0
  13. package/dist/tools/commons/get_configurations.js +17 -0
  14. package/dist/tools/{get_labels.js → commons/get_labels.js} +2 -1
  15. package/dist/tools/{get_priorities.js → commons/get_priorities.js} +2 -1
  16. package/dist/tools/{get_statuses.js → commons/get_statuses.js} +2 -1
  17. package/dist/tools/{get_templates.js → commons/get_templates.js} +2 -1
  18. package/dist/tools/{get_tests.js → commons/get_tests.js} +2 -1
  19. package/dist/tools/commons/get_users.js +46 -0
  20. package/dist/tools/commons/index.js +18 -0
  21. package/dist/tools/commons/types.js +49 -0
  22. package/dist/tools/projects/index.js +4 -0
  23. package/dist/tools/projects/query_project.js +29 -0
  24. package/dist/tools/projects/types.js +14 -0
  25. package/dist/tools/registry.js +46 -0
  26. package/dist/tools/{add_results.js → results/add_results.js} +2 -1
  27. package/dist/tools/{add_results_for_cases.js → results/add_results_for_cases.js} +2 -1
  28. package/dist/tools/results/get_results.js +18 -0
  29. package/dist/tools/results/index.js +8 -0
  30. package/dist/tools/results/types.js +8 -0
  31. package/dist/tools/{add_attachment_to_run.js → runs/add_attachment_to_run.js} +2 -1
  32. package/dist/tools/runs/index.js +8 -0
  33. package/dist/tools/runs/mutate_run.js +40 -0
  34. package/dist/tools/runs/query_run.js +35 -0
  35. package/dist/tools/runs/types.js +43 -0
  36. package/dist/tools/{get_sections.js → sections/get_sections.js} +2 -1
  37. package/dist/tools/sections/index.js +6 -0
  38. package/dist/tools/sections/mutate_section.js +25 -0
  39. package/dist/tools/sections/types.js +21 -0
  40. package/dist/tools/shared_steps/add_shared_step.js +2 -1
  41. package/dist/tools/shared_steps/get_shared_step.js +2 -1
  42. package/dist/tools/shared_steps/get_shared_step_history.js +2 -1
  43. package/dist/tools/shared_steps/get_shared_steps.js +2 -1
  44. package/dist/tools/shared_steps/index.js +12 -0
  45. package/dist/tools/shared_steps/types.js +18 -0
  46. package/dist/tools/shared_steps/update_shared_step.js +2 -1
  47. package/dist/tools/suites/index.js +6 -0
  48. package/dist/tools/suites/mutate_suite.js +25 -0
  49. package/dist/tools/suites/query_suite.js +28 -0
  50. package/dist/tools/suites/types.js +28 -0
  51. package/dist/utils/mutate_handler.js +22 -0
  52. package/dist/utils/query_handler.js +20 -0
  53. package/dist/utils/sanitizer.js +11 -0
  54. package/dist/utils/validator.js +3 -2
  55. package/package.json +1 -1
  56. package/dist/tools/add_run.js +0 -19
  57. package/dist/tools/get_projects.js +0 -15
  58. package/dist/tools/shared_steps/delete_shared_step.js +0 -18
  59. package/dist/types/testrail.js +0 -125
package/README.md CHANGED
@@ -131,6 +131,7 @@ Any MCP-compliant client can utilize this server. The pattern is universal—poi
131
131
  Once configured, turbo-charge your QA workflow by asking your AI assistant:
132
132
 
133
133
  - *"List all projects in TestRail to find the latest active project."*
134
+ - *"Show me all active users in the project to find the right assignee."*
134
135
  - *"Show me all test cases in section 5 of project 3."*
135
136
  - *"Create a comprehensive test case for 'Login Validation' with detailed steps."*
136
137
  - *"Start a new test run containing cases from section 5."*
@@ -138,14 +139,17 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
138
139
 
139
140
  ---
140
141
 
141
- ## ⚙️ Environment Variables
142
+ ## ⚙️ Environment Variables & Security Controls
142
143
 
143
- | Variable | Description | Required |
144
- |----------|-------------|:--------:|
145
- | `TESTRAIL_INSTANCE_URL` | Your TestRail instance URL (e.g., `https://example.testrail.io`) | ✅ |
146
- | `TESTRAIL_USERNAME` | Your TestRail user email address | ✅ |
147
- | `TESTRAIL_API_KEY` | Your TestRail API key ([Guide](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)) | ✅ |
148
- | `TESTRAIL_ENABLE_SHARED_STEPS` | Set to `true` to enable Shared Steps management tools | |
144
+ | Variable | Description | Required | Default |
145
+ |----------|-------------|:--------:|:-------:|
146
+ | `TESTRAIL_INSTANCE_URL` | Your TestRail instance URL (e.g., `https://example.testrail.io`) | ✅ | |
147
+ | `TESTRAIL_USERNAME` | Your TestRail user email address | ✅ | |
148
+ | `TESTRAIL_API_KEY` | Your TestRail API key ([Guide](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)) | ✅ | |
149
+ | `TESTRAIL_ENABLE_SHARED_STEPS` | Set to `true` to enable Shared Steps management tools | | `false` |
150
+ | `TESTRAIL_ALLOW_WRITE_OPERATIONS` | Allow write operations (e.g. adding/updating test cases, test runs, sections) | | `true` |
151
+ | `TESTRAIL_ALLOW_READ_OPERATIONS` | Allow read operations (e.g. retrieving projects, test cases, templates) | | `true` |
152
+ | `TESTRAIL_ALLOW_DELETE_OPERATIONS` | Allow delete operations (e.g. deleting cases or shared steps). Enabled strictly via `true`. | | `false` |
149
153
 
150
154
  ---
151
155
 
@@ -153,14 +157,19 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
153
157
 
154
158
  The TestRail MCP Server provides heavily typed, descriptive tools designed specifically for LLM interaction:
155
159
 
156
- > **💡 Pro Tip: Handling Large Data Sets**
157
- > For list-based tools like `get_cases` and `get_sections`, you can supply the `output_file` parameter with an absolute file path. The server will bypass the LLM context window by writing the raw JSON directly to disk and returning a concise summary, allowing you to run external scripts against the export!
160
+ > **💡 Pro Tip: Handling Large Data Sets & Permissions**
161
+ > - **Large Data Sets**: For list-based tools like `get_cases` and `get_sections`, you can supply the `output_file` parameter with an absolute file path. The server will bypass the LLM context window by writing the raw JSON directly to disk and returning a concise summary, allowing you to run external scripts against the export!
162
+ > - **Intelligent User Fallback**: When fetching users using `get_users` without admin rights, TestRail denies access globally. The server dynamically catches this permission restriction and falls back to polling all active projects in parallel, retrieving and deduplicating active users so assignees and reviewers can always be mapped.
158
163
 
159
164
  ### 🔭 Discovery & Navigation
160
165
  | Tool | Functionality |
161
166
  |------|-------------|
162
- | `get_projects` | List all available active and completed projects in your instance. |
167
+ | `query_project` | Retrieve a single project (`action: "one"`) or all active projects (`action: "many"`). |
168
+ | `query_suite` | Retrieve a single test suite (`action: "one"`) or all test suites for a project (`action: "many"`). |
163
169
  | `get_sections` | Navigate the precise folder/section hierarchy of any test suite. |
170
+ | `mutate_suite` | Create a new test suite or update an existing one in TestRail. |
171
+ | `mutate_section` | Create a new section or update an existing section in TestRail. |
172
+ | `get_users` | Retrieve active users. Handles per-project filtering and automatic fallback polling for non-admin accounts. |
164
173
 
165
174
  ### 📋 Test Case Management
166
175
  | Tool | Functionality |
@@ -174,8 +183,10 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
174
183
  ### ▶️ Test Execution & Tracking
175
184
  | Tool | Functionality |
176
185
  |------|-------------|
177
- | `add_run` | Generate a new, focused test run directly in TestRail. |
186
+ | `query_run` | Retrieve a single test run (`action: "one"`) or all test runs for a project (`action: "many"`, supporting filters). |
187
+ | `mutate_run` | Create a new test run or update an existing one in TestRail. |
178
188
  | `get_tests` | Retrieve individual tests for a specific test run, with optional status filtering. |
189
+ | `get_results` | Retrieve paginated results for a specific test ID. |
179
190
  | `add_results` | Submit test results to a test run using the specific `test_id`. |
180
191
  | `add_results_for_cases` | Submits results to a run mapping directly to `case_id`s, streamlining automation workflows. |
181
192
  | `add_attachment_to_run` | Attach logs, files, or zipped artifacts directly to an ongoing test run. |
@@ -188,7 +199,11 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
188
199
  | `get_shared_step_history` | View the complete audit trail and version history of a shared step set. |
189
200
  | `add_shared_step` | Create a new reusable set of shared test steps. |
190
201
  | `update_shared_step` | Modify an existing shared step set (changes propagate to all linked test cases). |
191
- | `delete_shared_step` | Securely delete a shared step entity. |
202
+
203
+ ### 🗑️ Deletion
204
+ | Tool | Functionality |
205
+ |------|-------------|
206
+ | `delete_entity` | Deletes a specified TestRail entity (supports `"case"` or `"shared_step"`) by its ID. Requires `TESTRAIL_ALLOW_DELETE_OPERATIONS` to be set to `true` in your environment. |
192
207
 
193
208
  ### 🧠 System Metadata
194
209
  | Tool | Functionality |
@@ -197,6 +212,7 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
197
212
  | `get_priorities` | Retrieve priority levels configured within your instance structure. |
198
213
  | `get_case_fields` | Discover custom field definitions, formats, and UI dropdown options. Supports filtering by `project_id`. |
199
214
  | `get_templates` | Identify available case templates to mandate correct AI structuring. |
215
+ | `get_configurations` | Retrieve all configuration groups and configurations for a project. |
200
216
 
201
217
  ---
202
218
 
@@ -1,3 +1,4 @@
1
+ import { isActive } from "../utils/sanitizer.js";
1
2
  import * as fs from "fs";
2
3
  const API_INDEX = '/index.php?';
3
4
  const API_BASE_V2 = `${API_INDEX}/api/v2`;
@@ -101,10 +102,54 @@ export class TestRailClient {
101
102
  async addRun(projectId, fields) {
102
103
  return this.post(`${API_BASE_V2}/add_run/${projectId}`, fields);
103
104
  }
105
+ async updateRun(runId, fields) {
106
+ return this.post(`${API_BASE_V2}/update_run/${runId}`, fields);
107
+ }
104
108
  async getSections(projectId) {
105
109
  const url = `${API_BASE_V2}/get_sections/${projectId}`;
106
110
  return this.paginateAll(url, 'sections');
107
111
  }
112
+ async addSection(projectId, data) {
113
+ return this.post(`${API_BASE_V2}/add_section/${projectId}`, data);
114
+ }
115
+ async updateSection(sectionId, data) {
116
+ return this.post(`${API_BASE_V2}/update_section/${sectionId}`, data);
117
+ }
118
+ async addSuite(projectId, data) {
119
+ return this.post(`${API_BASE_V2}/add_suite/${projectId}`, data);
120
+ }
121
+ async updateSuite(suiteId, data) {
122
+ return this.post(`${API_BASE_V2}/update_suite/${suiteId}`, data);
123
+ }
124
+ async getSuite(suiteId) {
125
+ return this.get(`${API_BASE_V2}/get_suite/${suiteId}`);
126
+ }
127
+ async getSuites(projectId) {
128
+ const url = `${API_BASE_V2}/get_suites/${projectId}`;
129
+ return this.paginateAll(url, 'suites');
130
+ }
131
+ async getRun(runId) {
132
+ return this.get(`${API_BASE_V2}/get_run/${runId}`);
133
+ }
134
+ async getRuns(projectId, filter) {
135
+ let url = `${API_BASE_V2}/get_runs/${projectId}`;
136
+ if (filter) {
137
+ for (const [key, value] of Object.entries(filter)) {
138
+ url += `&${key}=${encodeURIComponent(value)}`;
139
+ }
140
+ }
141
+ return this.paginateAll(url, 'runs');
142
+ }
143
+ async getResults(testId) {
144
+ const url = `${API_BASE_V2}/get_results/${testId}`;
145
+ return this.paginateAll(url, 'results');
146
+ }
147
+ async getConfigs(projectId) {
148
+ return this.get(`${API_BASE_V2}/get_configs/${projectId}`);
149
+ }
150
+ async getProject(projectId) {
151
+ return this.get(`${API_BASE_V2}/get_project/${projectId}`);
152
+ }
108
153
  async getProjects() {
109
154
  if (!this.projectsPromise) {
110
155
  this.projectsPromise = this.get(`${API_BASE_V2}/get_projects`)
@@ -158,9 +203,19 @@ export class TestRailClient {
158
203
  async updateSharedStep(sharedStepId, data) {
159
204
  return this.post(`${API_BASE_V2}/update_shared_step/${sharedStepId}`, data);
160
205
  }
206
+ async deleteCase(caseId) {
207
+ return this._executeRequest('POST', `${API_BASE_V2}/delete_case/${caseId}`, this.headers, JSON.stringify({}), false);
208
+ }
161
209
  async deleteSharedStep(sharedStepId) {
162
210
  return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}), false);
163
211
  }
212
+ async getUsers(projectId) {
213
+ const url = projectId
214
+ ? `${API_BASE_V2}/get_users/${projectId}`
215
+ : `${API_BASE_V2}/get_users`;
216
+ const allUsers = await this.paginateAll(url, 'users');
217
+ return allUsers.filter(isActive);
218
+ }
164
219
  async paginateAll(url, dataKey) {
165
220
  const allItems = [];
166
221
  let nextUrl = url;
package/dist/index.js CHANGED
@@ -3,79 +3,40 @@ import "dotenv/config";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { TestRailClient } from "./client/testrail.js";
6
- import { getCaseTool } from "./tools/get_case.js";
7
- import { getCasesTool } from "./tools/get_cases.js";
8
- import { getCaseFieldsTool } from "./tools/get_case_fields.js";
9
- import { getTemplatesTool } from "./tools/get_templates.js";
10
- import { updateCaseTool } from "./tools/update_case.js";
11
- import { updateCasesTool } from "./tools/update_cases.js";
12
- import { addCaseTool } from "./tools/add_case.js";
13
- import { getSectionsTool } from "./tools/get_sections.js";
14
- import { getProjectsTool } from "./tools/get_projects.js";
15
- import { addRunTool } from "./tools/add_run.js";
16
- import { getStatusesTool } from "./tools/get_statuses.js";
17
- import { getPrioritiesTool } from "./tools/get_priorities.js";
18
- import { getTestsTool } from "./tools/get_tests.js";
19
- import { addResultsTool } from "./tools/add_results.js";
20
- import { addAttachmentToRunTool } from "./tools/add_attachment_to_run.js";
21
- import { addResultsForCasesTool } from "./tools/add_results_for_cases.js";
22
- import { getLabelsTool } from "./tools/get_labels.js";
23
- import { getSharedStepsTool } from "./tools/shared_steps/get_shared_steps.js";
24
- import { getSharedStepTool } from "./tools/shared_steps/get_shared_step.js";
25
- import { getSharedStepHistoryTool } from "./tools/shared_steps/get_shared_step_history.js";
26
- import { addSharedStepTool } from "./tools/shared_steps/add_shared_step.js";
27
- import { updateSharedStepTool } from "./tools/shared_steps/update_shared_step.js";
28
- import { deleteSharedStepTool } from "./tools/shared_steps/delete_shared_step.js";
6
+ import { getToolsToRegister } from "./tools/registry.js";
29
7
  import { removeNullish } from "./utils/sanitizer.js";
30
8
  import z from "zod";
31
9
  const EnvSchema = z.object({
32
10
  TESTRAIL_INSTANCE_URL: z.url('Must be a valid TestRail URL'),
33
11
  TESTRAIL_USERNAME: z.email('Must be a valid email address'),
34
12
  TESTRAIL_API_KEY: z.string().min(1, 'API key is required'),
35
- TESTRAIL_ENABLE_SHARED_STEPS: z.string().optional().transform(val => val === 'true')
13
+ TESTRAIL_ENABLE_SHARED_STEPS: z.string().optional().transform(val => val === 'true'),
14
+ TESTRAIL_ALLOW_WRITE_OPERATIONS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
15
+ TESTRAIL_ALLOW_READ_OPERATIONS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
16
+ TESTRAIL_ALLOW_DELETE_OPERATIONS: z.string().optional().transform(val => val === 'true')
36
17
  });
37
18
  const parseResult = EnvSchema.safeParse(process.env);
38
19
  if (!parseResult.success) {
39
20
  console.error("Invalid TestRail environment configuration:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
40
21
  process.exit(1);
41
22
  }
42
- const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS } = parseResult.data;
23
+ const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS, TESTRAIL_ALLOW_WRITE_OPERATIONS, TESTRAIL_ALLOW_READ_OPERATIONS, TESTRAIL_ALLOW_DELETE_OPERATIONS } = parseResult.data;
43
24
  const server = new McpServer({
44
25
  name: "TestRail MCP Server",
45
- version: "1.10.0",
26
+ version: "2.0.0",
46
27
  });
47
28
  const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
48
- const tools = [
49
- getProjectsTool,
50
- getCaseTool,
51
- getCasesTool,
52
- getCaseFieldsTool,
53
- getTemplatesTool,
54
- getSectionsTool,
55
- updateCaseTool,
56
- updateCasesTool,
57
- addCaseTool,
58
- addRunTool,
59
- getStatusesTool,
60
- getPrioritiesTool,
61
- getTestsTool,
62
- addResultsTool,
63
- addResultsForCasesTool,
64
- addAttachmentToRunTool,
65
- getLabelsTool,
66
- ];
67
- if (TESTRAIL_ENABLE_SHARED_STEPS) {
68
- tools.push(getSharedStepsTool);
69
- tools.push(getSharedStepTool);
70
- tools.push(getSharedStepHistoryTool);
71
- tools.push(addSharedStepTool);
72
- tools.push(updateSharedStepTool);
73
- tools.push(deleteSharedStepTool);
74
- }
29
+ const tools = getToolsToRegister({
30
+ enableSharedSteps: TESTRAIL_ENABLE_SHARED_STEPS,
31
+ allowWrite: TESTRAIL_ALLOW_WRITE_OPERATIONS,
32
+ allowRead: TESTRAIL_ALLOW_READ_OPERATIONS,
33
+ allowDelete: TESTRAIL_ALLOW_DELETE_OPERATIONS
34
+ });
75
35
  for (const tool of tools) {
76
36
  server.registerTool(tool.name, {
77
37
  description: tool.description,
78
38
  inputSchema: tool.parameters,
39
+ annotations: tool.annotations,
79
40
  }, async (args) => {
80
41
  try {
81
42
  const output = await tool.handler(args, client);
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { validateCaseFields } from "../utils/validator.js";
2
+ import { validateCaseFields } from "../../utils/validator.js";
3
3
  import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
4
4
  const parameters = {
5
5
  section_id: z.number().describe("The ID of the section where the case should be created. Use get_sections to find available sections"),
@@ -12,6 +12,7 @@ The create operation requires knowing valid field names that are returned by get
12
12
  `;
13
13
  export const addCaseTool = {
14
14
  name: "add_case",
15
+ mode: "write",
15
16
  description: description.trim(),
16
17
  parameters,
17
18
  handler: async ({ section_id, title, fields }, client) => {
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
- import { processCustomFields } from "../utils/mapper.js";
3
- import { LabelSchema } from "../types/testrail.js";
2
+ import { processCustomFields } from "../../utils/mapper.js";
3
+ import { LabelSchema } from "../commons/types.js";
4
4
  const parameters = {
5
5
  case_id: z.string().describe("The ID of the test case (e.g. '123' or 'C123')"),
6
6
  };
7
7
  export const getCaseTool = {
8
8
  name: "get_case",
9
+ mode: "read",
9
10
  description: "Get detailed information about a test case including its custom fields",
10
11
  parameters,
11
12
  handler: async ({ case_id }, client) => {
@@ -1,15 +1,17 @@
1
1
  import { z } from "zod";
2
- import { parseDropdownOptions } from "../utils/mapper.js";
2
+ import { parseDropdownOptions } from "../../utils/mapper.js";
3
+ import { isActive } from "../../utils/sanitizer.js";
3
4
  // Field remains optional for backward compatibility with previous implementation.
4
5
  // However, it is recommended to always provide project_id to get fields applicable to the project.
5
6
  const parameters = {
6
7
  project_id: z.number().optional().describe("The project ID to get fields for. This is the primary way to use this tool, as it returns only the fields relevant to your current project. If omitted, returns all fields across all projects."),
7
8
  };
8
9
  export const CASE_FIELDS_PARAM_DESCRIPTION = `
10
+ A flat key-value map of case fields. The server merges all entries directly into the root of the TestRail API request body — there is no nested "fields" key in the API call.
9
11
  Must use system_name values from get_case_fields.
12
+ Example: {"priority_id": 2, "template_id": 1, "labels": [1, 2]}
10
13
  Call get_case_fields with project_id first if field names are not already known.
11
14
  Using an unknown field name (e.g. 'label_ids') will result in an error.
12
- Example: {"priority_id": 2, "template_id": 1, "labels": [1, 2]}
13
15
  `;
14
16
  const FIELD_TYPE_NAMES = {
15
17
  1: "String",
@@ -84,11 +86,12 @@ Returns available fields with their types and options (for dropdown fields).
84
86
  `;
85
87
  export const getCaseFieldsTool = {
86
88
  name: "get_case_fields",
89
+ mode: "read",
87
90
  description: description.trim(),
88
91
  parameters,
89
92
  handler: async ({ project_id }, client) => {
90
93
  const caseFields = await client.getCaseFields();
91
- const activeFields = caseFields.filter(field => field.is_active);
94
+ const activeFields = caseFields.filter(isActive);
92
95
  const filtered = project_id !== undefined
93
96
  ? activeFields.filter(field => isFieldForProject(field, project_id))
94
97
  : activeFields;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import fs from "fs";
3
- import { validateCaseFields } from "../utils/validator.js";
3
+ import { validateCaseFields } from "../../utils/validator.js";
4
4
  const parameters = {
5
5
  project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
6
6
  section: z.object({
@@ -15,6 +15,7 @@ const parameters = {
15
15
  };
16
16
  export const getCasesTool = {
17
17
  name: "get_cases",
18
+ mode: "read",
18
19
  description: "Get all test cases for a project. Filter by section, API params (priority, type), or any field including custom fields via 'where'. Returns case IDs, titles, and any additional requested fields.",
19
20
  parameters,
20
21
  handler: async ({ project_id, section, filter, where, fields, output_file }, client) => {
@@ -0,0 +1,14 @@
1
+ import { getCaseTool } from "./get_case.js";
2
+ import { getCasesTool } from "./get_cases.js";
3
+ import { getCaseFieldsTool } from "./get_case_fields.js";
4
+ import { updateCaseTool } from "./update_case.js";
5
+ import { updateCasesTool } from "./update_cases.js";
6
+ import { addCaseTool } from "./add_case.js";
7
+ export const casesTools = [
8
+ getCaseTool,
9
+ getCasesTool,
10
+ getCaseFieldsTool,
11
+ updateCaseTool,
12
+ updateCasesTool,
13
+ addCaseTool,
14
+ ];
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { LabelSchema } from "../commons/types.js";
3
+ export const CaseFieldConfigSchema = z.object({
4
+ context: z.object({
5
+ is_global: z.boolean(),
6
+ project_ids: z.array(z.number()),
7
+ }).optional(),
8
+ options: z.object({
9
+ default_value: z.string().optional(),
10
+ is_required: z.boolean().optional(),
11
+ items: z.string().optional(),
12
+ }),
13
+ });
14
+ export const CaseFieldSchema = z.object({
15
+ id: z.number(),
16
+ name: z.string(),
17
+ system_name: z.string(),
18
+ label: z.string(),
19
+ type_id: z.number(),
20
+ template_ids: z.array(z.number()),
21
+ is_active: z.boolean(),
22
+ description: z.string().nullable(),
23
+ include_all: z.boolean(),
24
+ configs: z.array(CaseFieldConfigSchema),
25
+ });
26
+ export const CaseSchema = z.object({
27
+ id: z.number(),
28
+ title: z.string(),
29
+ section_id: z.number(),
30
+ template_id: z.number(),
31
+ type_id: z.number(),
32
+ priority_id: z.number(),
33
+ milestone_id: z.number().nullable(),
34
+ refs: z.string().nullable(),
35
+ created_on: z.number(),
36
+ updated_on: z.number(),
37
+ estimate: z.string().nullable(),
38
+ suite_id: z.number(),
39
+ labels: z.array(LabelSchema),
40
+ }).loose();
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { validateCaseFields } from "../utils/validator.js";
2
+ import { validateCaseFields } from "../../utils/validator.js";
3
3
  import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
4
4
  const parameters = {
5
5
  case_id: z.string().describe("The ID of the test case to update (e.g. '123' or 'C123')"),
@@ -12,6 +12,7 @@ Supports partial updates — only specify the fields you want to change.
12
12
  `;
13
13
  export const updateCaseTool = {
14
14
  name: "update_case",
15
+ mode: "write",
15
16
  description: description.trim(),
16
17
  parameters,
17
18
  handler: async ({ case_id, fields }, client) => {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { validateCaseFields } from "../utils/validator.js";
2
+ import { validateCaseFields } from "../../utils/validator.js";
3
3
  import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
4
4
  const parameters = {
5
5
  case_ids: z.array(z.number()).min(1).describe("Array of case IDs to update (e.g. [123, 456, 789])"),
@@ -12,6 +12,7 @@ More efficient than calling update_case multiple times.
12
12
  `;
13
13
  export const updateCasesTool = {
14
14
  name: "update_cases",
15
+ mode: "write",
15
16
  description: description.trim(),
16
17
  parameters,
17
18
  handler: async ({ case_ids, fields }, client) => {
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ const parameters = {
3
+ entity_type: z.enum(["case", "shared_step"]).describe("The type of the entity to delete"),
4
+ entity_id: z.number().describe("The ID of the entity to delete"),
5
+ };
6
+ export const deleteEntityTool = {
7
+ name: "delete_entity",
8
+ mode: "delete",
9
+ description: "Deletes a specified TestRail entity (case or shared_step) by its ID.",
10
+ parameters,
11
+ handler: async (args, client) => {
12
+ const { entity_type, entity_id } = args;
13
+ switch (entity_type) {
14
+ case "case":
15
+ await client.deleteCase(entity_id);
16
+ return { message: `Case ${entity_id} deleted successfully.` };
17
+ case "shared_step":
18
+ await client.deleteSharedStep(entity_id);
19
+ return { message: `Shared step ${entity_id} deleted successfully.` };
20
+ default:
21
+ throw new Error(`Unsupported entity type: ${entity_type}`);
22
+ }
23
+ }
24
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { ConfigurationGroupSchema } from "./types.js";
3
+ const parameters = {
4
+ project_id: z.number().int().describe("The ID of the project"),
5
+ };
6
+ export const getConfigurationsTool = {
7
+ name: "get_configurations",
8
+ mode: "read",
9
+ description: "Get all configuration groups and configurations for a project in TestRail.",
10
+ parameters,
11
+ handler: async (args, client) => {
12
+ const configs = await client.getConfigs(args.project_id);
13
+ return {
14
+ configurations: configs.map(c => ConfigurationGroupSchema.parse(c)),
15
+ };
16
+ }
17
+ };
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- import { LabelSchema } from "../types/testrail.js";
2
+ import { LabelSchema } from "./types.js";
3
3
  const parameters = {
4
4
  project_id: z.number().describe("The ID of the project to retrieve labels for"),
5
5
  };
6
6
  export const getLabelsTool = {
7
7
  name: "get_labels",
8
+ mode: "read",
8
9
  description: "Get all available test case labels (sometimes called tags) for a project. Returns label IDs and titles that can be used when creating or updating test cases.",
9
10
  parameters,
10
11
  handler: async ({ project_id }, client) => {
@@ -1,7 +1,8 @@
1
- import { PrioritySchema } from "../types/testrail.js";
1
+ import { PrioritySchema } from "./types.js";
2
2
  const parameters = {};
3
3
  export const getPrioritiesTool = {
4
4
  name: "get_priorities",
5
+ mode: "read",
5
6
  description: "Get all available test case priorities (e.g. Critical, High, Medium, Low). Returns priority IDs and names that can be used when creating or updating test cases.",
6
7
  parameters,
7
8
  handler: async (_args, client) => {
@@ -1,7 +1,8 @@
1
- import { StatusSchema } from "../types/testrail.js";
1
+ import { StatusSchema } from "./types.js";
2
2
  const parameters = {};
3
3
  export const getStatusesTool = {
4
4
  name: "get_statuses",
5
+ mode: "read",
5
6
  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
7
  parameters,
7
8
  handler: async (args, client) => {
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- import { TemplateSchema } from "../types/testrail.js";
2
+ import { TemplateSchema } from "./types.js";
3
3
  const parameters = {
4
4
  project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
5
5
  };
6
6
  export const getTemplatesTool = {
7
7
  name: "get_templates",
8
+ mode: "read",
8
9
  description: "Get available test case templates for a project. Template IDs determine which fields are available when creating or updating test cases",
9
10
  parameters,
10
11
  handler: async ({ project_id }, client) => {
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
- import { TestSchema } from "../types/testrail.js";
2
+ import { TestSchema } from "./types.js";
3
3
  const parameters = {
4
4
  run_id: z.number().describe("The ID of the test run"),
5
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
6
  };
7
7
  export const getTestsTool = {
8
8
  name: "get_tests",
9
+ mode: "read",
9
10
  description: "Get tests for a test run, optionally filtered by status",
10
11
  parameters,
11
12
  handler: async ({ run_id, status_id }, client) => {
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { UserSchema } from "./types.js";
3
+ const parameters = {
4
+ project_id: z.number().optional().describe("Optional ID of the project to retrieve users for. If omitted, attempts to fetch all users globally."),
5
+ fallback_all_projects: z.boolean().optional().default(true).describe("If true and global user retrieval is forbidden (non-admin), automatically falls back to fetching users across all active projects."),
6
+ };
7
+ async function getUsersFallbackAllProjects(client) {
8
+ const projects = await client.getProjects();
9
+ const activeProjects = projects.filter(p => !p.is_completed);
10
+ const userPromises = activeProjects.map(project => client.getUsers(project.id).catch(err => {
11
+ console.error(`[get_users] Failed to fetch users for project ${project.id} (${project.name}): ${err.message}`);
12
+ return [];
13
+ }));
14
+ const results = await Promise.all(userPromises);
15
+ const allUsers = results.flat();
16
+ const uniqueUsersMap = new Map();
17
+ for (const user of allUsers) {
18
+ uniqueUsersMap.set(user.id, user);
19
+ }
20
+ return Array.from(uniqueUsersMap.values());
21
+ }
22
+ export const getUsersTool = {
23
+ name: "get_users",
24
+ mode: "read",
25
+ description: "Get active users from TestRail. Resolves active users globally or per-project. If global fetch is forbidden (for non-admin accounts), falls back to merging users across all active projects so users referenced as reviewers or assignees can be resolved.",
26
+ parameters,
27
+ handler: async ({ project_id, fallback_all_projects }, client) => {
28
+ let users;
29
+ try {
30
+ users = await client.getUsers(project_id);
31
+ }
32
+ catch (err) {
33
+ const isPermissionError = err.message?.includes("Access Denied");
34
+ if (!project_id && fallback_all_projects !== false && isPermissionError) {
35
+ console.error("[get_users] Global fetch failed (Access Denied). Falling back to multi-project polling...");
36
+ users = await getUsersFallbackAllProjects(client);
37
+ }
38
+ else {
39
+ throw err;
40
+ }
41
+ }
42
+ return {
43
+ users: users.map(user => UserSchema.parse(user))
44
+ };
45
+ }
46
+ };
@@ -0,0 +1,18 @@
1
+ import { deleteEntityTool } from "./delete_entity.js";
2
+ import { getConfigurationsTool } from "./get_configurations.js";
3
+ import { getLabelsTool } from "./get_labels.js";
4
+ import { getPrioritiesTool } from "./get_priorities.js";
5
+ import { getStatusesTool } from "./get_statuses.js";
6
+ import { getTemplatesTool } from "./get_templates.js";
7
+ import { getTestsTool } from "./get_tests.js";
8
+ import { getUsersTool } from "./get_users.js";
9
+ export const commonsTools = [
10
+ deleteEntityTool,
11
+ getConfigurationsTool,
12
+ getLabelsTool,
13
+ getPrioritiesTool,
14
+ getStatusesTool,
15
+ getTemplatesTool,
16
+ getTestsTool,
17
+ getUsersTool,
18
+ ];