@smartbear/mcp 0.23.0 → 0.25.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.
Files changed (148) hide show
  1. package/README.md +10 -2
  2. package/dist/bearq/client.js +12 -13
  3. package/dist/bearq/tool/tasks/chat-with-qa-lead.js +1 -0
  4. package/dist/bearq/tool/tasks/expand-application-model.js +1 -0
  5. package/dist/bearq/tool/tasks/get-task-status.js +1 -0
  6. package/dist/bearq/tool/tasks/get-task.js +1 -0
  7. package/dist/bearq/tool/tasks/refine-all-draft-tests.js +1 -0
  8. package/dist/bearq/tool/tasks/refine-test-cases.js +1 -0
  9. package/dist/bearq/tool/tasks/refine-tests-in-functional-areas.js +1 -0
  10. package/dist/bearq/tool/tasks/run-regression-tests.js +1 -0
  11. package/dist/bearq/tool/tasks/run-test-cases.js +1 -0
  12. package/dist/bearq/tool/tasks/run-tests-in-functional-areas.js +1 -0
  13. package/dist/bearq/tool/tasks/stop-task.js +1 -0
  14. package/dist/bearq/tool/tasks/wait-for-task.js +1 -0
  15. package/dist/bugsnag/client.js +24 -44
  16. package/dist/bugsnag/tool/error/get-error.js +1 -0
  17. package/dist/bugsnag/tool/error/list-project-errors.js +1 -0
  18. package/dist/bugsnag/tool/error/update-error.js +1 -0
  19. package/dist/bugsnag/tool/event/get-event-details-from-dashboard-url.js +1 -0
  20. package/dist/bugsnag/tool/event/get-event.js +1 -0
  21. package/dist/bugsnag/tool/event/list-error-events.js +1 -0
  22. package/dist/bugsnag/tool/performance/get-network-endpoint-groupings.js +1 -0
  23. package/dist/bugsnag/tool/performance/get-span-group.js +1 -0
  24. package/dist/bugsnag/tool/performance/get-trace.js +1 -0
  25. package/dist/bugsnag/tool/performance/list-span-groups.js +1 -0
  26. package/dist/bugsnag/tool/performance/list-spans.js +1 -0
  27. package/dist/bugsnag/tool/performance/list-trace-fields.js +1 -0
  28. package/dist/bugsnag/tool/performance/set-network-endpoint-groupings.js +1 -0
  29. package/dist/bugsnag/tool/project/get-current-project.js +1 -0
  30. package/dist/bugsnag/tool/project/list-project-event-filters.js +1 -0
  31. package/dist/bugsnag/tool/project/list-projects.js +1 -0
  32. package/dist/bugsnag/tool/release/get-build.js +1 -0
  33. package/dist/bugsnag/tool/release/get-release.js +1 -0
  34. package/dist/bugsnag/tool/release/list-releases.js +1 -0
  35. package/dist/collaborator/client.js +24 -19
  36. package/dist/common/client-registry.js +63 -29
  37. package/dist/common/server.js +57 -1
  38. package/dist/common/transport-http.js +90 -69
  39. package/dist/common/transport-stdio.js +29 -24
  40. package/dist/package.json.js +1 -1
  41. package/dist/pactflow/client/base.js +3 -3
  42. package/dist/pactflow/client/tools.js +102 -0
  43. package/dist/pactflow/client.js +30 -43
  44. package/dist/qmetry/client/tools/automation-tools.js +2 -0
  45. package/dist/qmetry/client/tools/issue-tools.js +6 -0
  46. package/dist/qmetry/client/tools/project-tools.js +9 -0
  47. package/dist/qmetry/client/tools/requirement-tools.js +5 -0
  48. package/dist/qmetry/client/tools/testcase-tools.js +7 -0
  49. package/dist/qmetry/client/tools/testsuite-tools.js +11 -0
  50. package/dist/qmetry/client.js +20 -18
  51. package/dist/qtm4j/client.js +57 -23
  52. package/dist/qtm4j/config/constants.js +197 -5
  53. package/dist/qtm4j/config/field-resolution.types.js +5 -2
  54. package/dist/qtm4j/http/api-client.js +90 -3
  55. package/dist/qtm4j/resolver/cache/cache.js +1 -1
  56. package/dist/qtm4j/resolver/resolver-registry.js +7 -1
  57. package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +1 -0
  58. package/dist/qtm4j/resolver/resolvers/component-resolver.js +2 -0
  59. package/dist/qtm4j/resolver/resolvers/label-resolver.js +2 -0
  60. package/dist/qtm4j/resolver/resolvers/requirement-id-resolver.js +28 -0
  61. package/dist/qtm4j/resolver/resolvers/test-cycle-uid-resolver.js +28 -0
  62. package/dist/qtm4j/schema/automation.schema.js +107 -0
  63. package/dist/qtm4j/schema/linked-items.schema.js +95 -0
  64. package/dist/qtm4j/schema/requirements.schema.js +109 -0
  65. package/dist/qtm4j/schema/search-test-cycle.schema.js +133 -0
  66. package/dist/qtm4j/schema/test-cycle.link.schema.js +260 -0
  67. package/dist/qtm4j/schema/test-cycle.schema.js +39 -0
  68. package/dist/qtm4j/schema/update-test-cycle.schema.js +54 -0
  69. package/dist/qtm4j/tool/project/get-projects.js +2 -1
  70. package/dist/qtm4j/tool/project/set-project-context.js +2 -1
  71. package/dist/qtm4j/tool/requirement/get-linked-testcases.js +93 -0
  72. package/dist/qtm4j/tool/requirement/link-testcases.js +107 -0
  73. package/dist/qtm4j/tool/requirement/unlink-testcases.js +97 -0
  74. package/dist/qtm4j/tool/test-automation/get-automation-history.js +70 -0
  75. package/dist/qtm4j/tool/test-automation/upload-automation-result.js +144 -0
  76. package/dist/qtm4j/tool/test-case/create-test-case.js +2 -1
  77. package/dist/qtm4j/tool/test-case/get-linked-requirements.js +67 -0
  78. package/dist/qtm4j/tool/test-case/get-test-cases.js +2 -1
  79. package/dist/qtm4j/tool/test-case/get-test-steps.js +2 -1
  80. package/dist/qtm4j/tool/test-case/link-requirements.js +124 -0
  81. package/dist/qtm4j/tool/test-case/unlink-requirements.js +116 -0
  82. package/dist/qtm4j/tool/test-case/update-test-case.js +2 -1
  83. package/dist/qtm4j/tool/test-cycle/create-test-cycle.js +81 -0
  84. package/dist/qtm4j/tool/test-cycle/get-linked-requirements.js +71 -0
  85. package/dist/qtm4j/tool/test-cycle/link-requirements.js +91 -0
  86. package/dist/qtm4j/tool/test-cycle/link-testcases.js +118 -0
  87. package/dist/qtm4j/tool/test-cycle/search-linked-testcases.js +114 -0
  88. package/dist/qtm4j/tool/test-cycle/search-test-cycle.js +137 -0
  89. package/dist/qtm4j/tool/test-cycle/unlink-requirements.js +87 -0
  90. package/dist/qtm4j/tool/test-cycle/unlink-testcases.js +103 -0
  91. package/dist/qtm4j/tool/test-cycle/update-test-cycle.js +162 -0
  92. package/dist/reflect/client.js +15 -24
  93. package/dist/reflect/config/constants.js +0 -2
  94. package/dist/reflect/tool/recording/add-prompt-step.js +1 -0
  95. package/dist/reflect/tool/recording/add-segment.js +1 -0
  96. package/dist/reflect/tool/recording/connect-to-session.js +1 -0
  97. package/dist/reflect/tool/recording/delete-previous-step.js +1 -0
  98. package/dist/reflect/tool/recording/get-screenshot.js +1 -0
  99. package/dist/reflect/tool/suites/cancel-suite-execution.js +1 -0
  100. package/dist/reflect/tool/suites/execute-suite.js +1 -0
  101. package/dist/reflect/tool/suites/get-suite-execution-status.js +1 -0
  102. package/dist/reflect/tool/suites/list-suite-executions.js +1 -0
  103. package/dist/reflect/tool/suites/list-suites.js +1 -0
  104. package/dist/reflect/tool/tests/get-test-status.js +1 -0
  105. package/dist/reflect/tool/tests/list-segments.js +1 -0
  106. package/dist/reflect/tool/tests/list-tests.js +1 -0
  107. package/dist/reflect/tool/tests/run-test.js +1 -0
  108. package/dist/swagger/client/portal-types.js +1 -1
  109. package/dist/swagger/client/tools.js +23 -0
  110. package/dist/swagger/client.js +25 -28
  111. package/dist/zephyr/client.js +14 -21
  112. package/dist/zephyr/tool/environment/get-environments.js +1 -0
  113. package/dist/zephyr/tool/folder/create-folder.js +1 -0
  114. package/dist/zephyr/tool/issue-link/get-test-cases.js +1 -0
  115. package/dist/zephyr/tool/issue-link/get-test-cycles.js +1 -0
  116. package/dist/zephyr/tool/issue-link/get-test-executions.js +1 -0
  117. package/dist/zephyr/tool/priority/get-priorities.js +1 -0
  118. package/dist/zephyr/tool/project/get-project.js +1 -0
  119. package/dist/zephyr/tool/project/get-projects.js +1 -0
  120. package/dist/zephyr/tool/status/get-statuses.js +1 -0
  121. package/dist/zephyr/tool/test-case/create-issue-link.js +1 -0
  122. package/dist/zephyr/tool/test-case/create-test-case.js +1 -0
  123. package/dist/zephyr/tool/test-case/create-test-script.js +1 -0
  124. package/dist/zephyr/tool/test-case/create-test-steps.js +1 -0
  125. package/dist/zephyr/tool/test-case/create-web-link.js +1 -0
  126. package/dist/zephyr/tool/test-case/get-links.js +1 -0
  127. package/dist/zephyr/tool/test-case/get-test-case.js +1 -0
  128. package/dist/zephyr/tool/test-case/get-test-cases.js +1 -0
  129. package/dist/zephyr/tool/test-case/get-test-script.js +1 -0
  130. package/dist/zephyr/tool/test-case/get-test-steps.js +1 -0
  131. package/dist/zephyr/tool/test-case/update-test-case.js +1 -0
  132. package/dist/zephyr/tool/test-cycle/create-issue-link.js +1 -0
  133. package/dist/zephyr/tool/test-cycle/create-test-cycle.js +1 -0
  134. package/dist/zephyr/tool/test-cycle/create-web-link.js +1 -0
  135. package/dist/zephyr/tool/test-cycle/get-links.js +1 -0
  136. package/dist/zephyr/tool/test-cycle/get-test-cycle.js +1 -0
  137. package/dist/zephyr/tool/test-cycle/get-test-cycles.js +1 -0
  138. package/dist/zephyr/tool/test-cycle/update-test-cycle.js +1 -0
  139. package/dist/zephyr/tool/test-execution/create-issue-link.js +1 -0
  140. package/dist/zephyr/tool/test-execution/create-test-execution.js +1 -0
  141. package/dist/zephyr/tool/test-execution/get-test-execution-links.js +1 -0
  142. package/dist/zephyr/tool/test-execution/get-test-execution.js +1 -0
  143. package/dist/zephyr/tool/test-execution/get-test-executions.js +1 -0
  144. package/dist/zephyr/tool/test-execution/get-test-steps.js +1 -0
  145. package/dist/zephyr/tool/test-execution/update-test-execution.js +1 -0
  146. package/dist/zephyr/tool/test-execution/update-test-steps.js +1 -0
  147. package/package.json +1 -1
  148. package/dist/common/request-context.js +0 -20
@@ -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
+ };
@@ -1,9 +1,10 @@
1
1
  import { Tool } from "../../../common/tools.js";
2
- import { TOOL_NAMES, RESPONSE_FIELDS, PAGINATION, ENDPOINTS } from "../../config/constants.js";
2
+ import { TOOL_NAMES, TOOLSETS, RESPONSE_FIELDS, PAGINATION, ENDPOINTS } from "../../config/constants.js";
3
3
  import { GetProjectsResponse, GetProjectsBody } from "../../schema/project.schema.js";
4
4
  class GetProjects extends Tool {
5
5
  specification = {
6
6
  title: TOOL_NAMES.GET_PROJECTS.TITLE,
7
+ toolset: TOOLSETS.PROJECTS,
7
8
  summary: TOOL_NAMES.GET_PROJECTS.SUMMARY,
8
9
  readOnly: true,
9
10
  idempotent: true,
@@ -1,5 +1,5 @@
1
1
  import { Tool, ToolError } from "../../../common/tools.js";
2
- import { TOOL_NAMES, ENDPOINTS, PAGINATION, RESPONSE_FIELDS } from "../../config/constants.js";
2
+ import { TOOL_NAMES, TOOLSETS, ENDPOINTS, PAGINATION, RESPONSE_FIELDS } from "../../config/constants.js";
3
3
  import { SetProjectContextResponse, SetProjectContextBody, GetProjectsResponse } from "../../schema/project.schema.js";
4
4
  const PROJECT_SEARCH_MAX_RESULTS = 10;
5
5
  const ERRORS = {
@@ -9,6 +9,7 @@ const ERRORS = {
9
9
  class SetProjectContext extends Tool {
10
10
  specification = {
11
11
  title: TOOL_NAMES.SET_PROJECT_CONTEXT.TITLE,
12
+ toolset: TOOLSETS.PROJECTS,
12
13
  summary: TOOL_NAMES.SET_PROJECT_CONTEXT.SUMMARY,
13
14
  readOnly: false,
14
15
  idempotent: true,
@@ -0,0 +1,93 @@
1
+ import { Tool, ToolError } from "../../../common/tools.js";
2
+ import { TOOLSETS, TOOL_NAMES, ENDPOINTS } from "../../config/constants.js";
3
+ import { ResolverKeys } from "../../config/field-resolution.types.js";
4
+ import { GetLinkedTestCasesForRequirementBody } from "../../schema/linked-items.schema.js";
5
+ import { SearchTestCaseResponse } from "../../schema/get-test-case.schema.js";
6
+ class GetLinkedTestCasesForRequirement extends Tool {
7
+ specification = {
8
+ title: TOOL_NAMES.GET_LINKED_TESTCASES_FOR_REQUIREMENT.TITLE,
9
+ summary: TOOL_NAMES.GET_LINKED_TESTCASES_FOR_REQUIREMENT.SUMMARY,
10
+ readOnly: true,
11
+ idempotent: true,
12
+ toolset: TOOLSETS.REQUIREMENTS,
13
+ inputSchema: GetLinkedTestCasesForRequirementBody,
14
+ outputSchema: SearchTestCaseResponse,
15
+ purpose: "Retrieve all test cases linked to a specific Jira requirement in QTM4J. The requirement's human-readable key (e.g. 'SCRUM-1') is resolved to the internal Jira issue ID automatically. Returns a paginated list of linked test cases with their metadata. Use the filter to narrow results by status, priority, labels, folder, and more. PREREQUISITE: set_project_context must be called before this tool.",
16
+ useCases: [
17
+ "Check which test cases cover a Jira story or bug",
18
+ "Audit requirement traceability — find all test cases for a given requirement",
19
+ "Filter linked test cases by status or priority before a release",
20
+ "Retrieve test case keys to use in update or link operations"
21
+ ],
22
+ examples: [
23
+ {
24
+ description: "Get all test cases linked to a requirement",
25
+ parameters: { requirementKey: "SCRUM-1" },
26
+ expectedOutput: "Paginated list of linked test cases"
27
+ },
28
+ {
29
+ description: "Get high priority linked test cases",
30
+ parameters: {
31
+ requirementKey: "SCRUM-5",
32
+ filter: { priority: ["High"] },
33
+ fields: "key,summary,status,priority",
34
+ sort: "key:asc"
35
+ },
36
+ expectedOutput: "Filtered high-priority test cases linked to requirement"
37
+ },
38
+ {
39
+ description: "Paginate through linked test cases",
40
+ parameters: {
41
+ requirementKey: "SCRUM-1",
42
+ maxResults: 20,
43
+ startAt: 20
44
+ },
45
+ expectedOutput: "Second page of linked test cases"
46
+ }
47
+ ],
48
+ hints: [
49
+ "PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
50
+ "REQUIREMENT KEY FORMAT: '{PROJECT_KEY}-{number}' — e.g. 'SCRUM-1'.",
51
+ "projectId in filter is auto-filled from the active project context — do not set it manually.",
52
+ "Use the fields param to limit response size — only request fields you need.",
53
+ "Paginate using startAt — increment by maxResults until startAt >= total.",
54
+ "filter.testCaseStatus can be 'active', 'archived', or 'deleted' to filter by archive state."
55
+ ],
56
+ outputDescription: "Paginated response with total, startAt, maxResults, and data array of linked test case objects."
57
+ };
58
+ handle = async (rawArgs) => {
59
+ const args = GetLinkedTestCasesForRequirementBody.parse(rawArgs);
60
+ const fieldResolver = this.client.getResolverRegistry();
61
+ const context = fieldResolver.requireProjectContext();
62
+ const reqMap = await fieldResolver.getResolver(ResolverKeys.SearchableField.REQUIREMENT_KEY_TO_ID).resolveAndReturn(context.projectId, [args.requirementKey]);
63
+ const reqEntry = reqMap[args.requirementKey];
64
+ if (!reqEntry) {
65
+ throw new ToolError(
66
+ `Requirement '${args.requirementKey}' not found in project '${context.projectKey}'. Verify the key is a valid Jira issue key.`
67
+ );
68
+ }
69
+ const requirementId = Number(reqEntry.id);
70
+ const queryParams = {
71
+ fields: args.fields,
72
+ maxResults: args.maxResults,
73
+ startAt: args.startAt,
74
+ sort: args.sort
75
+ };
76
+ const body = {};
77
+ if (args.filter) {
78
+ body.filter = { ...args.filter, projectId: context.projectId };
79
+ }
80
+ const response = await this.client.getApiClient().post(
81
+ ENDPOINTS.GET_LINKED_TESTCASES_FOR_REQUIREMENT(requirementId),
82
+ body,
83
+ queryParams
84
+ );
85
+ return {
86
+ structuredContent: SearchTestCaseResponse.parse(response),
87
+ content: []
88
+ };
89
+ };
90
+ }
91
+ export {
92
+ GetLinkedTestCasesForRequirement
93
+ };
@@ -0,0 +1,107 @@
1
+ import { Tool, ToolError } from "../../../common/tools.js";
2
+ import { TOOLSETS, TOOL_NAMES, ENDPOINTS } from "../../config/constants.js";
3
+ import { ResolverKeys } from "../../config/field-resolution.types.js";
4
+ import { RequirementTestCaseLinkResponse, LinkTestCasesToRequirementBody } from "../../schema/requirements.schema.js";
5
+ class LinkTestCasesToRequirement extends Tool {
6
+ specification = {
7
+ title: TOOL_NAMES.LINK_TESTCASES_TO_REQUIREMENT.TITLE,
8
+ summary: TOOL_NAMES.LINK_TESTCASES_TO_REQUIREMENT.SUMMARY,
9
+ readOnly: false,
10
+ idempotent: false,
11
+ toolset: TOOLSETS.REQUIREMENTS,
12
+ inputSchema: LinkTestCasesToRequirementBody,
13
+ outputSchema: RequirementTestCaseLinkResponse,
14
+ purpose: "Link test cases to a Jira requirement in QTM4J using the requirement's human-readable key. Test cases can be specified by their keys (e.g. 'SCRUM-TC-1'), which are resolved to internal IDs and latest versions automatically. Alternatively, use a filter object to select test cases by criteria such as status, priority, labels, or folder. The active project ID is injected automatically into the filter. Test cases that cannot be linked are reported in warnings. PREREQUISITE: set_project_context must be called before this tool.",
15
+ useCases: [
16
+ "Link specific test cases to a Jira story or bug by requirement key",
17
+ "Build traceability between Jira requirements and QTM4J test cases",
18
+ "Link all test cases matching a filter to a requirement",
19
+ "Associate test cases with a requirement as part of sprint planning"
20
+ ],
21
+ examples: [
22
+ {
23
+ description: "Link two test cases by key",
24
+ parameters: {
25
+ requirementKey: "SCRUM-1",
26
+ testCaseKeys: ["SCRUM-TC-10", "SCRUM-TC-11"]
27
+ },
28
+ expectedOutput: "Test cases linked to requirement SCRUM-1"
29
+ },
30
+ {
31
+ description: "Link test cases matching a filter",
32
+ parameters: {
33
+ requirementKey: "SCRUM-1",
34
+ filter: { priority: ["High"], status: ["To Do"] }
35
+ },
36
+ expectedOutput: "Filtered test cases linked to requirement"
37
+ },
38
+ {
39
+ description: "Link test cases in a specific folder",
40
+ parameters: {
41
+ requirementKey: "SCRUM-5",
42
+ filter: { folderId: 42, withChild: true },
43
+ sort: "key:asc"
44
+ },
45
+ expectedOutput: "Folder test cases linked to requirement"
46
+ }
47
+ ],
48
+ hints: [
49
+ "PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
50
+ "REQUIREMENT KEY FORMAT: '{PROJECT_KEY}-{number}' — e.g. 'SCRUM-1'.",
51
+ "TEST CASE KEY FORMAT: '{PROJECT_KEY}-TC-{number}' — e.g. 'SCRUM-TC-145'.",
52
+ "Provide either testCaseKeys or filter — not both.",
53
+ "projectId in filter is auto-filled from the active project context — do not set it manually.",
54
+ "filter.excludeRequirementId excludes test cases already linked to that requirement ID.",
55
+ "If a test case key cannot be resolved, it is reported in warnings and other test cases are still linked."
56
+ ],
57
+ outputDescription: "Confirmation with the requirement key and linked: true. Warnings included if any test cases could not be resolved or linked."
58
+ };
59
+ handle = async (rawArgs) => {
60
+ const args = LinkTestCasesToRequirementBody.parse(rawArgs);
61
+ const fieldResolver = this.client.getResolverRegistry();
62
+ const context = fieldResolver.requireProjectContext();
63
+ const warnings = [];
64
+ const reqMap = await fieldResolver.getResolver(ResolverKeys.SearchableField.REQUIREMENT_KEY_TO_ID).resolveAndReturn(context.projectId, [args.requirementKey]);
65
+ const reqEntry = reqMap[args.requirementKey];
66
+ if (!reqEntry) {
67
+ throw new ToolError(
68
+ `Requirement '${args.requirementKey}' not found in project '${context.projectKey}'. Verify the key is a valid Jira issue key.`
69
+ );
70
+ }
71
+ const requirementId = Number(reqEntry.id);
72
+ const body = {};
73
+ if (args.testCaseKeys?.length) {
74
+ const uidMap = await fieldResolver.getResolver(ResolverKeys.SearchableField.TEST_CASE_KEY_TO_UID).resolveAndReturn(context.projectId, args.testCaseKeys);
75
+ const testcases = [];
76
+ for (const tcKey of args.testCaseKeys) {
77
+ const resolved = uidMap[tcKey];
78
+ if (resolved) {
79
+ testcases.push({
80
+ id: resolved.uid,
81
+ versionNo: resolved.latestVersion
82
+ });
83
+ } else {
84
+ warnings.push(
85
+ `Test case '${tcKey}' could not be resolved and was skipped.`
86
+ );
87
+ }
88
+ }
89
+ if (testcases.length > 0) body.testcases = testcases;
90
+ }
91
+ if (args.filter) {
92
+ body.filter = { ...args.filter, projectId: context.projectId };
93
+ }
94
+ if (args.sort) body.sort = args.sort;
95
+ await this.client.getApiClient().post(ENDPOINTS.LINK_TESTCASES_TO_REQUIREMENT(requirementId), body);
96
+ return {
97
+ structuredContent: RequirementTestCaseLinkResponse.parse({
98
+ requirementKey: args.requirementKey,
99
+ linked: true
100
+ }),
101
+ content: warnings.length > 0 ? [{ type: "text", text: `Note: ${warnings.join(" | ")}` }] : []
102
+ };
103
+ };
104
+ }
105
+ export {
106
+ LinkTestCasesToRequirement
107
+ };
@@ -0,0 +1,97 @@
1
+ import { Tool, ToolError } from "../../../common/tools.js";
2
+ import { TOOLSETS, TOOL_NAMES, ENDPOINTS } from "../../config/constants.js";
3
+ import { ResolverKeys } from "../../config/field-resolution.types.js";
4
+ import { RequirementTestCaseLinkResponse, UnlinkTestCasesFromRequirementBody } from "../../schema/requirements.schema.js";
5
+ class UnlinkTestCasesFromRequirement extends Tool {
6
+ specification = {
7
+ title: TOOL_NAMES.UNLINK_TESTCASES_FROM_REQUIREMENT.TITLE,
8
+ summary: TOOL_NAMES.UNLINK_TESTCASES_FROM_REQUIREMENT.SUMMARY,
9
+ readOnly: false,
10
+ idempotent: false,
11
+ toolset: TOOLSETS.REQUIREMENTS,
12
+ inputSchema: UnlinkTestCasesFromRequirementBody,
13
+ outputSchema: RequirementTestCaseLinkResponse,
14
+ purpose: "Unlink test cases from a Jira requirement in QTM4J using the requirement's human-readable key. Test cases can be specified by their keys (e.g. 'SCRUM-TC-1'), which are resolved to internal IDs and latest versions automatically. Alternatively, use a filter object to select test cases to unlink by criteria. The active project ID is injected automatically into the filter. Test cases that cannot be unlinked are reported in warnings. PREREQUISITE: set_project_context must be called before this tool.",
15
+ useCases: [
16
+ "Remove specific test cases from a Jira requirement",
17
+ "Unlink test cases matching a filter from a requirement",
18
+ "Clean up stale or incorrect test case links on a requirement",
19
+ "Remove traceability links as part of sprint cleanup"
20
+ ],
21
+ examples: [
22
+ {
23
+ description: "Unlink specific test cases by key",
24
+ parameters: {
25
+ requirementKey: "SCRUM-1",
26
+ testCaseKeys: ["SCRUM-TC-10", "SCRUM-TC-11"]
27
+ },
28
+ expectedOutput: "Test cases unlinked from requirement SCRUM-1"
29
+ },
30
+ {
31
+ description: "Unlink test cases matching a filter",
32
+ parameters: {
33
+ requirementKey: "SCRUM-1",
34
+ filter: { status: ["Done"], labels: ["Deprecated"] }
35
+ },
36
+ expectedOutput: "Filtered test cases unlinked from requirement"
37
+ }
38
+ ],
39
+ hints: [
40
+ "PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
41
+ "REQUIREMENT KEY FORMAT: '{PROJECT_KEY}-{number}' — e.g. 'SCRUM-1'.",
42
+ "TEST CASE KEY FORMAT: '{PROJECT_KEY}-TC-{number}' — e.g. 'SCRUM-TC-145'.",
43
+ "Provide either testCaseKeys or filter — not both.",
44
+ "projectId in filter is auto-filled from the active project context — do not set it manually.",
45
+ "filter.excludeTestCases can be used to exclude specific test cases when using filter-based unlinking.",
46
+ "If a test case key cannot be resolved, it is reported in warnings and other test cases are still unlinked."
47
+ ],
48
+ outputDescription: "Confirmation with the requirement key and unlinked: true. Warnings included if any test cases could not be resolved or unlinked."
49
+ };
50
+ handle = async (rawArgs) => {
51
+ const args = UnlinkTestCasesFromRequirementBody.parse(rawArgs);
52
+ const fieldResolver = this.client.getResolverRegistry();
53
+ const context = fieldResolver.requireProjectContext();
54
+ const warnings = [];
55
+ const reqMap = await fieldResolver.getResolver(ResolverKeys.SearchableField.REQUIREMENT_KEY_TO_ID).resolveAndReturn(context.projectId, [args.requirementKey]);
56
+ const reqEntry = reqMap[args.requirementKey];
57
+ if (!reqEntry) {
58
+ throw new ToolError(
59
+ `Requirement '${args.requirementKey}' not found in project '${context.projectKey}'. Verify the key is a valid Jira issue key.`
60
+ );
61
+ }
62
+ const requirementId = Number(reqEntry.id);
63
+ const body = {};
64
+ if (args.testCaseKeys?.length) {
65
+ const uidMap = await fieldResolver.getResolver(ResolverKeys.SearchableField.TEST_CASE_KEY_TO_UID).resolveAndReturn(context.projectId, args.testCaseKeys);
66
+ const testcases = [];
67
+ for (const tcKey of args.testCaseKeys) {
68
+ const resolved = uidMap[tcKey];
69
+ if (resolved) {
70
+ testcases.push({
71
+ id: resolved.uid,
72
+ versionNo: resolved.latestVersion
73
+ });
74
+ } else {
75
+ warnings.push(
76
+ `Test case '${tcKey}' could not be resolved and was skipped.`
77
+ );
78
+ }
79
+ }
80
+ if (testcases.length > 0) body.testcases = testcases;
81
+ }
82
+ if (args.filter) {
83
+ body.filter = { ...args.filter, projectId: context.projectId };
84
+ }
85
+ await this.client.getApiClient().delete(ENDPOINTS.UNLINK_TESTCASES_FROM_REQUIREMENT(requirementId), body);
86
+ return {
87
+ structuredContent: RequirementTestCaseLinkResponse.parse({
88
+ requirementKey: args.requirementKey,
89
+ unlinked: true
90
+ }),
91
+ content: warnings.length > 0 ? [{ type: "text", text: `Note: ${warnings.join(" | ")}` }] : []
92
+ };
93
+ };
94
+ }
95
+ export {
96
+ UnlinkTestCasesFromRequirement
97
+ };
@@ -0,0 +1,70 @@
1
+ import { Tool } from "../../../common/tools.js";
2
+ import { TOOL_NAMES, TOOLSETS, 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
+ toolset: TOOLSETS.TEST_AUTOMATION,
8
+ summary: TOOL_NAMES.GET_AUTOMATION_HISTORY.SUMMARY,
9
+ readOnly: true,
10
+ idempotent: true,
11
+ inputSchema: GetAutomationHistoryBody,
12
+ outputSchema: GetAutomationHistoryResponse,
13
+ 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.",
14
+ useCases: [
15
+ "Review past automation result uploads for a project",
16
+ "Check the status of recent automation imports",
17
+ "Audit CI/CD automation upload history",
18
+ "Paginate through all historical automation uploads"
19
+ ],
20
+ examples: [
21
+ {
22
+ description: "Get the first page of automation upload history (default page size 20)",
23
+ parameters: {},
24
+ expectedOutput: "Paginated list of automation history records with upload status and metadata"
25
+ },
26
+ {
27
+ description: "Get the second page of automation upload history",
28
+ parameters: {
29
+ startAt: 20,
30
+ maxResults: 20
31
+ },
32
+ expectedOutput: "Next 20 automation history records"
33
+ },
34
+ {
35
+ description: "Get up to 50 records starting from the beginning",
36
+ parameters: {
37
+ startAt: 0,
38
+ maxResults: 50
39
+ },
40
+ expectedOutput: "Up to 50 automation history records"
41
+ }
42
+ ],
43
+ hints: [
44
+ "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.",
45
+ "PAGINATION: startAt is zero-indexed (default: 0), maxResults controls page size (default: 20, max: 100). Increment startAt by maxResults to fetch the next page.",
46
+ "Returns an empty data array (not an error) when no history records exist.",
47
+ "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."
48
+ ],
49
+ 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."
50
+ };
51
+ handle = async (rawArgs) => {
52
+ const args = GetAutomationHistoryBody.parse(rawArgs);
53
+ const apiClient = this.client.getApiClient();
54
+ const response = await apiClient.getAutomation(
55
+ ENDPOINTS.AUTOMATION_HISTORY,
56
+ {
57
+ startAt: args.startAt,
58
+ maxResults: args.maxResults
59
+ }
60
+ );
61
+ const result = GetAutomationHistoryResponse.parse(response);
62
+ return {
63
+ structuredContent: result,
64
+ content: []
65
+ };
66
+ };
67
+ }
68
+ export {
69
+ GetAutomationHistory
70
+ };
@@ -0,0 +1,144 @@
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, TOOLSETS, 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
+ toolset: TOOLSETS.TEST_AUTOMATION,
11
+ summary: TOOL_NAMES.UPLOAD_AUTOMATION_RESULT.SUMMARY,
12
+ readOnly: false,
13
+ idempotent: false,
14
+ inputSchema: UploadAutomationResultBody,
15
+ outputSchema: UploadAutomationResultResponse,
16
+ 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.`,
17
+ useCases: [
18
+ "Upload automation results to QTM4J",
19
+ "Import test results from a CI/CD pipeline run",
20
+ "Link test results to an existing test cycle",
21
+ "Create a new test cycle from automation results",
22
+ "Upload JUnit, TestNG, Cucumber, QAF, HP UFT, or SpecFlow result files"
23
+ ],
24
+ examples: [
25
+ {
26
+ description: "User says 'upload my test results to QTM4J' — scan workspace, find single result file, confirm and upload",
27
+ parameters: {
28
+ filePath: "./target/surefire-reports/TEST-results.xml",
29
+ format: "junit"
30
+ },
31
+ expectedOutput: "trackingId returned; import processing started in QTM4J"
32
+ },
33
+ {
34
+ description: "User wants results linked to an existing test cycle",
35
+ parameters: {
36
+ filePath: "./reports/cucumber.json",
37
+ format: "cucumber",
38
+ testCycleToReuse: "TR-PRJ-5",
39
+ environment: "Chrome",
40
+ build: "2.1.0"
41
+ },
42
+ expectedOutput: "Results mapped to test cycle TR-PRJ-5"
43
+ },
44
+ {
45
+ description: "Upload QAF ZIP and set test cycle metadata",
46
+ parameters: {
47
+ filePath: "./results/qaf-results.zip",
48
+ format: "qaf",
49
+ isZip: true,
50
+ fields: {
51
+ testCycle: {
52
+ summary: "Regression Run 2024-Q1",
53
+ labels: ["regression"],
54
+ priority: "High"
55
+ }
56
+ }
57
+ },
58
+ expectedOutput: "ZIP uploaded; test cycle created with summary, labels, and priority"
59
+ },
60
+ {
61
+ description: "User provides an unrecognised priority value — do NOT silently map to a similar word; ask the user first",
62
+ parameters: {
63
+ filePath: "./reports/cucumber.json",
64
+ format: "cucumber",
65
+ fields: { testCycle: { priority: "critical" } }
66
+ },
67
+ 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."
68
+ }
69
+ ],
70
+ hints: [
71
+ "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.",
72
+ `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.`,
73
+ "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.",
74
+ "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.",
75
+ "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.",
76
+ "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').",
77
+ "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.",
78
+ "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.",
79
+ "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."
80
+ ],
81
+ outputDescription: "trackingId to poll import status, a message from the API, the filePath uploaded, and the format used."
82
+ };
83
+ handle = async (rawArgs) => {
84
+ const args = UploadAutomationResultBody.parse(rawArgs);
85
+ const { filePath, format, isZip, fields, ...rest } = args;
86
+ const ext = extname(filePath).toLowerCase();
87
+ if (!SUPPORTED_EXTENSIONS.has(ext)) {
88
+ throw new ToolError(
89
+ `Unsupported file extension '${ext}'. Supported extensions: .xml, .json, .zip`
90
+ );
91
+ }
92
+ if (format === "qaf" && !isZip) {
93
+ throw new ToolError(
94
+ "QAF format requires a ZIP file. Set isZip: true and provide a .zip file."
95
+ );
96
+ }
97
+ let fileBuffer;
98
+ try {
99
+ fileBuffer = await readFile(filePath);
100
+ } catch (err) {
101
+ throw new ToolError(
102
+ `Could not read file at '${filePath}': ${err.message}`
103
+ );
104
+ }
105
+ if (fileBuffer.byteLength > AUTOMATION_LIMITS.MAX_FILE_SIZE_BYTES) {
106
+ const sizeMB = (fileBuffer.byteLength / (1024 * 1024)).toFixed(2);
107
+ throw new ToolError(
108
+ `File is too large (${sizeMB} MB). Maximum allowed size is 10 MB.`
109
+ );
110
+ }
111
+ const apiClient = this.client.getApiClient();
112
+ const importBody = {
113
+ format,
114
+ isZip: isZip ?? false,
115
+ ...rest,
116
+ ...fields ? { fields } : {}
117
+ };
118
+ const initResponse = await apiClient.postAutomation(
119
+ ENDPOINTS.AUTOMATION_IMPORT,
120
+ importBody
121
+ );
122
+ const uploadUrl = initResponse?.url;
123
+ const trackingId = initResponse?.trackingId;
124
+ if (!uploadUrl || !trackingId) {
125
+ throw new ToolError(
126
+ "QTM4J did not return a valid upload URL. Check your API key and project configuration."
127
+ );
128
+ }
129
+ await apiClient.uploadFileMultipart(uploadUrl, fileBuffer);
130
+ const result = {
131
+ trackingId,
132
+ message: initResponse.message ?? "File uploaded successfully. Import is processing.",
133
+ filePath,
134
+ format
135
+ };
136
+ return {
137
+ structuredContent: UploadAutomationResultResponse.parse(result),
138
+ content: []
139
+ };
140
+ };
141
+ }
142
+ export {
143
+ UploadAutomationResult
144
+ };
@@ -1,5 +1,5 @@
1
1
  import { Tool } from "../../../common/tools.js";
2
- import { TOOL_NAMES, ENDPOINTS } from "../../config/constants.js";
2
+ import { TOOL_NAMES, TOOLSETS, ENDPOINTS } from "../../config/constants.js";
3
3
  import { ResolverKeys, InputField } from "../../config/field-resolution.types.js";
4
4
  import { CreateTestCaseResponse, CreateTestCaseBody } from "../../schema/test-case.schema.js";
5
5
  const FIELD_CONFIG = {
@@ -13,6 +13,7 @@ class CreateTestCase extends Tool {
13
13
  // ─── Tool Specification ────────────────────────────────────────────────────
14
14
  specification = {
15
15
  title: TOOL_NAMES.CREATE_TEST_CASE.TITLE,
16
+ toolset: TOOLSETS.TEST_CASES,
16
17
  summary: TOOL_NAMES.CREATE_TEST_CASE.SUMMARY,
17
18
  readOnly: false,
18
19
  idempotent: false,