@uarlouski/testrail-mcp-server 2.2.0 → 2.3.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
@@ -150,6 +150,17 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
150
150
  | `TESTRAIL_ALLOW_WRITE_OPERATIONS` | Allow write operations (e.g. adding/updating test cases, test runs, sections) | | `true` |
151
151
  | `TESTRAIL_ALLOW_READ_OPERATIONS` | Allow read operations (e.g. retrieving projects, test cases, templates) | | `true` |
152
152
  | `TESTRAIL_ALLOW_DELETE_OPERATIONS` | Allow delete operations (e.g. deleting cases or shared steps). Enabled strictly via `true`. | | `false` |
153
+ | `TESTRAIL_ENABLE_DEPRECATED_TOOLS` | Enable deprecated tools for backward compatibility. Set to `false` to reduce context token overhead. | | `true` |
154
+
155
+ ### ⚠️ Deprecation Lifecycle & Features Scheduled for Removal
156
+
157
+ To ensure smooth transitions, deprecated tools remain available by default (`TESTRAIL_ENABLE_DEPRECATED_TOOLS=true`) and will be removed in future major releases:
158
+
159
+ | Deprecated Tool | Replacement | Status |
160
+ |-----------------|-------------|--------|
161
+ | `add_attachment_to_run` | `add_attachment` (`entity_type: "case" \| "run"`) | Deprecated in `2.3.0`, scheduled for removal in `3.0.0` |
162
+
163
+ > **💡 Token Tip**: If you are not using legacy tools, set `TESTRAIL_ENABLE_DEPRECATED_TOOLS=false` in your environment to eliminate deprecated tool definitions from the LLM prompt and save tokens!
153
164
 
154
165
  ---
155
166
 
@@ -189,7 +200,12 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
189
200
  | `get_results` | Retrieve paginated results for a specific test ID. |
190
201
  | `add_results` | Submit test results to a test run using the specific `test_id`. |
191
202
  | `add_results_for_cases` | Submits results to a run mapping directly to `case_id`s, streamlining automation workflows. |
192
- | `add_attachment_to_run` | Attach logs, files, or zipped artifacts directly to an ongoing test run. |
203
+
204
+ ### 📎 Attachments & Media
205
+ | Tool | Functionality |
206
+ |------|-------------|
207
+ | `add_attachment` | Add an attachment (screenshot, log, file, or auto-zipped directory) to a test case or test run (`entity_type: "case" \| "run"`). |
208
+ | `query_attachment` | Download an attachment to a local file (`action: "one"`) or retrieve attachment metadata for a test case or test run (`action: "many"`, supporting `output_file` export). |
193
209
 
194
210
  ### 🔗 Shared Steps (Optional)
195
211
  | Tool | Functionality |
@@ -203,7 +219,7 @@ The TestRail MCP Server provides heavily typed, descriptive tools designed speci
203
219
  ### 🗑️ Deletion
204
220
  | Tool | Functionality |
205
221
  |------|-------------|
206
- | `delete_entity` | Deletes a specified TestRail entity (supports `"case"` or `"shared_step"`) by its ID. Requires `TESTRAIL_ALLOW_DELETE_OPERATIONS` to be set to `true` in your environment. |
222
+ | `delete_entity` | Deletes a specified TestRail entity (supports `"case"`, `"shared_step"`, or `"attachment"`) by its ID. Requires `TESTRAIL_ALLOW_DELETE_OPERATIONS` to be set to `true` in your environment. |
207
223
 
208
224
  ### 🧠 System Metadata
209
225
  | Tool | Functionality |
@@ -1,5 +1,6 @@
1
1
  import { isActive } from "../utils/sanitizer.js";
2
2
  import * as fs from "fs";
3
+ import * as path from "path";
3
4
  const API_INDEX = '/index.php?';
4
5
  const API_BASE_V2 = `${API_INDEX}/api/v2`;
5
6
  export class TestRailClient {
@@ -178,7 +179,7 @@ export class TestRailClient {
178
179
  async addResultsForCases(runId, results) {
179
180
  return this.post(`${API_BASE_V2}/add_results_for_cases/${runId}`, { results });
180
181
  }
181
- async addAttachmentToRun(runId, filePath, filename) {
182
+ async addAttachment(entityType, entityId, filePath, filename) {
182
183
  const fileBuffer = await fs.promises.readFile(filePath);
183
184
  const blob = new Blob([fileBuffer]);
184
185
  const formData = new FormData();
@@ -186,7 +187,11 @@ export class TestRailClient {
186
187
  const headers = {
187
188
  'Authorization': this.auth,
188
189
  };
189
- return this._executeRequest('POST', `${API_BASE_V2}/add_attachment_to_run/${runId}`, headers, formData);
190
+ return this._executeRequest('POST', `${API_BASE_V2}/add_attachment_to_${entityType}/${entityId}`, headers, formData);
191
+ }
192
+ async getAttachments(entityType, entityId) {
193
+ const url = `${API_BASE_V2}/get_attachments_for_${entityType}/${entityId}`;
194
+ return this.paginateAll(url, 'attachments');
190
195
  }
191
196
  async getSharedSteps(projectId, options) {
192
197
  let url = `${API_BASE_V2}/get_shared_steps/${projectId}`;
@@ -207,11 +212,26 @@ export class TestRailClient {
207
212
  async updateSharedStep(sharedStepId, data) {
208
213
  return this.post(`${API_BASE_V2}/update_shared_step/${sharedStepId}`, data);
209
214
  }
215
+ async getAttachment(attachmentId, outputFilePath) {
216
+ const dir = path.dirname(outputFilePath);
217
+ if (dir && !fs.existsSync(dir)) {
218
+ await fs.promises.mkdir(dir, { recursive: true });
219
+ }
220
+ const buffer = await this._executeRequest('GET', `${API_BASE_V2}/get_attachment/${attachmentId}`, this.headers, undefined, 'buffer');
221
+ await fs.promises.writeFile(outputFilePath, buffer);
222
+ return {
223
+ file: outputFilePath,
224
+ size: buffer.byteLength,
225
+ };
226
+ }
210
227
  async deleteCase(caseId) {
211
- return this._executeRequest('POST', `${API_BASE_V2}/delete_case/${caseId}`, this.headers, JSON.stringify({}), false);
228
+ return this._executeRequest('POST', `${API_BASE_V2}/delete_case/${caseId}`, this.headers, JSON.stringify({}), 'text');
212
229
  }
213
230
  async deleteSharedStep(sharedStepId) {
214
- return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}), false);
231
+ return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}), 'text');
232
+ }
233
+ async deleteAttachment(attachmentId) {
234
+ return this._executeRequest('POST', `${API_BASE_V2}/delete_attachment/${attachmentId}`, this.headers, JSON.stringify({}), 'text');
215
235
  }
216
236
  async getUsers(projectId) {
217
237
  const url = projectId
@@ -254,7 +274,7 @@ export class TestRailClient {
254
274
  }
255
275
  return this._executeRequest(method, endpoint, this.headers, jsonData);
256
276
  }
257
- async _executeRequest(method, endpoint, headers, body, parseBody = true) {
277
+ async _executeRequest(method, endpoint, headers, body, responseType = 'json') {
258
278
  const url = `${this.baseUrl}${endpoint}`;
259
279
  const params = {
260
280
  method,
@@ -292,9 +312,13 @@ export class TestRailClient {
292
312
  console.error(`[TestRailClient] Error Details: ${errorMessage}`);
293
313
  throw new Error(errorMessage);
294
314
  }
295
- if (parseBody) {
315
+ if (responseType === 'json') {
296
316
  return await response.json();
297
317
  }
318
+ else if (responseType === 'buffer') {
319
+ const arrayBuffer = await response.arrayBuffer();
320
+ return Buffer.from(arrayBuffer);
321
+ }
298
322
  return await response.text();
299
323
  }
300
324
  catch (error) {
package/dist/index.js CHANGED
@@ -13,24 +13,26 @@ const EnvSchema = z.object({
13
13
  TESTRAIL_ENABLE_SHARED_STEPS: z.string().optional().transform(val => val === 'true'),
14
14
  TESTRAIL_ALLOW_WRITE_OPERATIONS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
15
15
  TESTRAIL_ALLOW_READ_OPERATIONS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
16
- TESTRAIL_ALLOW_DELETE_OPERATIONS: z.string().optional().transform(val => val === 'true')
16
+ TESTRAIL_ALLOW_DELETE_OPERATIONS: z.string().optional().transform(val => val === 'true'),
17
+ TESTRAIL_ENABLE_DEPRECATED_TOOLS: z.string().optional().transform(val => val === undefined ? true : val === 'true')
17
18
  });
18
19
  const parseResult = EnvSchema.safeParse(process.env);
19
20
  if (!parseResult.success) {
20
21
  console.error("Invalid TestRail environment configuration:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
21
22
  process.exit(1);
22
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
+ 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, TESTRAIL_ENABLE_DEPRECATED_TOOLS } = parseResult.data;
24
25
  const server = new McpServer({
25
26
  name: "TestRail MCP Server",
26
- version: "2.2.0",
27
+ version: "2.3.0",
27
28
  });
28
29
  const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
29
30
  const tools = getToolsToRegister({
30
31
  enableSharedSteps: TESTRAIL_ENABLE_SHARED_STEPS,
31
32
  allowWrite: TESTRAIL_ALLOW_WRITE_OPERATIONS,
32
33
  allowRead: TESTRAIL_ALLOW_READ_OPERATIONS,
33
- allowDelete: TESTRAIL_ALLOW_DELETE_OPERATIONS
34
+ allowDelete: TESTRAIL_ALLOW_DELETE_OPERATIONS,
35
+ enableDeprecatedTools: TESTRAIL_ENABLE_DEPRECATED_TOOLS
34
36
  });
35
37
  for (const tool of tools) {
36
38
  server.registerTool(tool.name, {
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { AttachmentSchema } from "./types.js";
3
+ import { normalizeEntityId, prepareUploadFile } from "../../utils/attachment_helper.js";
4
+ const parameters = {
5
+ entity_type: z.enum(["case", "run"]).describe("The type of entity to attach the file to ('case' or 'run')"),
6
+ entity_id: z.union([z.number(), z.string()]).describe("The ID of the entity to attach the file to (e.g. 123 or 'C123' for cases, or numeric run ID)"),
7
+ file_path: z.string().describe("The path to the file or directory to attach. Directories will be automatically zipped."),
8
+ };
9
+ const description = `
10
+ Add an attachment to a test case or test run in TestRail.
11
+ If the file_path points to a directory, it will be automatically zipped before uploading.
12
+ Maximum upload size is 256MB.
13
+ `;
14
+ export const addAttachmentTool = {
15
+ name: "add_attachment",
16
+ mode: "write",
17
+ description: description.trim(),
18
+ parameters,
19
+ handler: async ({ entity_type, entity_id, file_path }, client) => {
20
+ const normalizedId = normalizeEntityId(entity_id);
21
+ return prepareUploadFile(file_path, async (uploadPath, filename) => {
22
+ const result = await client.addAttachment(entity_type, normalizedId, uploadPath, filename);
23
+ return AttachmentSchema.parse(result);
24
+ });
25
+ },
26
+ };
@@ -0,0 +1,6 @@
1
+ import { addAttachmentTool } from "./add_attachment.js";
2
+ import { queryAttachmentTool } from "./query_attachment.js";
3
+ export const attachmentsTools = [
4
+ addAttachmentTool,
5
+ queryAttachmentTool,
6
+ ];
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import * as fs from "fs";
3
+ import { AttachmentItemSchema, GetOneAttachmentSchema, GetManyAttachmentsSchema } from "./types.js";
4
+ import { normalizeEntityId } from "../../utils/attachment_helper.js";
5
+ import { handleQuery } from "../../utils/query_handler.js";
6
+ const parameters = {
7
+ payload: z.discriminatedUnion("action", [
8
+ GetOneAttachmentSchema,
9
+ GetManyAttachmentsSchema,
10
+ ]).describe("The payload containing the action ('one' or 'many') and corresponding parameters"),
11
+ };
12
+ export const queryAttachmentTool = {
13
+ name: "query_attachment",
14
+ mode: "read",
15
+ description: "Download a single attachment or list all attachments for a test case or 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 downloadResult = await client.getAttachment(p.attachment_id, p.output_file);
20
+ return {
21
+ success: true,
22
+ message: `Successfully downloaded attachment ${p.attachment_id} (${downloadResult.size} bytes) to ${downloadResult.file}`,
23
+ file: downloadResult.file,
24
+ size: downloadResult.size,
25
+ };
26
+ }, async (p) => {
27
+ const normalizedId = normalizeEntityId(p.entity_id);
28
+ const attachments = await client.getAttachments(p.entity_type, normalizedId);
29
+ const parsedAttachments = attachments.map(item => AttachmentItemSchema.parse(item));
30
+ const response = {
31
+ attachments: parsedAttachments,
32
+ };
33
+ if (p.output_file) {
34
+ await fs.promises.writeFile(p.output_file, JSON.stringify(response), "utf-8");
35
+ return {
36
+ success: true,
37
+ message: `Successfully exported ${response.attachments.length} attachments to ${p.output_file}`,
38
+ file: p.output_file,
39
+ };
40
+ }
41
+ return response;
42
+ });
43
+ },
44
+ };
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export const AttachmentSchema = z.object({
3
+ attachment_id: z.union([z.number(), z.string()]),
4
+ });
5
+ export const AttachmentItemSchema = z.object({
6
+ id: z.union([z.number(), z.string()]),
7
+ filename: z.string().optional(),
8
+ size: z.number().optional(),
9
+ file_type: z.string().optional(),
10
+ is_image: z.union([z.boolean(), z.number()]).optional(),
11
+ });
12
+ export const AddAttachmentSchema = z.object({
13
+ entity_type: z.enum(["case", "run"]).describe("The type of entity to attach the file to ('case' or 'run')"),
14
+ entity_id: z.union([z.number(), z.string()]).describe("The ID of the entity to attach the file to (e.g. 123 or 'C123' for cases, or numeric run ID)"),
15
+ file_path: z.string().describe("The path to the file or directory to attach. Directories will be automatically zipped."),
16
+ });
17
+ export const GetOneAttachmentSchema = z.object({
18
+ action: z.literal("one").describe("Download a single attachment by ID and save to local filesystem"),
19
+ attachment_id: z.union([z.number(), z.string()]).describe("The ID of the attachment to download (numeric ID or UUID)"),
20
+ output_file: z.string().describe("Absolute local file path where the downloaded attachment will be saved"),
21
+ });
22
+ export const GetManyAttachmentsSchema = z.object({
23
+ action: z.literal("many").describe("Retrieve attachment metadata for a test case or test run"),
24
+ entity_type: z.enum(["case", "run"]).describe("The type of entity to get attachments for ('case' or 'run')"),
25
+ entity_id: z.union([z.number(), z.string()]).describe("The ID of the entity (e.g. 123 or 'C123' for cases, or numeric run ID)"),
26
+ output_file: z.string().optional().describe("Optional absolute file path to export large metadata JSON responses"),
27
+ });
@@ -1,22 +1,28 @@
1
1
  import { z } from "zod";
2
2
  const parameters = {
3
- entity_type: z.enum(["case", "shared_step"]).describe("The type of the entity to delete"),
3
+ entity_type: z.enum(["case", "shared_step", "attachment"]).describe("The type of the entity to delete"),
4
4
  entity_id: z.number().describe("The ID of the entity to delete"),
5
5
  };
6
6
  export const deleteEntityTool = {
7
7
  name: "delete_entity",
8
8
  mode: "delete",
9
- description: "Deletes a specified TestRail entity (case or shared_step) by its ID.",
9
+ description: "Deletes a specified TestRail entity (case, shared_step, or attachment) by its ID.",
10
10
  parameters,
11
11
  handler: async (args, client) => {
12
12
  const { entity_type, entity_id } = args;
13
13
  switch (entity_type) {
14
- case "case":
14
+ case "case": {
15
15
  await client.deleteCase(entity_id);
16
16
  return { message: `Case ${entity_id} deleted successfully.` };
17
- case "shared_step":
17
+ }
18
+ case "shared_step": {
18
19
  await client.deleteSharedStep(entity_id);
19
20
  return { message: `Shared step ${entity_id} deleted successfully.` };
21
+ }
22
+ case "attachment": {
23
+ await client.deleteAttachment(entity_id);
24
+ return { message: `Attachment ${entity_id} deleted successfully.` };
25
+ }
20
26
  default:
21
27
  throw new Error(`Unsupported entity type: ${entity_type}`);
22
28
  }
@@ -6,6 +6,7 @@ import { runsTools } from "./runs/index.js";
6
6
  import { sharedStepsTools } from "./shared_steps/index.js";
7
7
  import { resultsTools } from "./results/index.js";
8
8
  import { commonsTools } from "./commons/index.js";
9
+ import { attachmentsTools } from "./attachments/index.js";
9
10
  export function getToolsToRegister(config) {
10
11
  const tools = [
11
12
  ...projectsTools,
@@ -14,6 +15,7 @@ export function getToolsToRegister(config) {
14
15
  ...sectionsTools,
15
16
  ...runsTools,
16
17
  ...resultsTools,
18
+ ...attachmentsTools,
17
19
  ...commonsTools,
18
20
  ];
19
21
  if (config.enableSharedSteps) {
@@ -22,7 +24,10 @@ export function getToolsToRegister(config) {
22
24
  const allowWrite = config.allowWrite !== false;
23
25
  const allowRead = config.allowRead !== false;
24
26
  const allowDelete = config.allowDelete === true;
27
+ const enableDeprecatedTools = config.enableDeprecatedTools !== false;
25
28
  const filteredTools = tools.filter(tool => {
29
+ if (tool.deprecated && !enableDeprecatedTools)
30
+ return false;
26
31
  if (tool.mode === 'write')
27
32
  return allowWrite;
28
33
  if (tool.mode === 'read')
@@ -1,53 +1,23 @@
1
1
  import { z } from "zod";
2
- import { AttachmentSchema } from "./types.js";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- import * as os from "os";
6
- import archiver from "archiver";
2
+ import { AttachmentSchema } from "../attachments/types.js";
3
+ import { prepareUploadFile } from "../../utils/attachment_helper.js";
7
4
  const parameters = {
8
5
  run_id: z.number().describe("The ID of the test run to attach the file to"),
9
6
  file_path: z.string().describe("The path to the file or directory to attach. Directories will be automatically zipped."),
10
7
  };
8
+ /**
9
+ * @deprecated Use `add_attachment` tool from the attachments domain instead.
10
+ */
11
11
  export const addAttachmentToRunTool = {
12
12
  name: "add_attachment_to_run",
13
13
  mode: "write",
14
- description: "Add an attachment to a test run in TestRail. If the file_path points to a directory, it will be automatically zipped before uploading. Maximum upload size is 256MB.",
14
+ deprecated: true,
15
+ description: "Add an attachment to a test run in TestRail. (Deprecated: Prefer add_attachment tool). Maximum upload size is 256MB.",
15
16
  parameters,
16
17
  handler: async ({ run_id, file_path }, client) => {
17
- if (!fs.existsSync(file_path)) {
18
- throw new Error(`File or directory not found: ${file_path}`);
19
- }
20
- const stats = fs.statSync(file_path);
21
- let uploadPath = file_path;
22
- let filename = path.basename(file_path);
23
- let isTemporary = false;
24
- if (stats.isDirectory()) {
25
- const basename = path.basename(file_path);
26
- const tempDir = os.tmpdir();
27
- const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
28
- await new Promise((resolve, reject) => {
29
- const output = fs.createWriteStream(zipPath);
30
- const archive = archiver('zip', {
31
- zlib: { level: 9 }
32
- });
33
- output.on('close', () => resolve());
34
- archive.on('error', (err) => reject(err));
35
- archive.pipe(output);
36
- archive.directory(file_path, basename);
37
- archive.finalize();
38
- });
39
- uploadPath = zipPath;
40
- filename = `${basename}.zip`;
41
- isTemporary = true;
42
- }
43
- try {
44
- const result = await client.addAttachmentToRun(run_id, uploadPath, filename);
18
+ return prepareUploadFile(file_path, async (uploadPath, filename) => {
19
+ const result = await client.addAttachment('run', run_id, uploadPath, filename);
45
20
  return AttachmentSchema.parse(result);
46
- }
47
- finally {
48
- if (isTemporary && fs.existsSync(uploadPath)) {
49
- fs.unlinkSync(uploadPath);
50
- }
51
- }
21
+ });
52
22
  },
53
23
  };
@@ -1,6 +1,6 @@
1
+ import { addAttachmentToRunTool } from "./add_attachment_to_run.js";
1
2
  import { mutateRunTool } from "./mutate_run.js";
2
3
  import { queryRunTool } from "./query_run.js";
3
- import { addAttachmentToRunTool } from "./add_attachment_to_run.js";
4
4
  export const runsTools = [
5
5
  mutateRunTool,
6
6
  queryRunTool,
@@ -0,0 +1,58 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import archiver from "archiver";
5
+ /**
6
+ * Normalizes an entity ID (case ID or run ID).
7
+ * Handles string IDs with 'C' prefixes (e.g. 'C123' -> 123) and numeric IDs.
8
+ */
9
+ export function normalizeEntityId(id) {
10
+ const idString = typeof id === "string" ? id.trim() : String(id);
11
+ const cleaned = idString.toUpperCase().startsWith("C") ? idString.substring(1) : idString;
12
+ const parsed = Number(cleaned);
13
+ if (isNaN(parsed)) {
14
+ throw new Error(`Invalid entity ID: ${id}`);
15
+ }
16
+ return parsed;
17
+ }
18
+ /**
19
+ * Prepares a file or directory for upload.
20
+ * If the path points to a directory, it will automatically be zipped to a temporary archive.
21
+ * Ensures temporary zip files are cleaned up in a finally block.
22
+ */
23
+ export async function prepareUploadFile(filePath, uploadCallback) {
24
+ if (!fs.existsSync(filePath)) {
25
+ throw new Error(`File or directory not found: ${filePath}`);
26
+ }
27
+ const stats = fs.statSync(filePath);
28
+ let uploadPath = filePath;
29
+ let filename = path.basename(filePath);
30
+ let isTemporary = false;
31
+ if (stats.isDirectory()) {
32
+ const basename = path.basename(filePath);
33
+ const tempDir = os.tmpdir();
34
+ const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
35
+ await new Promise((resolve, reject) => {
36
+ const output = fs.createWriteStream(zipPath);
37
+ const archive = archiver("zip", {
38
+ zlib: { level: 9 },
39
+ });
40
+ output.on("close", () => resolve());
41
+ archive.on("error", (err) => reject(err));
42
+ archive.pipe(output);
43
+ archive.directory(filePath, basename);
44
+ archive.finalize();
45
+ });
46
+ uploadPath = zipPath;
47
+ filename = `${basename}.zip`;
48
+ isTemporary = true;
49
+ }
50
+ try {
51
+ return await uploadCallback(uploadPath, filename);
52
+ }
53
+ finally {
54
+ if (isTemporary && fs.existsSync(uploadPath)) {
55
+ fs.unlinkSync(uploadPath);
56
+ }
57
+ }
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uarlouski/testrail-mcp-server",
3
- "version": "2.2.0",
3
+ "version": "2.3.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",