@smartbear/mcp 0.19.2 → 0.21.1
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 +37 -7
- package/assets/icon.png +0 -0
- package/dist/bugsnag/client.js +19 -12
- package/dist/collaborator/client.js +10 -10
- package/dist/common/client-registry.js +2 -2
- package/dist/common/register-clients.js +2 -0
- package/dist/common/server.js +74 -111
- package/dist/common/shutdown.js +165 -0
- package/dist/common/transport-http.js +94 -12
- package/dist/common/transport-stdio.js +16 -2
- package/dist/common/zod-utils.js +62 -7
- package/dist/index.js +2 -0
- package/dist/package.json.js +1 -1
- package/dist/pactflow/client/prompts.js +19 -18
- package/dist/pactflow/client/tools.js +8 -13
- package/dist/pactflow/client.js +26 -12
- package/dist/qmetry/client/tools/testsuite-tools.js +2 -2
- package/dist/qmetry/client.js +1 -1
- package/dist/qtm4j/client.js +109 -0
- package/dist/qtm4j/config/constants.js +169 -0
- package/dist/qtm4j/config/field-resolution.types.js +34 -0
- package/dist/qtm4j/http/api-client.js +123 -0
- package/dist/qtm4j/http/auth-service.js +23 -0
- package/dist/qtm4j/resolver/cache/cache.js +52 -0
- package/dist/qtm4j/resolver/resolver-registry.js +70 -0
- package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +56 -0
- package/dist/qtm4j/resolver/resolvers/component-resolver.js +56 -0
- package/dist/qtm4j/resolver/resolvers/label-resolver.js +56 -0
- package/dist/qtm4j/resolver/resolvers/resolver.js +6 -0
- package/dist/qtm4j/resolver/resolvers/test-case-uid-resolver.js +28 -0
- package/dist/qtm4j/schema/get-test-case.schema.js +153 -0
- package/dist/qtm4j/schema/get-test-steps.schema.js +74 -0
- package/dist/qtm4j/schema/project.schema.js +43 -0
- package/dist/qtm4j/schema/test-case.schema.js +41 -0
- package/dist/qtm4j/schema/update-test-case.schema.js +45 -0
- package/dist/qtm4j/tool/project/get-projects.js +111 -0
- package/dist/qtm4j/tool/project/set-project-context.js +99 -0
- package/dist/qtm4j/tool/test-case/create-test-case.js +113 -0
- package/dist/qtm4j/tool/test-case/get-test-cases.js +295 -0
- package/dist/qtm4j/tool/test-case/get-test-steps.js +111 -0
- package/dist/qtm4j/tool/test-case/update-test-case.js +158 -0
- package/dist/reflect/client.js +3 -3
- package/dist/reflect/prompt/sap-test.js +5 -5
- package/dist/reflect/tool/recording/add-prompt-step.js +6 -14
- package/dist/reflect/tool/recording/add-segment.js +4 -14
- package/dist/reflect/tool/recording/connect-to-session.js +3 -8
- package/dist/reflect/tool/recording/delete-previous-step.js +3 -8
- package/dist/reflect/tool/recording/get-screenshot.js +4 -14
- package/dist/reflect/tool/suites/cancel-suite-execution.js +4 -14
- package/dist/reflect/tool/suites/execute-suite.js +3 -8
- package/dist/reflect/tool/suites/get-suite-execution-status.js +4 -14
- package/dist/reflect/tool/suites/list-suite-executions.js +3 -8
- package/dist/reflect/tool/suites/list-suites.js +2 -1
- package/dist/reflect/tool/tests/get-test-status.js +3 -8
- package/dist/reflect/tool/tests/list-segments.js +5 -20
- package/dist/reflect/tool/tests/list-tests.js +2 -1
- package/dist/reflect/tool/tests/run-test.js +3 -8
- package/dist/swagger/client/api.js +11 -2
- package/dist/swagger/client/portal-types.js +0 -3
- package/dist/swagger/client/tools.js +0 -1
- package/dist/swagger/client.js +1 -1
- package/dist/zephyr/client.js +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../../config/constants.js";
|
|
2
|
+
import { ResolverKeys } from "../../config/field-resolution.types.js";
|
|
3
|
+
import { Cache } from "../cache/cache.js";
|
|
4
|
+
import { Resolver } from "./resolver.js";
|
|
5
|
+
class ComponentResolver extends Resolver {
|
|
6
|
+
fieldKeys = [
|
|
7
|
+
ResolverKeys.SearchableField.COMPONENTS
|
|
8
|
+
];
|
|
9
|
+
cache;
|
|
10
|
+
apiClient;
|
|
11
|
+
constructor(apiClient, cacheService) {
|
|
12
|
+
super();
|
|
13
|
+
this.apiClient = apiClient;
|
|
14
|
+
this.cache = new Cache(cacheService);
|
|
15
|
+
}
|
|
16
|
+
async resolve(inputField, resolverKey, body, context, warnings) {
|
|
17
|
+
const raw = body[inputField];
|
|
18
|
+
if (raw == null) return;
|
|
19
|
+
const isArray = Array.isArray(raw);
|
|
20
|
+
const names = isArray ? raw : [raw];
|
|
21
|
+
const ids = [];
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
const id = await this.resolveAndReturn(
|
|
24
|
+
context.projectKey,
|
|
25
|
+
context.projectId,
|
|
26
|
+
resolverKey,
|
|
27
|
+
name
|
|
28
|
+
);
|
|
29
|
+
if (id !== void 0) {
|
|
30
|
+
ids.push(Number(id));
|
|
31
|
+
} else {
|
|
32
|
+
warnings.push(
|
|
33
|
+
`Skipped ${inputField} '${name}' — not available in the current project.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (ids.length > 0) {
|
|
38
|
+
body[inputField] = isArray ? ids : ids[0];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async resolveAndReturn(projectKey, projectId, resolverKey, name) {
|
|
42
|
+
const cached = this.cache.matchValue(projectKey, resolverKey, name);
|
|
43
|
+
if (cached !== void 0) return cached;
|
|
44
|
+
const response = await this.apiClient.get(ENDPOINTS.COMPONENTS(projectId), {
|
|
45
|
+
search: name
|
|
46
|
+
});
|
|
47
|
+
this.cache.set(projectKey, resolverKey, response);
|
|
48
|
+
return this.cache.matchValue(projectKey, resolverKey, name);
|
|
49
|
+
}
|
|
50
|
+
clearCache(projectKey) {
|
|
51
|
+
this.cache.clear(projectKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
ComponentResolver
|
|
56
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../../config/constants.js";
|
|
2
|
+
import { ResolverKeys } from "../../config/field-resolution.types.js";
|
|
3
|
+
import { Cache } from "../cache/cache.js";
|
|
4
|
+
import { Resolver } from "./resolver.js";
|
|
5
|
+
class LabelResolver extends Resolver {
|
|
6
|
+
fieldKeys = [
|
|
7
|
+
ResolverKeys.SearchableField.LABEL
|
|
8
|
+
];
|
|
9
|
+
cache;
|
|
10
|
+
apiClient;
|
|
11
|
+
constructor(apiClient, cacheService) {
|
|
12
|
+
super();
|
|
13
|
+
this.apiClient = apiClient;
|
|
14
|
+
this.cache = new Cache(cacheService);
|
|
15
|
+
}
|
|
16
|
+
async resolve(inputField, resolverKey, body, context, warnings) {
|
|
17
|
+
const raw = body[inputField];
|
|
18
|
+
if (raw == null) return;
|
|
19
|
+
const isArray = Array.isArray(raw);
|
|
20
|
+
const names = isArray ? raw : [raw];
|
|
21
|
+
const ids = [];
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
const id = await this.resolveAndReturn(
|
|
24
|
+
context.projectKey,
|
|
25
|
+
context.projectId,
|
|
26
|
+
resolverKey,
|
|
27
|
+
name
|
|
28
|
+
);
|
|
29
|
+
if (id !== void 0) {
|
|
30
|
+
ids.push(Number(id));
|
|
31
|
+
} else {
|
|
32
|
+
warnings.push(
|
|
33
|
+
`Skipped ${inputField} '${name}' — not available in the current project.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (ids.length > 0) {
|
|
38
|
+
body[inputField] = isArray ? ids : ids[0];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async resolveAndReturn(projectKey, projectId, resolverKey, name) {
|
|
42
|
+
const cached = this.cache.matchValue(projectKey, resolverKey, name);
|
|
43
|
+
if (cached !== void 0) return cached;
|
|
44
|
+
const response = await this.apiClient.get(ENDPOINTS.LABELS(projectId), {
|
|
45
|
+
search: name
|
|
46
|
+
});
|
|
47
|
+
this.cache.set(projectKey, resolverKey, response);
|
|
48
|
+
return this.cache.matchValue(projectKey, resolverKey, name);
|
|
49
|
+
}
|
|
50
|
+
clearCache(projectKey) {
|
|
51
|
+
this.cache.clear(projectKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
LabelResolver
|
|
56
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ENDPOINTS } from "../../config/constants.js";
|
|
2
|
+
import { ResolverKeys } from "../../config/field-resolution.types.js";
|
|
3
|
+
import { Resolver } from "./resolver.js";
|
|
4
|
+
class TestCaseUidResolver extends Resolver {
|
|
5
|
+
fieldKeys = [
|
|
6
|
+
ResolverKeys.SearchableField.TEST_CASE_KEY_TO_UID
|
|
7
|
+
];
|
|
8
|
+
apiClient;
|
|
9
|
+
constructor(apiClient) {
|
|
10
|
+
super();
|
|
11
|
+
this.apiClient = apiClient;
|
|
12
|
+
}
|
|
13
|
+
async resolve(_inputField, _resolverKey, _body, _context, _warnings) {
|
|
14
|
+
}
|
|
15
|
+
async resolveAndReturn(projectId, keys) {
|
|
16
|
+
if (keys.length === 0) return {};
|
|
17
|
+
const response = await this.apiClient.get(
|
|
18
|
+
ENDPOINTS.RESOLVE_TEST_CASE_IDS(projectId),
|
|
19
|
+
{ keys: keys.join(",") }
|
|
20
|
+
);
|
|
21
|
+
return response;
|
|
22
|
+
}
|
|
23
|
+
clearCache(_projectKey) {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
TestCaseUidResolver
|
|
28
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as zod from "zod";
|
|
2
|
+
import { PAGINATION } from "../config/constants.js";
|
|
3
|
+
const SearchTestCaseFilter = zod.object({
|
|
4
|
+
projectId: zod.string().optional().describe(
|
|
5
|
+
"Project ID as a numeric string (e.g. '10000'). Auto-populated from active project context if omitted."
|
|
6
|
+
),
|
|
7
|
+
searchText: zod.string().optional().describe(
|
|
8
|
+
"Free-text search across summary and description. Case-insensitive. Example: 'login functionality'"
|
|
9
|
+
),
|
|
10
|
+
status: zod.array(zod.string()).optional().describe(
|
|
11
|
+
"Status names to include (OR logic within array). Examples: ['Done'], ['To Do', 'In Progress']. Common values: 'Done', 'To Do', 'In Progress'."
|
|
12
|
+
),
|
|
13
|
+
priority: zod.array(zod.string()).optional().describe(
|
|
14
|
+
"Priority names to include (OR logic within array). Examples: ['High'], ['High', 'Medium']. Common values: 'High', 'Medium', 'Low'."
|
|
15
|
+
),
|
|
16
|
+
labels: zod.array(zod.string()).optional().describe(
|
|
17
|
+
"Label names to include (OR logic within array). Example: ['Release_1', 'Sprint 1']. Use exact label names as configured in the project."
|
|
18
|
+
),
|
|
19
|
+
components: zod.array(zod.string()).optional().describe(
|
|
20
|
+
"Component names to include (OR logic within array). Example: ['UI', 'Cloud', 'API']. Use exact component names as configured in the project."
|
|
21
|
+
),
|
|
22
|
+
folders: zod.array(zod.number()).optional().describe(
|
|
23
|
+
"Folder IDs (numeric, OR logic within array). Example: [123, 456]. Retrieve folder IDs from the project's folder structure."
|
|
24
|
+
),
|
|
25
|
+
assignee: zod.array(zod.string()).optional().describe(
|
|
26
|
+
"Jira account IDs of assignees (OR logic within array). Example: ['712020:ddc8e24b-2de7-404b-b9ed-3d7b241e2ced']. Multiple IDs match test cases assigned to any of them."
|
|
27
|
+
),
|
|
28
|
+
reporter: zod.array(zod.string()).optional().describe(
|
|
29
|
+
"Jira account IDs of reporters (OR logic within array). Example: ['712020:b8479b55-6d23-478c-a2ad-4c8ce176e1fc']."
|
|
30
|
+
),
|
|
31
|
+
isAutomated: zod.boolean().optional().describe(
|
|
32
|
+
"Automation status filter: true = automated tests only, false = manual tests only. Omit to include both."
|
|
33
|
+
),
|
|
34
|
+
createdOnFrom: zod.string().optional().describe(
|
|
35
|
+
"Lower bound for creation date (inclusive). Format: 'dd/MMM/yyyy', e.g. '01/Jan/2026'."
|
|
36
|
+
),
|
|
37
|
+
createdOnTo: zod.string().optional().describe(
|
|
38
|
+
"Upper bound for creation date (inclusive). Format: 'dd/MMM/yyyy', e.g. '31/Dec/2026'."
|
|
39
|
+
),
|
|
40
|
+
updatedOnFrom: zod.string().optional().describe(
|
|
41
|
+
"Lower bound for last-updated date (inclusive). Format: 'dd/MMM/yyyy', e.g. '01/Apr/2026'."
|
|
42
|
+
),
|
|
43
|
+
updatedOnTo: zod.string().optional().describe(
|
|
44
|
+
"Upper bound for last-updated date (inclusive). Format: 'dd/MMM/yyyy', e.g. '30/Apr/2026'."
|
|
45
|
+
),
|
|
46
|
+
executedOnFrom: zod.string().optional().describe(
|
|
47
|
+
"Lower bound for last-execution date (inclusive). Format: 'dd/MMM/yyyy', e.g. '27/Apr/2026'."
|
|
48
|
+
),
|
|
49
|
+
executedOnTo: zod.string().optional().describe(
|
|
50
|
+
"Upper bound for last-execution date (inclusive). Format: 'dd/MMM/yyyy', e.g. '03/May/2026'."
|
|
51
|
+
),
|
|
52
|
+
fixVersions: zod.array(zod.number()).optional().describe(
|
|
53
|
+
"Fix version IDs (numeric, OR logic within array). Example: [789]. Retrieve IDs from the project's fix version list."
|
|
54
|
+
),
|
|
55
|
+
sprint: zod.array(zod.number()).optional().describe(
|
|
56
|
+
"Sprint IDs (numeric, OR logic within array). Example: [10, 11]. Retrieve IDs from the project's sprint list."
|
|
57
|
+
),
|
|
58
|
+
aiGenerated: zod.boolean().optional().describe(
|
|
59
|
+
"AI generation flag: true = AI-generated test cases only, false = human-authored only. Omit to include both."
|
|
60
|
+
)
|
|
61
|
+
}).describe(
|
|
62
|
+
"Filter criteria — multiple fields are combined with AND; multiple values within one field use OR."
|
|
63
|
+
);
|
|
64
|
+
const FIELDS_DESCRIPTION = "Fields to include in each result object. If omitted, all fields are returned. Available fields: key, summary, description, priority, status, assignee, isAutomated, reporter, estimatedTime, labels, components, fixVersions, sprint, folders, updated, created, executed, flakyScore, passRateScore, aiGenerated, precondition, orderNo, seqNo, version. Example: ['key', 'summary', 'status', 'priority', 'assignee']";
|
|
65
|
+
const SearchTestCaseBody = zod.object({
|
|
66
|
+
filter: SearchTestCaseFilter.optional(),
|
|
67
|
+
fields: zod.array(zod.string()).optional().describe(FIELDS_DESCRIPTION),
|
|
68
|
+
startAt: zod.number().min(0).optional().default(PAGINATION.DEFAULT_START_AT).describe(
|
|
69
|
+
"Zero-indexed offset for pagination (URL query param). First page: 0. Second page: 50 (when maxResults=50). Default: 0."
|
|
70
|
+
),
|
|
71
|
+
maxResults: zod.number().min(PAGINATION.MIN_ALLOWED_RESULTS).max(PAGINATION.MAX_ALLOWED_RESULTS_TEST_CASES).optional().default(PAGINATION.DEFAULT_MAX_RESULTS_TEST_CASES).describe(
|
|
72
|
+
"Number of results per page (URL query param). Default: 50. Maximum: 50 (backend enforced). To page through results, increment startAt by 50 until startAt >= total."
|
|
73
|
+
),
|
|
74
|
+
sort: zod.string().optional().describe(
|
|
75
|
+
"Sort pattern sent as a URL query param. Format: 'fieldName:order'. For multiple fields, comma-separate: 'priority:asc,created:desc'. Order values: 'asc' (oldest/lowest first) or 'desc' (newest/highest first). Sortable fields: key, summary, created, updated, status, priority, executed. Examples: 'created:desc', 'key:asc', 'priority:desc,created:asc'"
|
|
76
|
+
)
|
|
77
|
+
});
|
|
78
|
+
zod.object({
|
|
79
|
+
createdBy: zod.string().optional(),
|
|
80
|
+
createdOn: zod.string().optional(),
|
|
81
|
+
updatedBy: zod.string().optional(),
|
|
82
|
+
updatedOn: zod.string().optional()
|
|
83
|
+
});
|
|
84
|
+
const VersionSchema = zod.object({
|
|
85
|
+
isLatestVersion: zod.boolean().optional(),
|
|
86
|
+
versionNo: zod.number().optional()
|
|
87
|
+
});
|
|
88
|
+
const StatusSchema = zod.object({
|
|
89
|
+
isArchive: zod.boolean().optional(),
|
|
90
|
+
color: zod.string().optional(),
|
|
91
|
+
name: zod.string().optional(),
|
|
92
|
+
id: zod.number().optional()
|
|
93
|
+
});
|
|
94
|
+
const PrioritySchema = zod.object({
|
|
95
|
+
name: zod.string().optional(),
|
|
96
|
+
id: zod.number().optional()
|
|
97
|
+
});
|
|
98
|
+
const LabelSchema = zod.object({
|
|
99
|
+
name: zod.string().optional(),
|
|
100
|
+
id: zod.number().optional()
|
|
101
|
+
});
|
|
102
|
+
const ComponentSchema = zod.object({
|
|
103
|
+
name: zod.string().optional(),
|
|
104
|
+
id: zod.number().optional()
|
|
105
|
+
});
|
|
106
|
+
const TestCaseSchema = zod.object({
|
|
107
|
+
id: zod.string().describe("Internal test case ID"),
|
|
108
|
+
key: zod.string().describe("Test case key, e.g. 'SCRUM-TC-145'"),
|
|
109
|
+
summary: zod.string().describe("Test case title"),
|
|
110
|
+
description: zod.string().nullable().optional(),
|
|
111
|
+
priority: PrioritySchema.optional(),
|
|
112
|
+
status: StatusSchema.optional(),
|
|
113
|
+
assignee: zod.string().nullable().optional(),
|
|
114
|
+
reporter: zod.string().optional(),
|
|
115
|
+
isAutomated: zod.boolean().optional(),
|
|
116
|
+
automated: zod.boolean().optional(),
|
|
117
|
+
estimatedTime: zod.string().nullable().optional(),
|
|
118
|
+
labels: zod.array(LabelSchema).nullable().optional(),
|
|
119
|
+
components: zod.array(ComponentSchema).nullable().optional(),
|
|
120
|
+
fixVersions: zod.array(zod.any()).nullable().optional(),
|
|
121
|
+
sprint: zod.any().nullable().optional(),
|
|
122
|
+
folders: zod.array(zod.any()).nullable().optional(),
|
|
123
|
+
created: zod.any().optional(),
|
|
124
|
+
updated: zod.any().optional(),
|
|
125
|
+
executed: zod.string().nullable().optional(),
|
|
126
|
+
flakyScore: zod.number().nullable().optional(),
|
|
127
|
+
passRateScore: zod.number().nullable().optional(),
|
|
128
|
+
aiGenerated: zod.boolean().optional(),
|
|
129
|
+
precondition: zod.string().nullable().optional(),
|
|
130
|
+
orderNo: zod.number().nullable().optional(),
|
|
131
|
+
seqNo: zod.number().nullable().optional(),
|
|
132
|
+
version: VersionSchema.optional(),
|
|
133
|
+
projectId: zod.number().optional(),
|
|
134
|
+
shareable: zod.boolean().optional(),
|
|
135
|
+
archived: zod.boolean().optional()
|
|
136
|
+
}).passthrough();
|
|
137
|
+
const SearchTestCaseResponse = zod.object({
|
|
138
|
+
total: zod.number().describe("Total test cases matching the filter (across all pages)"),
|
|
139
|
+
startAt: zod.number().describe("Offset of this page"),
|
|
140
|
+
maxResults: zod.number().describe("Page size used for this response"),
|
|
141
|
+
data: zod.array(TestCaseSchema).describe("Test cases on this page")
|
|
142
|
+
});
|
|
143
|
+
export {
|
|
144
|
+
ComponentSchema,
|
|
145
|
+
LabelSchema,
|
|
146
|
+
PrioritySchema,
|
|
147
|
+
SearchTestCaseBody,
|
|
148
|
+
SearchTestCaseFilter,
|
|
149
|
+
SearchTestCaseResponse,
|
|
150
|
+
StatusSchema,
|
|
151
|
+
TestCaseSchema,
|
|
152
|
+
VersionSchema
|
|
153
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as zod from "zod";
|
|
2
|
+
import { PAGINATION } from "../config/constants.js";
|
|
3
|
+
const GetTestStepsFilter = zod.object({
|
|
4
|
+
stepDetails: zod.string().optional().describe(
|
|
5
|
+
"Substring match on step details (action text). Example: 'Open the application'"
|
|
6
|
+
),
|
|
7
|
+
testData: zod.string().optional().describe(
|
|
8
|
+
"Substring match on test data. Example: 'Username: user1, Password: pass123'"
|
|
9
|
+
),
|
|
10
|
+
expectedResult: zod.string().optional().describe(
|
|
11
|
+
"Substring match on expected result. Example: 'User should be logged in successfully'"
|
|
12
|
+
)
|
|
13
|
+
}).describe(
|
|
14
|
+
"Text filters for test steps — each field performs a substring match. Multiple fields are combined with AND."
|
|
15
|
+
);
|
|
16
|
+
const GetTestStepsBody = zod.object({
|
|
17
|
+
key: zod.string().describe(
|
|
18
|
+
"Test case key in the format '{PROJECT_KEY}-TC-{number}', e.g. 'SCRUM-TC-145'. PROJECT_KEY is the Jira project key (e.g. 'SCRUM'). The number is the test case counter within that project (auto-incremented, not related to seqNo). Obtain keys from the search_test_cases tool or directly from QTM4J."
|
|
19
|
+
),
|
|
20
|
+
versionNo: zod.number().int().min(1).optional().describe(
|
|
21
|
+
"Test case version number. Defaults to the latest version if omitted. Obtain from search_test_cases response field: version.versionNo"
|
|
22
|
+
),
|
|
23
|
+
filter: GetTestStepsFilter.optional(),
|
|
24
|
+
startAt: zod.number().min(0).optional().default(PAGINATION.DEFAULT_START_AT).describe(
|
|
25
|
+
"Zero-indexed offset for pagination (URL query param). Default: 0."
|
|
26
|
+
),
|
|
27
|
+
maxResults: zod.number().min(PAGINATION.MIN_ALLOWED_RESULTS).max(PAGINATION.MAX_ALLOWED_RESULTS_TEST_STEPS).optional().default(PAGINATION.DEFAULT_MAX_RESULTS_TEST_STEPS).describe(
|
|
28
|
+
"Number of steps per page (URL query param). Default: 50. Maximum: 100."
|
|
29
|
+
),
|
|
30
|
+
sort: zod.string().optional().describe(
|
|
31
|
+
"Sort pattern (URL query param). Format: 'fieldName:order'. Sortable fields: stepDetails, testData, seqNo, expectedResult. Order values: 'asc' or 'desc'. Example: 'seqNo:asc'"
|
|
32
|
+
)
|
|
33
|
+
});
|
|
34
|
+
const ShareableTestStepSchema = zod.object({
|
|
35
|
+
id: zod.number().optional(),
|
|
36
|
+
seqNo: zod.union([zod.string(), zod.number()]).optional().describe("Sequence number within the shared test case, e.g. '1.1', '1.3'"),
|
|
37
|
+
stepDetails: zod.string().describe("Step action / description — always required"),
|
|
38
|
+
testData: zod.string().nullish().describe("Input data for this step"),
|
|
39
|
+
expectedResult: zod.string().nullish().describe("Expected outcome of this step"),
|
|
40
|
+
testcase_version_id: zod.number().optional()
|
|
41
|
+
});
|
|
42
|
+
const ShareableSchema = zod.object({
|
|
43
|
+
shareableTestcaseUID: zod.string().optional(),
|
|
44
|
+
shareableVersionNo: zod.number().optional(),
|
|
45
|
+
archived: zod.boolean().optional(),
|
|
46
|
+
projectId: zod.number().optional(),
|
|
47
|
+
shareableTestSteps: zod.array(ShareableTestStepSchema).optional().describe("Embedded steps from the shared test case")
|
|
48
|
+
}).describe(
|
|
49
|
+
"Present when this step is a reference to a shared (reusable) test case. Contains its embedded sub-steps."
|
|
50
|
+
);
|
|
51
|
+
const TestStepSchema = zod.object({
|
|
52
|
+
id: zod.number().describe("Internal test step ID"),
|
|
53
|
+
seqNo: zod.union([zod.string(), zod.number()]).optional().describe("Step sequence number within the test case (e.g. 1, 2, 3)"),
|
|
54
|
+
stepDetails: zod.string().describe("Step action / description — always required"),
|
|
55
|
+
testData: zod.string().nullish().describe("Input data for this step"),
|
|
56
|
+
expectedResult: zod.string().nullish().describe("Expected outcome of this step"),
|
|
57
|
+
testcase_version_id: zod.number().optional(),
|
|
58
|
+
attachmentCount: zod.number().optional().describe("Number of attachments on this step"),
|
|
59
|
+
shareable: ShareableSchema.nullable().optional()
|
|
60
|
+
}).passthrough();
|
|
61
|
+
const GetTestStepsResponse = zod.object({
|
|
62
|
+
total: zod.number().describe("Total steps matching the filter (across all pages)"),
|
|
63
|
+
startAt: zod.number().describe("Offset of this page"),
|
|
64
|
+
maxResults: zod.number().describe("Page size used for this response"),
|
|
65
|
+
data: zod.array(TestStepSchema).describe("Test steps on this page")
|
|
66
|
+
});
|
|
67
|
+
export {
|
|
68
|
+
GetTestStepsBody,
|
|
69
|
+
GetTestStepsFilter,
|
|
70
|
+
GetTestStepsResponse,
|
|
71
|
+
ShareableSchema,
|
|
72
|
+
ShareableTestStepSchema,
|
|
73
|
+
TestStepSchema
|
|
74
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as zod from "zod";
|
|
2
|
+
import { PAGINATION, SCHEMA_DESCRIPTIONS } from "../config/constants.js";
|
|
3
|
+
const ProjectSchema = zod.object({
|
|
4
|
+
id: zod.number().describe("Project ID"),
|
|
5
|
+
key: zod.string().describe("Project key (e.g., 'SCRUM', 'AD')"),
|
|
6
|
+
name: zod.string().describe("Project name"),
|
|
7
|
+
favorite: zod.boolean().optional().describe("Whether project is marked as favorite"),
|
|
8
|
+
avatarUrl: zod.string().optional().nullable().describe("Project avatar URL"),
|
|
9
|
+
projectTypeKey: zod.string().optional().nullable().describe("Project type key (e.g., 'software')"),
|
|
10
|
+
qmetryEnabled: zod.boolean().optional().describe("Whether QMetry is enabled for this project")
|
|
11
|
+
}).passthrough().describe(SCHEMA_DESCRIPTIONS.PROJECT_OBJECT);
|
|
12
|
+
const GetProjectsBody = zod.object({
|
|
13
|
+
projectId: zod.number().optional().describe("Filter by specific project ID"),
|
|
14
|
+
search: zod.string().optional().describe(SCHEMA_DESCRIPTIONS.SEARCH_TEXT),
|
|
15
|
+
qmetryEnabled: zod.boolean().optional().describe(SCHEMA_DESCRIPTIONS.QMETRY_ENABLED),
|
|
16
|
+
startAt: zod.number().min(PAGINATION.MIN_ALLOWED_RESULTS - 1).default(PAGINATION.DEFAULT_START_AT).describe(SCHEMA_DESCRIPTIONS.START_AT),
|
|
17
|
+
maxResults: zod.number().min(PAGINATION.MIN_ALLOWED_RESULTS).max(PAGINATION.MAX_ALLOWED_RESULTS).default(PAGINATION.DEFAULT_MAX_RESULTS_PROJECTS).describe(SCHEMA_DESCRIPTIONS.MAX_RESULTS_PROJECTS)
|
|
18
|
+
});
|
|
19
|
+
const GetProjectsResponse = zod.object({
|
|
20
|
+
total: zod.number().min(PAGINATION.MIN_ALLOWED_RESULTS - 1).describe("Total number of projects"),
|
|
21
|
+
data: zod.array(ProjectSchema).describe("List of projects")
|
|
22
|
+
});
|
|
23
|
+
const SetProjectContextBody = zod.object({
|
|
24
|
+
projectKey: zod.string().describe(
|
|
25
|
+
"Project key (e.g., 'SCRUM'). Use the get_projects tool to discover available project keys."
|
|
26
|
+
)
|
|
27
|
+
});
|
|
28
|
+
const SetProjectContextResponse = zod.object({
|
|
29
|
+
projectId: zod.number().describe("Numeric project ID"),
|
|
30
|
+
projectKey: zod.string().describe("Project key"),
|
|
31
|
+
projectName: zod.string().describe("Project name"),
|
|
32
|
+
message: zod.string().describe("Confirmation message"),
|
|
33
|
+
availableFields: zod.record(zod.string(), zod.record(zod.string(), zod.string())).optional().describe(
|
|
34
|
+
"Available field values keyed by field name (e.g. 'priority', 'testcase_status'). Use these to map user input via NLP (e.g. user says 'Major' → send 'High')."
|
|
35
|
+
)
|
|
36
|
+
});
|
|
37
|
+
export {
|
|
38
|
+
GetProjectsBody,
|
|
39
|
+
GetProjectsResponse,
|
|
40
|
+
ProjectSchema,
|
|
41
|
+
SetProjectContextBody,
|
|
42
|
+
SetProjectContextResponse
|
|
43
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as zod from "zod";
|
|
2
|
+
const TestStepSchema = zod.object({
|
|
3
|
+
stepDetails: zod.string().describe("Step description/action — always required for each step"),
|
|
4
|
+
testData: zod.string().optional().describe(
|
|
5
|
+
"Test data for this step. Always include this field for each step — use empty string if no test data is applicable."
|
|
6
|
+
),
|
|
7
|
+
expectedResult: zod.string().optional().describe(
|
|
8
|
+
"Expected result after performing the step. Always include this field for each step — describe what should happen."
|
|
9
|
+
)
|
|
10
|
+
});
|
|
11
|
+
const CreateTestCaseBody = zod.object({
|
|
12
|
+
summary: zod.string().describe("Test case summary/title"),
|
|
13
|
+
description: zod.string().optional().describe("Test case description"),
|
|
14
|
+
folderId: zod.number().optional().describe("Folder ID to place the test case in"),
|
|
15
|
+
priority: zod.string().optional().describe(
|
|
16
|
+
"Priority name (e.g., 'High', 'Medium', 'Low'). Auto-resolved to ID."
|
|
17
|
+
),
|
|
18
|
+
status: zod.string().optional().describe(
|
|
19
|
+
"Status name (e.g., 'To Do', 'In Progress', 'Done'). Auto-resolved to ID."
|
|
20
|
+
),
|
|
21
|
+
assignee: zod.string().optional().describe("Assignee account ID"),
|
|
22
|
+
reporter: zod.string().optional().describe("Reporter account ID"),
|
|
23
|
+
components: zod.array(zod.string()).optional().describe(
|
|
24
|
+
"List of component names (e.g., ['UI', 'Cloud']). Auto-resolved to IDs."
|
|
25
|
+
),
|
|
26
|
+
labels: zod.array(zod.string()).optional().describe(
|
|
27
|
+
"List of label names (e.g., ['Release_1', 'Sprint 1']). Auto-resolved to IDs."
|
|
28
|
+
),
|
|
29
|
+
steps: zod.array(TestStepSchema).optional().describe("List of test steps")
|
|
30
|
+
});
|
|
31
|
+
const CreateTestCaseResponse = zod.object({
|
|
32
|
+
id: zod.string().describe("Unique test case ID"),
|
|
33
|
+
key: zod.string().describe("Test case key (e.g., 'SCRUM-TC-190')"),
|
|
34
|
+
versionNo: zod.number().describe("Version number"),
|
|
35
|
+
summary: zod.string().describe("Test case summary")
|
|
36
|
+
});
|
|
37
|
+
export {
|
|
38
|
+
CreateTestCaseBody,
|
|
39
|
+
CreateTestCaseResponse,
|
|
40
|
+
TestStepSchema
|
|
41
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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., ['Release_1', 'Sprint 1']). Auto-resolved to IDs."
|
|
5
|
+
),
|
|
6
|
+
delete: zod.array(zod.string()).optional().describe("Names to remove (e.g., ['Sprint 1']). 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 UpdateTestCaseBody = zod.object({
|
|
11
|
+
key: zod.string().describe(
|
|
12
|
+
"Test case key in the format '{PROJECT_KEY}-TC-{number}', e.g. 'SCRUM-TC-145'. Automatically resolved to the internal ID and latest version."
|
|
13
|
+
),
|
|
14
|
+
versionNo: zod.number().int().min(1).optional().describe(
|
|
15
|
+
"Test case version number to update. Defaults to the latest version if omitted."
|
|
16
|
+
),
|
|
17
|
+
summary: zod.string().min(1).optional().describe("Updated test case summary/title."),
|
|
18
|
+
description: zod.string().optional().describe("Updated test case description."),
|
|
19
|
+
precondition: zod.string().optional().describe(
|
|
20
|
+
"Updated precondition — conditions that must be true before the test is executed."
|
|
21
|
+
),
|
|
22
|
+
priority: zod.string().optional().describe(
|
|
23
|
+
"Priority name (e.g., 'High', 'Medium', 'Low'). Auto-resolved to ID. Use values from set_project_context response."
|
|
24
|
+
),
|
|
25
|
+
status: zod.string().optional().describe(
|
|
26
|
+
"Status name (e.g., 'To Do', 'In Progress', 'Done'). Auto-resolved to ID. Use values from set_project_context response."
|
|
27
|
+
),
|
|
28
|
+
assignee: zod.string().optional().describe("Assignee Jira account ID (e.g., '5b10a2844c20165700ede21f')."),
|
|
29
|
+
estimatedTime: zod.string().regex(/^\d{2}:\d{2}:\d{2}$/, "Must be in HH:MM:SS format").optional().describe("Estimated time in HH:MM:SS format (e.g., '02:30:00')."),
|
|
30
|
+
labels: MetadataAddDelete.optional().describe(
|
|
31
|
+
"Labels to add or remove by name. Each name is auto-resolved to its ID."
|
|
32
|
+
),
|
|
33
|
+
components: MetadataAddDelete.optional().describe(
|
|
34
|
+
"Components to add or remove by name. Each name is auto-resolved to its ID."
|
|
35
|
+
)
|
|
36
|
+
});
|
|
37
|
+
const UpdateTestCaseResponse = zod.object({
|
|
38
|
+
key: zod.string().describe("Test case key that was updated"),
|
|
39
|
+
versionNo: zod.number().describe("Version number that was updated"),
|
|
40
|
+
updated: zod.literal(true).describe("Confirms the update was applied")
|
|
41
|
+
});
|
|
42
|
+
export {
|
|
43
|
+
UpdateTestCaseBody,
|
|
44
|
+
UpdateTestCaseResponse
|
|
45
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Tool } from "../../../common/tools.js";
|
|
2
|
+
import { TOOL_NAMES, RESPONSE_FIELDS, PAGINATION, ENDPOINTS } from "../../config/constants.js";
|
|
3
|
+
import { GetProjectsResponse, GetProjectsBody } from "../../schema/project.schema.js";
|
|
4
|
+
class GetProjects extends Tool {
|
|
5
|
+
specification = {
|
|
6
|
+
title: TOOL_NAMES.GET_PROJECTS.TITLE,
|
|
7
|
+
summary: TOOL_NAMES.GET_PROJECTS.SUMMARY,
|
|
8
|
+
readOnly: true,
|
|
9
|
+
idempotent: true,
|
|
10
|
+
inputSchema: GetProjectsBody,
|
|
11
|
+
outputSchema: GetProjectsResponse,
|
|
12
|
+
purpose: "Retrieve project information including IDs, keys, and names from QTM4J. Useful for discovering available projects, validating project access, and getting project metadata.",
|
|
13
|
+
useCases: [
|
|
14
|
+
"Discover all projects available in QTM4J instance",
|
|
15
|
+
"Get project IDs and keys for reference in other operations",
|
|
16
|
+
"Find specific projects by ID",
|
|
17
|
+
"Search projects by text in project key or name",
|
|
18
|
+
"Filter projects by QMetry integration status",
|
|
19
|
+
"List projects with pagination for large QTM4J instances",
|
|
20
|
+
"Retrieve complete project details (ID, key, name, avatarUrl, projectTypeKey, qmetryEnabled, favorite)",
|
|
21
|
+
"Validate project access and permissions",
|
|
22
|
+
"Browse available projects before performing other operations"
|
|
23
|
+
],
|
|
24
|
+
examples: [
|
|
25
|
+
{
|
|
26
|
+
description: "Get all projects (default pagination - first 100)",
|
|
27
|
+
parameters: {},
|
|
28
|
+
expectedOutput: "List of all projects with IDs, keys, and names (first 100 projects)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
description: "Get the first 10 projects",
|
|
32
|
+
parameters: {
|
|
33
|
+
maxResults: 10
|
|
34
|
+
},
|
|
35
|
+
expectedOutput: "List of first 10 projects with their details"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
description: "Get a specific project by ID",
|
|
39
|
+
parameters: {
|
|
40
|
+
projectId: 1e4
|
|
41
|
+
},
|
|
42
|
+
expectedOutput: "Single project with ID 10000 including key, name, and QMetry status"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
description: "Search projects by text in project key or name",
|
|
46
|
+
parameters: {
|
|
47
|
+
search: "SCRUM"
|
|
48
|
+
},
|
|
49
|
+
expectedOutput: "Projects matching 'SCRUM' search text in their project keys or names"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
description: "Get only QMetry-enabled projects",
|
|
53
|
+
parameters: {
|
|
54
|
+
qmetryEnabled: true
|
|
55
|
+
},
|
|
56
|
+
expectedOutput: "List of projects that have QMetry integration enabled"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
description: "Get projects with custom pagination (page 2)",
|
|
60
|
+
parameters: {
|
|
61
|
+
startAt: 50,
|
|
62
|
+
maxResults: 50
|
|
63
|
+
},
|
|
64
|
+
expectedOutput: "Second page of projects (items 51-100) with their details"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
description: "Search QMetry-enabled projects by text",
|
|
68
|
+
parameters: {
|
|
69
|
+
search: "TEST",
|
|
70
|
+
qmetryEnabled: true
|
|
71
|
+
},
|
|
72
|
+
expectedOutput: "QMetry-enabled projects containing 'TEST' in their project keys"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
hints: [
|
|
76
|
+
"Project IDs are numeric (e.g., 10000), project keys are strings (e.g., 'SCRUM')",
|
|
77
|
+
"Use 'projectId' parameter to filter by a specific project ID",
|
|
78
|
+
"Use 'search' parameter to search by text in project key or project name",
|
|
79
|
+
"Use 'qmetryEnabled' parameter to filter projects by QMetry integration status",
|
|
80
|
+
"Response contains complete project details: id, key, name, favorite, avatarUrl, projectTypeKey, qmetryEnabled",
|
|
81
|
+
"Pagination: startAt is zero-indexed, maxResults max is 100, default is 100",
|
|
82
|
+
"Default (no parameters) returns first 100 projects",
|
|
83
|
+
"Use 'isLast' in response to check if more pages are available",
|
|
84
|
+
"To get next page: increment startAt by maxResults (0 → 100 → 200)",
|
|
85
|
+
"Use 'total' in response for total count of matching projects"
|
|
86
|
+
],
|
|
87
|
+
outputDescription: "JSON object containing paginated list of projects with IDs, keys, names, and QMetry status, along with pagination metadata"
|
|
88
|
+
};
|
|
89
|
+
handle = async (args) => {
|
|
90
|
+
const input = GetProjectsBody.parse(args);
|
|
91
|
+
const body = {
|
|
92
|
+
[RESPONSE_FIELDS.START_AT]: input.startAt ?? PAGINATION.DEFAULT_START_AT,
|
|
93
|
+
[RESPONSE_FIELDS.MAX_RESULTS]: input.maxResults ?? PAGINATION.DEFAULT_MAX_RESULTS_PROJECTS,
|
|
94
|
+
...input.projectId !== void 0 && { projectId: input.projectId },
|
|
95
|
+
...input.search && { search: input.search },
|
|
96
|
+
...input.qmetryEnabled !== void 0 && {
|
|
97
|
+
qmetryEnabled: input.qmetryEnabled
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const apiClient = this.client.getApiClient();
|
|
101
|
+
const response = await apiClient.post(ENDPOINTS.PROJECTS, body);
|
|
102
|
+
const validatedResponse = GetProjectsResponse.parse(response);
|
|
103
|
+
return {
|
|
104
|
+
structuredContent: validatedResponse,
|
|
105
|
+
content: []
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
GetProjects
|
|
111
|
+
};
|