@uarlouski/testrail-mcp-server 1.10.0 → 1.11.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 +5 -2
- package/dist/client/testrail.js +8 -0
- package/dist/index.js +3 -1
- package/dist/tools/get_case_fields.js +2 -1
- package/dist/tools/get_users.js +45 -0
- package/dist/types/testrail.js +5 -0
- package/dist/utils/sanitizer.js +11 -0
- package/dist/utils/validator.js +2 -1
- package/package.json +1 -1
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."*
|
|
@@ -153,14 +154,16 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
|
|
|
153
154
|
|
|
154
155
|
The TestRail MCP Server provides heavily typed, descriptive tools designed specifically for LLM interaction:
|
|
155
156
|
|
|
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!
|
|
157
|
+
> **💡 Pro Tip: Handling Large Data Sets & Permissions**
|
|
158
|
+
> - **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!
|
|
159
|
+
> - **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
160
|
|
|
159
161
|
### 🔭 Discovery & Navigation
|
|
160
162
|
| Tool | Functionality |
|
|
161
163
|
|------|-------------|
|
|
162
164
|
| `get_projects` | List all available active and completed projects in your instance. |
|
|
163
165
|
| `get_sections` | Navigate the precise folder/section hierarchy of any test suite. |
|
|
166
|
+
| `get_users` | Retrieve active users. Handles per-project filtering and automatic fallback polling for non-admin accounts. |
|
|
164
167
|
|
|
165
168
|
### 📋 Test Case Management
|
|
166
169
|
| Tool | Functionality |
|
package/dist/client/testrail.js
CHANGED
|
@@ -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`;
|
|
@@ -161,6 +162,13 @@ export class TestRailClient {
|
|
|
161
162
|
async deleteSharedStep(sharedStepId) {
|
|
162
163
|
return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}), false);
|
|
163
164
|
}
|
|
165
|
+
async getUsers(projectId) {
|
|
166
|
+
const url = projectId
|
|
167
|
+
? `${API_BASE_V2}/get_users/${projectId}`
|
|
168
|
+
: `${API_BASE_V2}/get_users`;
|
|
169
|
+
const allUsers = await this.paginateAll(url, 'users');
|
|
170
|
+
return allUsers.filter(isActive);
|
|
171
|
+
}
|
|
164
172
|
async paginateAll(url, dataKey) {
|
|
165
173
|
const allItems = [];
|
|
166
174
|
let nextUrl = url;
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ 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 { getUsersTool } from "./tools/get_users.js";
|
|
23
24
|
import { getSharedStepsTool } from "./tools/shared_steps/get_shared_steps.js";
|
|
24
25
|
import { getSharedStepTool } from "./tools/shared_steps/get_shared_step.js";
|
|
25
26
|
import { getSharedStepHistoryTool } from "./tools/shared_steps/get_shared_step_history.js";
|
|
@@ -42,7 +43,7 @@ if (!parseResult.success) {
|
|
|
42
43
|
const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS } = parseResult.data;
|
|
43
44
|
const server = new McpServer({
|
|
44
45
|
name: "TestRail MCP Server",
|
|
45
|
-
version: "1.
|
|
46
|
+
version: "1.11.0",
|
|
46
47
|
});
|
|
47
48
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
48
49
|
const tools = [
|
|
@@ -63,6 +64,7 @@ const tools = [
|
|
|
63
64
|
addResultsForCasesTool,
|
|
64
65
|
addAttachmentToRunTool,
|
|
65
66
|
getLabelsTool,
|
|
67
|
+
getUsersTool,
|
|
66
68
|
];
|
|
67
69
|
if (TESTRAIL_ENABLE_SHARED_STEPS) {
|
|
68
70
|
tools.push(getSharedStepsTool);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
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 = {
|
|
@@ -88,7 +89,7 @@ export const getCaseFieldsTool = {
|
|
|
88
89
|
parameters,
|
|
89
90
|
handler: async ({ project_id }, client) => {
|
|
90
91
|
const caseFields = await client.getCaseFields();
|
|
91
|
-
const activeFields = caseFields.filter(
|
|
92
|
+
const activeFields = caseFields.filter(isActive);
|
|
92
93
|
const filtered = project_id !== undefined
|
|
93
94
|
? activeFields.filter(field => isFieldForProject(field, project_id))
|
|
94
95
|
: activeFields;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { UserSchema } from "../types/testrail.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
|
+
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.",
|
|
25
|
+
parameters,
|
|
26
|
+
handler: async ({ project_id, fallback_all_projects }, client) => {
|
|
27
|
+
let users;
|
|
28
|
+
try {
|
|
29
|
+
users = await client.getUsers(project_id);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const isPermissionError = err.message?.includes("Access Denied");
|
|
33
|
+
if (!project_id && fallback_all_projects !== false && isPermissionError) {
|
|
34
|
+
console.error("[get_users] Global fetch failed (Access Denied). Falling back to multi-project polling...");
|
|
35
|
+
users = await getUsersFallbackAllProjects(client);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
users: users.map(user => UserSchema.parse(user))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
package/dist/types/testrail.js
CHANGED
|
@@ -123,3 +123,8 @@ export const SharedStepHistorySchema = z.object({
|
|
|
123
123
|
timestamp: z.number().optional(),
|
|
124
124
|
custom_steps_separated: z.array(SharedStepContentSchema).optional(),
|
|
125
125
|
});
|
|
126
|
+
export const UserSchema = z.object({
|
|
127
|
+
id: z.number(),
|
|
128
|
+
name: z.string(),
|
|
129
|
+
email: z.string(),
|
|
130
|
+
});
|
package/dist/utils/sanitizer.js
CHANGED
|
@@ -44,3 +44,14 @@ export function removeNullish(value) {
|
|
|
44
44
|
}
|
|
45
45
|
return value;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Checks if an entity is active by checking its active flag.
|
|
49
|
+
* Supports passing either the direct primitive value or the parent object containing `.is_active`.
|
|
50
|
+
*/
|
|
51
|
+
export function isActive(value) {
|
|
52
|
+
if (value == null) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const activeFlag = (typeof value === 'object') ? value.is_active : value;
|
|
56
|
+
return activeFlag === 1 || activeFlag === true || activeFlag === '1' || activeFlag === 'true';
|
|
57
|
+
}
|
package/dist/utils/validator.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SYSTEM_FIELDS, mapToFieldSchema } from "../tools/get_case_fields.js";
|
|
2
|
+
import { isActive } from "./sanitizer.js";
|
|
2
3
|
/**
|
|
3
4
|
* Validates that the provided fields exist in the TestRail case schema.
|
|
4
5
|
* Throws an Error if any field is invalid.
|
|
@@ -11,7 +12,7 @@ export function validateCaseFields(fields, caseFields) {
|
|
|
11
12
|
if (fieldKeys.length === 0) {
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
|
-
const customFieldSchemas = caseFields.filter(
|
|
15
|
+
const customFieldSchemas = caseFields.filter(isActive).map(mapToFieldSchema);
|
|
15
16
|
const validFieldNames = new Set([...SYSTEM_FIELDS, ...customFieldSchemas].map(f => f.system_name));
|
|
16
17
|
validFieldNames.add('id');
|
|
17
18
|
validFieldNames.add('suite_id');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uarlouski/testrail-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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",
|