@uarlouski/testrail-mcp-server 2.2.0 → 2.4.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 +28 -60
- package/dist/client/testrail.js +33 -6
- package/dist/index.js +8 -4
- package/dist/tools/attachments/add_attachment.js +26 -0
- package/dist/tools/attachments/index.js +6 -0
- package/dist/tools/attachments/query_attachment.js +44 -0
- package/dist/tools/attachments/types.js +27 -0
- package/dist/tools/cases/get_case.js +2 -2
- package/dist/tools/cases/get_case_history.js +66 -0
- package/dist/tools/cases/index.js +2 -0
- package/dist/tools/cases/types.js +16 -0
- package/dist/tools/cases/update_case.js +2 -2
- package/dist/tools/commons/delete_entity.js +10 -4
- package/dist/tools/registry.js +9 -1
- package/dist/tools/runs/add_attachment_to_run.js +10 -40
- package/dist/tools/runs/index.js +1 -1
- package/dist/utils/attachment_helper.js +46 -0
- package/dist/utils/sanitizer.js +13 -0
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -36,6 +36,9 @@ Managing test cases manually is tedious and error-prone. With the **TestRail MCP
|
|
|
36
36
|
|
|
37
37
|
**No context switching. No tedious copy-pasting. Just ask your AI.**
|
|
38
38
|
|
|
39
|
+
> [!NOTE]
|
|
40
|
+
> **Compatibility Baseline**: The primary baseline version this MCP server is tested and validated against is **TestRail 10.6.2** (API v2). Older TestRail instances (including pre-7.x pagination) are also supported via built-in backward compatibility.
|
|
41
|
+
|
|
39
42
|
## ✨ Key Features & Capabilities
|
|
40
43
|
|
|
41
44
|
| Capability | Description |
|
|
@@ -147,72 +150,37 @@ Once configured, turbo-charge your QA workflow by asking your AI assistant:
|
|
|
147
150
|
| `TESTRAIL_USERNAME` | Your TestRail user email address | ✅ | |
|
|
148
151
|
| `TESTRAIL_API_KEY` | Your TestRail API key ([Guide](https://support.testrail.com/hc/en-us/articles/7077039051412-Accessing-the-TestRail-API)) | ✅ | |
|
|
149
152
|
| `TESTRAIL_ENABLE_SHARED_STEPS` | Set to `true` to enable Shared Steps management tools | | `false` |
|
|
153
|
+
| `TESTRAIL_ENABLE_CASE_HISTORY` | Set to `true` to enable Case History and revision tracking tools | | `false` |
|
|
150
154
|
| `TESTRAIL_ALLOW_WRITE_OPERATIONS` | Allow write operations (e.g. adding/updating test cases, test runs, sections) | | `true` |
|
|
151
155
|
| `TESTRAIL_ALLOW_READ_OPERATIONS` | Allow read operations (e.g. retrieving projects, test cases, templates) | | `true` |
|
|
152
156
|
| `TESTRAIL_ALLOW_DELETE_OPERATIONS` | Allow delete operations (e.g. deleting cases or shared steps). Enabled strictly via `true`. | | `false` |
|
|
157
|
+
| `TESTRAIL_ENABLE_DEPRECATED_TOOLS` | Enable deprecated tools for backward compatibility. Set to `false` to reduce context token overhead. | | `true` |
|
|
158
|
+
|
|
159
|
+
### ⚠️ Deprecation Lifecycle & Features Scheduled for Removal
|
|
160
|
+
|
|
161
|
+
To ensure smooth transitions, deprecated tools remain available by default (`TESTRAIL_ENABLE_DEPRECATED_TOOLS=true`) and will be removed in future major releases:
|
|
162
|
+
|
|
163
|
+
| Deprecated Tool | Replacement | Status |
|
|
164
|
+
|-----------------|-------------|--------|
|
|
165
|
+
| `add_attachment_to_run` | `add_attachment` (`entity_type: "case" \| "run"`) | Deprecated in `2.3.0`, scheduled for removal in `3.0.0` |
|
|
166
|
+
|
|
167
|
+
> **💡 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
168
|
|
|
154
169
|
---
|
|
155
170
|
|
|
156
|
-
##
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
| `get_sections` | Navigate the precise folder/section hierarchy of any test suite. |
|
|
170
|
-
| `mutate_suite` | Create a new test suite or update an existing one in TestRail. |
|
|
171
|
-
| `mutate_section` | Create a new section or update an existing section in TestRail. |
|
|
172
|
-
| `get_users` | Retrieve active users. Handles per-project filtering and automatic fallback polling for non-admin accounts. |
|
|
173
|
-
|
|
174
|
-
### 📋 Test Case Management
|
|
175
|
-
| Tool | Functionality |
|
|
176
|
-
|------|-------------|
|
|
177
|
-
| `get_cases` | Query test cases with advanced filtering (priority, template, type, etc.) and pagination. |
|
|
178
|
-
| `get_case` | Fetch complete, structured details of a specific test case, including custom steps and fields. |
|
|
179
|
-
| `add_case` | Seamlessly create a new test case equipped with robust custom field validation. |
|
|
180
|
-
| `update_case` | Modify an existing test case's steps, metadata, or titles. |
|
|
181
|
-
| `update_cases` | Execute bulk-updates on multiple test cases simultaneously to save time. |
|
|
182
|
-
|
|
183
|
-
### ▶️ Test Execution & Tracking
|
|
184
|
-
| Tool | Functionality |
|
|
185
|
-
|------|-------------|
|
|
186
|
-
| `query_run` | Retrieve a single test run (`action: "one"`) or all test runs for a project (`action: "many"`, supporting filters). |
|
|
187
|
-
| `mutate_run` | Create a new test run or update an existing one in TestRail. |
|
|
188
|
-
| `get_tests` | Retrieve individual tests for a specific test run, with optional status filtering. |
|
|
189
|
-
| `get_results` | Retrieve paginated results for a specific test ID. |
|
|
190
|
-
| `add_results` | Submit test results to a test run using the specific `test_id`. |
|
|
191
|
-
| `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. |
|
|
193
|
-
|
|
194
|
-
### 🔗 Shared Steps (Optional)
|
|
195
|
-
| Tool | Functionality |
|
|
196
|
-
|------|-------------|
|
|
197
|
-
| `get_shared_steps` | List shared test steps for a project with optional reference filtering. |
|
|
198
|
-
| `get_shared_step` | Retrieve precise details of a specific shared test step set. |
|
|
199
|
-
| `get_shared_step_history` | View the complete audit trail and version history of a shared step set. |
|
|
200
|
-
| `add_shared_step` | Create a new reusable set of shared test steps. |
|
|
201
|
-
| `update_shared_step` | Modify an existing shared step set (changes propagate to all linked test cases). |
|
|
202
|
-
|
|
203
|
-
### 🗑️ Deletion
|
|
204
|
-
| Tool | Functionality |
|
|
205
|
-
|------|-------------|
|
|
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. |
|
|
207
|
-
|
|
208
|
-
### 🧠 System Metadata
|
|
209
|
-
| Tool | Functionality |
|
|
210
|
-
|------|-------------|
|
|
211
|
-
| `get_statuses` | Systematically list all configured test statuses (Passed, Failed, Blocked, Retest, etc.). |
|
|
212
|
-
| `get_priorities` | Retrieve priority levels configured within your instance structure. |
|
|
213
|
-
| `get_case_fields` | Discover custom field definitions, formats, and UI dropdown options. Supports filtering by `project_id`. |
|
|
214
|
-
| `get_templates` | Identify available case templates to mandate correct AI structuring. |
|
|
215
|
-
| `get_configurations` | Retrieve all configuration groups and configurations for a project. |
|
|
171
|
+
## 📚 Documentation & Complete Tool Reference
|
|
172
|
+
|
|
173
|
+
For a comprehensive guide, detailed configuration options, and a complete breakdown of all available tools, please visit our official documentation site:
|
|
174
|
+
|
|
175
|
+
**👉 [TestRail MCP Server Documentation](https://uarlouski.github.io/testrail-mcp-server/)**
|
|
176
|
+
|
|
177
|
+
The documentation includes detailed explanations for:
|
|
178
|
+
- 🔭 **Discovery & Navigation**: Exploring projects, suites, and sections.
|
|
179
|
+
- 📋 **Test Case Management**: Fetching, creating, and bulk-updating test cases.
|
|
180
|
+
- ▶️ **Execution & Tracking**: Managing test runs and submitting test results.
|
|
181
|
+
- 📎 **Attachments**: Automatically zipping and uploading files or directories.
|
|
182
|
+
- 🔗 **Shared Steps**: Managing reusable step definitions.
|
|
183
|
+
|
|
216
184
|
|
|
217
185
|
---
|
|
218
186
|
|
package/dist/client/testrail.js
CHANGED
|
@@ -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 {
|
|
@@ -24,6 +25,9 @@ export class TestRailClient {
|
|
|
24
25
|
async getCase(caseId) {
|
|
25
26
|
return this.get(`${API_BASE_V2}/get_case/${caseId}`);
|
|
26
27
|
}
|
|
28
|
+
async getCaseHistory(caseId) {
|
|
29
|
+
return this.paginateAll(`${API_BASE_V2}/get_history_for_case/${caseId}`, 'history');
|
|
30
|
+
}
|
|
27
31
|
async getCasesRecursively(projectId, sectionId, filter, excludedSectionNames) {
|
|
28
32
|
// Extract suite_id from filter if present, for getSections call
|
|
29
33
|
const suiteId = filter?.suite_id ? parseInt(filter.suite_id) : undefined;
|
|
@@ -178,7 +182,7 @@ export class TestRailClient {
|
|
|
178
182
|
async addResultsForCases(runId, results) {
|
|
179
183
|
return this.post(`${API_BASE_V2}/add_results_for_cases/${runId}`, { results });
|
|
180
184
|
}
|
|
181
|
-
async
|
|
185
|
+
async addAttachment(entityType, entityId, filePath, filename) {
|
|
182
186
|
const fileBuffer = await fs.promises.readFile(filePath);
|
|
183
187
|
const blob = new Blob([fileBuffer]);
|
|
184
188
|
const formData = new FormData();
|
|
@@ -186,7 +190,11 @@ export class TestRailClient {
|
|
|
186
190
|
const headers = {
|
|
187
191
|
'Authorization': this.auth,
|
|
188
192
|
};
|
|
189
|
-
return this._executeRequest('POST', `${API_BASE_V2}/
|
|
193
|
+
return this._executeRequest('POST', `${API_BASE_V2}/add_attachment_to_${entityType}/${entityId}`, headers, formData);
|
|
194
|
+
}
|
|
195
|
+
async getAttachments(entityType, entityId) {
|
|
196
|
+
const url = `${API_BASE_V2}/get_attachments_for_${entityType}/${entityId}`;
|
|
197
|
+
return this.paginateAll(url, 'attachments');
|
|
190
198
|
}
|
|
191
199
|
async getSharedSteps(projectId, options) {
|
|
192
200
|
let url = `${API_BASE_V2}/get_shared_steps/${projectId}`;
|
|
@@ -207,11 +215,26 @@ export class TestRailClient {
|
|
|
207
215
|
async updateSharedStep(sharedStepId, data) {
|
|
208
216
|
return this.post(`${API_BASE_V2}/update_shared_step/${sharedStepId}`, data);
|
|
209
217
|
}
|
|
218
|
+
async getAttachment(attachmentId, outputFilePath) {
|
|
219
|
+
const dir = path.dirname(outputFilePath);
|
|
220
|
+
if (dir && !fs.existsSync(dir)) {
|
|
221
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
const buffer = await this._executeRequest('GET', `${API_BASE_V2}/get_attachment/${attachmentId}`, this.headers, undefined, 'buffer');
|
|
224
|
+
await fs.promises.writeFile(outputFilePath, buffer);
|
|
225
|
+
return {
|
|
226
|
+
file: outputFilePath,
|
|
227
|
+
size: buffer.byteLength,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
210
230
|
async deleteCase(caseId) {
|
|
211
|
-
return this._executeRequest('POST', `${API_BASE_V2}/delete_case/${caseId}`, this.headers, JSON.stringify({}),
|
|
231
|
+
return this._executeRequest('POST', `${API_BASE_V2}/delete_case/${caseId}`, this.headers, JSON.stringify({}), 'text');
|
|
212
232
|
}
|
|
213
233
|
async deleteSharedStep(sharedStepId) {
|
|
214
|
-
return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}),
|
|
234
|
+
return this._executeRequest('POST', `${API_BASE_V2}/delete_shared_step/${sharedStepId}`, this.headers, JSON.stringify({}), 'text');
|
|
235
|
+
}
|
|
236
|
+
async deleteAttachment(attachmentId) {
|
|
237
|
+
return this._executeRequest('POST', `${API_BASE_V2}/delete_attachment/${attachmentId}`, this.headers, JSON.stringify({}), 'text');
|
|
215
238
|
}
|
|
216
239
|
async getUsers(projectId) {
|
|
217
240
|
const url = projectId
|
|
@@ -254,7 +277,7 @@ export class TestRailClient {
|
|
|
254
277
|
}
|
|
255
278
|
return this._executeRequest(method, endpoint, this.headers, jsonData);
|
|
256
279
|
}
|
|
257
|
-
async _executeRequest(method, endpoint, headers, body,
|
|
280
|
+
async _executeRequest(method, endpoint, headers, body, responseType = 'json') {
|
|
258
281
|
const url = `${this.baseUrl}${endpoint}`;
|
|
259
282
|
const params = {
|
|
260
283
|
method,
|
|
@@ -292,9 +315,13 @@ export class TestRailClient {
|
|
|
292
315
|
console.error(`[TestRailClient] Error Details: ${errorMessage}`);
|
|
293
316
|
throw new Error(errorMessage);
|
|
294
317
|
}
|
|
295
|
-
if (
|
|
318
|
+
if (responseType === 'json') {
|
|
296
319
|
return await response.json();
|
|
297
320
|
}
|
|
321
|
+
else if (responseType === 'buffer') {
|
|
322
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
323
|
+
return Buffer.from(arrayBuffer);
|
|
324
|
+
}
|
|
298
325
|
return await response.text();
|
|
299
326
|
}
|
|
300
327
|
catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -11,26 +11,30 @@ const EnvSchema = z.object({
|
|
|
11
11
|
TESTRAIL_USERNAME: z.email('Must be a valid email address'),
|
|
12
12
|
TESTRAIL_API_KEY: z.string().min(1, 'API key is required'),
|
|
13
13
|
TESTRAIL_ENABLE_SHARED_STEPS: z.string().optional().transform(val => val === 'true'),
|
|
14
|
+
TESTRAIL_ENABLE_CASE_HISTORY: z.string().optional().transform(val => val === 'true'),
|
|
14
15
|
TESTRAIL_ALLOW_WRITE_OPERATIONS: z.string().optional().transform(val => val === undefined ? true : val === 'true'),
|
|
15
16
|
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')
|
|
17
|
+
TESTRAIL_ALLOW_DELETE_OPERATIONS: z.string().optional().transform(val => val === 'true'),
|
|
18
|
+
TESTRAIL_ENABLE_DEPRECATED_TOOLS: z.string().optional().transform(val => val === undefined ? true : val === 'true')
|
|
17
19
|
});
|
|
18
20
|
const parseResult = EnvSchema.safeParse(process.env);
|
|
19
21
|
if (!parseResult.success) {
|
|
20
22
|
console.error("Invalid TestRail environment configuration:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
|
|
21
23
|
process.exit(1);
|
|
22
24
|
}
|
|
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;
|
|
25
|
+
const { TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY, TESTRAIL_ENABLE_SHARED_STEPS, TESTRAIL_ENABLE_CASE_HISTORY, TESTRAIL_ALLOW_WRITE_OPERATIONS, TESTRAIL_ALLOW_READ_OPERATIONS, TESTRAIL_ALLOW_DELETE_OPERATIONS, TESTRAIL_ENABLE_DEPRECATED_TOOLS } = parseResult.data;
|
|
24
26
|
const server = new McpServer({
|
|
25
27
|
name: "TestRail MCP Server",
|
|
26
|
-
version: "2.
|
|
28
|
+
version: "2.4.0",
|
|
27
29
|
});
|
|
28
30
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
29
31
|
const tools = getToolsToRegister({
|
|
30
32
|
enableSharedSteps: TESTRAIL_ENABLE_SHARED_STEPS,
|
|
33
|
+
enableCaseHistory: TESTRAIL_ENABLE_CASE_HISTORY,
|
|
31
34
|
allowWrite: TESTRAIL_ALLOW_WRITE_OPERATIONS,
|
|
32
35
|
allowRead: TESTRAIL_ALLOW_READ_OPERATIONS,
|
|
33
|
-
allowDelete: TESTRAIL_ALLOW_DELETE_OPERATIONS
|
|
36
|
+
allowDelete: TESTRAIL_ALLOW_DELETE_OPERATIONS,
|
|
37
|
+
enableDeprecatedTools: TESTRAIL_ENABLE_DEPRECATED_TOOLS
|
|
34
38
|
});
|
|
35
39
|
for (const tool of tools) {
|
|
36
40
|
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,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,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { processCustomFields } from "../../utils/mapper.js";
|
|
3
|
+
import { normalizeEntityId } from "../../utils/sanitizer.js";
|
|
3
4
|
import { LabelSchema } from "../commons/types.js";
|
|
4
5
|
const parameters = {
|
|
5
6
|
case_id: z.string().describe("The ID of the test case (e.g. '123' or 'C123')"),
|
|
@@ -10,8 +11,7 @@ export const getCaseTool = {
|
|
|
10
11
|
description: "Get detailed information about a test case including its custom fields",
|
|
11
12
|
parameters,
|
|
12
13
|
handler: async ({ case_id }, client) => {
|
|
13
|
-
const
|
|
14
|
-
const id = Number(idString);
|
|
14
|
+
const id = normalizeEntityId(case_id);
|
|
15
15
|
const testCase = await client.getCase(id);
|
|
16
16
|
const [section, caseTypes, priorities, caseFields] = await Promise.all([
|
|
17
17
|
client.getSection(Number(testCase.section_id)),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { normalizeEntityId } from "../../utils/sanitizer.js";
|
|
5
|
+
import { CaseHistoryEntrySchema } from "./types.js";
|
|
6
|
+
const parameters = {
|
|
7
|
+
case_id: z.string().describe("The ID of the test case (e.g. '123' or 'C123')"),
|
|
8
|
+
limit: z.number().min(1).optional().describe("Maximum number of revisions to return (e.g. 5 for the last 5 revisions)"),
|
|
9
|
+
after_revision: z.number().optional().describe("Filter to only include revisions strictly newer than this revision ID (id > after_revision)"),
|
|
10
|
+
after_timestamp: z.number().optional().describe("Filter to only include revisions created after this unix timestamp (created_on > after_timestamp)"),
|
|
11
|
+
order: z.enum(["desc", "asc"]).optional().default("desc").describe("Sort order for the returned history entries ('desc' for newest-to-oldest, 'asc' for oldest-to-newest). Default is 'desc'"),
|
|
12
|
+
output_file: z.string().optional().describe("Absolute file path to save the JSON response to. Useful for large history change logs to avoid context limits."),
|
|
13
|
+
};
|
|
14
|
+
export const getCaseHistoryTool = {
|
|
15
|
+
name: "get_case_history",
|
|
16
|
+
mode: "read",
|
|
17
|
+
description: "Returns the revision and change history of a test case. Useful for tracking case updates, checking changes since a specific revision or timestamp, or getting the last N revisions.",
|
|
18
|
+
parameters,
|
|
19
|
+
handler: async ({ case_id, limit, after_revision, after_timestamp, order = "desc", output_file }, client) => {
|
|
20
|
+
const id = normalizeEntityId(case_id);
|
|
21
|
+
const allHistory = await client.getCaseHistory(id);
|
|
22
|
+
let latestRevision = null;
|
|
23
|
+
if (allHistory.length > 0) {
|
|
24
|
+
const sortedByRevisionDesc = [...allHistory].sort((a, b) => b.id - a.id);
|
|
25
|
+
latestRevision = sortedByRevisionDesc[0].id;
|
|
26
|
+
}
|
|
27
|
+
let filtered = [...allHistory];
|
|
28
|
+
if (after_revision !== undefined) {
|
|
29
|
+
filtered = filtered.filter(entry => entry.id > after_revision);
|
|
30
|
+
}
|
|
31
|
+
if (after_timestamp !== undefined) {
|
|
32
|
+
filtered = filtered.filter(entry => (entry.created_on ?? 0) > after_timestamp);
|
|
33
|
+
}
|
|
34
|
+
filtered.sort((a, b) => {
|
|
35
|
+
if (order === "asc") {
|
|
36
|
+
return a.id - b.id;
|
|
37
|
+
}
|
|
38
|
+
return b.id - a.id;
|
|
39
|
+
});
|
|
40
|
+
if (limit !== undefined) {
|
|
41
|
+
filtered = filtered.slice(0, limit);
|
|
42
|
+
}
|
|
43
|
+
const parsedHistory = filtered.map(h => CaseHistoryEntrySchema.parse(h));
|
|
44
|
+
const response = {
|
|
45
|
+
case_id: id,
|
|
46
|
+
latest_revision: latestRevision,
|
|
47
|
+
has_changes: parsedHistory.length > 0,
|
|
48
|
+
history: parsedHistory,
|
|
49
|
+
};
|
|
50
|
+
if (output_file) {
|
|
51
|
+
const dir = path.dirname(output_file);
|
|
52
|
+
if (dir && !fs.existsSync(dir)) {
|
|
53
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
await fs.promises.writeFile(output_file, JSON.stringify(response, null, 2), "utf-8");
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
message: `Successfully exported ${response.history.length} history records to ${output_file}`,
|
|
59
|
+
file: output_file,
|
|
60
|
+
latest_revision: latestRevision,
|
|
61
|
+
has_changes: response.has_changes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return response;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -4,6 +4,8 @@ import { getCaseFieldsTool } from "./get_case_fields.js";
|
|
|
4
4
|
import { updateCaseTool } from "./update_case.js";
|
|
5
5
|
import { updateCasesTool } from "./update_cases.js";
|
|
6
6
|
import { addCaseTool } from "./add_case.js";
|
|
7
|
+
import { getCaseHistoryTool } from "./get_case_history.js";
|
|
8
|
+
export { getCaseHistoryTool };
|
|
7
9
|
export const casesTools = [
|
|
8
10
|
getCaseTool,
|
|
9
11
|
getCasesTool,
|
|
@@ -38,3 +38,19 @@ export const CaseSchema = z.object({
|
|
|
38
38
|
suite_id: z.number(),
|
|
39
39
|
labels: z.array(LabelSchema),
|
|
40
40
|
}).loose();
|
|
41
|
+
export const CaseHistoryChangeSchema = z.object({
|
|
42
|
+
field: z.string(),
|
|
43
|
+
old_value: z.any().nullable().optional(),
|
|
44
|
+
new_value: z.any().nullable().optional(),
|
|
45
|
+
});
|
|
46
|
+
export const CaseHistoryEntrySchema = z.object({
|
|
47
|
+
id: z.number(),
|
|
48
|
+
created_on: z.number().optional(),
|
|
49
|
+
changes: z.array(CaseHistoryChangeSchema).optional().default([]),
|
|
50
|
+
});
|
|
51
|
+
export const CaseHistoryResponseSchema = z.object({
|
|
52
|
+
case_id: z.number(),
|
|
53
|
+
latest_revision: z.number().nullable(),
|
|
54
|
+
has_changes: z.boolean(),
|
|
55
|
+
history: z.array(CaseHistoryEntrySchema),
|
|
56
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { validateCaseFields } from "../../utils/validator.js";
|
|
3
|
+
import { normalizeEntityId } from "../../utils/sanitizer.js";
|
|
3
4
|
import { CASE_FIELDS_PARAM_DESCRIPTION } from "./get_case_fields.js";
|
|
4
5
|
const parameters = {
|
|
5
6
|
case_id: z.string().describe("The ID of the test case to update (e.g. '123' or 'C123')"),
|
|
@@ -17,8 +18,7 @@ export const updateCaseTool = {
|
|
|
17
18
|
parameters,
|
|
18
19
|
handler: async ({ case_id, fields }, client) => {
|
|
19
20
|
validateCaseFields(fields, await client.getCaseFields());
|
|
20
|
-
const
|
|
21
|
-
const id = Number(idString);
|
|
21
|
+
const id = normalizeEntityId(case_id);
|
|
22
22
|
const updatedCase = await client.updateCase(id, fields);
|
|
23
23
|
return {
|
|
24
24
|
success: true,
|
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { casesTools } from "./cases/index.js";
|
|
1
|
+
import { casesTools, getCaseHistoryTool } from "./cases/index.js";
|
|
2
2
|
import { projectsTools } from "./projects/index.js";
|
|
3
3
|
import { suitesTools } from "./suites/index.js";
|
|
4
4
|
import { sectionsTools } from "./sections/index.js";
|
|
@@ -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,15 +15,22 @@ 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) {
|
|
20
22
|
tools.push(...sharedStepsTools);
|
|
21
23
|
}
|
|
24
|
+
if (config.enableCaseHistory) {
|
|
25
|
+
tools.push(getCaseHistoryTool);
|
|
26
|
+
}
|
|
22
27
|
const allowWrite = config.allowWrite !== false;
|
|
23
28
|
const allowRead = config.allowRead !== false;
|
|
24
29
|
const allowDelete = config.allowDelete === true;
|
|
30
|
+
const enableDeprecatedTools = config.enableDeprecatedTools !== false;
|
|
25
31
|
const filteredTools = tools.filter(tool => {
|
|
32
|
+
if (tool.deprecated && !enableDeprecatedTools)
|
|
33
|
+
return false;
|
|
26
34
|
if (tool.mode === 'write')
|
|
27
35
|
return allowWrite;
|
|
28
36
|
if (tool.mode === 'read')
|
|
@@ -1,53 +1,23 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { AttachmentSchema } from "
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
};
|
package/dist/tools/runs/index.js
CHANGED
|
@@ -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,46 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import archiver from "archiver";
|
|
5
|
+
export { normalizeEntityId } from "./sanitizer.js";
|
|
6
|
+
/**
|
|
7
|
+
* Prepares a file or directory for upload.
|
|
8
|
+
* If the path points to a directory, it will automatically be zipped to a temporary archive.
|
|
9
|
+
* Ensures temporary zip files are cleaned up in a finally block.
|
|
10
|
+
*/
|
|
11
|
+
export async function prepareUploadFile(filePath, uploadCallback) {
|
|
12
|
+
if (!fs.existsSync(filePath)) {
|
|
13
|
+
throw new Error(`File or directory not found: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
const stats = fs.statSync(filePath);
|
|
16
|
+
let uploadPath = filePath;
|
|
17
|
+
let filename = path.basename(filePath);
|
|
18
|
+
let isTemporary = false;
|
|
19
|
+
if (stats.isDirectory()) {
|
|
20
|
+
const basename = path.basename(filePath);
|
|
21
|
+
const tempDir = os.tmpdir();
|
|
22
|
+
const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
const output = fs.createWriteStream(zipPath);
|
|
25
|
+
const archive = archiver("zip", {
|
|
26
|
+
zlib: { level: 9 },
|
|
27
|
+
});
|
|
28
|
+
output.on("close", () => resolve());
|
|
29
|
+
archive.on("error", (err) => reject(err));
|
|
30
|
+
archive.pipe(output);
|
|
31
|
+
archive.directory(filePath, basename);
|
|
32
|
+
archive.finalize();
|
|
33
|
+
});
|
|
34
|
+
uploadPath = zipPath;
|
|
35
|
+
filename = `${basename}.zip`;
|
|
36
|
+
isTemporary = true;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return await uploadCallback(uploadPath, filename);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (isTemporary && fs.existsSync(uploadPath)) {
|
|
43
|
+
fs.unlinkSync(uploadPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/utils/sanitizer.js
CHANGED
|
@@ -55,3 +55,16 @@ export function isActive(value) {
|
|
|
55
55
|
const activeFlag = (typeof value === 'object') ? value.is_active : value;
|
|
56
56
|
return activeFlag === 1 || activeFlag === true || activeFlag === '1' || activeFlag === 'true';
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Normalizes an entity ID (e.g. case ID, run ID, attachment ID).
|
|
60
|
+
* Handles string IDs with 'C' prefixes (e.g. 'C123' -> 123), numeric IDs, and trims whitespace.
|
|
61
|
+
*/
|
|
62
|
+
export function normalizeEntityId(id) {
|
|
63
|
+
const idString = typeof id === "string" ? id.trim() : String(id);
|
|
64
|
+
const cleaned = idString.toUpperCase().startsWith("C") ? idString.substring(1) : idString;
|
|
65
|
+
const parsed = Number(cleaned);
|
|
66
|
+
if (isNaN(parsed)) {
|
|
67
|
+
throw new Error(`Invalid entity ID: ${id}`);
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uarlouski/testrail-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|
|
@@ -14,13 +14,21 @@
|
|
|
14
14
|
"keywords": [
|
|
15
15
|
"mcp",
|
|
16
16
|
"model-context-protocol",
|
|
17
|
+
"mcp-server",
|
|
17
18
|
"testrail",
|
|
18
19
|
"server",
|
|
19
20
|
"typescript",
|
|
20
21
|
"llm",
|
|
21
22
|
"ai",
|
|
23
|
+
"ai-agents",
|
|
22
24
|
"testing",
|
|
23
|
-
"automation"
|
|
25
|
+
"automation",
|
|
26
|
+
"qa-automation",
|
|
27
|
+
"qa-tools",
|
|
28
|
+
"test-management",
|
|
29
|
+
"claude",
|
|
30
|
+
"cursor",
|
|
31
|
+
"windsurf"
|
|
24
32
|
],
|
|
25
33
|
"author": "Uladzislau Arlouski",
|
|
26
34
|
"repository": {
|
|
@@ -39,6 +47,7 @@
|
|
|
39
47
|
"zod": "^4.4.3"
|
|
40
48
|
},
|
|
41
49
|
"devDependencies": {
|
|
50
|
+
"@jest/globals": "^29.7.0",
|
|
42
51
|
"@types/archiver": "^7.0.0",
|
|
43
52
|
"@types/jest": "^30.0.0",
|
|
44
53
|
"@types/node": "^25.5.0",
|
|
@@ -47,7 +56,8 @@
|
|
|
47
56
|
"ts-jest": "^29.4.6",
|
|
48
57
|
"ts-node": "^10.9.2",
|
|
49
58
|
"typescript": "^5.9.3",
|
|
50
|
-
"
|
|
59
|
+
"vitepress": "^1.6.4",
|
|
60
|
+
"vue": "^3.5.41"
|
|
51
61
|
},
|
|
52
62
|
"devEngines": {
|
|
53
63
|
"packageManager": {
|
|
@@ -61,6 +71,9 @@
|
|
|
61
71
|
"start": "node dist/index.js",
|
|
62
72
|
"dev": "ts-node src/index.ts",
|
|
63
73
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
64
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
|
|
74
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
75
|
+
"docs:dev": "vitepress dev docs",
|
|
76
|
+
"docs:build": "vitepress build docs",
|
|
77
|
+
"docs:preview": "vitepress preview docs"
|
|
65
78
|
}
|
|
66
79
|
}
|