@uarlouski/testrail-mcp-server 2.0.0 → 2.2.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/dist/client/testrail.js +15 -5
- package/dist/index.js +1 -1
- package/dist/tools/cases/get_case_fields.js +34 -28
- package/dist/tools/cases/get_cases.js +8 -2
- package/dist/tools/sections/get_sections.js +5 -2
- package/dist/tools/sections/mutate_section.js +2 -0
- package/dist/tools/sections/types.js +1 -0
- package/dist/utils/validator.js +15 -0
- package/package.json +1 -1
package/dist/client/testrail.js
CHANGED
|
@@ -25,7 +25,9 @@ export class TestRailClient {
|
|
|
25
25
|
return this.get(`${API_BASE_V2}/get_case/${caseId}`);
|
|
26
26
|
}
|
|
27
27
|
async getCasesRecursively(projectId, sectionId, filter, excludedSectionNames) {
|
|
28
|
-
|
|
28
|
+
// Extract suite_id from filter if present, for getSections call
|
|
29
|
+
const suiteId = filter?.suite_id ? parseInt(filter.suite_id) : undefined;
|
|
30
|
+
const sections = await this.getSections(projectId, suiteId);
|
|
29
31
|
const sectionIds = this.getSubSectionIds(sections, sectionId, excludedSectionNames);
|
|
30
32
|
sectionIds.push(sectionId);
|
|
31
33
|
const promises = sectionIds.map(id => this.getCases(projectId, id, filter));
|
|
@@ -105,8 +107,11 @@ export class TestRailClient {
|
|
|
105
107
|
async updateRun(runId, fields) {
|
|
106
108
|
return this.post(`${API_BASE_V2}/update_run/${runId}`, fields);
|
|
107
109
|
}
|
|
108
|
-
async getSections(projectId) {
|
|
109
|
-
|
|
110
|
+
async getSections(projectId, suiteId) {
|
|
111
|
+
let url = `${API_BASE_V2}/get_sections/${projectId}`;
|
|
112
|
+
if (suiteId) {
|
|
113
|
+
url += `&suite_id=${suiteId}`;
|
|
114
|
+
}
|
|
110
115
|
return this.paginateAll(url, 'sections');
|
|
111
116
|
}
|
|
112
117
|
async addSection(projectId, data) {
|
|
@@ -152,8 +157,7 @@ export class TestRailClient {
|
|
|
152
157
|
}
|
|
153
158
|
async getProjects() {
|
|
154
159
|
if (!this.projectsPromise) {
|
|
155
|
-
this.projectsPromise = this.
|
|
156
|
-
.then(response => response.projects);
|
|
160
|
+
this.projectsPromise = this.paginateAll(`${API_BASE_V2}/get_projects`, 'projects');
|
|
157
161
|
}
|
|
158
162
|
return this.projectsPromise;
|
|
159
163
|
}
|
|
@@ -221,6 +225,12 @@ export class TestRailClient {
|
|
|
221
225
|
let nextUrl = url;
|
|
222
226
|
while (nextUrl) {
|
|
223
227
|
const response = await this.get(nextUrl);
|
|
228
|
+
// Handle older TestRail versions (pre-7.x) that return flat arrays
|
|
229
|
+
// instead of paginated objects like { "suites": [...], "_links": {...} }
|
|
230
|
+
if (Array.isArray(response)) {
|
|
231
|
+
allItems.push(...response);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
224
234
|
if (response[dataKey] && Array.isArray(response[dataKey])) {
|
|
225
235
|
allItems.push(...response[dataKey]);
|
|
226
236
|
}
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ if (!parseResult.success) {
|
|
|
23
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;
|
|
24
24
|
const server = new McpServer({
|
|
25
25
|
name: "TestRail MCP Server",
|
|
26
|
-
version: "2.
|
|
26
|
+
version: "2.2.0",
|
|
27
27
|
});
|
|
28
28
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
29
29
|
const tools = getToolsToRegister({
|
|
@@ -13,35 +13,41 @@ Example: {"priority_id": 2, "template_id": 1, "labels": [1, 2]}
|
|
|
13
13
|
Call get_case_fields with project_id first if field names are not already known.
|
|
14
14
|
Using an unknown field name (e.g. 'label_ids') will result in an error.
|
|
15
15
|
`;
|
|
16
|
-
const
|
|
17
|
-
1: "String",
|
|
18
|
-
2: "Integer",
|
|
19
|
-
3: "Text",
|
|
20
|
-
4: "URL",
|
|
21
|
-
5: "Checkbox",
|
|
22
|
-
6: "Dropdown",
|
|
23
|
-
7: "User",
|
|
24
|
-
8: "Date",
|
|
25
|
-
9: "Milestone",
|
|
26
|
-
10: "Steps",
|
|
27
|
-
11: "Step Results",
|
|
28
|
-
12: "Multi-select",
|
|
29
|
-
13: "Scenarios",
|
|
30
|
-
14: "List",
|
|
31
|
-
|
|
32
|
-
function
|
|
33
|
-
|
|
16
|
+
export const FIELD_TYPES = [
|
|
17
|
+
{ typeId: 1, name: "String" },
|
|
18
|
+
{ typeId: 2, name: "Integer" },
|
|
19
|
+
{ typeId: 3, name: "Text" },
|
|
20
|
+
{ typeId: 4, name: "URL" },
|
|
21
|
+
{ typeId: 5, name: "Checkbox", description: "Boolean value, either true or false." },
|
|
22
|
+
{ typeId: 6, name: "Dropdown" },
|
|
23
|
+
{ typeId: 7, name: "User" },
|
|
24
|
+
{ typeId: 8, name: "Date" },
|
|
25
|
+
{ typeId: 9, name: "Milestone" },
|
|
26
|
+
{ typeId: 10, name: "Steps" },
|
|
27
|
+
{ typeId: 11, name: "Step Results" },
|
|
28
|
+
{ typeId: 12, name: "Multi-select" },
|
|
29
|
+
{ typeId: 13, name: "Scenarios" },
|
|
30
|
+
{ typeId: 14, name: "List" },
|
|
31
|
+
];
|
|
32
|
+
function getFieldType(typeId) {
|
|
33
|
+
const found = FIELD_TYPES.find(t => t.typeId === typeId);
|
|
34
|
+
return found ?? { typeId, name: `Unknown (${typeId})` };
|
|
34
35
|
}
|
|
35
36
|
function isFieldRequired(field) {
|
|
36
37
|
return field.configs.some(config => config.options?.is_required === true);
|
|
37
38
|
}
|
|
38
39
|
export function mapToFieldSchema(field) {
|
|
40
|
+
const fieldType = getFieldType(field.type_id);
|
|
41
|
+
const descriptions = [field.description, fieldType.description].filter(Boolean);
|
|
39
42
|
const schema = {
|
|
40
43
|
system_name: field.system_name,
|
|
41
44
|
label: field.label,
|
|
42
|
-
type:
|
|
45
|
+
type: fieldType.name,
|
|
43
46
|
is_required: isFieldRequired(field),
|
|
44
47
|
};
|
|
48
|
+
if (descriptions.length > 0) {
|
|
49
|
+
schema.description = descriptions.join(" ");
|
|
50
|
+
}
|
|
45
51
|
if (!field.include_all && field.template_ids.length > 0) {
|
|
46
52
|
schema.template_ids = field.template_ids;
|
|
47
53
|
}
|
|
@@ -68,15 +74,15 @@ function isFieldForProject(field, projectId) {
|
|
|
68
74
|
return contexts.some(ctx => ctx.is_global || ctx.project_ids.includes(projectId));
|
|
69
75
|
}
|
|
70
76
|
export const SYSTEM_FIELDS = [
|
|
71
|
-
{ system_name: "title", label: "Title", type:
|
|
72
|
-
{ system_name: "section_id", label: "Section", type:
|
|
73
|
-
{ system_name: "template_id", label: "Template", type:
|
|
74
|
-
{ system_name: "type_id", label: "Type", type:
|
|
75
|
-
{ system_name: "priority_id", label: "Priority", type:
|
|
76
|
-
{ system_name: "estimate", label: "Estimate", type:
|
|
77
|
-
{ system_name: "milestone_id", label: "Milestone", type:
|
|
78
|
-
{ system_name: "refs", label: "References", type:
|
|
79
|
-
{ system_name: "labels", label: "Labels", type:
|
|
77
|
+
{ system_name: "title", label: "Title", type: getFieldType(1).name, is_required: true, project_scope: { scope: "global" } },
|
|
78
|
+
{ system_name: "section_id", label: "Section", type: getFieldType(2).name, is_required: true, project_scope: { scope: "global" } },
|
|
79
|
+
{ system_name: "template_id", label: "Template", type: getFieldType(2).name, is_required: false, project_scope: { scope: "global" } },
|
|
80
|
+
{ system_name: "type_id", label: "Type", type: getFieldType(2).name, is_required: false, project_scope: { scope: "global" } },
|
|
81
|
+
{ system_name: "priority_id", label: "Priority", type: getFieldType(2).name, is_required: false, project_scope: { scope: "global" } },
|
|
82
|
+
{ system_name: "estimate", label: "Estimate", type: getFieldType(1).name, is_required: false, project_scope: { scope: "global" } },
|
|
83
|
+
{ system_name: "milestone_id", label: "Milestone", type: getFieldType(2).name, is_required: false, project_scope: { scope: "global" } },
|
|
84
|
+
{ system_name: "refs", label: "References", type: getFieldType(1).name, is_required: false, project_scope: { scope: "global" } },
|
|
85
|
+
{ system_name: "labels", label: "Labels", type: getFieldType(14).name, is_required: false, project_scope: { scope: "global" }, description: "Use get_labels tool to get available labels/tags." },
|
|
80
86
|
];
|
|
81
87
|
const description = `
|
|
82
88
|
Get the field schema for test cases for a specific project.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import { validateCaseFields } from "../../utils/validator.js";
|
|
3
|
+
import { validateCaseFields, validateSuiteId } 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
|
+
suite_id: z.number().optional().describe("The ID of the test suite (required for multi-suite projects, i.e. suite_mode=3). Use get_suites to find available suites"),
|
|
6
7
|
section: z.object({
|
|
7
8
|
id: z.number(),
|
|
8
9
|
recursive: z.boolean().optional().default(false).describe("If true, fetches cases from the section and all its child sections"),
|
|
@@ -18,7 +19,8 @@ export const getCasesTool = {
|
|
|
18
19
|
mode: "read",
|
|
19
20
|
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.",
|
|
20
21
|
parameters,
|
|
21
|
-
handler: async ({ project_id, section, filter, where, fields, output_file }, client) => {
|
|
22
|
+
handler: async ({ project_id, suite_id, section, filter, where, fields, output_file }, client) => {
|
|
23
|
+
await validateSuiteId(client, project_id, suite_id);
|
|
22
24
|
const caseFields = await client.getCaseFields();
|
|
23
25
|
if (fields) {
|
|
24
26
|
validateCaseFields(fields, caseFields);
|
|
@@ -26,6 +28,10 @@ export const getCasesTool = {
|
|
|
26
28
|
if (where) {
|
|
27
29
|
validateCaseFields(Object.keys(where), caseFields);
|
|
28
30
|
}
|
|
31
|
+
// Inject suite_id into filter for multi-suite projects
|
|
32
|
+
if (suite_id) {
|
|
33
|
+
filter = { ...filter, suite_id: suite_id.toString() };
|
|
34
|
+
}
|
|
29
35
|
let cases;
|
|
30
36
|
if (section?.recursive) {
|
|
31
37
|
cases = await client.getCasesRecursively(project_id, section.id, filter, section.excludes);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { SectionSchema } from "./types.js";
|
|
4
|
+
import { validateSuiteId } from "../../utils/validator.js";
|
|
4
5
|
const parameters = {
|
|
5
6
|
project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
|
|
7
|
+
suite_id: z.number().optional().describe("The ID of the test suite (required for multi-suite projects, i.e. suite_mode=3). Use get_suites to find available suites"),
|
|
6
8
|
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."),
|
|
7
9
|
};
|
|
8
10
|
export const getSectionsTool = {
|
|
@@ -10,8 +12,9 @@ export const getSectionsTool = {
|
|
|
10
12
|
mode: "read",
|
|
11
13
|
description: "Get all sections for a project. Returns section IDs and names that can be used with add_case",
|
|
12
14
|
parameters,
|
|
13
|
-
handler: async ({ project_id, output_file }, client) => {
|
|
14
|
-
|
|
15
|
+
handler: async ({ project_id, suite_id, output_file }, client) => {
|
|
16
|
+
await validateSuiteId(client, project_id, suite_id);
|
|
17
|
+
const sections = await client.getSections(project_id, suite_id);
|
|
15
18
|
const response = {
|
|
16
19
|
sections: sections.map(s => SectionSchema.parse(s)),
|
|
17
20
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SectionSchema, CreateSectionSchema, UpdateSectionSchema } from "./types.js";
|
|
3
3
|
import { handleMutate } from "../../utils/mutate_handler.js";
|
|
4
|
+
import { validateSuiteId } from "../../utils/validator.js";
|
|
4
5
|
const parameters = {
|
|
5
6
|
payload: z.discriminatedUnion("action", [
|
|
6
7
|
CreateSectionSchema,
|
|
@@ -15,6 +16,7 @@ export const mutateSectionTool = {
|
|
|
15
16
|
handler: async (args, client) => {
|
|
16
17
|
const { payload } = args;
|
|
17
18
|
return handleMutate(payload, async (createPayload) => {
|
|
19
|
+
await validateSuiteId(client, createPayload.project_id, createPayload.suite_id);
|
|
18
20
|
const { project_id, action, ...data } = createPayload;
|
|
19
21
|
return client.addSection(project_id, data);
|
|
20
22
|
}, async (updatePayload) => {
|
|
@@ -13,6 +13,7 @@ export const BaseSectionFields = z.object({
|
|
|
13
13
|
export const CreateSectionSchema = BaseSectionFields.extend({
|
|
14
14
|
action: z.literal("create").describe("The operation to perform: create a new section"),
|
|
15
15
|
project_id: z.number().describe("The ID of the project the section should be added to"),
|
|
16
|
+
suite_id: z.number().optional().describe("The ID of the test suite (required for multi-suite projects, i.e. suite_mode=2 or 3). Use get_suites to find available suites"),
|
|
16
17
|
parent_id: z.number().optional().describe("The ID of the parent section (to build section hierarchies)"),
|
|
17
18
|
});
|
|
18
19
|
export const UpdateSectionSchema = BaseSectionFields.partial().extend({
|
package/dist/utils/validator.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { SYSTEM_FIELDS, mapToFieldSchema } from "../tools/cases/get_case_fields.js";
|
|
2
2
|
import { isActive } from "./sanitizer.js";
|
|
3
|
+
const SUITE_MODE_SINGLE_WITH_BASELINES = 2;
|
|
4
|
+
const SUITE_MODE_MULTI = 3;
|
|
5
|
+
/**
|
|
6
|
+
* Validates that suite_id is provided when the project uses multiple test suites or baselines (suite_mode=2 or 3).
|
|
7
|
+
* Throws a descriptive Error if suite_id is required but missing.
|
|
8
|
+
*/
|
|
9
|
+
export async function validateSuiteId(client, projectId, suiteId) {
|
|
10
|
+
if (suiteId)
|
|
11
|
+
return;
|
|
12
|
+
const project = await client.getProject(projectId);
|
|
13
|
+
if (project.suite_mode === SUITE_MODE_MULTI || project.suite_mode === SUITE_MODE_SINGLE_WITH_BASELINES) {
|
|
14
|
+
throw new Error(`Project "${project.name}" (id: ${project.id}) uses multiple test suites/baselines (suite_mode=${project.suite_mode}). ` +
|
|
15
|
+
`The suite_id parameter is required. Use get_suites to find available suites for this project.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
3
18
|
/**
|
|
4
19
|
* Validates that the provided fields exist in the TestRail case schema.
|
|
5
20
|
* Throws an Error if any field is invalid.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uarlouski/testrail-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|