@uarlouski/testrail-mcp-server 3.1.0 → 3.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/README.md +5 -6
- package/dist/cli.js +11 -1
- package/dist/client/testrail.js +25 -8
- package/dist/index.js +11 -3
- package/dist/tools/cases/export_cases_for_rag.js +4 -3
- package/dist/tools/commons/index.js +0 -2
- package/dist/tools/commons/types.js +1 -8
- package/dist/tools/registry.js +4 -0
- package/dist/tools/{commons → tests}/get_tests.js +9 -3
- package/dist/tools/tests/index.js +7 -0
- package/dist/tools/tests/query_test.js +56 -0
- package/dist/tools/tests/types.js +20 -0
- package/dist/utils/mapper.js +18 -3
- package/dist/utils/sanitizer.js +6 -3
- package/dist/version.js +1 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
The **TestRail MCP Server** is a free, open-source [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI assistants direct, structured access to a [TestRail](https://www.testrail.com/) instance through the TestRail API v2. Once configured, an assistant such as **Claude Desktop, Cursor, Windsurf, or GitHub Copilot in VS Code** can search test cases, draft new ones, start test runs, record results, and upload attachments on your behalf — without you leaving the chat window.
|
|
44
44
|
|
|
45
|
-
It exposes **
|
|
45
|
+
It exposes **35 tools**, runs locally on **Node.js 18+** over the MCP stdio transport, and is licensed under **Apache 2.0**. There is nothing to host or deploy: your MCP client launches it on demand with `npx`.
|
|
46
46
|
|
|
47
47
|
**No context switching. No tedious copy-pasting. Just ask your AI.**
|
|
48
48
|
|
|
@@ -279,19 +279,19 @@ See the [CLI & CI/CD guide](https://uarlouski.github.io/testrail-mcp-server/guid
|
|
|
279
279
|
| `TESTRAIL_ENABLE_DEPRECATED_TOOLS` | Preserved for backward compatibility with existing host configurations. | | `true` |
|
|
280
280
|
| `TESTRAIL_DISABLED_TOOLS` | Comma-separated list of specific tool names to disable (e.g., `mutate_suite,delete_entity`). Fails if invalid tool names are specified. | | - |
|
|
281
281
|
|
|
282
|
-
With only the three required credentials, **
|
|
282
|
+
With only the three required credentials, **27 of the 35 tools** are registered. Ready-made read-only and least-privilege configurations are in the [Configuration guide](https://uarlouski.github.io/testrail-mcp-server/guide/configuration).
|
|
283
283
|
|
|
284
284
|
---
|
|
285
285
|
|
|
286
286
|
## 🧰 Available Tools
|
|
287
287
|
|
|
288
|
-
All
|
|
288
|
+
All 35 tools, grouped by area. Each declares a `read`, `write`, or `delete` mode, which the server surfaces to clients as MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`).
|
|
289
289
|
|
|
290
290
|
| Area | Tools | Reference |
|
|
291
291
|
|------|-------|-----------|
|
|
292
292
|
| **Discovery & Navigation** | `query_project`, `query_suite`, `mutate_suite`, `query_section`, `mutate_section`, `get_users` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/discovery) |
|
|
293
293
|
| **Test Case Management** | `get_case`, `get_cases`, `add_case`, `update_case`, `update_cases`, `get_case_fields`, `resolve_case_field`, `get_case_history`, `export_cases_for_rag` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/cases) |
|
|
294
|
-
| **Execution & Tracking** | `query_run`, `mutate_run`, `get_tests`, `get_results`, `add_results`, `add_results_for_cases` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/execution) |
|
|
294
|
+
| **Execution & Tracking** | `query_run`, `mutate_run`, `query_test`, `get_tests`, `get_results`, `add_results`, `add_results_for_cases` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/execution) |
|
|
295
295
|
| **Attachments & Media** | `add_attachment`, `query_attachment` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/attachments) |
|
|
296
296
|
| **Shared Steps** | `get_shared_step`, `get_shared_steps`, `get_shared_step_history`, `add_shared_step`, `update_shared_step` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/shared-steps) |
|
|
297
297
|
| **System Metadata** | `get_statuses`, `get_priorities`, `get_case_fields`, `get_templates`, `get_labels`, `get_configurations` | [Docs](https://uarlouski.github.io/testrail-mcp-server/reference/metadata) |
|
|
@@ -355,14 +355,13 @@ For a comprehensive guide, detailed configuration options, and a complete breakd
|
|
|
355
355
|
|
|
356
356
|
- 🚀 **[Getting Started](https://uarlouski.github.io/testrail-mcp-server/guide/getting-started)**: Per-client setup for Claude, Cursor, Windsurf, and VS Code.
|
|
357
357
|
- ⚙️ **[Configuration](https://uarlouski.github.io/testrail-mcp-server/guide/configuration)**: Every environment variable, permission, and feature flag.
|
|
358
|
-
- 🧰 **[All Tools](https://uarlouski.github.io/testrail-mcp-server/reference/)**: All
|
|
358
|
+
- 🧰 **[All Tools](https://uarlouski.github.io/testrail-mcp-server/reference/)**: All 35 tools with modes and feature flags in one table.
|
|
359
359
|
- 🔭 **[Discovery & Navigation](https://uarlouski.github.io/testrail-mcp-server/reference/discovery)**: Exploring projects, suites, and sections.
|
|
360
360
|
- 📋 **[Test Case Management](https://uarlouski.github.io/testrail-mcp-server/reference/cases)**: Fetching, creating, and bulk-updating test cases.
|
|
361
361
|
- ▶️ **[Execution & Tracking](https://uarlouski.github.io/testrail-mcp-server/reference/execution)**: Managing test runs and submitting test results.
|
|
362
362
|
- 📎 **[Attachments](https://uarlouski.github.io/testrail-mcp-server/reference/attachments)**: Automatically zipping and uploading files or directories.
|
|
363
363
|
- 🔗 **[Shared Steps](https://uarlouski.github.io/testrail-mcp-server/reference/shared-steps)**: Managing reusable step definitions.
|
|
364
364
|
- 🖥️ **[CLI & CI/CD](https://uarlouski.github.io/testrail-mcp-server/guide/cli)**: Pipeline automation without an LLM.
|
|
365
|
-
|
|
366
365
|
---
|
|
367
366
|
|
|
368
367
|
## 🤝 Contributing
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,17 @@ export function resolveClient(options, env) {
|
|
|
29
29
|
console.error("or supply them via --url, --username, and --api-key options.");
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
const timeoutEnv = env.TESTRAIL_REQUEST_TIMEOUT_MS;
|
|
33
|
+
let timeoutMs;
|
|
34
|
+
if (timeoutEnv) {
|
|
35
|
+
const parsed = parseInt(timeoutEnv, 10);
|
|
36
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
37
|
+
timeoutMs = parsed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return new TestRailClient(credResult.data.url, credResult.data.username, credResult.data.apiKey, {
|
|
41
|
+
timeoutMs,
|
|
42
|
+
});
|
|
33
43
|
}
|
|
34
44
|
export function validateToolArgs(tool, flags) {
|
|
35
45
|
const coercedArgs = coerceFlagsForTool(flags, tool.parameters);
|
package/dist/client/testrail.js
CHANGED
|
@@ -7,6 +7,7 @@ export class TestRailClient {
|
|
|
7
7
|
baseUrl;
|
|
8
8
|
headers;
|
|
9
9
|
auth;
|
|
10
|
+
timeoutMs;
|
|
10
11
|
prioritiesPromise = null;
|
|
11
12
|
caseTypesPromise = null;
|
|
12
13
|
caseFieldsPromise = null;
|
|
@@ -14,7 +15,7 @@ export class TestRailClient {
|
|
|
14
15
|
projectsPromise = null;
|
|
15
16
|
templatesPromiseMap = new Map();
|
|
16
17
|
sectionsPromiseMap = new Map();
|
|
17
|
-
constructor(baseUrl, email, apiKey) {
|
|
18
|
+
constructor(baseUrl, email, apiKey, options) {
|
|
18
19
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
19
20
|
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
|
20
21
|
this.auth = `Basic ${auth}`;
|
|
@@ -22,6 +23,7 @@ export class TestRailClient {
|
|
|
22
23
|
"Content-Type": "application/json",
|
|
23
24
|
"Authorization": this.auth,
|
|
24
25
|
};
|
|
26
|
+
this.timeoutMs = options?.timeoutMs ?? 30_000;
|
|
25
27
|
}
|
|
26
28
|
async getCase(caseId) {
|
|
27
29
|
return this.get(`${API_BASE_V2}/get_case/${caseId}`);
|
|
@@ -173,6 +175,9 @@ export class TestRailClient {
|
|
|
173
175
|
const url = `${API_BASE_V2}/get_labels/${projectId}`;
|
|
174
176
|
return this.paginateAll(url, 'labels');
|
|
175
177
|
}
|
|
178
|
+
async getTest(testId) {
|
|
179
|
+
return this.get(`${API_BASE_V2}/get_test/${testId}`);
|
|
180
|
+
}
|
|
176
181
|
async getTests(runId, statusId) {
|
|
177
182
|
let url = `${API_BASE_V2}/get_tests/${runId}`;
|
|
178
183
|
if (statusId && statusId.length > 0) {
|
|
@@ -283,17 +288,19 @@ export class TestRailClient {
|
|
|
283
288
|
}
|
|
284
289
|
async _executeRequest(method, endpoint, headers, body, responseType = 'json') {
|
|
285
290
|
const url = `${this.baseUrl}${endpoint}`;
|
|
286
|
-
const params = {
|
|
287
|
-
method,
|
|
288
|
-
headers,
|
|
289
|
-
body,
|
|
290
|
-
};
|
|
291
291
|
const maxRetries = 3;
|
|
292
292
|
const baseDelayMs = 1000;
|
|
293
293
|
let attempt = 0;
|
|
294
294
|
while (attempt <= maxRetries) {
|
|
295
295
|
console.error(`[TestRailClient] Executing ${method} request to ${endpoint}${attempt > 0 ? ` (Attempt ${attempt + 1})` : ''}`);
|
|
296
296
|
const startTime = Date.now();
|
|
297
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
298
|
+
const params = {
|
|
299
|
+
method,
|
|
300
|
+
headers,
|
|
301
|
+
body,
|
|
302
|
+
signal,
|
|
303
|
+
};
|
|
297
304
|
try {
|
|
298
305
|
const response = await fetch(url, params);
|
|
299
306
|
const duration = Date.now() - startTime;
|
|
@@ -329,10 +336,20 @@ export class TestRailClient {
|
|
|
329
336
|
return await response.text();
|
|
330
337
|
}
|
|
331
338
|
catch (error) {
|
|
332
|
-
const
|
|
339
|
+
const errName = error?.name;
|
|
340
|
+
const errMsg = typeof error?.message === 'string' ? error.message : '';
|
|
341
|
+
const isTimeout = signal.aborted ||
|
|
342
|
+
errName === 'TimeoutError' ||
|
|
343
|
+
signal.reason?.name === 'TimeoutError';
|
|
344
|
+
if (isTimeout) {
|
|
345
|
+
const timeoutMessage = `TestRail request timed out after ${this.timeoutMs}ms: ${method} ${endpoint}`;
|
|
346
|
+
console.error(`[TestRailClient] Request Failed: ${method} ${endpoint} - ${timeoutMessage}`);
|
|
347
|
+
throw new Error(timeoutMessage);
|
|
348
|
+
}
|
|
349
|
+
const networkError = !errMsg.startsWith('TestRail API Error');
|
|
333
350
|
if (networkError && attempt < maxRetries) {
|
|
334
351
|
const delayMs = baseDelayMs * Math.pow(2, attempt);
|
|
335
|
-
console.error(`[TestRailClient] Network error (${
|
|
352
|
+
console.error(`[TestRailClient] Network error (${errMsg}). Retrying in ${delayMs}ms...`);
|
|
336
353
|
await this.delay(delayMs);
|
|
337
354
|
attempt++;
|
|
338
355
|
continue;
|
package/dist/index.js
CHANGED
|
@@ -27,19 +27,27 @@ const EnvSchema = z.object({
|
|
|
27
27
|
TESTRAIL_ALLOW_READ_OPERATIONS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
|
|
28
28
|
TESTRAIL_ALLOW_DELETE_OPERATIONS: z.string().optional().transform(val => val === 'true'),
|
|
29
29
|
TESTRAIL_ENABLE_DEPRECATED_TOOLS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
|
|
30
|
-
TESTRAIL_DISABLED_TOOLS: z.string().optional().transform(val => val ? val.split(',').map(s => s.trim()).filter(Boolean) : [])
|
|
30
|
+
TESTRAIL_DISABLED_TOOLS: z.string().optional().transform(val => val ? val.split(',').map(s => s.trim()).filter(Boolean) : []),
|
|
31
|
+
TESTRAIL_REQUEST_TIMEOUT_MS: z.string().optional().refine(val => {
|
|
32
|
+
if (!val)
|
|
33
|
+
return true;
|
|
34
|
+
const num = Number(val);
|
|
35
|
+
return Number.isInteger(num) && num > 0;
|
|
36
|
+
}, { message: 'Must be a positive integer' }).transform(val => val ? parseInt(val, 10) : undefined),
|
|
31
37
|
});
|
|
32
38
|
const parseResult = EnvSchema.safeParse(process.env);
|
|
33
39
|
if (!parseResult.success) {
|
|
34
40
|
console.error("Invalid TestRail environment configuration:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
|
|
35
41
|
process.exit(1);
|
|
36
42
|
}
|
|
37
|
-
const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS, TESTRAIL_ENABLE_CASE_HISTORY, TESTRAIL_ENABLE_RAG_TOOLS, TESTRAIL_ALLOW_WRITE_OPERATIONS, TESTRAIL_ALLOW_READ_OPERATIONS, TESTRAIL_ALLOW_DELETE_OPERATIONS, TESTRAIL_ENABLE_DEPRECATED_TOOLS, TESTRAIL_DISABLED_TOOLS } = parseResult.data;
|
|
43
|
+
const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS, TESTRAIL_ENABLE_CASE_HISTORY, TESTRAIL_ENABLE_RAG_TOOLS, TESTRAIL_ALLOW_WRITE_OPERATIONS, TESTRAIL_ALLOW_READ_OPERATIONS, TESTRAIL_ALLOW_DELETE_OPERATIONS, TESTRAIL_ENABLE_DEPRECATED_TOOLS, TESTRAIL_DISABLED_TOOLS, TESTRAIL_REQUEST_TIMEOUT_MS, } = parseResult.data;
|
|
38
44
|
const server = new McpServer({
|
|
39
45
|
name: "TestRail MCP Server",
|
|
40
46
|
version: VERSION,
|
|
41
47
|
});
|
|
42
|
-
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY
|
|
48
|
+
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, {
|
|
49
|
+
timeoutMs: TESTRAIL_REQUEST_TIMEOUT_MS,
|
|
50
|
+
});
|
|
43
51
|
let tools;
|
|
44
52
|
try {
|
|
45
53
|
tools = getToolsToRegister({
|
|
@@ -81,9 +81,6 @@ function categorizeCustomFields(testCase, applicableFieldDefs, ignoredFieldsSet)
|
|
|
81
81
|
if (!systemName.startsWith("custom_")) {
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
|
-
if (rawValue === null || rawValue === undefined || rawValue === "" || (Array.isArray(rawValue) && rawValue.length === 0)) {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
84
|
const strippedName = systemName.replace(/^custom_/, "");
|
|
88
85
|
if (ignoredFieldsSet.has(systemName) || ignoredFieldsSet.has(strippedName)) {
|
|
89
86
|
continue;
|
|
@@ -92,6 +89,10 @@ function categorizeCustomFields(testCase, applicableFieldDefs, ignoredFieldsSet)
|
|
|
92
89
|
if (!fieldDef) {
|
|
93
90
|
continue;
|
|
94
91
|
}
|
|
92
|
+
const isCheckbox = fieldDef.type_id === CaseFieldTypeId.Checkbox;
|
|
93
|
+
if (!isCheckbox && (rawValue === null || rawValue === undefined || rawValue === "" || (Array.isArray(rawValue) && rawValue.length === 0))) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
95
96
|
const val = resolveCustomFieldValue(fieldDef, rawValue);
|
|
96
97
|
if (isMarkdownField(fieldDef, val)) {
|
|
97
98
|
const header = fieldDef.label || systemName.replace(/^custom_/, "");
|
|
@@ -4,7 +4,6 @@ import { getLabelsTool } from "./get_labels.js";
|
|
|
4
4
|
import { getPrioritiesTool } from "./get_priorities.js";
|
|
5
5
|
import { getStatusesTool } from "./get_statuses.js";
|
|
6
6
|
import { getTemplatesTool } from "./get_templates.js";
|
|
7
|
-
import { getTestsTool } from "./get_tests.js";
|
|
8
7
|
import { getUsersTool } from "./get_users.js";
|
|
9
8
|
export const commonsTools = [
|
|
10
9
|
deleteEntityTool,
|
|
@@ -13,6 +12,5 @@ export const commonsTools = [
|
|
|
13
12
|
getPrioritiesTool,
|
|
14
13
|
getStatusesTool,
|
|
15
14
|
getTemplatesTool,
|
|
16
|
-
getTestsTool,
|
|
17
15
|
getUsersTool,
|
|
18
16
|
];
|
|
@@ -23,14 +23,7 @@ export const StatusSchema = z.object({
|
|
|
23
23
|
name: z.string(),
|
|
24
24
|
label: z.string(),
|
|
25
25
|
});
|
|
26
|
-
export
|
|
27
|
-
id: z.number(),
|
|
28
|
-
case_id: z.number(),
|
|
29
|
-
status_id: z.number(),
|
|
30
|
-
title: z.string(),
|
|
31
|
-
run_id: z.number(),
|
|
32
|
-
});
|
|
33
|
-
export const TestsSchema = z.array(TestSchema);
|
|
26
|
+
export { TestSchema, TestsSchema } from "../tests/types.js";
|
|
34
27
|
export const UserSchema = z.object({
|
|
35
28
|
id: z.number(),
|
|
36
29
|
name: z.string(),
|
package/dist/tools/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ import { sharedStepsTools } from "./shared_steps/index.js";
|
|
|
7
7
|
import { resultsTools } from "./results/index.js";
|
|
8
8
|
import { commonsTools } from "./commons/index.js";
|
|
9
9
|
import { attachmentsTools } from "./attachments/index.js";
|
|
10
|
+
import { testsTools } from "./tests/index.js";
|
|
10
11
|
export const ALL_TOOLS = [
|
|
11
12
|
...projectsTools,
|
|
12
13
|
...suitesTools,
|
|
@@ -15,6 +16,7 @@ export const ALL_TOOLS = [
|
|
|
15
16
|
exportCasesForRagTool,
|
|
16
17
|
...sectionsTools,
|
|
17
18
|
...runsTools,
|
|
19
|
+
...testsTools,
|
|
18
20
|
...resultsTools,
|
|
19
21
|
...attachmentsTools,
|
|
20
22
|
...commonsTools,
|
|
@@ -34,6 +36,7 @@ export function getToolsToRegister(config) {
|
|
|
34
36
|
...casesTools,
|
|
35
37
|
...sectionsTools,
|
|
36
38
|
...runsTools,
|
|
39
|
+
...testsTools,
|
|
37
40
|
...resultsTools,
|
|
38
41
|
...attachmentsTools,
|
|
39
42
|
...commonsTools,
|
|
@@ -68,6 +71,7 @@ export function getToolsToRegister(config) {
|
|
|
68
71
|
readOnlyHint: tool.mode === 'read',
|
|
69
72
|
destructiveHint: tool.mode === 'delete',
|
|
70
73
|
idempotentHint: tool.mode === 'read',
|
|
74
|
+
openWorldHint: false,
|
|
71
75
|
};
|
|
72
76
|
return {
|
|
73
77
|
...tool,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestSchema } from "./types.js";
|
|
3
2
|
const parameters = {
|
|
4
3
|
run_id: z.number().describe("The ID of the test run"),
|
|
5
4
|
status_id: z.array(z.number()).optional().describe("Optional array of status IDs to filter by. Use get_statuses to retrieve available status IDs"),
|
|
@@ -7,12 +6,19 @@ const parameters = {
|
|
|
7
6
|
export const getTestsTool = {
|
|
8
7
|
name: "get_tests",
|
|
9
8
|
mode: "read",
|
|
10
|
-
|
|
9
|
+
deprecated: true,
|
|
10
|
+
description: "Get tests for a test run, optionally filtered by status (Deprecated: use query_test with action 'many' instead)",
|
|
11
11
|
parameters,
|
|
12
12
|
handler: async ({ run_id, status_id }, client) => {
|
|
13
13
|
const tests = await client.getTests(run_id, status_id);
|
|
14
14
|
return {
|
|
15
|
-
tests: tests.map(test =>
|
|
15
|
+
tests: tests.map(test => ({
|
|
16
|
+
id: test.id,
|
|
17
|
+
case_id: test.case_id,
|
|
18
|
+
status_id: test.status_id,
|
|
19
|
+
title: test.title,
|
|
20
|
+
run_id: test.run_id,
|
|
21
|
+
})),
|
|
16
22
|
};
|
|
17
23
|
},
|
|
18
24
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { GetOneTestSchema, GetManyTestsSchema, TestSchema } from "./types.js";
|
|
4
|
+
import { handleQuery } from "../../utils/query_handler.js";
|
|
5
|
+
import { normalizeEntityId } from "../../utils/sanitizer.js";
|
|
6
|
+
const parameters = {
|
|
7
|
+
payload: z.discriminatedUnion("action", [
|
|
8
|
+
GetOneTestSchema,
|
|
9
|
+
GetManyTestsSchema,
|
|
10
|
+
]).describe("The payload containing the action ('one' or 'many') and corresponding parameters"),
|
|
11
|
+
};
|
|
12
|
+
export const queryTestTool = {
|
|
13
|
+
name: "query_test",
|
|
14
|
+
mode: "read",
|
|
15
|
+
description: "Retrieve a single test by ID or all tests for a test run in TestRail. Set payload.action to 'one' or 'many' to specify the operation.",
|
|
16
|
+
parameters,
|
|
17
|
+
handler: async (args, client) => {
|
|
18
|
+
return handleQuery(args.payload, async (p) => {
|
|
19
|
+
const testId = normalizeEntityId(p.test_id);
|
|
20
|
+
const test = await client.getTest(testId);
|
|
21
|
+
return {
|
|
22
|
+
test: TestSchema.parse(test),
|
|
23
|
+
};
|
|
24
|
+
}, async (p) => {
|
|
25
|
+
const tests = await client.getTests(p.run_id, p.status_id);
|
|
26
|
+
const response = {
|
|
27
|
+
tests: tests.map(t => {
|
|
28
|
+
const baseTest = {
|
|
29
|
+
id: t.id,
|
|
30
|
+
case_id: t.case_id,
|
|
31
|
+
status_id: t.status_id,
|
|
32
|
+
title: t.title,
|
|
33
|
+
run_id: t.run_id,
|
|
34
|
+
};
|
|
35
|
+
if (p.fields && p.fields.length > 0) {
|
|
36
|
+
for (const field of p.fields) {
|
|
37
|
+
if (field in t) {
|
|
38
|
+
baseTest[field] = t[field];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return baseTest;
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
if (p.output_file) {
|
|
46
|
+
await fs.promises.writeFile(p.output_file, JSON.stringify(response), "utf-8");
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
message: `Successfully exported ${response.tests.length} tests to ${p.output_file}`,
|
|
50
|
+
file: p.output_file,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return response;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const TestSchema = z.looseObject({
|
|
3
|
+
id: z.number(),
|
|
4
|
+
case_id: z.number(),
|
|
5
|
+
status_id: z.number(),
|
|
6
|
+
title: z.string(),
|
|
7
|
+
run_id: z.number(),
|
|
8
|
+
});
|
|
9
|
+
export const TestsSchema = z.array(TestSchema);
|
|
10
|
+
export const GetOneTestSchema = z.object({
|
|
11
|
+
action: z.literal("one").describe("Retrieve a single test by ID"),
|
|
12
|
+
test_id: z.union([z.number(), z.string()]).describe("The ID of the test (e.g. 123 or 'T123')"),
|
|
13
|
+
});
|
|
14
|
+
export const GetManyTestsSchema = z.object({
|
|
15
|
+
action: z.literal("many").describe("Retrieve tests for a test run"),
|
|
16
|
+
run_id: z.number().int().describe("The ID of the test run"),
|
|
17
|
+
status_id: z.array(z.number()).optional().describe("Optional array of status IDs to filter by. Use get_statuses to retrieve available status IDs"),
|
|
18
|
+
fields: z.array(z.string()).optional().describe("Additional fields to include in response beyond default fields (id, case_id, status_id, title, run_id). Example: ['assignedto_id', 'priority_id', 'refs']"),
|
|
19
|
+
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."),
|
|
20
|
+
});
|
package/dist/utils/mapper.js
CHANGED
|
@@ -23,15 +23,23 @@ export function processCustomFields(testCase, caseFields) {
|
|
|
23
23
|
for (const [key, value] of Object.entries(testCase)) {
|
|
24
24
|
if (!key.startsWith("custom_"))
|
|
25
25
|
continue;
|
|
26
|
-
if (value === null || value === undefined)
|
|
27
|
-
continue;
|
|
28
26
|
if (!fieldNameMap.has(key)) {
|
|
27
|
+
if (value === null || value === undefined)
|
|
28
|
+
continue;
|
|
29
29
|
console.error(`No field mapping found for: ${key}`);
|
|
30
30
|
result[key] = value;
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
|
-
const outputKey = fieldNameMap.get(key);
|
|
34
33
|
const field = applicableFields.find(f => f.system_name === key);
|
|
34
|
+
const isCheckbox = field.type_id === CaseFieldTypeId.Checkbox;
|
|
35
|
+
if (value === null || value === undefined) {
|
|
36
|
+
if (isCheckbox) {
|
|
37
|
+
const outputKey = fieldNameMap.get(key);
|
|
38
|
+
result[outputKey] = resolveCustomFieldValue(field, value);
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const outputKey = fieldNameMap.get(key);
|
|
35
43
|
const options = dropdownOptionsMap.get(key);
|
|
36
44
|
result[outputKey] = resolveCustomFieldValue(field, value, options);
|
|
37
45
|
}
|
|
@@ -41,9 +49,16 @@ export function processCustomFields(testCase, caseFields) {
|
|
|
41
49
|
* Resolves a custom field value against its field definition:
|
|
42
50
|
* - Maps dropdown option IDs to their display labels
|
|
43
51
|
* - Normalizes multi-select values into arrays
|
|
52
|
+
* - Normalizes checkbox / boolean values (defaults null/undefined to false)
|
|
44
53
|
* - Sanitizes HTML formatting in strings
|
|
45
54
|
*/
|
|
46
55
|
export function resolveCustomFieldValue(field, value, optionsMap) {
|
|
56
|
+
if (field.type_id === CaseFieldTypeId.Checkbox) {
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
61
|
+
}
|
|
47
62
|
if (value === null || value === undefined) {
|
|
48
63
|
return value;
|
|
49
64
|
}
|
package/dist/utils/sanitizer.js
CHANGED
|
@@ -145,12 +145,15 @@ export function isActive(value) {
|
|
|
145
145
|
return activeFlag === 1 || activeFlag === true || activeFlag === '1' || activeFlag === 'true';
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
-
* Normalizes an entity ID (e.g. case ID, run ID, attachment ID).
|
|
149
|
-
* Handles string IDs with 'C' prefixes (e.g. 'C123' -> 123), numeric IDs, and trims whitespace.
|
|
148
|
+
* Normalizes an entity ID (e.g. case ID, test ID, run ID, attachment ID).
|
|
149
|
+
* Handles string IDs with 'C', 'T', or 'R' prefixes (e.g. 'C123' -> 123, 'T456' -> 456), numeric IDs, and trims whitespace.
|
|
150
150
|
*/
|
|
151
151
|
export function normalizeEntityId(id) {
|
|
152
152
|
const idString = typeof id === "string" ? id.trim() : String(id);
|
|
153
|
-
const
|
|
153
|
+
const upper = idString.toUpperCase();
|
|
154
|
+
const cleaned = (upper.startsWith("C") || upper.startsWith("T") || upper.startsWith("R"))
|
|
155
|
+
? idString.substring(1)
|
|
156
|
+
: idString;
|
|
154
157
|
const parsed = Number(cleaned);
|
|
155
158
|
if (isNaN(parsed)) {
|
|
156
159
|
throw new Error(`Invalid entity ID: ${id}`);
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "3.
|
|
1
|
+
export const VERSION = "3.2.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uarlouski/testrail-mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"mcpName": "io.github.uarlouski/testrail-mcp-server",
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for TestRail: let Claude, Cursor, and Windsurf manage test cases, runs, results, and attachments.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"testrail",
|
|
20
20
|
"testrail-api",
|
|
21
21
|
"testrail-mcp",
|
|
22
|
+
"testrail-mcp-server",
|
|
23
|
+
"mcp-testrail",
|
|
22
24
|
"testrail-integration",
|
|
23
25
|
"test-management",
|
|
24
26
|
"test-case-management",
|
|
@@ -37,10 +39,14 @@
|
|
|
37
39
|
"ai-agents",
|
|
38
40
|
"llm",
|
|
39
41
|
"claude",
|
|
42
|
+
"claude-desktop",
|
|
43
|
+
"claude-code",
|
|
40
44
|
"cursor",
|
|
41
45
|
"windsurf",
|
|
42
46
|
"vscode",
|
|
43
47
|
"copilot",
|
|
48
|
+
"github-copilot",
|
|
49
|
+
"test-case-generation",
|
|
44
50
|
"rag",
|
|
45
51
|
"typescript",
|
|
46
52
|
"cli"
|
|
@@ -53,7 +59,7 @@
|
|
|
53
59
|
"bugs": {
|
|
54
60
|
"url": "https://github.com/uarlouski/testrail-mcp-server/issues"
|
|
55
61
|
},
|
|
56
|
-
"homepage": "https://github.
|
|
62
|
+
"homepage": "https://uarlouski.github.io/testrail-mcp-server/",
|
|
57
63
|
"license": "Apache-2.0",
|
|
58
64
|
"dependencies": {
|
|
59
65
|
"@modelcontextprotocol/sdk": "^1.30.0",
|