@smartbear/mcp 0.20.0 → 0.22.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 +23 -6
- package/dist/common/register-clients.js +2 -0
- package/dist/common/server.js +1 -4
- package/dist/package.json.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/package.json +5 -4
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Tool, ToolError } from "../../../common/tools.js";
|
|
2
|
+
import { TOOL_NAMES, RESPONSE_FIELDS, ENDPOINTS } from "../../config/constants.js";
|
|
3
|
+
import { ResolverKeys } from "../../config/field-resolution.types.js";
|
|
4
|
+
import { GetTestStepsResponse, GetTestStepsBody } from "../../schema/get-test-steps.schema.js";
|
|
5
|
+
class GetTestSteps extends Tool {
|
|
6
|
+
specification = {
|
|
7
|
+
title: TOOL_NAMES.GET_TEST_STEPS.TITLE,
|
|
8
|
+
summary: TOOL_NAMES.GET_TEST_STEPS.SUMMARY,
|
|
9
|
+
readOnly: true,
|
|
10
|
+
idempotent: true,
|
|
11
|
+
inputSchema: GetTestStepsBody,
|
|
12
|
+
outputSchema: GetTestStepsResponse,
|
|
13
|
+
purpose: "Retrieve the test steps for a specific test case version using the human-readable test case key. The key is automatically resolved to the internal test case UID via a dedicated batch API — no separate lookup required. Test steps describe the step-by-step actions (stepDetails), input data (testData), and expected results (expectedResult) for executing a test case. Steps may also reference shared (reusable) test cases, surfaced via the 'shareable' field. PREREQUISITE: set_project_context must be called before this tool.",
|
|
14
|
+
useCases: [
|
|
15
|
+
"View all steps of a test case before executing it",
|
|
16
|
+
"Review steps for a specific test case version",
|
|
17
|
+
"Filter steps by action text, test data, or expected result",
|
|
18
|
+
"Get steps for a test case found via search_test_cases",
|
|
19
|
+
"Inspect shared (reusable) steps embedded in a test case",
|
|
20
|
+
"Sort steps by sequence number to view them in execution order",
|
|
21
|
+
"Paginate through test cases that have a large number of steps"
|
|
22
|
+
],
|
|
23
|
+
examples: [
|
|
24
|
+
{
|
|
25
|
+
description: "Get all steps for a test case (latest version)",
|
|
26
|
+
parameters: { key: "SCRUM-TC-145" },
|
|
27
|
+
expectedOutput: "All steps for SCRUM-TC-145 with stepDetails, testData, expectedResult, and any shared step blocks"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
description: "Get steps for a specific version",
|
|
31
|
+
parameters: { key: "SCRUM-TC-145", versionNo: 2 },
|
|
32
|
+
expectedOutput: "Steps for version 2 of SCRUM-TC-145"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
description: "Get steps in execution order",
|
|
36
|
+
parameters: { key: "SCRUM-TC-85", sort: "seqNo:asc" },
|
|
37
|
+
expectedOutput: "All steps sorted by sequence number ascending"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
description: "Filter steps by action text",
|
|
41
|
+
parameters: {
|
|
42
|
+
key: "SCRUM-TC-32",
|
|
43
|
+
filter: { stepDetails: "Open the application" }
|
|
44
|
+
},
|
|
45
|
+
expectedOutput: "Steps whose stepDetails contain 'Open the application'"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
description: "Filter steps by expected result",
|
|
49
|
+
parameters: {
|
|
50
|
+
key: "SCRUM-TC-65",
|
|
51
|
+
filter: { expectedResult: "logged in successfully" }
|
|
52
|
+
},
|
|
53
|
+
expectedOutput: "Steps whose expectedResult contains 'logged in successfully'"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
description: "Filter steps by test data",
|
|
57
|
+
parameters: {
|
|
58
|
+
key: "SCRUM-TC-125",
|
|
59
|
+
filter: { testData: "Username: user1" }
|
|
60
|
+
},
|
|
61
|
+
expectedOutput: "Steps with testData containing 'Username: user1'"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
description: "Paginate through many steps",
|
|
65
|
+
parameters: {
|
|
66
|
+
key: "SCRUM-TC-105",
|
|
67
|
+
startAt: 0,
|
|
68
|
+
maxResults: 10,
|
|
69
|
+
sort: "seqNo:asc"
|
|
70
|
+
},
|
|
71
|
+
expectedOutput: "First 10 steps in sequence order"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
hints: [
|
|
75
|
+
"PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
|
|
76
|
+
"KEY FORMAT: '{PROJECT_KEY}-TC-{number}' — e.g. 'SCRUM-TC-145'. PROJECT_KEY is the Jira project key; the number is the test case counter within that project (auto-incremented, not the same as seqNo).",
|
|
77
|
+
"VERSION: versionNo defaults to the latest version. Get the version number from search_test_cases response: version.versionNo.",
|
|
78
|
+
"Use search_test_cases to discover test case keys before calling this tool.",
|
|
79
|
+
"SHAREABLE STEPS: Steps with a non-null 'shareable' field are references to shared/reusable test cases. The 'shareable.shareableTestSteps' array contains the embedded sub-steps with decimal seqNo values (e.g. '1.1', '1.2').",
|
|
80
|
+
"FILTER: Each filter field is a substring match (case-insensitive). Multiple fields combine with AND.",
|
|
81
|
+
"SORT: 'seqNo:asc' shows steps in their natural execution order. Allowed sort fields: stepDetails, testData, seqNo, expectedResult.",
|
|
82
|
+
"PAGINATION: startAt and maxResults are URL query params. Default page size is 50, maximum is 100."
|
|
83
|
+
],
|
|
84
|
+
outputDescription: "JSON object with total (total matching steps), startAt, maxResults, and data (array of step objects). Each step has: id, seqNo, stepDetails, testData, expectedResult, attachmentCount. Shared steps also have a 'shareable' object containing shareableTestcaseUID and shareableTestSteps array."
|
|
85
|
+
};
|
|
86
|
+
handle = async (rawArgs) => {
|
|
87
|
+
const args = GetTestStepsBody.parse(rawArgs);
|
|
88
|
+
const fieldResolver = this.client.getResolverRegistry();
|
|
89
|
+
const context = fieldResolver.requireProjectContext();
|
|
90
|
+
const resolved = await fieldResolver.getResolver(ResolverKeys.SearchableField.TEST_CASE_KEY_TO_UID).resolveAndReturn(context.projectId, [args.key]);
|
|
91
|
+
const entry = resolved[args.key];
|
|
92
|
+
if (!entry) {
|
|
93
|
+
throw new ToolError(
|
|
94
|
+
`Test case '${args.key}' not found in project '${context.projectKey}'. Verify the key using the search_test_cases tool.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const versionNo = args.versionNo ?? entry.latestVersion;
|
|
98
|
+
const params = new URLSearchParams();
|
|
99
|
+
params.set(RESPONSE_FIELDS.START_AT, String(args.startAt));
|
|
100
|
+
params.set(RESPONSE_FIELDS.MAX_RESULTS, String(args.maxResults));
|
|
101
|
+
if (args.sort) params.set(RESPONSE_FIELDS.SORT, args.sort);
|
|
102
|
+
const endpoint = `${ENDPOINTS.TEST_STEPS(entry.uid, versionNo)}?${params.toString()}`;
|
|
103
|
+
const filterBody = args.filter && Object.keys(args.filter).length > 0 ? { filter: args.filter } : {};
|
|
104
|
+
const response = await this.client.getApiClient().post(endpoint, filterBody);
|
|
105
|
+
const validated = GetTestStepsResponse.parse(response);
|
|
106
|
+
return { structuredContent: validated, content: [] };
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
GetTestSteps
|
|
111
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Tool, ToolError } 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 { UpdateTestCaseResponse, UpdateTestCaseBody } from "../../schema/update-test-case.schema.js";
|
|
5
|
+
const SIMPLE_FIELD_CONFIG = {
|
|
6
|
+
[InputField.PRIORITY]: ResolverKeys.CommonAttribute.PRIORITY,
|
|
7
|
+
[InputField.STATUS]: ResolverKeys.CommonAttribute.TESTCASE_STATUS
|
|
8
|
+
};
|
|
9
|
+
const ADD_DELETE_FIELD_CONFIG = {
|
|
10
|
+
[InputField.LABELS]: ResolverKeys.SearchableField.LABEL,
|
|
11
|
+
[InputField.COMPONENTS]: ResolverKeys.SearchableField.COMPONENTS
|
|
12
|
+
};
|
|
13
|
+
async function resolveAddDelete(resolver, inputField, resolverKey, field, context, warnings) {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const op of ["add", "delete"]) {
|
|
16
|
+
const names = field[op];
|
|
17
|
+
if (!names?.length) continue;
|
|
18
|
+
const tempBody = { [inputField]: names };
|
|
19
|
+
await resolver.resolve(
|
|
20
|
+
inputField,
|
|
21
|
+
resolverKey,
|
|
22
|
+
tempBody,
|
|
23
|
+
context,
|
|
24
|
+
warnings
|
|
25
|
+
);
|
|
26
|
+
const val = tempBody[inputField];
|
|
27
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "number") {
|
|
28
|
+
result[op] = val;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
class UpdateTestCase extends Tool {
|
|
34
|
+
specification = {
|
|
35
|
+
title: TOOL_NAMES.UPDATE_TEST_CASE.TITLE,
|
|
36
|
+
summary: TOOL_NAMES.UPDATE_TEST_CASE.SUMMARY,
|
|
37
|
+
readOnly: false,
|
|
38
|
+
idempotent: true,
|
|
39
|
+
inputSchema: UpdateTestCaseBody,
|
|
40
|
+
outputSchema: UpdateTestCaseResponse,
|
|
41
|
+
purpose: "Update an existing test case in QTM4J using its human-readable key (e.g. 'SCRUM-TC-145'). The key is automatically resolved to the internal UID and latest version — no manual ID lookup needed. Only the fields you provide are changed — omitted fields are left as-is. For priority and status, supply the human-readable name and it is auto-resolved to the internal ID. For labels and components, use the add/delete object to atomically add or remove entries by name. PREREQUISITE: set_project_context must be called before this tool.",
|
|
42
|
+
useCases: [
|
|
43
|
+
"Change the priority of a test case (e.g., escalate to 'High')",
|
|
44
|
+
"Update the status of a test case after review",
|
|
45
|
+
"Add new labels or remove outdated ones without affecting other labels",
|
|
46
|
+
"Add or remove components from a test case",
|
|
47
|
+
"Update summary, description, or precondition text",
|
|
48
|
+
"Reassign a test case to a different team member",
|
|
49
|
+
"Set or update the estimated time for a test case",
|
|
50
|
+
"Batch-update metadata as part of sprint planning"
|
|
51
|
+
],
|
|
52
|
+
examples: [
|
|
53
|
+
{
|
|
54
|
+
description: "Change the priority of a test case",
|
|
55
|
+
parameters: { key: "SCRUM-TC-145", priority: "High" },
|
|
56
|
+
expectedOutput: "Test case updated with new priority"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
description: "Add a label and remove an old one",
|
|
60
|
+
parameters: {
|
|
61
|
+
key: "SCRUM-TC-145",
|
|
62
|
+
labels: { add: ["Release_2"], delete: ["Release_1"] }
|
|
63
|
+
},
|
|
64
|
+
expectedOutput: "Test case updated — Release_2 added, Release_1 removed"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
description: "Update summary, status, and add a component",
|
|
68
|
+
parameters: {
|
|
69
|
+
key: "SCRUM-TC-32",
|
|
70
|
+
summary: "Verify login with MFA enabled",
|
|
71
|
+
status: "In Progress",
|
|
72
|
+
components: { add: ["Auth"] }
|
|
73
|
+
},
|
|
74
|
+
expectedOutput: "Test case summary and status updated, Auth component added"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
description: "Update a specific version",
|
|
78
|
+
parameters: {
|
|
79
|
+
key: "SCRUM-TC-85",
|
|
80
|
+
versionNo: 2,
|
|
81
|
+
assignee: "5b10a2844c20165700ede21f",
|
|
82
|
+
estimatedTime: "01:30:00"
|
|
83
|
+
},
|
|
84
|
+
expectedOutput: "Version 2 of test case updated with new assignee and estimated time"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
hints: [
|
|
88
|
+
"PREREQUISITE: set_project_context must be called before this tool. NEVER auto-select a project.",
|
|
89
|
+
"KEY FORMAT: '{PROJECT_KEY}-TC-{number}' — e.g. 'SCRUM-TC-145'.",
|
|
90
|
+
"Priority and status values come from set_project_context. Use NLP to map user intent to available names.",
|
|
91
|
+
"If priority or status name is not found, the field is skipped with a warning and other fields are still updated.",
|
|
92
|
+
"Labels and components use add/delete — you can add and delete in a single call. Names are auto-resolved.",
|
|
93
|
+
"To delete ALL current entries of any add/delete field: first call search_test_cases with filter.searchText set to the test case key and include the relevant field in the fields list, extract all current names from the response, then pass them in the delete array of this tool.",
|
|
94
|
+
"Only provide the fields you want to change. Omitted fields remain unchanged on the server.",
|
|
95
|
+
"estimatedTime must be in HH:MM:SS format (e.g., '02:30:00').",
|
|
96
|
+
"versionNo defaults to the latest version. Use search_test_cases to find available versions if needed."
|
|
97
|
+
],
|
|
98
|
+
outputDescription: "Confirmation object with the test case key, versionNo updated, and updated: true. Warnings are included if any field names could not be resolved."
|
|
99
|
+
};
|
|
100
|
+
handle = async (rawArgs) => {
|
|
101
|
+
const args = UpdateTestCaseBody.parse(rawArgs);
|
|
102
|
+
const fieldResolver = this.client.getResolverRegistry();
|
|
103
|
+
const context = fieldResolver.requireProjectContext();
|
|
104
|
+
const warnings = [];
|
|
105
|
+
const uidMap = await fieldResolver.getResolver(ResolverKeys.SearchableField.TEST_CASE_KEY_TO_UID).resolveAndReturn(context.projectId, [args.key]);
|
|
106
|
+
const entry = uidMap[args.key];
|
|
107
|
+
if (!entry) {
|
|
108
|
+
throw new ToolError(
|
|
109
|
+
`Test case '${args.key}' not found in project '${context.projectKey}'. Verify the key using the search_test_cases tool.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const versionNo = args.versionNo ?? entry.latestVersion;
|
|
113
|
+
const {
|
|
114
|
+
key: _key,
|
|
115
|
+
versionNo: _vno,
|
|
116
|
+
labels,
|
|
117
|
+
components,
|
|
118
|
+
...scalarArgs
|
|
119
|
+
} = args;
|
|
120
|
+
const body = { ...scalarArgs };
|
|
121
|
+
await Promise.all(
|
|
122
|
+
Object.entries(SIMPLE_FIELD_CONFIG).map(
|
|
123
|
+
([inputField, resolverKey]) => fieldResolver.getResolver(resolverKey).resolve(inputField, resolverKey, body, context, warnings)
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
for (const field of Object.keys(SIMPLE_FIELD_CONFIG)) {
|
|
127
|
+
if (typeof body[field] === "string") delete body[field];
|
|
128
|
+
}
|
|
129
|
+
for (const [inputField, resolverKey] of Object.entries(
|
|
130
|
+
ADD_DELETE_FIELD_CONFIG
|
|
131
|
+
)) {
|
|
132
|
+
const field = args[inputField];
|
|
133
|
+
if (!field) continue;
|
|
134
|
+
const resolvedField = await resolveAddDelete(
|
|
135
|
+
fieldResolver.getResolver(resolverKey),
|
|
136
|
+
inputField,
|
|
137
|
+
resolverKey,
|
|
138
|
+
field,
|
|
139
|
+
context,
|
|
140
|
+
warnings
|
|
141
|
+
);
|
|
142
|
+
if (Object.keys(resolvedField).length > 0)
|
|
143
|
+
body[inputField] = resolvedField;
|
|
144
|
+
}
|
|
145
|
+
await this.client.getApiClient().put(ENDPOINTS.UPDATE_TEST_CASE(entry.uid, versionNo), body);
|
|
146
|
+
return {
|
|
147
|
+
structuredContent: UpdateTestCaseResponse.parse({
|
|
148
|
+
key: args.key,
|
|
149
|
+
versionNo,
|
|
150
|
+
updated: true
|
|
151
|
+
}),
|
|
152
|
+
content: warnings.length > 0 ? [{ type: "text", text: `Note: ${warnings.join(" | ")}` }] : []
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
UpdateTestCase
|
|
158
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartbear/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "MCP server for interacting SmartBear Products",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"smartbear",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"reflect",
|
|
10
10
|
"swagger",
|
|
11
11
|
"pactflow",
|
|
12
|
-
"zephyr"
|
|
12
|
+
"zephyr",
|
|
13
|
+
"qtm4j"
|
|
13
14
|
],
|
|
14
15
|
"homepage": "https://developer.smartbear.com/smartbear-mcp",
|
|
15
16
|
"mcpName": "com.smartbear/smartbear-mcp",
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsc && vite build && shx chmod +x dist/*.js",
|
|
36
|
+
"build:mcpb": "npm run build && node scripts/pack-mcpb.js",
|
|
35
37
|
"lint": "biome lint .",
|
|
36
38
|
"lint:fix": "biome lint . --fix",
|
|
37
39
|
"format": "biome format . --write",
|
|
@@ -44,8 +46,7 @@
|
|
|
44
46
|
"test:coverage:ci": "vitest --coverage --reporter=verbose",
|
|
45
47
|
"test:run": "vitest run",
|
|
46
48
|
"coverage:check": "vitest --coverage --reporter=verbose",
|
|
47
|
-
"bump": "node scripts/bump.js"
|
|
48
|
-
"postpack": "node scripts/pack-mcpb.js"
|
|
49
|
+
"bump": "node scripts/bump.js"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@bugsnag/js": "^8.8.1",
|