@smartbear/mcp 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,7 +107,8 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
107
107
  "COLLABORATOR_USERNAME": "${input:collab_username}",
108
108
  "COLLABORATOR_LOGIN_TICKET": "${input:collab_login_ticket}",
109
109
  "QTM4J_API_KEY": "${input:qtm4j_api_key}",
110
- "QTM4J_BASE_URL": "${input:qtm4j_base_url}"
110
+ "QTM4J_BASE_URL": "${input:qtm4j_base_url}",
111
+ "QTM4J_AUTOMATION_API_KEY": "${input:qtm4j_automation_api_key}"
111
112
  }
112
113
  }
113
114
  },
@@ -243,6 +244,12 @@ Alternatively, you can use `npx` (or globally install) the `@smartbear/mcp` pack
243
244
  "type": "promptString",
244
245
  "description": "US region (default): https://qtmcloud.qmetry.com. Australia region: https://syd-qtmcloud.qmetry.com.",
245
246
  "password": false
247
+ },
248
+ {
249
+ "id": "qtm4j_automation_api_key",
250
+ "type": "promptString",
251
+ "description": "QTM4J Automation API Key - required for automation tools, leave blank to disable them",
252
+ "password": true
246
253
  }
247
254
  ]
248
255
  }
@@ -283,7 +290,8 @@ Add the following configuration to your `claude_desktop_config.json` to launch t
283
290
  "COLLABORATOR_USERNAME": "your collab user name",
284
291
  "COLLABORATOR_LOGIN_TICKET": "your collab login ticket",
285
292
  "QTM4J_API_KEY": "your_qtm4j_key",
286
- "QTM4J_BASE_URL": "https://qtmcloud.qmetry.com"
293
+ "QTM4J_BASE_URL": "https://qtmcloud.qmetry.com",
294
+ "QTM4J_AUTOMATION_API_KEY": "your_qtm4j_automation_api_key"
287
295
  }
288
296
  }
289
297
  }
@@ -1,4 +1,4 @@
1
- const version = "0.23.0";
1
+ const version = "0.24.0";
2
2
  const config = { "mcpServerName": "SmartBear MCP Server" };
3
3
  const packageJson = {
4
4
  version,
@@ -308,7 +308,7 @@ const AdminUserIdSchema = z.object({
308
308
  userId: z.string().describe("UUID of the user")
309
309
  });
310
310
  const CreateAdminUserSchema = z.object({
311
- email: z.string().email().describe("Email address of the new user"),
311
+ email: z.string().describe("Email address of the new user"),
312
312
  name: z.string().describe("Display name of the new user"),
313
313
  firstName: z.string().optional().describe("First name"),
314
314
  lastName: z.string().optional().describe("Last name"),
@@ -318,7 +318,7 @@ const CreateAdminUserSchema = z.object({
318
318
  const UpdateAdminUserSchema = z.object({
319
319
  userId: z.string().describe("UUID of the user to update"),
320
320
  active: z.boolean().optional().describe("Whether the user is active"),
321
- email: z.string().email().optional().describe("New email address"),
321
+ email: z.string().optional().describe("New email address"),
322
322
  firstName: z.string().optional().describe("First name"),
323
323
  lastName: z.string().optional().describe("Last name"),
324
324
  name: z.string().optional().describe("Display name")
@@ -326,7 +326,7 @@ const UpdateAdminUserSchema = z.object({
326
326
  const InviteUsersSchema = z.object({
327
327
  users: z.array(
328
328
  z.object({
329
- email: z.string().email().describe("Email address"),
329
+ email: z.string().describe("Email address"),
330
330
  name: z.string().min(1).describe("Display name")
331
331
  })
332
332
  ).min(1).describe("List of users to invite")
@@ -5,6 +5,7 @@ import { ApiClient } from "./http/api-client.js";
5
5
  import { ResolverRegistry } from "./resolver/resolver-registry.js";
6
6
  const ConfigurationSchema = zod__default.object({
7
7
  [CONFIG_KEYS.API_KEY]: zod__default.string().describe(SCHEMA_DESCRIPTIONS.API_KEY),
8
+ [CONFIG_KEYS.AUTOMATION_API_KEY]: zod__default.string().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_API_KEY),
8
9
  [CONFIG_KEYS.BASE_URL]: zod__default.string().url().optional().default(API_CONFIG.DEFAULT_BASE_URL).describe(SCHEMA_DESCRIPTIONS.BASE_URL)
9
10
  });
10
11
  class Qtm4jClient {
@@ -13,6 +14,7 @@ class Qtm4jClient {
13
14
  configPrefix = CLIENT_CONFIG.CONFIG_PREFIX;
14
15
  config = ConfigurationSchema;
15
16
  _apiKey;
17
+ _automationApiKey;
16
18
  baseUrl = API_CONFIG.DEFAULT_BASE_URL;
17
19
  apiClient;
18
20
  resolverRegistry;
@@ -23,10 +25,15 @@ class Qtm4jClient {
23
25
  */
24
26
  async configure(server, config) {
25
27
  this._apiKey = config[CONFIG_KEYS.API_KEY];
28
+ this._automationApiKey = config[CONFIG_KEYS.AUTOMATION_API_KEY];
26
29
  if (config[CONFIG_KEYS.BASE_URL]) {
27
30
  this.baseUrl = config[CONFIG_KEYS.BASE_URL];
28
31
  }
29
- this.apiClient = new ApiClient(() => this.getAuthToken(), this.baseUrl);
32
+ this.apiClient = new ApiClient(
33
+ () => this.getAuthToken(),
34
+ this.baseUrl,
35
+ () => this.getAutomationApiKey()
36
+ );
30
37
  this.resolverRegistry = new ResolverRegistry(
31
38
  this.apiClient,
32
39
  server.getCache()
@@ -48,6 +55,13 @@ class Qtm4jClient {
48
55
  }
49
56
  return this._apiKey || null;
50
57
  }
58
+ getAutomationApiKey() {
59
+ const headerKey = getRequestHeader("Qtm4j-Automation-Api-Key");
60
+ if (headerKey) {
61
+ return Array.isArray(headerKey) ? headerKey[0] : headerKey;
62
+ }
63
+ return this._automationApiKey || null;
64
+ }
51
65
  /**
52
66
  * Check if the client is properly configured
53
67
  * @returns true if API key is set and client is ready
@@ -91,13 +105,23 @@ class Qtm4jClient {
91
105
  const { GetTestCases } = await import("./tool/test-case/get-test-cases.js");
92
106
  const { GetTestSteps } = await import("./tool/test-case/get-test-steps.js");
93
107
  const { UpdateTestCase } = await import("./tool/test-case/update-test-case.js");
108
+ const { UploadAutomationResult } = await import("./tool/test-automation/upload-automation-result.js");
109
+ const { GetAutomationHistory } = await import("./tool/test-automation/get-automation-history.js");
110
+ const { CreateTestCycle } = await import("./tool/test-cycle/create-test-cycle.js");
111
+ const { SearchTestCycles } = await import("./tool/test-cycle/search-test-cycle.js");
112
+ const { UpdateTestCycle } = await import("./tool/test-cycle/update-test-cycle.js");
94
113
  const tools = [
95
114
  new GetProjects(this),
96
115
  new SetProjectContext(this),
97
116
  new CreateTestCase(this),
98
117
  new GetTestCases(this),
99
118
  new GetTestSteps(this),
100
- new UpdateTestCase(this)
119
+ new UpdateTestCase(this),
120
+ new CreateTestCycle(this),
121
+ new SearchTestCycles(this),
122
+ new UpdateTestCycle(this),
123
+ new UploadAutomationResult(this),
124
+ new GetAutomationHistory(this)
101
125
  ];
102
126
  for (const tool of tools) {
103
127
  register(tool.specification, tool.handle);
@@ -13,6 +13,12 @@ const ENDPOINTS = {
13
13
  SEARCH_TEST_CASES: `${API_CONFIG.API_VERSION}/testcases/search`,
14
14
  /** Resolve test case keys → internal UIDs for a given project */
15
15
  RESOLVE_TEST_CASE_IDS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/testcases/resolve-ids`,
16
+ /** Update test cycle endpoint */
17
+ UPDATE_TEST_CYCLE: (id) => `${API_CONFIG.API_VERSION}/testcycles/${id}`,
18
+ /** Create test cycle endpoint */
19
+ CREATE_TEST_CYCLE: `${API_CONFIG.API_VERSION}/testcycles`,
20
+ /** Search test cycles endpoint */
21
+ SEARCH_TEST_CYCLES: `${API_CONFIG.API_VERSION}/testcycles/search`,
16
22
  /** Update test case endpoint */
17
23
  UPDATE_TEST_CASE: (id, versionNo) => `${API_CONFIG.API_VERSION}/testcases/${id}/versions/${versionNo}`,
18
24
  /** Test steps search endpoint */
@@ -22,7 +28,13 @@ const ENDPOINTS = {
22
28
  /** Labels search endpoint */
23
29
  LABELS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/labels`,
24
30
  /** Components search endpoint */
25
- COMPONENTS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/components`
31
+ COMPONENTS: (projectId) => `${API_CONFIG.API_VERSION}/projects/${projectId}/mcp/components`,
32
+ /** Automation: initiate result file import — returns pre-signed S3 upload URL + trackingId */
33
+ AUTOMATION_IMPORT: "/rest/api/automation/importresult",
34
+ /** Automation: poll import progress by trackingId */
35
+ AUTOMATION_IMPORT_TRACK: "/rest/api/automation/importresult/track",
36
+ /** Automation: paginated history of past automation result uploads */
37
+ AUTOMATION_HISTORY: "/rest/api/automation/importresult/history"
26
38
  };
27
39
  const HTTP_HEADERS = {
28
40
  /** API key header name */
@@ -38,7 +50,9 @@ const HTTP_HEADERS = {
38
50
  };
39
51
  const CONTENT_TYPES = {
40
52
  /** JSON content type */
41
- JSON: "application/json"
53
+ JSON: "application/json",
54
+ /** Multipart form-data content type (used for S3 automation file uploads) */
55
+ MULTIPART: "multipart/form-data"
42
56
  };
43
57
  const HTTP_METHODS = {
44
58
  GET: "GET",
@@ -72,14 +86,24 @@ const PAGINATION = {
72
86
  DEFAULT_MAX_RESULTS_TEST_STEPS: 50,
73
87
  /** Maximum allowed results per request for test steps */
74
88
  MAX_ALLOWED_RESULTS_TEST_STEPS: 100,
89
+ /** Default maximum results for test cycles */
90
+ DEFAULT_MAX_RESULTS_TEST_CYCLES: 20,
91
+ /** Maximum allowed results per request for test cycles */
92
+ MAX_ALLOWED_RESULTS_TEST_CYCLES: 100,
75
93
  /** Minimum allowed results per request */
76
94
  MIN_ALLOWED_RESULTS: 1
77
95
  };
96
+ const AUTOMATION_LIMITS = {
97
+ /** Maximum allowed upload file size in bytes (10 MB) */
98
+ MAX_FILE_SIZE_BYTES: 10 * 1024 * 1024
99
+ };
78
100
  const ERROR_MESSAGES = {
79
101
  /** Client not configured error */
80
102
  CLIENT_NOT_CONFIGURED: "QTM4J client not configured. Please set API key.",
81
103
  /** Request failed template */
82
- REQUEST_FAILED: (status, errorText) => `Request failed with status ${status}: ${errorText}`
104
+ REQUEST_FAILED: (status, errorText) => `Request failed with status ${status}: ${errorText}`,
105
+ /** Automation API key not configured */
106
+ AUTOMATION_API_KEY_NOT_CONFIGURED: "QTM4J Automation API key not configured. Set the QTM4J_AUTOMATION_API_KEY environment variable or pass the Qtm4j-Automation-Api-Key header."
83
107
  };
84
108
  const TOOL_NAMES = {
85
109
  /** Get Projects tool */
@@ -111,17 +135,46 @@ const TOOL_NAMES = {
111
135
  UPDATE_TEST_CASE: {
112
136
  TITLE: "Update Test Case",
113
137
  SUMMARY: "Update an existing test case in QTM4J. Supports auto-resolving human-readable names for priority, status, labels, and components. Labels and components support add/delete operations."
138
+ },
139
+ /** Upload Automation Result tool */
140
+ UPLOAD_AUTOMATION_RESULT: {
141
+ TITLE: "Upload Automation Result",
142
+ SUMMARY: "Upload an automation result file to QTM4J and map the results to a test cycle. Supports JUnit XML, TestNG XML, Cucumber JSON, QAF, HP UFT, and SpecFlow formats."
143
+ },
144
+ /** Get Automation History tool */
145
+ GET_AUTOMATION_HISTORY: {
146
+ TITLE: "Get Automation History",
147
+ SUMMARY: "Retrieve a paginated history of past automation result uploads for a QTM4J project."
148
+ },
149
+ /** Search Test Cycles tool */
150
+ SEARCH_TEST_CYCLES: {
151
+ TITLE: "Search Test Cycles",
152
+ SUMMARY: "Search for test cycles in a QTM4J project by status, owner, folder, date range, or keyword. projectId is injected automatically from the active project context."
153
+ },
154
+ /** Create Test Cycle tool */
155
+ CREATE_TEST_CYCLE: {
156
+ TITLE: "Create Test Cycle",
157
+ SUMMARY: "Create a new test cycle in a QTM4J project. Supports auto-resolving human-readable names for priority and status. Always creates in the 'MCP Generated' folder. projectId is injected automatically from the active project context."
158
+ },
159
+ /** Update Test Cycle tool */
160
+ UPDATE_TEST_CYCLE: {
161
+ TITLE: "Update Test Cycle",
162
+ SUMMARY: "Update an existing test cycle in QTM4J by its human-readable key (e.g. 'SCRUM-TR-101'). Supports auto-resolving human-readable names for status and priority. Labels and components support add/delete operations. Only the fields you provide are changed — omitted fields are left as-is. projectId is injected automatically from the active project context."
114
163
  }
115
164
  };
116
165
  const CONFIG_KEYS = {
117
166
  /** API key configuration key */
118
167
  API_KEY: "api_key",
168
+ /** Automation API key configuration key */
169
+ AUTOMATION_API_KEY: "automation_api_key",
119
170
  /** Base URL configuration key */
120
171
  BASE_URL: "base_url"
121
172
  };
122
173
  const SCHEMA_DESCRIPTIONS = {
123
174
  /** API key description */
124
175
  API_KEY: "QTM4J API key for authentication",
176
+ /** Automation API key description */
177
+ AUTOMATION_API_KEY: "QTM4J Automation API key for uploading automation result files. This is a separate key from the regular API key and can be found in QTM4J under Automation settings.",
125
178
  /** Base URL description */
126
179
  BASE_URL: "QTM4J base URL (default: https://qtmcloud.qmetry.com). Can be customized for on-premise installations.",
127
180
  /** Start at description */
@@ -133,8 +186,40 @@ const SCHEMA_DESCRIPTIONS = {
133
186
  /** QMetry enabled description */
134
187
  QMETRY_ENABLED: "Filter by QMetry enabled status",
135
188
  /** Project object description */
136
- PROJECT_OBJECT: "Project object"
189
+ PROJECT_OBJECT: "Project object",
190
+ /** Automation result file path */
191
+ AUTOMATION_FILE_PATH: "Path to the automation result file on disk. Filesystem contents can change between turns — always resolve this from a fresh scan, never from a previously seen path. Supported extensions: .xml, .json, .zip",
192
+ /** Automation result format */
193
+ AUTOMATION_FORMAT: "Format of the result file. Supported values: cucumber, testng, junit, qaf, hpuft, specflow",
194
+ /** Test cycle to reuse */
195
+ AUTOMATION_TEST_CYCLE_TO_REUSE: "Work key of an existing test cycle to reuse (e.g. 'TR-PRJ-1'). If omitted, a new test cycle is created.",
196
+ /** Automation environment */
197
+ AUTOMATION_ENVIRONMENT: "Name of the environment on which the test cycle was executed (e.g. 'Chrome', 'Staging'). Defaults to 'No Environment'.",
198
+ /** Automation build */
199
+ AUTOMATION_BUILD: "Build name or version for the test cycle execution (e.g. '1.0.0-beta'). Defaults to blank.",
200
+ /** isZip flag */
201
+ AUTOMATION_IS_ZIP: "Set to true when uploading a ZIP archive containing result files. Required for QAF format.",
202
+ /** attachFile flag */
203
+ AUTOMATION_ATTACH_FILE: "Set to true to upload attachments referenced in execution results.",
204
+ /** matchTestSteps flag */
205
+ AUTOMATION_MATCH_TEST_STEPS: "true — match test cases by summary AND test steps. false — match by summary or key only.",
206
+ /** appendTestName flag */
207
+ AUTOMATION_APPEND_TEST_NAME: "Applicable to JUnit/TestNG only. Appends suite/test name to method name in test case summary.",
208
+ /** fields object */
209
+ AUTOMATION_FIELDS: "Additional fields to set on the test cycle, test case, and/or test case execution created during import.",
210
+ /** getAutomationHistory — pagination */
211
+ AUTOMATION_HISTORY_START_AT: "Zero-indexed starting position for pagination (default: 0).",
212
+ AUTOMATION_HISTORY_MAX_RESULTS: "Maximum number of records to return per page (default: 20, max: 100)."
137
213
  };
214
+ const AUTOMATION_RESULT_DIRS = [
215
+ "target/surefire-reports",
216
+ "target/failsafe-reports",
217
+ "build/reports/tests",
218
+ "build/test-results",
219
+ "test-results",
220
+ "reports",
221
+ "cucumber-reports"
222
+ ];
138
223
  const RESPONSE_FIELDS = {
139
224
  /** Start at field */
140
225
  START_AT: "startAt",
@@ -143,6 +228,10 @@ const RESPONSE_FIELDS = {
143
228
  FIELDS: "fields",
144
229
  SORT: "sort"
145
230
  };
231
+ const SORT_DEFAULTS = {
232
+ /** Default sort expression for test cycle search */
233
+ TEST_CYCLES: "key:asc"
234
+ };
146
235
  const EMPTY_VALUES = {
147
236
  /** Empty object */
148
237
  OBJECT: {},
@@ -153,6 +242,8 @@ const EMPTY_VALUES = {
153
242
  };
154
243
  export {
155
244
  API_CONFIG,
245
+ AUTOMATION_LIMITS,
246
+ AUTOMATION_RESULT_DIRS,
156
247
  CLIENT_CONFIG,
157
248
  CONFIG_KEYS,
158
249
  CONTENT_TYPES,
@@ -165,5 +256,6 @@ export {
165
256
  PAGINATION,
166
257
  RESPONSE_FIELDS,
167
258
  SCHEMA_DESCRIPTIONS,
259
+ SORT_DEFAULTS,
168
260
  TOOL_NAMES
169
261
  };
@@ -8,7 +8,8 @@ const ResolverKeys = {
8
8
  TEST_PLAN_STATUS: "testplan_status",
9
9
  TEST_CYCLE_STATUS: "testcycle_status",
10
10
  PRIORITY: "priority",
11
- TESTCASE_FOLDER: "testcase_folder"
11
+ TESTCASE_FOLDER: "testcase_folder",
12
+ TEST_CYCLE_FOLDER: "testcycle_folder"
12
13
  },
13
14
  /**
14
15
  * Fields with dedicated search APIs (fetched on-demand, not batch-loaded).
@@ -4,13 +4,15 @@ import { AuthService } from "./auth-service.js";
4
4
  class ApiClient {
5
5
  baseUrl;
6
6
  tokenProvider;
7
- constructor(tokenOrProvider, baseUrl) {
7
+ automationTokenProvider;
8
+ constructor(tokenOrProvider, baseUrl, automationTokenProvider) {
8
9
  this.baseUrl = baseUrl.trim().replace(/\/$/, EMPTY_VALUES.STRING);
9
10
  if (typeof tokenOrProvider === "string") {
10
11
  this.tokenProvider = () => tokenOrProvider;
11
12
  } else {
12
13
  this.tokenProvider = tokenOrProvider;
13
14
  }
15
+ this.automationTokenProvider = automationTokenProvider;
14
16
  }
15
17
  /**
16
18
  * Get authentication headers for current request
@@ -25,6 +27,17 @@ class ApiClient {
25
27
  }
26
28
  return new AuthService(token).getAuthHeaders();
27
29
  }
30
+ /**
31
+ * Get authentication headers using the automation API key.
32
+ * @throws ToolError if automation API key is not configured
33
+ */
34
+ getAutomationHeaders() {
35
+ const token = this.automationTokenProvider?.();
36
+ if (!token) {
37
+ throw new ToolError(ERROR_MESSAGES.AUTOMATION_API_KEY_NOT_CONFIGURED);
38
+ }
39
+ return new AuthService(token).getAuthHeaders();
40
+ }
28
41
  /**
29
42
  * Construct full URL with query parameters
30
43
  * @param endpoint - API endpoint path
@@ -89,6 +102,62 @@ class ApiClient {
89
102
  });
90
103
  return await this.validateAndGetResponseBody(response);
91
104
  }
105
+ /**
106
+ * Perform GET request using the automation API key (QTM4J_AUTOMATION_API_KEY).
107
+ * Used for automation endpoints that require the automation key instead of the regular API key.
108
+ * @param endpoint - API endpoint path
109
+ * @param params - Optional query parameters
110
+ * @returns Parsed response data
111
+ */
112
+ async getAutomation(endpoint, params) {
113
+ const response = await fetch(this.getUrl(endpoint, params), {
114
+ method: HTTP_METHODS.GET,
115
+ headers: this.getAutomationHeaders()
116
+ });
117
+ return await this.validateAndGetResponseBody(response);
118
+ }
119
+ /**
120
+ * Perform POST request using the automation API key (QTM4J_AUTOMATION_API_KEY).
121
+ * Used for automation import endpoints — same apiKey header as all other APIs,
122
+ * but the value is the automation key instead of the regular API key.
123
+ * @param endpoint - API endpoint path
124
+ * @param body - Request body object
125
+ * @returns Parsed response data
126
+ */
127
+ async postAutomation(endpoint, body) {
128
+ const url = this.getUrl(endpoint);
129
+ const requestHeaders = {
130
+ ...this.getAutomationHeaders(),
131
+ [HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON
132
+ };
133
+ const response = await fetch(url, {
134
+ method: HTTP_METHODS.POST,
135
+ headers: requestHeaders,
136
+ body: JSON.stringify(body)
137
+ });
138
+ return await this.validateAndGetResponseBody(response);
139
+ }
140
+ /**
141
+ * Upload a file as multipart/form-data using the automation API key.
142
+ * Content-Type is not set manually — fetch sets it with the correct multipart boundary.
143
+ * @param uploadUrl - Full URL returned by the automation import initiation step
144
+ * @param fileBuffer - Raw file contents as a Buffer
145
+ */
146
+ async uploadFileMultipart(uploadUrl, fileBuffer) {
147
+ const response = await fetch(uploadUrl, {
148
+ method: HTTP_METHODS.PUT,
149
+ headers: {
150
+ [HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.MULTIPART
151
+ },
152
+ body: new Uint8Array(fileBuffer)
153
+ });
154
+ if (!response.ok) {
155
+ const errorText = await response.text();
156
+ throw new ToolError(
157
+ ERROR_MESSAGES.REQUEST_FAILED(response.status, errorText)
158
+ );
159
+ }
160
+ }
92
161
  /**
93
162
  * Validate HTTP response and extract body
94
163
  * Handles various response types: JSON, text, empty responses
@@ -19,7 +19,7 @@ class Cache {
19
19
  if (!this.trackedKeys.has(projectKey)) {
20
20
  this.trackedKeys.set(projectKey, /* @__PURE__ */ new Set());
21
21
  }
22
- this.trackedKeys.get(projectKey).add(key);
22
+ this.trackedKeys.get(projectKey)?.add(key);
23
23
  }
24
24
  has(projectKey, fieldKey) {
25
25
  return this.cacheService.get(this.compositeKey(projectKey, fieldKey)) !== void 0;
@@ -26,6 +26,7 @@ class CommonAttributeResolver extends Resolver {
26
26
  if (id !== void 0) {
27
27
  body[inputField] = Number(id);
28
28
  } else {
29
+ delete body[inputField];
29
30
  warnings.push(
30
31
  `Skipped ${inputField} '${name}' — not available in the current project.`
31
32
  );
@@ -36,6 +36,8 @@ class ComponentResolver extends Resolver {
36
36
  }
37
37
  if (ids.length > 0) {
38
38
  body[inputField] = isArray ? ids : ids[0];
39
+ } else {
40
+ delete body[inputField];
39
41
  }
40
42
  }
41
43
  async resolveAndReturn(projectKey, projectId, resolverKey, name) {
@@ -36,6 +36,8 @@ class LabelResolver extends Resolver {
36
36
  }
37
37
  if (ids.length > 0) {
38
38
  body[inputField] = isArray ? ids : ids[0];
39
+ } else {
40
+ delete body[inputField];
39
41
  }
40
42
  }
41
43
  async resolveAndReturn(projectKey, projectId, resolverKey, name) {
@@ -0,0 +1,107 @@
1
+ import zod__default from "zod";
2
+ import { SCHEMA_DESCRIPTIONS } from "../config/constants.js";
3
+ const TestCycleFields = zod__default.object({
4
+ labels: zod__default.array(zod__default.string()).nullable().optional(),
5
+ components: zod__default.array(zod__default.string()).nullable().optional(),
6
+ priority: zod__default.string().nullable().optional(),
7
+ status: zod__default.string().nullable().optional(),
8
+ summary: zod__default.string().nullable().optional(),
9
+ description: zod__default.string().nullable().optional(),
10
+ assignee: zod__default.string().nullable().optional().describe(
11
+ "Jira Account ID of the assignee. Ask the user to provide their Account ID directly."
12
+ ),
13
+ reporter: zod__default.string().nullable().optional().describe(
14
+ "Jira Account ID of the reporter. Ask the user to provide their Account ID directly."
15
+ ),
16
+ folderId: zod__default.number().int().nullable().optional().describe(
17
+ "Numeric folder ID to place the test cycle in. Right-click a folder in QTM4J and select 'Copy Folder Id' to get this value."
18
+ ),
19
+ plannedStartDate: zod__default.string().nullable().optional().describe("Date in 'dd/MMM/yyyy HH:mm' format, e.g. '14/May/2026 10:30'"),
20
+ plannedEndDate: zod__default.string().nullable().optional().describe("Date in 'dd/MMM/yyyy HH:mm' format, e.g. '14/May/2026 10:30'")
21
+ }).nullable().optional();
22
+ const TestCaseFields = zod__default.object({
23
+ labels: zod__default.array(zod__default.string()).nullable().optional(),
24
+ components: zod__default.array(zod__default.string()).nullable().optional(),
25
+ priority: zod__default.string().nullable().optional(),
26
+ status: zod__default.string().nullable().optional(),
27
+ description: zod__default.string().nullable().optional(),
28
+ precondition: zod__default.string().nullable().optional(),
29
+ assignee: zod__default.string().nullable().optional().describe(
30
+ "Jira Account ID of the assignee. Ask the user to provide their Account ID directly."
31
+ ),
32
+ reporter: zod__default.string().nullable().optional().describe(
33
+ "Jira Account ID of the reporter. Ask the user to provide their Account ID directly."
34
+ ),
35
+ estimatedTime: zod__default.string().nullable().optional(),
36
+ folderId: zod__default.number().int().nullable().optional().describe(
37
+ "Numeric folder ID to place the test cases in. Right-click a folder in QTM4J and select 'Copy Folder Id' to get this value."
38
+ )
39
+ }).nullable().optional();
40
+ const TestCaseExecutionFields = zod__default.object({
41
+ comment: zod__default.string().nullable().optional(),
42
+ actualTime: zod__default.string().nullable().optional(),
43
+ executionPlannedDate: zod__default.string().nullable().optional(),
44
+ assignee: zod__default.string().nullable().optional()
45
+ }).nullable().optional();
46
+ const UploadAutomationResultBody = zod__default.object({
47
+ filePath: zod__default.string().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_FILE_PATH),
48
+ format: zod__default.enum(["cucumber", "testng", "junit", "qaf", "hpuft", "specflow"]).describe(SCHEMA_DESCRIPTIONS.AUTOMATION_FORMAT),
49
+ testCycleToReuse: zod__default.string().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_TEST_CYCLE_TO_REUSE),
50
+ environment: zod__default.string().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_ENVIRONMENT),
51
+ build: zod__default.string().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_BUILD),
52
+ isZip: zod__default.boolean().optional().default(false).describe(SCHEMA_DESCRIPTIONS.AUTOMATION_IS_ZIP),
53
+ attachFile: zod__default.boolean().optional().default(false).describe(SCHEMA_DESCRIPTIONS.AUTOMATION_ATTACH_FILE),
54
+ matchTestSteps: zod__default.boolean().optional().default(true).describe(SCHEMA_DESCRIPTIONS.AUTOMATION_MATCH_TEST_STEPS),
55
+ appendTestName: zod__default.boolean().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_APPEND_TEST_NAME),
56
+ fields: zod__default.object({
57
+ testCycle: TestCycleFields,
58
+ testCase: TestCaseFields,
59
+ testCaseExecution: TestCaseExecutionFields
60
+ }).optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_FIELDS)
61
+ });
62
+ const UploadAutomationResultResponse = zod__default.object({
63
+ trackingId: zod__default.string(),
64
+ message: zod__default.string(),
65
+ filePath: zod__default.string(),
66
+ format: zod__default.enum(["cucumber", "testng", "junit", "qaf", "hpuft", "specflow"])
67
+ });
68
+ const GetAutomationHistoryBody = zod__default.object({
69
+ startAt: zod__default.number().int().min(0).optional().default(0).describe(SCHEMA_DESCRIPTIONS.AUTOMATION_HISTORY_START_AT),
70
+ maxResults: zod__default.number().int().min(1).max(100).optional().default(20).describe(SCHEMA_DESCRIPTIONS.AUTOMATION_HISTORY_MAX_RESULTS)
71
+ });
72
+ const AutomationHistorySummary = zod__default.object({
73
+ testCycle: zod__default.string().nullable().optional(),
74
+ testCasesCreated: zod__default.number().nullable().optional(),
75
+ testCaseVersionsCreated: zod__default.number().nullable().optional(),
76
+ testCaseVersionsReused: zod__default.number().nullable().optional(),
77
+ testStepsCreated: zod__default.number().nullable().optional(),
78
+ testCycleIssueKey: zod__default.string().nullable().optional(),
79
+ testCycleSummary: zod__default.string().nullable().optional()
80
+ });
81
+ const AutomationHistoryRecord = zod__default.object({
82
+ trackingId: zod__default.string().nullable().optional(),
83
+ fileName: zod__default.string().nullable().optional(),
84
+ format: zod__default.string().nullable().optional(),
85
+ processStatus: zod__default.string().nullable().optional(),
86
+ importStatus: zod__default.string().nullable().optional(),
87
+ startTime: zod__default.string().nullable().optional(),
88
+ endTime: zod__default.string().nullable().optional(),
89
+ fileSize: zod__default.number().nullable().optional(),
90
+ detailedMessage: zod__default.string().nullable().optional(),
91
+ extraAttributes: zod__default.record(zod__default.string(), zod__default.any()).nullable().optional(),
92
+ childFileSummaryResponses: zod__default.array(zod__default.any()).nullable().optional(),
93
+ summary: zod__default.array(AutomationHistorySummary).nullable().optional()
94
+ });
95
+ const GetAutomationHistoryResponse = zod__default.object({
96
+ startAt: zod__default.number().nullable().optional(),
97
+ maxResults: zod__default.number().nullable().optional(),
98
+ total: zod__default.number().nullable().optional(),
99
+ data: zod__default.array(AutomationHistoryRecord).nullable().optional()
100
+ });
101
+ export {
102
+ AutomationHistoryRecord,
103
+ GetAutomationHistoryBody,
104
+ GetAutomationHistoryResponse,
105
+ UploadAutomationResultBody,
106
+ UploadAutomationResultResponse
107
+ };