@uarlouski/testrail-mcp-server 1.8.0 → 1.10.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.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  <div align="center">
2
2
  <h1>🚀 TestRail MCP Server</h1>
3
3
  <p>
4
- <strong>A high-performance Model Context Protocol (MCP) server connecting AI assistants (Claude, Cursor, Windsurf) directly to TestRail.</strong><br>
5
- <em>Seamlessly search, create, update, and execute test cases using natural language.</em>
4
+ <strong>An open-source Model Context Protocol (MCP) server that connects Claude, Cursor, Windsurf, and other AI assistants directly to TestRail.</strong>
5
+ </p>
6
+ <p>
7
+ Manage TestRail projects, search and create test cases, kick off test runs, record results, and attach files — all through natural-language conversation with your AI assistant. Built for QA engineers and AI-assisted test automation.
6
8
  </p>
7
9
  </div>
8
10
 
@@ -13,8 +15,19 @@
13
15
  <a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License"></a>
14
16
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.9-blue.svg" alt="TypeScript"></a>
15
17
  <a href="https://github.com/uarlouski/testrail-mcp-server"><img src="https://img.shields.io/github/stars/uarlouski/testrail-mcp-server.svg?style=social&label=Star" alt="GitHub stars"></a>
18
+ <a href="https://glama.ai/mcp/servers/uarlouski/testrail-mcp-server"><img src="https://glama.ai/mcp/servers/uarlouski/testrail-mcp-server/badges/score.svg" alt="Score Badge"></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <strong>Compatible with:</strong>
23
+ <img src="https://img.shields.io/badge/Claude%20Desktop-D97757?logo=anthropic&logoColor=white" alt="Claude Desktop">
24
+ <img src="https://img.shields.io/badge/Cursor-000000?logo=cursor&logoColor=white" alt="Cursor">
25
+ <img src="https://img.shields.io/badge/Windsurf-19A1BC?logo=codeium&logoColor=white" alt="Windsurf">
26
+ <img src="https://img.shields.io/badge/VS%20Code-007ACC?logo=visualstudiocode&logoColor=white" alt="VS Code">
16
27
  </p>
17
28
 
29
+ <!-- TODO: insert demo.gif — short screencast of asking "create a test case" in Claude or Cursor and seeing the case appear in TestRail -->
30
+
18
31
  ---
19
32
 
20
33
  ## 🌟 Why Choose TestRail MCP Server?
@@ -40,10 +53,9 @@ Navigate to **My Settings → API Keys** in your TestRail platform and generate
40
53
 
41
54
  ### 2. Configure Your MCP Client
42
55
 
43
- Add the server to your chosen MCP client configuration. Below are examples for popular AI IDEs and assistants:
56
+ Add the server to your chosen MCP client configuration. The Claude Desktop example is shown below; Cursor, Windsurf, and other clients use the same pattern (see the collapsible sections further down).
44
57
 
45
- <details>
46
- <summary><strong>🤖 Claude Desktop</strong></summary>
58
+ #### 🤖 Claude Desktop
47
59
 
48
60
  Add this to your `claude_desktop_config.json`:
49
61
 
@@ -56,13 +68,13 @@ Add this to your `claude_desktop_config.json`:
56
68
  "env": {
57
69
  "TESTRAIL_INSTANCE_URL": "https://your-instance.testrail.io",
58
70
  "TESTRAIL_USERNAME": "your@email.com",
59
- "TESTRAIL_API_KEY": "your-api-key"
71
+ "TESTRAIL_API_KEY": "your-api-key",
72
+ "TESTRAIL_ENABLE_SHARED_STEPS": "true"
60
73
  }
61
74
  }
62
75
  }
63
76
  }
64
77
  ```
65
- </details>
66
78
 
67
79
  <details>
68
80
  <summary><strong>⌨️ Cursor</strong></summary>
@@ -133,6 +145,7 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
133
145
  | `TESTRAIL_INSTANCE_URL` | Your TestRail instance URL (e.g., `https://example.testrail.io`) | ✅ |
134
146
  | `TESTRAIL_USERNAME` | Your TestRail user email address | ✅ |
135
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 | |
136
149
 
137
150
  ---
138
151
 
@@ -140,6 +153,9 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
140
153
 
141
154
  The TestRail MCP Server provides heavily typed, descriptive tools designed specifically for LLM interaction:
142
155
 
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!
158
+
143
159
  ### 🔭 Discovery & Navigation
144
160
  | Tool | Functionality |
145
161
  |------|-------------|
@@ -151,7 +167,7 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
151
167
  |------|-------------|
152
168
  | `get_cases` | Query test cases with advanced filtering (priority, template, type, etc.) and pagination. |
153
169
  | `get_case` | Fetch complete, structured details of a specific test case, including custom steps and fields. |
154
- | `create_case` | Seamlessly create a new test case equipped with robust custom field validation. |
170
+ | `add_case` | Seamlessly create a new test case equipped with robust custom field validation. |
155
171
  | `update_case` | Modify an existing test case's steps, metadata, or titles. |
156
172
  | `update_cases` | Execute bulk-updates on multiple test cases simultaneously to save time. |
157
173
 
@@ -164,12 +180,22 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
164
180
  | `add_results_for_cases` | Submits results to a run mapping directly to `case_id`s, streamlining automation workflows. |
165
181
  | `add_attachment_to_run` | Attach logs, files, or zipped artifacts directly to an ongoing test run. |
166
182
 
183
+ ### 🔗 Shared Steps (Optional)
184
+ | Tool | Functionality |
185
+ |------|-------------|
186
+ | `get_shared_steps` | List shared test steps for a project with optional reference filtering. |
187
+ | `get_shared_step` | Retrieve precise details of a specific shared test step set. |
188
+ | `get_shared_step_history` | View the complete audit trail and version history of a shared step set. |
189
+ | `add_shared_step` | Create a new reusable set of shared test steps. |
190
+ | `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. |
192
+
167
193
  ### 🧠 System Metadata
168
194
  | Tool | Functionality |
169
195
  |------|-------------|
170
196
  | `get_statuses` | Systematically list all configured test statuses (Passed, Failed, Blocked, Retest, etc.). |
171
197
  | `get_priorities` | Retrieve priority levels configured within your instance structure. |
172
- | `get_case_fields` | Discover custom field definitions, formats, and UI dropdown options. |
198
+ | `get_case_fields` | Discover custom field definitions, formats, and UI dropdown options. Supports filtering by `project_id`. |
173
199
  | `get_templates` | Identify available case templates to mandate correct AI structuring. |
174
200
 
175
201
  ---
@@ -185,12 +211,3 @@ This project is securely licensed under the [Apache License 2.0](LICENSE).
185
211
  <p align="center">
186
212
  <b>TestRail MCP Server</b> · Engineered with the <a href="https://modelcontextprotocol.io">Model Context Protocol</a>
187
213
  </p>
188
-
189
- <!-- SEO Keywords -->
190
- <div align="center">
191
- <p>
192
- <small>
193
- <b>Keywords:</b> TestRail MCP Server, Model Context Protocol, TestRail API Integration, AI Test Management, LLM Testing Tools, Claude TestRail Plugin, Cursor TestRail, Windsurf QA Tool, Test Case Automation, QA AI Assistant, MCP Server Developer Tools, Prompt Engineering TestRail, Test Run Tracking, QA Productivity Automation.
194
- </small>
195
- </p>
196
- </div>
@@ -95,7 +95,7 @@ export class TestRailClient {
95
95
  });
96
96
  return response.updated_cases;
97
97
  }
98
- async createCase(sectionId, fields) {
98
+ async addCase(sectionId, fields) {
99
99
  return this.post(`${API_BASE_V2}/add_case/${sectionId}`, fields);
100
100
  }
101
101
  async addRun(projectId, fields) {
@@ -139,6 +139,28 @@ export class TestRailClient {
139
139
  };
140
140
  return this._executeRequest('POST', `${API_BASE_V2}/add_attachment_to_run/${runId}`, headers, formData);
141
141
  }
142
+ async getSharedSteps(projectId, options) {
143
+ let url = `${API_BASE_V2}/get_shared_steps/${projectId}`;
144
+ if (options?.refs) {
145
+ url += `&refs=${options.refs}`;
146
+ }
147
+ return this.paginateAll(url, 'shared_steps');
148
+ }
149
+ async getSharedStep(sharedStepId) {
150
+ return this.get(`${API_BASE_V2}/get_shared_step/${sharedStepId}`);
151
+ }
152
+ async getSharedStepHistory(sharedStepId) {
153
+ return this.get(`${API_BASE_V2}/get_shared_step_history/${sharedStepId}`);
154
+ }
155
+ async addSharedStep(projectId, data) {
156
+ return this.post(`${API_BASE_V2}/add_shared_step/${projectId}`, data);
157
+ }
158
+ async updateSharedStep(sharedStepId, data) {
159
+ return this.post(`${API_BASE_V2}/update_shared_step/${sharedStepId}`, data);
160
+ }
161
+ async deleteSharedStep(sharedStepId) {
162
+ return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}), false);
163
+ }
142
164
  async paginateAll(url, dataKey) {
143
165
  const allItems = [];
144
166
  let nextUrl = url;
@@ -167,7 +189,7 @@ export class TestRailClient {
167
189
  }
168
190
  return this._executeRequest(method, endpoint, this.headers, jsonData);
169
191
  }
170
- async _executeRequest(method, endpoint, headers, body) {
192
+ async _executeRequest(method, endpoint, headers, body, parseBody = true) {
171
193
  const url = `${this.baseUrl}${endpoint}`;
172
194
  const params = {
173
195
  method,
@@ -205,7 +227,10 @@ export class TestRailClient {
205
227
  console.error(`[TestRailClient] Error Details: ${errorMessage}`);
206
228
  throw new Error(errorMessage);
207
229
  }
208
- return await response.json();
230
+ if (parseBody) {
231
+ return await response.json();
232
+ }
233
+ return await response.text();
209
234
  }
210
235
  catch (error) {
211
236
  const networkError = error instanceof Error && !error.message.startsWith('TestRail API Error');
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { getCaseFieldsTool } from "./tools/get_case_fields.js";
9
9
  import { getTemplatesTool } from "./tools/get_templates.js";
10
10
  import { updateCaseTool } from "./tools/update_case.js";
11
11
  import { updateCasesTool } from "./tools/update_cases.js";
12
- import { createCaseTool } from "./tools/create_case.js";
12
+ import { addCaseTool } from "./tools/add_case.js";
13
13
  import { getSectionsTool } from "./tools/get_sections.js";
14
14
  import { getProjectsTool } from "./tools/get_projects.js";
15
15
  import { addRunTool } from "./tools/add_run.js";
@@ -20,22 +20,29 @@ import { addResultsTool } from "./tools/add_results.js";
20
20
  import { addAttachmentToRunTool } from "./tools/add_attachment_to_run.js";
21
21
  import { addResultsForCasesTool } from "./tools/add_results_for_cases.js";
22
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";
23
29
  import { removeNullish } from "./utils/sanitizer.js";
24
30
  import z from "zod";
25
31
  const EnvSchema = z.object({
26
32
  TESTRAIL_INSTANCE_URL: z.url('Must be a valid TestRail URL'),
27
33
  TESTRAIL_USERNAME: z.email('Must be a valid email address'),
28
- TESTRAIL_API_KEY: z.string().min(1, 'API key is required')
34
+ TESTRAIL_API_KEY: z.string().min(1, 'API key is required'),
35
+ TESTRAIL_ENABLE_SHARED_STEPS: z.string().optional().transform(val => val === 'true')
29
36
  });
30
37
  const parseResult = EnvSchema.safeParse(process.env);
31
38
  if (!parseResult.success) {
32
39
  console.error("Invalid TestRail environment configuration:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
33
40
  process.exit(1);
34
41
  }
35
- const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY } = parseResult.data;
42
+ const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS } = parseResult.data;
36
43
  const server = new McpServer({
37
44
  name: "TestRail MCP Server",
38
- version: "1.8.0",
45
+ version: "1.10.0",
39
46
  });
40
47
  const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
41
48
  const tools = [
@@ -47,7 +54,7 @@ const tools = [
47
54
  getSectionsTool,
48
55
  updateCaseTool,
49
56
  updateCasesTool,
50
- createCaseTool,
57
+ addCaseTool,
51
58
  addRunTool,
52
59
  getStatusesTool,
53
60
  getPrioritiesTool,
@@ -57,6 +64,14 @@ const tools = [
57
64
  addAttachmentToRunTool,
58
65
  getLabelsTool,
59
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
+ }
60
75
  for (const tool of tools) {
61
76
  server.registerTool(tool.name, {
62
77
  description: tool.description,
@@ -1,13 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { validateCaseFields } from "../utils/validator.js";
3
+ import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
3
4
  const parameters = {
4
5
  section_id: z.number().describe("The ID of the section where the case should be created. Use get_sections to find available sections"),
5
6
  title: z.string().describe("The title of the test case"),
6
- fields: z.record(z.string(), z.any()).optional().describe("Optional fields for the test case. First use get_templates to discover available templates, then get_case_fields to see available fields for your template. Example: {\"priority_id\": 2, \"template_id\": 1, \"custom_automation_priority\": 1}"),
7
+ fields: z.record(z.string(), z.any()).optional().describe(CASE_FIELDS_PARAM_DESCRIPTION),
7
8
  };
8
- export const createCaseTool = {
9
- name: "create_case",
10
- description: "Create a new test case in TestRail",
9
+ const description = `
10
+ Create a new test case in TestRail.
11
+ The create operation requires knowing valid field names that are returned by get_case_fields tool.
12
+ `;
13
+ export const addCaseTool = {
14
+ name: "add_case",
15
+ description: description.trim(),
11
16
  parameters,
12
17
  handler: async ({ section_id, title, fields }, client) => {
13
18
  if (fields) {
@@ -17,7 +22,7 @@ export const createCaseTool = {
17
22
  title,
18
23
  ...fields,
19
24
  };
20
- const createdCase = await client.createCase(section_id, caseData);
25
+ const createdCase = await client.addCase(section_id, caseData);
21
26
  return {
22
27
  success: true,
23
28
  case_id: createdCase.id,
@@ -1,5 +1,16 @@
1
+ import { z } from "zod";
1
2
  import { parseDropdownOptions } from "../utils/mapper.js";
2
- const parameters = {};
3
+ // Field remains optional for backward compatibility with previous implementation.
4
+ // However, it is recommended to always provide project_id to get fields applicable to the project.
5
+ const parameters = {
6
+ 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
+ export const CASE_FIELDS_PARAM_DESCRIPTION = `
9
+ Must use system_name values from get_case_fields.
10
+ Call get_case_fields with project_id first if field names are not already known.
11
+ 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
+ `;
3
14
  const FIELD_TYPE_NAMES = {
4
15
  1: "String",
5
16
  2: "Integer",
@@ -35,29 +46,57 @@ export function mapToFieldSchema(field) {
35
46
  if (field.type_id === 6 || field.type_id === 12) {
36
47
  schema.options = Array.from(parseDropdownOptions(field).values());
37
48
  }
49
+ const contexts = field.configs.map(c => c.context).filter(Boolean);
50
+ if (contexts.length > 0) {
51
+ const isGlobal = contexts.some(ctx => ctx.is_global);
52
+ if (isGlobal) {
53
+ schema.project_scope = { scope: "global" };
54
+ }
55
+ else {
56
+ const projectIds = [...new Set(contexts.flatMap(ctx => ctx.project_ids))];
57
+ schema.project_scope = { scope: "projects", project_ids: projectIds };
58
+ }
59
+ }
38
60
  return schema;
39
61
  }
62
+ function isFieldForProject(field, projectId) {
63
+ const contexts = field.configs.map(c => c.context).filter(Boolean);
64
+ if (contexts.length === 0)
65
+ return true;
66
+ return contexts.some(ctx => ctx.is_global || ctx.project_ids.includes(projectId));
67
+ }
40
68
  export const SYSTEM_FIELDS = [
41
- { system_name: "title", label: "Title", type: "String", is_required: true },
42
- { system_name: "section_id", label: "Section", type: "Integer", is_required: true },
43
- { system_name: "template_id", label: "Template", type: "Integer", is_required: false },
44
- { system_name: "type_id", label: "Type", type: "Integer", is_required: false },
45
- { system_name: "priority_id", label: "Priority", type: "Integer", is_required: false },
46
- { system_name: "estimate", label: "Estimate", type: "String", is_required: false },
47
- { system_name: "milestone_id", label: "Milestone", type: "Integer", is_required: false },
48
- { system_name: "refs", label: "References", type: "String", is_required: false },
49
- { system_name: "labels", label: "Labels", type: "List", is_required: false, comment: "Use get_labels tool to get available labels/tags." },
69
+ { system_name: "title", label: "Title", type: "String", is_required: true, project_scope: { scope: "global" } },
70
+ { system_name: "section_id", label: "Section", type: "Integer", is_required: true, project_scope: { scope: "global" } },
71
+ { system_name: "template_id", label: "Template", type: "Integer", is_required: false, project_scope: { scope: "global" } },
72
+ { system_name: "type_id", label: "Type", type: "Integer", is_required: false, project_scope: { scope: "global" } },
73
+ { system_name: "priority_id", label: "Priority", type: "Integer", is_required: false, project_scope: { scope: "global" } },
74
+ { system_name: "estimate", label: "Estimate", type: "String", is_required: false, project_scope: { scope: "global" } },
75
+ { system_name: "milestone_id", label: "Milestone", type: "Integer", is_required: false, project_scope: { scope: "global" } },
76
+ { system_name: "refs", label: "References", type: "String", is_required: false, project_scope: { scope: "global" } },
77
+ { system_name: "labels", label: "Labels", type: "List", is_required: false, project_scope: { scope: "global" }, comment: "Use get_labels tool to get available labels/tags." },
50
78
  ];
79
+ const description = `
80
+ Get the field schema for test cases for a specific project.
81
+ You should normally provide project_id to get fields applicable to your project.
82
+ If you truly need all fields across all projects, you may omit project_id, but this is rarely what you want.
83
+ Returns available fields with their types and options (for dropdown fields).
84
+ `;
51
85
  export const getCaseFieldsTool = {
52
86
  name: "get_case_fields",
53
- description: "Get the field schema for test cases. Returns all available fields with their types, descriptions, and options (for dropdown fields). Use this to understand what fields can be set when creating or updating test cases.",
87
+ description: description.trim(),
54
88
  parameters,
55
- handler: async (_args, client) => {
89
+ handler: async ({ project_id }, client) => {
56
90
  const caseFields = await client.getCaseFields();
57
- const customFieldSchemas = caseFields.filter(field => field.is_active).map(mapToFieldSchema);
58
- const response = {
59
- fields: [...SYSTEM_FIELDS, ...customFieldSchemas],
60
- };
61
- return response;
91
+ const activeFields = caseFields.filter(field => field.is_active);
92
+ const filtered = project_id !== undefined
93
+ ? activeFields.filter(field => isFieldForProject(field, project_id))
94
+ : activeFields;
95
+ const customFieldSchemas = filtered.map(mapToFieldSchema);
96
+ const allFields = [...SYSTEM_FIELDS, ...customFieldSchemas];
97
+ const fields = project_id !== undefined
98
+ ? allFields.map(({ project_scope: _, ...rest }) => rest)
99
+ : allFields;
100
+ return { fields };
62
101
  }
63
102
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import fs from "fs";
2
3
  import { validateCaseFields } from "../utils/validator.js";
3
4
  const parameters = {
4
5
  project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
@@ -10,12 +11,13 @@ const parameters = {
10
11
  filter: z.record(z.string(), z.string()).optional().describe("Optional API-side filters (more efficient for large datasets). Supported: priority_id, type_id, created_by, updated_by, milestone_id, refs, created_after, created_before, updated_after, updated_before. Use comma-separated values for IDs. Example: {\"priority_id\": \"1,2\", \"type_id\": \"3\"}"),
11
12
  where: z.record(z.string(), z.any()).optional().describe("Optional client-side filter for any field including custom fields (filters after fetching all cases). Supports exact value matching. Example: {\"custom_automation_status\": 1, \"priority_id\": 2}"),
12
13
  fields: z.array(z.string()).optional().describe("Additional fields to include in response beyond id, title, and suite_id. Use get_case_fields to see available fields. Example: [\"priority_id\", \"type_id\", \"custom_automation_status\"]"),
14
+ output_file: z.string().optional().describe("Absolute file path to save the JSON response to. Use this for large datasets to avoid blowing up context limits."),
13
15
  };
14
16
  export const getCasesTool = {
15
17
  name: "get_cases",
16
18
  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.",
17
19
  parameters,
18
- handler: async ({ project_id, section, filter, where, fields }, client) => {
20
+ handler: async ({ project_id, section, filter, where, fields, output_file }, client) => {
19
21
  const caseFields = await client.getCaseFields();
20
22
  if (fields) {
21
23
  validateCaseFields(fields, caseFields);
@@ -59,6 +61,14 @@ export const getCasesTool = {
59
61
  return result;
60
62
  }),
61
63
  };
64
+ if (output_file) {
65
+ await fs.promises.writeFile(output_file, JSON.stringify(response), "utf-8");
66
+ return {
67
+ success: true,
68
+ message: `Successfully exported ${response.cases.length} cases to ${output_file}`,
69
+ file: output_file
70
+ };
71
+ }
62
72
  return response;
63
73
  }
64
74
  };
@@ -1,16 +1,27 @@
1
1
  import { z } from "zod";
2
+ import fs from "fs";
2
3
  import { SectionSchema } from "../types/testrail.js";
3
4
  const parameters = {
4
5
  project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
6
+ output_file: z.string().optional().describe("Absolute file path to save the JSON response to. Use this for large datasets to avoid blowing up context limits."),
5
7
  };
6
8
  export const getSectionsTool = {
7
9
  name: "get_sections",
8
- description: "Get all sections for a project. Returns section IDs and names that can be used with create_case",
10
+ description: "Get all sections for a project. Returns section IDs and names that can be used with add_case",
9
11
  parameters,
10
- handler: async ({ project_id }, client) => {
12
+ handler: async ({ project_id, output_file }, client) => {
11
13
  const sections = await client.getSections(project_id);
12
- return {
14
+ const response = {
13
15
  sections: sections.map(s => SectionSchema.parse(s)),
14
16
  };
17
+ if (output_file) {
18
+ await fs.promises.writeFile(output_file, JSON.stringify(response), "utf-8");
19
+ return {
20
+ success: true,
21
+ message: `Successfully exported ${response.sections.length} sections to ${output_file}`,
22
+ file: output_file
23
+ };
24
+ }
25
+ return response;
15
26
  }
16
27
  };
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { SharedStepSchema } from "../../types/testrail.js";
3
+ const parameters = {
4
+ project_id: z.number().describe("The ID of the project where the shared step will be created"),
5
+ title: z.string().describe("The title of the shared test step"),
6
+ custom_steps_separated: z.array(z.object({
7
+ content: z.string().describe("The step instruction"),
8
+ expected: z.string().optional().describe("The expected result of the step"),
9
+ })).optional().describe("The steps of the shared test step"),
10
+ };
11
+ export const addSharedStepTool = {
12
+ name: "add_shared_step",
13
+ description: "Creates a new set of shared test steps.",
14
+ parameters,
15
+ handler: async (args, client) => {
16
+ const { project_id, ...data } = args;
17
+ const sharedStep = await client.addSharedStep(project_id, data);
18
+ return { shared_step: SharedStepSchema.parse(sharedStep) };
19
+ }
20
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ const parameters = {
3
+ shared_step_id: z.number().describe("The ID of the shared test step to delete"),
4
+ };
5
+ const description = `
6
+ Deletes an existing shared step entity.
7
+ By default, this removes the shared step entity but keeps the steps in the test cases that used them.
8
+ `;
9
+ export const deleteSharedStepTool = {
10
+ name: "delete_shared_step",
11
+ description: description.trim(),
12
+ parameters,
13
+ handler: async (args, client) => {
14
+ const { shared_step_id } = args;
15
+ await client.deleteSharedStep(shared_step_id);
16
+ return { message: `Shared step ${shared_step_id} deleted successfully.` };
17
+ }
18
+ };
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { SharedStepSchema } from "../../types/testrail.js";
3
+ const parameters = {
4
+ shared_step_id: z.number().describe("The ID of the shared test step"),
5
+ };
6
+ export const getSharedStepTool = {
7
+ name: "get_shared_step",
8
+ description: "Returns the details of a specific shared test step.",
9
+ parameters,
10
+ handler: async (args, client) => {
11
+ const { shared_step_id } = args;
12
+ const sharedStep = await client.getSharedStep(shared_step_id);
13
+ return { shared_step: SharedStepSchema.parse(sharedStep) };
14
+ }
15
+ };
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { SharedStepHistorySchema } from "../../types/testrail.js";
3
+ const parameters = {
4
+ shared_step_id: z.number().describe("The ID of the shared test step"),
5
+ };
6
+ export const getSharedStepHistoryTool = {
7
+ name: "get_shared_step_history",
8
+ description: "Returns the change history of a set of shared test steps.",
9
+ parameters,
10
+ handler: async (args, client) => {
11
+ const { shared_step_id } = args;
12
+ const history = await client.getSharedStepHistory(shared_step_id);
13
+ return { history: history.map(h => SharedStepHistorySchema.parse(h)) };
14
+ }
15
+ };
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { SharedStepSchema } from "../../types/testrail.js";
3
+ const parameters = {
4
+ project_id: z.number().describe("The ID of the project"),
5
+ refs: z.string().optional().describe("Filter by a single Reference ID"),
6
+ };
7
+ export const getSharedStepsTool = {
8
+ name: "get_shared_steps",
9
+ description: "Returns a list of shared test steps for a specified project.",
10
+ parameters,
11
+ handler: async (args, client) => {
12
+ const { project_id, refs } = args;
13
+ const sharedSteps = await client.getSharedSteps(project_id, { refs });
14
+ return { shared_steps: sharedSteps.map(s => SharedStepSchema.parse(s)) };
15
+ }
16
+ };
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { SharedStepSchema } from "../../types/testrail.js";
3
+ const parameters = {
4
+ shared_step_id: z.number().describe("The ID of the shared test step to update"),
5
+ title: z.string().optional().describe("The updated title of the shared test step"),
6
+ custom_steps_separated: z.array(z.object({
7
+ content: z.string().describe("The step instruction"),
8
+ expected: z.string().optional().describe("The expected result of the step"),
9
+ })).optional().describe("The updated steps (replaces all existing steps)"),
10
+ };
11
+ const description = `
12
+ Updates an existing set of shared test steps.
13
+ Updates are propagated automatically to all test cases using these steps.
14
+ `;
15
+ export const updateSharedStepTool = {
16
+ name: "update_shared_step",
17
+ description: description.trim(),
18
+ parameters,
19
+ handler: async (args, client) => {
20
+ const { shared_step_id, ...data } = args;
21
+ const sharedStep = await client.updateSharedStep(shared_step_id, data);
22
+ return { shared_step: SharedStepSchema.parse(sharedStep) };
23
+ }
24
+ };
@@ -1,12 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { validateCaseFields } from "../utils/validator.js";
3
+ import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
3
4
  const parameters = {
4
5
  case_id: z.string().describe("The ID of the test case to update (e.g. '123' or 'C123')"),
5
- 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}"),
6
+ fields: z.record(z.string(), z.any()).describe(CASE_FIELDS_PARAM_DESCRIPTION),
6
7
  };
8
+ const description = `
9
+ Update a test case in TestRail.
10
+ The update operation requires knowing valid field names that are returned by get_case_fields tool.
11
+ Supports partial updates — only specify the fields you want to change.
12
+ `;
7
13
  export const updateCaseTool = {
8
14
  name: "update_case",
9
- description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change",
15
+ description: description.trim(),
10
16
  parameters,
11
17
  handler: async ({ case_id, fields }, client) => {
12
18
  validateCaseFields(fields, await client.getCaseFields());
@@ -1,12 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { validateCaseFields } from "../utils/validator.js";
3
+ import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
3
4
  const parameters = {
4
5
  case_ids: z.array(z.number()).min(1).describe("Array of case IDs to update (e.g. [123, 456, 789])"),
5
- 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}"),
6
+ fields: z.record(z.string(), z.any()).describe(CASE_FIELDS_PARAM_DESCRIPTION),
6
7
  };
8
+ const description = `
9
+ Bulk update multiple test cases with the same field values.
10
+ The update operation requires knowing valid field names that are returned by get_case_fields tool.
11
+ More efficient than calling update_case multiple times.
12
+ `;
7
13
  export const updateCasesTool = {
8
14
  name: "update_cases",
9
- description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times",
15
+ description: description.trim(),
10
16
  parameters,
11
17
  handler: async ({ case_ids, fields }, client) => {
12
18
  validateCaseFields(fields, await client.getCaseFields());
@@ -1,5 +1,9 @@
1
1
  import { z } from "zod";
2
2
  export const CaseFieldConfigSchema = z.object({
3
+ context: z.object({
4
+ is_global: z.boolean(),
5
+ project_ids: z.array(z.number()),
6
+ }).optional(),
3
7
  options: z.object({
4
8
  default_value: z.string().optional(),
5
9
  is_required: z.boolean().optional(),
@@ -102,3 +106,20 @@ export const ResultSchema = z.object({
102
106
  export const AttachmentSchema = z.object({
103
107
  attachment_id: z.number(),
104
108
  });
109
+ export const SharedStepContentSchema = z.object({
110
+ content: z.string(),
111
+ expected: z.string().nullable().optional(),
112
+ });
113
+ export const SharedStepSchema = z.object({
114
+ id: z.number(),
115
+ title: z.string(),
116
+ project_id: z.number().optional(),
117
+ custom_steps_separated: z.array(SharedStepContentSchema).optional(),
118
+ case_ids: z.array(z.number()).optional(),
119
+ });
120
+ export const SharedStepHistorySchema = z.object({
121
+ id: z.number(),
122
+ title: z.string(),
123
+ timestamp: z.number().optional(),
124
+ custom_steps_separated: z.array(SharedStepContentSchema).optional(),
125
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uarlouski/testrail-mcp-server",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "mcpName": "io.github.uarlouski/testrail-mcp-server",
5
5
  "description": "Model Context Protocol (MCP) server for TestRail integration, enabling LLMs to fetch test cases and metadata.",
6
6
  "main": "dist/index.js",
@@ -11,13 +11,6 @@
11
11
  "dist"
12
12
  ],
13
13
  "type": "module",
14
- "scripts": {
15
- "build": "rimraf dist && tsc",
16
- "start": "node dist/index.js",
17
- "dev": "ts-node src/index.ts",
18
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
19
- "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
20
- },
21
14
  "keywords": [
22
15
  "mcp",
23
16
  "model-context-protocol",
@@ -40,10 +33,10 @@
40
33
  "homepage": "https://github.com/uarlouski/testrail-mcp-server#readme",
41
34
  "license": "Apache-2.0",
42
35
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.27.1",
36
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
37
  "archiver": "^7.0.1",
45
- "dotenv": "^17.3.1",
46
- "zod": "^4.3.6"
38
+ "dotenv": "^17.4.2",
39
+ "zod": "^4.4.3"
47
40
  },
48
41
  "devDependencies": {
49
42
  "@types/archiver": "^7.0.0",
@@ -53,6 +46,21 @@
53
46
  "rimraf": "^6.1.3",
54
47
  "ts-jest": "^29.4.6",
55
48
  "ts-node": "^10.9.2",
56
- "typescript": "^5.9.3"
49
+ "typescript": "^5.9.3",
50
+ "@jest/globals": "^29.7.0"
51
+ },
52
+ "devEngines": {
53
+ "packageManager": {
54
+ "name": "pnpm",
55
+ "version": "11.1.2",
56
+ "onFail": "error"
57
+ }
58
+ },
59
+ "scripts": {
60
+ "build": "rimraf dist && tsc",
61
+ "start": "node dist/index.js",
62
+ "dev": "ts-node src/index.ts",
63
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
64
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
57
65
  }
58
66
  }