@smartbear/mcp 0.22.0 → 0.24.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 -4
- package/dist/bearq/client.js +81 -0
- package/dist/bearq/config/constants.js +6 -0
- package/dist/bearq/tool/tasks/chat-with-qa-lead.js +32 -0
- package/dist/bearq/tool/tasks/expand-application-model.js +34 -0
- package/dist/bearq/tool/tasks/get-task-status.js +32 -0
- package/dist/bearq/tool/tasks/get-task.js +29 -0
- package/dist/bearq/tool/tasks/refine-all-draft-tests.js +27 -0
- package/dist/bearq/tool/tasks/refine-test-cases.js +30 -0
- package/dist/bearq/tool/tasks/refine-tests-in-functional-areas.js +34 -0
- package/dist/bearq/tool/tasks/run-regression-tests.js +27 -0
- package/dist/bearq/tool/tasks/run-test-cases.js +30 -0
- package/dist/bearq/tool/tasks/run-tests-in-functional-areas.js +30 -0
- package/dist/bearq/tool/tasks/stop-task.js +29 -0
- package/dist/bearq/tool/tasks/wait-for-task.js +83 -0
- package/dist/common/register-clients.js +2 -0
- package/dist/package.json.js +1 -1
- package/dist/pactflow/client/base.js +3 -3
- package/dist/qtm4j/client.js +26 -2
- package/dist/qtm4j/config/constants.js +96 -4
- package/dist/qtm4j/config/field-resolution.types.js +2 -1
- package/dist/qtm4j/http/api-client.js +70 -1
- package/dist/qtm4j/resolver/cache/cache.js +1 -1
- package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +1 -0
- package/dist/qtm4j/resolver/resolvers/component-resolver.js +2 -0
- package/dist/qtm4j/resolver/resolvers/label-resolver.js +2 -0
- package/dist/qtm4j/schema/automation.schema.js +107 -0
- package/dist/qtm4j/schema/search-test-cycle.schema.js +133 -0
- package/dist/qtm4j/schema/test-cycle.schema.js +39 -0
- package/dist/qtm4j/schema/update-test-cycle.schema.js +54 -0
- package/dist/qtm4j/tool/test-automation/get-automation-history.js +69 -0
- package/dist/qtm4j/tool/test-automation/upload-automation-result.js +143 -0
- package/dist/qtm4j/tool/test-cycle/create-test-cycle.js +80 -0
- package/dist/qtm4j/tool/test-cycle/search-test-cycle.js +136 -0
- package/dist/qtm4j/tool/test-cycle/update-test-cycle.js +161 -0
- package/dist/swagger/client/portal-types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as zod from "zod";
|
|
2
|
+
const CreateTestCycleBody = zod.object({
|
|
3
|
+
summary: zod.string().min(1).max(255).describe(
|
|
4
|
+
"Short title of the test cycle. Must not be blank. Max 255 chars."
|
|
5
|
+
),
|
|
6
|
+
description: zod.string().max(65535).optional().describe("Detailed description of the test cycle. Max 65 535 characters."),
|
|
7
|
+
priority: zod.string().optional().describe(
|
|
8
|
+
"Priority name (e.g., 'High', 'Medium', 'Low'). Auto-resolved to ID."
|
|
9
|
+
),
|
|
10
|
+
status: zod.string().optional().describe(
|
|
11
|
+
"Status name (e.g., 'To Do', 'In Progress', 'Done'). Auto-resolved to ID."
|
|
12
|
+
),
|
|
13
|
+
assignee: zod.string().optional().describe("Assignee account ID"),
|
|
14
|
+
reporter: zod.string().optional().describe("Reporter account ID"),
|
|
15
|
+
labels: zod.array(zod.string()).optional().describe(
|
|
16
|
+
"List of label names (e.g., ['Release_1', 'Sprint 1']). Auto-resolved to IDs."
|
|
17
|
+
),
|
|
18
|
+
components: zod.array(zod.string()).optional().describe(
|
|
19
|
+
"List of component names (e.g., ['UI', 'Cloud']). Auto-resolved to IDs."
|
|
20
|
+
),
|
|
21
|
+
plannedStartDate: zod.string().regex(/^\d{2}\/[A-Za-z]{3}\/\d{4} \d{2}:\d{2}$/).optional().describe(
|
|
22
|
+
"Planned start date. Format: 'dd/MMM/yyyy HH:mm' e.g. '10/May/2026 00:00'. Must be ≤ plannedEndDate when both are provided."
|
|
23
|
+
),
|
|
24
|
+
plannedEndDate: zod.string().regex(/^\d{2}\/[A-Za-z]{3}\/\d{4} \d{2}:\d{2}$/).optional().describe(
|
|
25
|
+
"Planned end date. Format: 'dd/MMM/yyyy HH:mm' e.g. '15/May/2026 00:00'. Must be ≥ plannedStartDate when both are provided."
|
|
26
|
+
)
|
|
27
|
+
});
|
|
28
|
+
const CreateTestCycleResponse = zod.object({
|
|
29
|
+
id: zod.string().describe(
|
|
30
|
+
"Opaque permanent identifier of the created test cycle. Use this in all subsequent API calls."
|
|
31
|
+
),
|
|
32
|
+
key: zod.string().describe(
|
|
33
|
+
"Human-readable project-scoped key in format '<PROJECT_KEY>-TR-<number>'. e.g. 'TRWT-TR-218'."
|
|
34
|
+
)
|
|
35
|
+
});
|
|
36
|
+
export {
|
|
37
|
+
CreateTestCycleBody,
|
|
38
|
+
CreateTestCycleResponse
|
|
39
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as zod from "zod";
|
|
2
|
+
const MetadataAddDelete = zod.object({
|
|
3
|
+
add: zod.array(zod.string()).optional().describe(
|
|
4
|
+
"Names to add (e.g., ['Regression', 'Smoke']). Auto-resolved to IDs."
|
|
5
|
+
),
|
|
6
|
+
delete: zod.array(zod.string()).optional().describe("Names to remove (e.g., ['Sprint1']). Auto-resolved to IDs.")
|
|
7
|
+
}).describe(
|
|
8
|
+
"Add or remove entries by name. Both add and delete are optional — omit either to skip that operation."
|
|
9
|
+
);
|
|
10
|
+
const DATETIME_REGEX = /^\d{2}\/[A-Za-z]{3}\/\d{4} \d{2}:\d{2}$/;
|
|
11
|
+
const DATETIME_DESCRIPTION = "Format: 'dd/MMM/yyyy HH:mm' e.g. '15/May/2026 09:00'. Month must be capitalised (May not may). Pass null to clear the existing value.";
|
|
12
|
+
const UpdateTestCycleBody = zod.object({
|
|
13
|
+
key: zod.string().describe(
|
|
14
|
+
"Test cycle key in the format '{PROJECT_KEY}-TR-{number}', e.g. 'SCRUM-TR-101'. Used directly as the API path parameter."
|
|
15
|
+
),
|
|
16
|
+
summary: zod.string().min(1, "Summary cannot be blank.").max(255, "Summary must not exceed 255 characters.").optional().describe("Updated test cycle name / title. Max 255 characters."),
|
|
17
|
+
description: zod.string().max(65535, "Description must not exceed 65,535 characters.").nullable().optional().describe(
|
|
18
|
+
"Updated description. Pass null to clear the existing value. Max 65 535 characters."
|
|
19
|
+
),
|
|
20
|
+
status: zod.string().nullable().optional().describe(
|
|
21
|
+
"Status name (e.g., 'To Do', 'In Progress', 'Done'). Auto-resolved to ID. Use values from set_project_context response. Pass null to clear."
|
|
22
|
+
),
|
|
23
|
+
priority: zod.string().nullable().optional().describe(
|
|
24
|
+
"Priority name (e.g., 'High', 'Medium', 'Low'). Auto-resolved to ID. Use values from set_project_context response. Pass null to clear."
|
|
25
|
+
),
|
|
26
|
+
plannedStartDate: zod.string().regex(
|
|
27
|
+
DATETIME_REGEX,
|
|
28
|
+
"Invalid format. Use dd/MMM/yyyy HH:mm e.g. 15/May/2026 09:00"
|
|
29
|
+
).nullable().optional().describe(DATETIME_DESCRIPTION),
|
|
30
|
+
plannedEndDate: zod.string().regex(
|
|
31
|
+
DATETIME_REGEX,
|
|
32
|
+
"Invalid format. Use dd/MMM/yyyy HH:mm e.g. 30/May/2026 18:00"
|
|
33
|
+
).nullable().optional().describe(DATETIME_DESCRIPTION),
|
|
34
|
+
assignee: zod.string().nullable().optional().describe(
|
|
35
|
+
"Assignee Jira account ID (e.g., '5b10a2844c20165700ede21f'). Pass null to unassign."
|
|
36
|
+
),
|
|
37
|
+
reporter: zod.string().nullable().optional().describe(
|
|
38
|
+
"Reporter Jira account ID (e.g., '5b10a2844c20165700ede21f'). Pass null to clear."
|
|
39
|
+
),
|
|
40
|
+
labels: MetadataAddDelete.optional().describe(
|
|
41
|
+
"Labels to add or remove by name. Each name is auto-resolved to its ID."
|
|
42
|
+
),
|
|
43
|
+
components: MetadataAddDelete.optional().describe(
|
|
44
|
+
"Components to add or remove by name. Each name is auto-resolved to its ID."
|
|
45
|
+
)
|
|
46
|
+
});
|
|
47
|
+
const UpdateTestCycleResponse = zod.object({
|
|
48
|
+
key: zod.string().describe("Human-readable key of the updated test cycle"),
|
|
49
|
+
updated: zod.literal(true).describe("Confirms the update was applied")
|
|
50
|
+
});
|
|
51
|
+
export {
|
|
52
|
+
UpdateTestCycleBody,
|
|
53
|
+
UpdateTestCycleResponse
|
|
54
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Tool } from "../../../common/tools.js";
|
|
2
|
+
import { TOOL_NAMES, ENDPOINTS } from "../../config/constants.js";
|
|
3
|
+
import { GetAutomationHistoryResponse, GetAutomationHistoryBody } from "../../schema/automation.schema.js";
|
|
4
|
+
class GetAutomationHistory extends Tool {
|
|
5
|
+
specification = {
|
|
6
|
+
title: TOOL_NAMES.GET_AUTOMATION_HISTORY.TITLE,
|
|
7
|
+
summary: TOOL_NAMES.GET_AUTOMATION_HISTORY.SUMMARY,
|
|
8
|
+
readOnly: true,
|
|
9
|
+
idempotent: true,
|
|
10
|
+
inputSchema: GetAutomationHistoryBody,
|
|
11
|
+
outputSchema: GetAutomationHistoryResponse,
|
|
12
|
+
purpose: "Retrieve a paginated list of past automation result uploads from QTM4J. Use this to review upload history, check upload statuses, and audit past CI/CD automation runs. No set_project_context call is required.",
|
|
13
|
+
useCases: [
|
|
14
|
+
"Review past automation result uploads for a project",
|
|
15
|
+
"Check the status of recent automation imports",
|
|
16
|
+
"Audit CI/CD automation upload history",
|
|
17
|
+
"Paginate through all historical automation uploads"
|
|
18
|
+
],
|
|
19
|
+
examples: [
|
|
20
|
+
{
|
|
21
|
+
description: "Get the first page of automation upload history (default page size 20)",
|
|
22
|
+
parameters: {},
|
|
23
|
+
expectedOutput: "Paginated list of automation history records with upload status and metadata"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
description: "Get the second page of automation upload history",
|
|
27
|
+
parameters: {
|
|
28
|
+
startAt: 20,
|
|
29
|
+
maxResults: 20
|
|
30
|
+
},
|
|
31
|
+
expectedOutput: "Next 20 automation history records"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: "Get up to 50 records starting from the beginning",
|
|
35
|
+
parameters: {
|
|
36
|
+
startAt: 0,
|
|
37
|
+
maxResults: 50
|
|
38
|
+
},
|
|
39
|
+
expectedOutput: "Up to 50 automation history records"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
hints: [
|
|
43
|
+
"NO PROJECT CONTEXT REQUIRED: Do NOT call set_project_context and do NOT ask the user for a project key, project ID, or any other project details. This tool works independently.",
|
|
44
|
+
"PAGINATION: startAt is zero-indexed (default: 0), maxResults controls page size (default: 20, max: 100). Increment startAt by maxResults to fetch the next page.",
|
|
45
|
+
"Returns an empty data array (not an error) when no history records exist.",
|
|
46
|
+
"DISPLAY FORMAT: Show '1–N of <total>' above all cards. Render each record as a card separated by --- dividers. Each card has two sections:\n\nPRIMARY SECTION (always first): heading with status emoji (✅ SUCCESS / ❌ FAILED) + test cycle key and name; then format, start→end time, message, summary stats (test cases/versions created/reused/test steps), tracking ID.\n\nEXTRA DETAILS SECTION (at the bottom of the card, under a 'Details' sub-label): any remaining non-null fields from the record such as fileSize, extraAttributes values, etc.\n\nSkip any field that is null, missing, or false. NEVER show the raw fileName. NEVER use a table."
|
|
47
|
+
],
|
|
48
|
+
outputDescription: "Paginated list of automation import history. Each record includes: format, processStatus, importStatus, startTime, endTime, trackingId, detailedMessage, and a summary array. summary[0] contains: testCycleIssueKey, testCycleSummary, testCasesCreated, testCaseVersionsCreated, testCaseVersionsReused, testStepsCreated. Render as individual cards separated by dividers, NOT a table. Show '1–N of total' count above. Never show raw fileName."
|
|
49
|
+
};
|
|
50
|
+
handle = async (rawArgs) => {
|
|
51
|
+
const args = GetAutomationHistoryBody.parse(rawArgs);
|
|
52
|
+
const apiClient = this.client.getApiClient();
|
|
53
|
+
const response = await apiClient.getAutomation(
|
|
54
|
+
ENDPOINTS.AUTOMATION_HISTORY,
|
|
55
|
+
{
|
|
56
|
+
startAt: args.startAt,
|
|
57
|
+
maxResults: args.maxResults
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const result = GetAutomationHistoryResponse.parse(response);
|
|
61
|
+
return {
|
|
62
|
+
structuredContent: result,
|
|
63
|
+
content: []
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
GetAutomationHistory
|
|
69
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { Tool, ToolError } from "../../../common/tools.js";
|
|
4
|
+
import { TOOL_NAMES, AUTOMATION_RESULT_DIRS, AUTOMATION_LIMITS, ENDPOINTS } from "../../config/constants.js";
|
|
5
|
+
import { UploadAutomationResultResponse, UploadAutomationResultBody } from "../../schema/automation.schema.js";
|
|
6
|
+
const SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".xml", ".json", ".zip"]);
|
|
7
|
+
class UploadAutomationResult extends Tool {
|
|
8
|
+
specification = {
|
|
9
|
+
title: TOOL_NAMES.UPLOAD_AUTOMATION_RESULT.TITLE,
|
|
10
|
+
summary: TOOL_NAMES.UPLOAD_AUTOMATION_RESULT.SUMMARY,
|
|
11
|
+
readOnly: false,
|
|
12
|
+
idempotent: false,
|
|
13
|
+
inputSchema: UploadAutomationResultBody,
|
|
14
|
+
outputSchema: UploadAutomationResultResponse,
|
|
15
|
+
purpose: `Upload an automation result file from disk to QTM4J and map results to a test cycle. No set_project_context call is required — this tool works independently of any project context. When the user asks to upload or import automation results, search these directories: ${AUTOMATION_RESULT_DIRS.join(", ")}. Infer the format from the file name where possible; confirm with the user only when the format is ambiguous. Returns a trackingId to track the asynchronous import progress.`,
|
|
16
|
+
useCases: [
|
|
17
|
+
"Upload automation results to QTM4J",
|
|
18
|
+
"Import test results from a CI/CD pipeline run",
|
|
19
|
+
"Link test results to an existing test cycle",
|
|
20
|
+
"Create a new test cycle from automation results",
|
|
21
|
+
"Upload JUnit, TestNG, Cucumber, QAF, HP UFT, or SpecFlow result files"
|
|
22
|
+
],
|
|
23
|
+
examples: [
|
|
24
|
+
{
|
|
25
|
+
description: "User says 'upload my test results to QTM4J' — scan workspace, find single result file, confirm and upload",
|
|
26
|
+
parameters: {
|
|
27
|
+
filePath: "./target/surefire-reports/TEST-results.xml",
|
|
28
|
+
format: "junit"
|
|
29
|
+
},
|
|
30
|
+
expectedOutput: "trackingId returned; import processing started in QTM4J"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
description: "User wants results linked to an existing test cycle",
|
|
34
|
+
parameters: {
|
|
35
|
+
filePath: "./reports/cucumber.json",
|
|
36
|
+
format: "cucumber",
|
|
37
|
+
testCycleToReuse: "TR-PRJ-5",
|
|
38
|
+
environment: "Chrome",
|
|
39
|
+
build: "2.1.0"
|
|
40
|
+
},
|
|
41
|
+
expectedOutput: "Results mapped to test cycle TR-PRJ-5"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
description: "Upload QAF ZIP and set test cycle metadata",
|
|
45
|
+
parameters: {
|
|
46
|
+
filePath: "./results/qaf-results.zip",
|
|
47
|
+
format: "qaf",
|
|
48
|
+
isZip: true,
|
|
49
|
+
fields: {
|
|
50
|
+
testCycle: {
|
|
51
|
+
summary: "Regression Run 2024-Q1",
|
|
52
|
+
labels: ["regression"],
|
|
53
|
+
priority: "High"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
expectedOutput: "ZIP uploaded; test cycle created with summary, labels, and priority"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
description: "User provides an unrecognised priority value — do NOT silently map to a similar word; ask the user first",
|
|
61
|
+
parameters: {
|
|
62
|
+
filePath: "./reports/cucumber.json",
|
|
63
|
+
format: "cucumber",
|
|
64
|
+
fields: { testCycle: { priority: "critical" } }
|
|
65
|
+
},
|
|
66
|
+
expectedOutput: "Tool is NOT called yet. Inform the user that 'critical' was not recognised as a valid priority and ask them to confirm the correct value (e.g. from the available options). Do not map 'critical' to 'Blocker' or any other value without explicit user confirmation."
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
hints: [
|
|
70
|
+
"NO PROJECT CONTEXT REQUIRED: Do NOT call set_project_context and do NOT ask the user for a project key, project ID, or any other project details. This tool works independently — never prompt the user for project information.",
|
|
71
|
+
`FILE DISCOVERY: Always do a fresh scan — never reuse a path from a previous turn. If no path is provided, search in order: ${AUTOMATION_RESULT_DIRS.join(", ")}. If exactly one file is found, show the path and inferred format to the user and confirm before uploading. If multiple files are found, list them all and wait for the user to pick one. If nothing is found, ask for the path. Never pick or upload silently.`,
|
|
72
|
+
"FORMAT INFERENCE: .json → cucumber (unambiguous). For .xml, infer from the file name — 'junit'/'surefire' → junit, 'testng' → testng, 'specflow' → specflow, 'hpuft'/'uft' → hpuft. For .zip, ALWAYS set isZip: true, but do NOT assume qaf — the zip could contain junit, testng, or cucumber results; if the format cannot be determined from the file name, ask the user. If the file name gives no clear signal for .xml either, ask the user to confirm the format.",
|
|
73
|
+
"TEST CYCLE: Only ask for testCycleToReuse if the user explicitly wants to link to an existing cycle. If not mentioned, omit it — QTM4J creates a new test cycle automatically.",
|
|
74
|
+
"DATE FORMAT: plannedStartDate and plannedEndDate in fields.testCycle MUST be formatted as 'dd/MMM/yyyy HH:mm' (e.g. '14/May/2026 10:30'). Convert any user-provided date (ISO, natural language, relative) to this exact format before sending.",
|
|
75
|
+
"FOLDER ID: folderId is a numeric ID. Apply it ONLY to the level the user specifies; if unspecified, default to fields.testCycle only — never copy it to both levels. Get the ID from the user directly (right-click folder in QTM4J → 'Copy Folder Id').",
|
|
76
|
+
"ASSIGNEE / REPORTER: assignee and reporter in fields.testCycle and fields.testCase require a Jira Account ID (not a display name or email). Ask the user to provide their Account ID directly.",
|
|
77
|
+
"FIELD MAPPING CONFIRMATION: Apply formatting transformations (case correction, date/time conversion) automatically. Only ask for user confirmation when you cannot find a recognised match and need to substitute an unrecognised value with a guessed alternative — never silently substitute in that case.",
|
|
78
|
+
"TRACKING: Import processing is asynchronous. To check status, call get_automation_history and find the record whose trackingId matches the one returned from this tool."
|
|
79
|
+
],
|
|
80
|
+
outputDescription: "trackingId to poll import status, a message from the API, the filePath uploaded, and the format used."
|
|
81
|
+
};
|
|
82
|
+
handle = async (rawArgs) => {
|
|
83
|
+
const args = UploadAutomationResultBody.parse(rawArgs);
|
|
84
|
+
const { filePath, format, isZip, fields, ...rest } = args;
|
|
85
|
+
const ext = extname(filePath).toLowerCase();
|
|
86
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
87
|
+
throw new ToolError(
|
|
88
|
+
`Unsupported file extension '${ext}'. Supported extensions: .xml, .json, .zip`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (format === "qaf" && !isZip) {
|
|
92
|
+
throw new ToolError(
|
|
93
|
+
"QAF format requires a ZIP file. Set isZip: true and provide a .zip file."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
let fileBuffer;
|
|
97
|
+
try {
|
|
98
|
+
fileBuffer = await readFile(filePath);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new ToolError(
|
|
101
|
+
`Could not read file at '${filePath}': ${err.message}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (fileBuffer.byteLength > AUTOMATION_LIMITS.MAX_FILE_SIZE_BYTES) {
|
|
105
|
+
const sizeMB = (fileBuffer.byteLength / (1024 * 1024)).toFixed(2);
|
|
106
|
+
throw new ToolError(
|
|
107
|
+
`File is too large (${sizeMB} MB). Maximum allowed size is 10 MB.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const apiClient = this.client.getApiClient();
|
|
111
|
+
const importBody = {
|
|
112
|
+
format,
|
|
113
|
+
isZip: isZip ?? false,
|
|
114
|
+
...rest,
|
|
115
|
+
...fields ? { fields } : {}
|
|
116
|
+
};
|
|
117
|
+
const initResponse = await apiClient.postAutomation(
|
|
118
|
+
ENDPOINTS.AUTOMATION_IMPORT,
|
|
119
|
+
importBody
|
|
120
|
+
);
|
|
121
|
+
const uploadUrl = initResponse?.url;
|
|
122
|
+
const trackingId = initResponse?.trackingId;
|
|
123
|
+
if (!uploadUrl || !trackingId) {
|
|
124
|
+
throw new ToolError(
|
|
125
|
+
"QTM4J did not return a valid upload URL. Check your API key and project configuration."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
await apiClient.uploadFileMultipart(uploadUrl, fileBuffer);
|
|
129
|
+
const result = {
|
|
130
|
+
trackingId,
|
|
131
|
+
message: initResponse.message ?? "File uploaded successfully. Import is processing.",
|
|
132
|
+
filePath,
|
|
133
|
+
format
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
structuredContent: UploadAutomationResultResponse.parse(result),
|
|
137
|
+
content: []
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
UploadAutomationResult
|
|
143
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Tool } from "../../../common/tools.js";
|
|
2
|
+
import { TOOL_NAMES, ENDPOINTS } from "../../config/constants.js";
|
|
3
|
+
import { ResolverKeys, InputField } from "../../config/field-resolution.types.js";
|
|
4
|
+
import { CreateTestCycleResponse, CreateTestCycleBody } from "../../schema/test-cycle.schema.js";
|
|
5
|
+
const FIELD_CONFIG = {
|
|
6
|
+
[InputField.PRIORITY]: ResolverKeys.CommonAttribute.PRIORITY,
|
|
7
|
+
[InputField.STATUS]: ResolverKeys.CommonAttribute.TEST_CYCLE_STATUS,
|
|
8
|
+
[InputField.FOLDER]: ResolverKeys.CommonAttribute.TEST_CYCLE_FOLDER,
|
|
9
|
+
[InputField.LABELS]: ResolverKeys.SearchableField.LABEL,
|
|
10
|
+
[InputField.COMPONENTS]: ResolverKeys.SearchableField.COMPONENTS
|
|
11
|
+
};
|
|
12
|
+
class CreateTestCycle extends Tool {
|
|
13
|
+
// ─── Tool Specification ────────────────────────────────────────────────────
|
|
14
|
+
specification = {
|
|
15
|
+
title: TOOL_NAMES.CREATE_TEST_CYCLE.TITLE,
|
|
16
|
+
summary: TOOL_NAMES.CREATE_TEST_CYCLE.SUMMARY,
|
|
17
|
+
readOnly: false,
|
|
18
|
+
idempotent: false,
|
|
19
|
+
inputSchema: CreateTestCycleBody,
|
|
20
|
+
outputSchema: CreateTestCycleResponse,
|
|
21
|
+
purpose: "Create a new test cycle in QTM4J. projectId is auto-injected from the active project context. priority, status, labels, and components are auto-resolved from human-readable names.",
|
|
22
|
+
useCases: [
|
|
23
|
+
"Create a test cycle with summary, priority, status, labels, or components",
|
|
24
|
+
"Set planned start and end dates on a new test cycle"
|
|
25
|
+
],
|
|
26
|
+
examples: [
|
|
27
|
+
{
|
|
28
|
+
description: "Create a simple test cycle (project must be set via set_project_context first)",
|
|
29
|
+
parameters: { summary: "Smoke Test Cycle" },
|
|
30
|
+
expectedOutput: "Test cycle created with key 'SCRUM-TR-xxx'"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
description: "Create a test cycle with priority, status, labels, and components",
|
|
34
|
+
parameters: {
|
|
35
|
+
summary: "Regression Suite – Sprint 42",
|
|
36
|
+
description: "End-to-end regression covering payment and checkout modules.",
|
|
37
|
+
priority: "High",
|
|
38
|
+
status: "To Do",
|
|
39
|
+
labels: ["Release_1", "Sprint 1"],
|
|
40
|
+
components: ["UI", "Cloud"],
|
|
41
|
+
plannedStartDate: "10/May/2026 00:00",
|
|
42
|
+
plannedEndDate: "15/May/2026 00:00"
|
|
43
|
+
},
|
|
44
|
+
expectedOutput: "Test cycle created with resolved priority, status, labels, and components"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
hints: [
|
|
48
|
+
"PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
|
|
49
|
+
"If any priority, status, label, or component name cannot be resolved, the cycle is still created but a warning is returned. Suggest the closest available value from the set_project_context response and ask the user to confirm before retrying.",
|
|
50
|
+
"All cycles are placed in the 'MCP Generated' folder — do not pass folderId.",
|
|
51
|
+
"Date format: 'dd/MMM/yyyy HH:mm' e.g. '10/May/2026 00:00'. Month must be capitalised. plannedStartDate must be ≤ plannedEndDate."
|
|
52
|
+
],
|
|
53
|
+
outputDescription: "JSON object with the new test cycle's id and key (e.g. 'TRWT-TR-218'). Warnings included if any fields were skipped."
|
|
54
|
+
};
|
|
55
|
+
// ─── Handle Implementation ──────────────────────────────────────────────────
|
|
56
|
+
handle = async (rawArgs) => {
|
|
57
|
+
const fieldResolver = this.client.getResolverRegistry();
|
|
58
|
+
const context = fieldResolver.requireProjectContext();
|
|
59
|
+
const body = {
|
|
60
|
+
...CreateTestCycleBody.parse(rawArgs),
|
|
61
|
+
projectId: context.projectId,
|
|
62
|
+
folderId: "MCP Generated"
|
|
63
|
+
};
|
|
64
|
+
const warnings = [];
|
|
65
|
+
await Promise.all(
|
|
66
|
+
Object.entries(FIELD_CONFIG).map(
|
|
67
|
+
([inputField, resolverKey]) => fieldResolver.getResolver(resolverKey).resolve(inputField, resolverKey, body, context, warnings)
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
const response = await this.client.getApiClient().post(ENDPOINTS.CREATE_TEST_CYCLE, body);
|
|
71
|
+
const validated = CreateTestCycleResponse.parse(response);
|
|
72
|
+
return {
|
|
73
|
+
structuredContent: validated,
|
|
74
|
+
content: warnings.length > 0 ? [{ type: "text", text: `Note: ${warnings.join(" | ")}` }] : []
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
CreateTestCycle
|
|
80
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Tool } from "../../../common/tools.js";
|
|
2
|
+
import { TOOL_NAMES, RESPONSE_FIELDS, ENDPOINTS } from "../../config/constants.js";
|
|
3
|
+
import { SearchTestCycleResponse, SearchTestCycleBody } from "../../schema/search-test-cycle.schema.js";
|
|
4
|
+
class SearchTestCycles extends Tool {
|
|
5
|
+
specification = {
|
|
6
|
+
title: TOOL_NAMES.SEARCH_TEST_CYCLES.TITLE,
|
|
7
|
+
summary: TOOL_NAMES.SEARCH_TEST_CYCLES.SUMMARY,
|
|
8
|
+
readOnly: true,
|
|
9
|
+
idempotent: true,
|
|
10
|
+
inputSchema: SearchTestCycleBody,
|
|
11
|
+
outputSchema: SearchTestCycleResponse,
|
|
12
|
+
purpose: "Search and filter test cycles in a QTM4J project. projectId is auto-injected from the active project context — do not provide it.",
|
|
13
|
+
useCases: [
|
|
14
|
+
"Find test cycles by status, priority, assignee, reporter, or folder",
|
|
15
|
+
"Find test cycles by planned execution date range (plannedStartDate / plannedEndDate)",
|
|
16
|
+
"Find test cycles created or updated within a date range (createdOn / updatedOn)",
|
|
17
|
+
"Search test cycles by keyword across key, summary, and description",
|
|
18
|
+
"Paginate, sort, and select specific response fields"
|
|
19
|
+
],
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
description: "Find all in-progress and to-do cycles",
|
|
23
|
+
parameters: { filter: { status: ["In Progress", "To Do"] } },
|
|
24
|
+
expectedOutput: "Paginated list of matching test cycles"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
description: "Find cycles owned by a specific user",
|
|
28
|
+
parameters: { filter: { assignee: ["5b10a2844c20165700ede21f"] } },
|
|
29
|
+
expectedOutput: "Test cycles assigned to that user"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
description: "Find cycles with planned start date in a range, requesting date fields explicitly",
|
|
33
|
+
parameters: {
|
|
34
|
+
filter: { plannedStartDate: "01/Apr/2026,30/Apr/2026" },
|
|
35
|
+
fields: [
|
|
36
|
+
"key",
|
|
37
|
+
"summary",
|
|
38
|
+
"status",
|
|
39
|
+
"assignee",
|
|
40
|
+
"plannedStartDate",
|
|
41
|
+
"plannedEndDate"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
expectedOutput: "Cycles with planned start date in April 2026 including date fields"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description: "Keyword search with sort, pagination, and selected fields",
|
|
48
|
+
parameters: {
|
|
49
|
+
filter: { searchText: "regression" },
|
|
50
|
+
fields: ["key", "summary", "status", "assignee"],
|
|
51
|
+
sort: "plannedStartDate:asc",
|
|
52
|
+
startAt: 0,
|
|
53
|
+
maxResults: 25
|
|
54
|
+
},
|
|
55
|
+
expectedOutput: "Cycles matching 'regression', sorted by planned start date"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
description: "Find cycles created last week",
|
|
59
|
+
parameters: {
|
|
60
|
+
filter: { createdOn: "01/May/2026,07/May/2026" },
|
|
61
|
+
fields: ["key", "summary", "status", "assignee"],
|
|
62
|
+
sort: "key:asc"
|
|
63
|
+
},
|
|
64
|
+
expectedOutput: "Test cycles created between 01 May and 07 May 2026"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
description: "Find high-priority cycles updated recently by reporter",
|
|
68
|
+
parameters: {
|
|
69
|
+
filter: {
|
|
70
|
+
priority: ["High"],
|
|
71
|
+
reporter: ["5b10a2844c20165700ede21f"],
|
|
72
|
+
updatedOn: "01/May/2026,21/May/2026"
|
|
73
|
+
},
|
|
74
|
+
fields: ["key", "summary", "status", "priority", "assignee"]
|
|
75
|
+
},
|
|
76
|
+
expectedOutput: "High-priority cycles updated in May 2026 reported by that user"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
description: "All filters combined with explicit field selection",
|
|
80
|
+
parameters: {
|
|
81
|
+
filter: {
|
|
82
|
+
status: ["In Progress"],
|
|
83
|
+
priority: ["High", "Medium"],
|
|
84
|
+
assignee: ["5b10a2844c20165700ede21f"],
|
|
85
|
+
folderId: 109987,
|
|
86
|
+
plannedStartDate: "02/Apr/2026,15/May/2026",
|
|
87
|
+
searchText: "regression"
|
|
88
|
+
},
|
|
89
|
+
fields: [
|
|
90
|
+
"key",
|
|
91
|
+
"summary",
|
|
92
|
+
"status",
|
|
93
|
+
"priority",
|
|
94
|
+
"assignee",
|
|
95
|
+
"plannedStartDate"
|
|
96
|
+
],
|
|
97
|
+
sort: "plannedStartDate:asc",
|
|
98
|
+
maxResults: 25
|
|
99
|
+
},
|
|
100
|
+
expectedOutput: "Test cycles matching all specified filters with selected fields"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
hints: [
|
|
104
|
+
"PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
|
|
105
|
+
"SUPPORTED FILTER FIELDS: status, priority, assignee, reporter, folderId, labels, components, plannedStartDate, plannedEndDate, searchText, createdOn, updatedOn, isAutomated, aiGenerated. Do NOT use any other filter field names.",
|
|
106
|
+
"DATE FILTERS: createdOn = creation date; updatedOn = last-updated date; plannedStartDate / plannedEndDate = planned execution window. Format: 'dd/MMM/yyyy,dd/MMM/yyyy' e.g. '01/May/2026,21/May/2026'. Month is case-sensitive. 'Created last week' → createdOn, NOT plannedStartDate.",
|
|
107
|
+
"FIELDS: Pass as an array to select what to return. plannedStartDate and plannedEndDate are NOT in the default response — include them explicitly. Available: key, summary, description, status, priority, assignee, reporter, isAutomated, plannedStartDate, plannedEndDate, labels, components, fixVersions, sprint, defectCount, estimatedTime, actualTime, created, updated.",
|
|
108
|
+
"REQUEST STRUCTURE: filter → request body; fields, sort, startAt, maxResults → URL query params.",
|
|
109
|
+
"SORT: Allowed fields: key, summary, status, plannedStartDate, plannedEndDate, defectCount. Format: 'fieldName:asc' or 'fieldName:desc' e.g. 'plannedStartDate:asc'.",
|
|
110
|
+
"FOLDER ID: folderId in fields.testCycle and fields.testCase is a numeric ID. Tell the user they can get it by right-clicking the target folder in QTM4J and selecting 'Copy Folder Id'. Always ask the user for the numeric ID directly — never try to look it up."
|
|
111
|
+
],
|
|
112
|
+
outputDescription: "JSON object with total (matching cycles across all pages), startAt, maxResults, and data (array of test cycle objects for this page). Each item always has id and key. Other fields depend on what was requested via the fields parameter."
|
|
113
|
+
};
|
|
114
|
+
handle = async (rawArgs) => {
|
|
115
|
+
const args = SearchTestCycleBody.parse(rawArgs);
|
|
116
|
+
const context = this.client.getResolverRegistry().requireProjectContext();
|
|
117
|
+
if (!args.filter) args.filter = {};
|
|
118
|
+
args.filter.projectId = context.projectId;
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
if (args.fields?.length)
|
|
121
|
+
params.set(RESPONSE_FIELDS.FIELDS, args.fields.join(","));
|
|
122
|
+
params.set(RESPONSE_FIELDS.START_AT, String(args.startAt));
|
|
123
|
+
params.set(RESPONSE_FIELDS.MAX_RESULTS, String(args.maxResults));
|
|
124
|
+
params.set(RESPONSE_FIELDS.SORT, args.sort);
|
|
125
|
+
const endpoint = `${ENDPOINTS.SEARCH_TEST_CYCLES}?${params.toString()}`;
|
|
126
|
+
const response = await this.client.getApiClient().post(endpoint, { filter: args.filter });
|
|
127
|
+
const validated = SearchTestCycleResponse.parse(response);
|
|
128
|
+
return {
|
|
129
|
+
structuredContent: validated,
|
|
130
|
+
content: []
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
SearchTestCycles
|
|
136
|
+
};
|