@uarlouski/testrail-mcp-server 1.8.0 โ†’ 1.9.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
 
@@ -15,6 +17,16 @@
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>
16
18
  </p>
17
19
 
20
+ <p align="center">
21
+ <strong>Compatible with:</strong>
22
+ <img src="https://img.shields.io/badge/Claude%20Desktop-D97757?logo=anthropic&logoColor=white" alt="Claude Desktop">
23
+ <img src="https://img.shields.io/badge/Cursor-000000?logo=cursor&logoColor=white" alt="Cursor">
24
+ <img src="https://img.shields.io/badge/Windsurf-19A1BC?logo=codeium&logoColor=white" alt="Windsurf">
25
+ <img src="https://img.shields.io/badge/VS%20Code-007ACC?logo=visualstudiocode&logoColor=white" alt="VS Code">
26
+ </p>
27
+
28
+ <!-- TODO: insert demo.gif โ€” short screencast of asking "create a test case" in Claude or Cursor and seeing the case appear in TestRail -->
29
+
18
30
  ---
19
31
 
20
32
  ## ๐ŸŒŸ Why Choose TestRail MCP Server?
@@ -40,10 +52,9 @@ Navigate to **My Settings โ†’ API Keys** in your TestRail platform and generate
40
52
 
41
53
  ### 2. Configure Your MCP Client
42
54
 
43
- Add the server to your chosen MCP client configuration. Below are examples for popular AI IDEs and assistants:
55
+ 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
56
 
45
- <details>
46
- <summary><strong>๐Ÿค– Claude Desktop</strong></summary>
57
+ #### ๐Ÿค– Claude Desktop
47
58
 
48
59
  Add this to your `claude_desktop_config.json`:
49
60
 
@@ -56,13 +67,13 @@ Add this to your `claude_desktop_config.json`:
56
67
  "env": {
57
68
  "TESTRAIL_INSTANCE_URL": "https://your-instance.testrail.io",
58
69
  "TESTRAIL_USERNAME": "your@email.com",
59
- "TESTRAIL_API_KEY": "your-api-key"
70
+ "TESTRAIL_API_KEY": "your-api-key",
71
+ "TESTRAIL_ENABLE_SHARED_STEPS": "true"
60
72
  }
61
73
  }
62
74
  }
63
75
  }
64
76
  ```
65
- </details>
66
77
 
67
78
  <details>
68
79
  <summary><strong>โŒจ๏ธ Cursor</strong></summary>
@@ -133,6 +144,7 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
133
144
  | `TESTRAIL_INSTANCE_URL` | Your TestRail instance URL (e.g., `https://example.testrail.io`) | โœ… |
134
145
  | `TESTRAIL_USERNAME` | Your TestRail user email address | โœ… |
135
146
  | `TESTRAIL_API_KEY` | Your TestRail API key ([Guide](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)) | โœ… |
147
+ | `TESTRAIL_ENABLE_SHARED_STEPS` | Set to `true` to enable Shared Steps management tools | |
136
148
 
137
149
  ---
138
150
 
@@ -151,7 +163,7 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
151
163
  |------|-------------|
152
164
  | `get_cases` | Query test cases with advanced filtering (priority, template, type, etc.) and pagination. |
153
165
  | `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. |
166
+ | `add_case` | Seamlessly create a new test case equipped with robust custom field validation. |
155
167
  | `update_case` | Modify an existing test case's steps, metadata, or titles. |
156
168
  | `update_cases` | Execute bulk-updates on multiple test cases simultaneously to save time. |
157
169
 
@@ -164,12 +176,22 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
164
176
  | `add_results_for_cases` | Submits results to a run mapping directly to `case_id`s, streamlining automation workflows. |
165
177
  | `add_attachment_to_run` | Attach logs, files, or zipped artifacts directly to an ongoing test run. |
166
178
 
179
+ ### ๐Ÿ”— Shared Steps (Optional)
180
+ | Tool | Functionality |
181
+ |------|-------------|
182
+ | `get_shared_steps` | List shared test steps for a project with optional reference filtering. |
183
+ | `get_shared_step` | Retrieve precise details of a specific shared test step set. |
184
+ | `get_shared_step_history` | View the complete audit trail and version history of a shared step set. |
185
+ | `add_shared_step` | Create a new reusable set of shared test steps. |
186
+ | `update_shared_step` | Modify an existing shared step set (changes propagate to all linked test cases). |
187
+ | `delete_shared_step` | Securely delete a shared step entity. |
188
+
167
189
  ### ๐Ÿง  System Metadata
168
190
  | Tool | Functionality |
169
191
  |------|-------------|
170
192
  | `get_statuses` | Systematically list all configured test statuses (Passed, Failed, Blocked, Retest, etc.). |
171
193
  | `get_priorities` | Retrieve priority levels configured within your instance structure. |
172
- | `get_case_fields` | Discover custom field definitions, formats, and UI dropdown options. |
194
+ | `get_case_fields` | Discover custom field definitions, formats, and UI dropdown options. Supports filtering by `project_id`. |
173
195
  | `get_templates` | Identify available case templates to mandate correct AI structuring. |
174
196
 
175
197
  ---
@@ -185,12 +207,3 @@ This project is securely licensed under the [Apache License 2.0](LICENSE).
185
207
  <p align="center">
186
208
  <b>TestRail MCP Server</b> ยท Engineered with the <a href="https://modelcontextprotocol.io">Model Context Protocol</a>
187
209
  </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.9.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
  };
@@ -5,7 +5,7 @@ const parameters = {
5
5
  };
6
6
  export const getSectionsTool = {
7
7
  name: "get_sections",
8
- description: "Get all sections for a project. Returns section IDs and names that can be used with create_case",
8
+ description: "Get all sections for a project. Returns section IDs and names that can be used with add_case",
9
9
  parameters,
10
10
  handler: async ({ project_id }, client) => {
11
11
  const sections = await client.getSections(project_id);
@@ -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.9.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",