@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 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 **34 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`.
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, **26 of the 34 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).
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 34 tools, grouped by area. Each declares a `read`, `write`, or `delete` mode, which the server surfaces to clients as MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`).
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 34 tools with modes and feature flags in one table.
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
- return new TestRailClient(credResult.data.url, credResult.data.username, credResult.data.apiKey);
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);
@@ -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 networkError = error instanceof Error && !error.message.startsWith('TestRail API Error');
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 (${error.message}). Retrying in ${delayMs}ms...`);
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 const TestSchema = z.object({
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(),
@@ -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
- description: "Get tests for a test run, optionally filtered by status",
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 => TestSchema.parse(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,7 @@
1
+ import { queryTestTool } from "./query_test.js";
2
+ import { getTestsTool } from "./get_tests.js";
3
+ export const testsTools = [
4
+ queryTestTool,
5
+ getTestsTool,
6
+ ];
7
+ export { queryTestTool, getTestsTool };
@@ -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
+ });
@@ -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
  }
@@ -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 cleaned = idString.toUpperCase().startsWith("C") ? idString.substring(1) : idString;
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.0";
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.1.0",
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.com/uarlouski/testrail-mcp-server#readme",
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",