@smartbear/mcp 0.26.1 → 0.27.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 (32) hide show
  1. package/README.md +10 -2
  2. package/dist/package.json.js +1 -1
  3. package/dist/qmetry/client/api/client-api.js +13 -1
  4. package/dist/qmetry/client/auto-resolve.js +23 -1
  5. package/dist/qmetry/client/handlers.js +10 -2
  6. package/dist/qmetry/client/issues.js +113 -1
  7. package/dist/qmetry/client/testcase.js +82 -1
  8. package/dist/qmetry/client/testsuite.js +20 -8
  9. package/dist/qmetry/client/tools/index.js +5 -2
  10. package/dist/qmetry/client/tools/issue-tools.js +163 -1
  11. package/dist/qmetry/client/tools/testcase-tools.js +81 -7
  12. package/dist/qmetry/client/tools/testsuite-tools.js +291 -26
  13. package/dist/qmetry/client/tools/udf-tools.js +294 -0
  14. package/dist/qmetry/client/udf.js +376 -0
  15. package/dist/qmetry/client.js +30 -3
  16. package/dist/qmetry/config/constants.js +63 -2
  17. package/dist/qmetry/config/rest-endpoints.js +7 -1
  18. package/dist/qmetry/types/common.js +85 -47
  19. package/dist/qmetry/types/issues.js +5 -0
  20. package/dist/qmetry/types/udf.js +77 -0
  21. package/dist/swagger/client/api.js +421 -8
  22. package/dist/swagger/client/configuration.js +9 -0
  23. package/dist/swagger/client/functional-testing-api.js +38 -2
  24. package/dist/swagger/client/functional-testing-tools.js +18 -0
  25. package/dist/swagger/client/functional-testing-types.js +11 -0
  26. package/dist/swagger/client/portal-types.js +35 -3
  27. package/dist/swagger/client/portal-utils.js +50 -0
  28. package/dist/swagger/client/registry-types.js +8 -0
  29. package/dist/swagger/client/tools.js +25 -4
  30. package/dist/swagger/client/utils.js +37 -0
  31. package/dist/swagger/client.js +35 -4
  32. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import { ToolError } from "../../common/tools.js";
2
+ import { buildSubdomainCandidate, buildSuffixedSubdomain, buildPortalName, isConflictError, isOrganizationPortalConflict } from "./portal-utils.js";
3
+ import { findTableOfContentsItem, buildPortalLiveUrl, normalizeSlug } from "./utils.js";
2
4
  const SWAGGER_URL_REGEX = /\/(apis|domains|templates)\/([^/]+)\/([^/]+)\/([^/]+)/;
3
5
  function hasId(value) {
4
6
  return typeof value === "object" && value !== null && "id" in value;
@@ -9,6 +11,9 @@ function hasMessage(value) {
9
11
  function hasErrorsFound(value) {
10
12
  return typeof value === "object" && value !== null && "errorsFound" in value;
11
13
  }
14
+ function isStandardizationResult(value) {
15
+ return typeof value === "object" && value !== null && "validation" in value && Array.isArray(value.validation);
16
+ }
12
17
  class SwaggerAPI {
13
18
  config;
14
19
  userAgent;
@@ -77,7 +82,9 @@ class SwaggerAPI {
77
82
  errorBody.detail ?? errorBody.message ?? ""
78
83
  ) || response.statusText;
79
84
  throw new ToolError(
80
- `HTTP ${response.status}${detail ? `: ${detail}` : ""}`
85
+ `HTTP ${response.status}${detail ? `: ${detail}` : ""}`,
86
+ void 0,
87
+ /* @__PURE__ */ new Map([["status", response.status]])
81
88
  );
82
89
  }
83
90
  return this.parseResponse(
@@ -160,6 +167,158 @@ class SwaggerAPI {
160
167
  );
161
168
  return this.handleResponse(response);
162
169
  }
170
+ /**
171
+ * Resolve portal details and product list for a Swagger organization.
172
+ * Finds the portal mapped to the organization, creating one if it does not
173
+ * exist yet, and returns the portal ID, subdomain, and products.
174
+ * @param params Parameters containing the organization UUID
175
+ * @returns Portal details and product list for the organization
176
+ */
177
+ async resolveOrganizationPortal(params) {
178
+ const { organizationId } = params;
179
+ let portal = await this.findPortalByOrganizationId(organizationId);
180
+ let portalCreated = false;
181
+ if (!portal) {
182
+ const created = await this.createPortalForOrganization(organizationId);
183
+ portal = created.portal;
184
+ portalCreated = created.created;
185
+ }
186
+ const products = this.extractListItems(
187
+ await this.getPortalProducts(portal.id)
188
+ );
189
+ const customDomain = typeof portal.customDomain === "string" && portal.customDomain.length > 0 ? portal.customDomain : void 0;
190
+ return {
191
+ organizationId,
192
+ portalId: portal.id,
193
+ subdomain: typeof portal.subdomain === "string" ? portal.subdomain : "",
194
+ ...customDomain ? { customDomain } : {},
195
+ portalCreated,
196
+ products: products.map(
197
+ (product) => ({
198
+ productId: product.id,
199
+ productSlug: typeof product.slug === "string" ? product.slug : "",
200
+ productName: product.name
201
+ })
202
+ )
203
+ };
204
+ }
205
+ /**
206
+ * Extract item arrays from list responses that may be returned either as a
207
+ * plain array or as a paginated object with an 'items' property.
208
+ */
209
+ extractListItems(result) {
210
+ if (Array.isArray(result)) {
211
+ return result;
212
+ }
213
+ if (typeof result === "object" && result !== null && Array.isArray(result.items)) {
214
+ return result.items;
215
+ }
216
+ return [];
217
+ }
218
+ /**
219
+ * Find the portal mapped to a Swagger organization. The Portal API does not
220
+ * support filtering by organization, so pages are scanned client-side.
221
+ *
222
+ * Permission/role problems surface as thrown errors: handleResponse throws on
223
+ * any non-2xx status (e.g. 401/403), so a lack of access never masquerades as
224
+ * an empty result. An empty list therefore means the user can see portals but
225
+ * none is mapped to this organization, in which case a new portal is created.
226
+ */
227
+ async findPortalByOrganizationId(organizationId) {
228
+ const size = 100;
229
+ const maxPages = 20;
230
+ const target = organizationId.toLowerCase();
231
+ for (let page = 1; page <= maxPages; page++) {
232
+ const response = await fetch(
233
+ `${this.config.portalBasePath}/portals?page=${page}&size=${size}`,
234
+ {
235
+ method: "GET",
236
+ headers: this.headers
237
+ }
238
+ );
239
+ const result = await this.handleResponse(response, []);
240
+ const portals = this.extractListItems(result);
241
+ const match = portals.find(
242
+ (portal) => typeof portal.swaggerHubOrganizationId === "string" && portal.swaggerHubOrganizationId.toLowerCase() === target
243
+ );
244
+ if (match) {
245
+ return match;
246
+ }
247
+ if (portals.length < size) {
248
+ return void 0;
249
+ }
250
+ }
251
+ return void 0;
252
+ }
253
+ /**
254
+ * Find an organization by UUID using the User Management API, which only
255
+ * supports listing the user's organizations (no lookup by ID).
256
+ */
257
+ async findOrganizationById(organizationId) {
258
+ const pageSize = 100;
259
+ const maxPages = 10;
260
+ const target = organizationId.toLowerCase();
261
+ for (let page = 0; page < maxPages; page++) {
262
+ const result = await this.getOrganizations({ page, pageSize });
263
+ const items = result.items ?? [];
264
+ const match = items.find((org) => org.id?.toLowerCase() === target);
265
+ if (match) {
266
+ return match;
267
+ }
268
+ if (items.length < pageSize) {
269
+ return void 0;
270
+ }
271
+ }
272
+ return void 0;
273
+ }
274
+ /**
275
+ * Create a portal for an organization that has none yet. The subdomain is
276
+ * derived from the organization name; on a subdomain conflict a candidate
277
+ * suffixed with part of the organization UUID is retried.
278
+ */
279
+ async createPortalForOrganization(organizationId) {
280
+ const organization = await this.findOrganizationById(organizationId);
281
+ const baseSubdomain = buildSubdomainCandidate(
282
+ organizationId,
283
+ organization?.name
284
+ );
285
+ const candidates = [
286
+ baseSubdomain,
287
+ buildSuffixedSubdomain(baseSubdomain, organizationId)
288
+ ];
289
+ const portalName = buildPortalName(organization?.name);
290
+ let lastError;
291
+ for (const subdomain of candidates) {
292
+ try {
293
+ const created = await this.createPortal({
294
+ subdomain,
295
+ swaggerHubOrganizationId: organizationId,
296
+ ...portalName ? { name: portalName } : {}
297
+ });
298
+ return {
299
+ portal: { ...created, subdomain: created.subdomain ?? subdomain },
300
+ created: true
301
+ };
302
+ } catch (error) {
303
+ if (!isConflictError(error)) {
304
+ throw error;
305
+ }
306
+ lastError = error;
307
+ const existing = await this.findPortalByOrganizationId(organizationId);
308
+ if (existing) {
309
+ return { portal: existing, created: false };
310
+ }
311
+ if (isOrganizationPortalConflict(error)) {
312
+ throw new ToolError(
313
+ `Access denied: a portal already exists for organization ${organizationId}, but you do not have permission to view it. A portal owner or designer role is required.`
314
+ );
315
+ }
316
+ }
317
+ }
318
+ throw lastError instanceof Error ? lastError : new ToolError(
319
+ `Failed to create portal for organization ${organizationId}`
320
+ );
321
+ }
163
322
  async getPortalProducts(portalId) {
164
323
  const response = await fetch(
165
324
  `${this.config.portalBasePath}/portals/${portalId}/products`,
@@ -226,7 +385,108 @@ class SwaggerAPI {
226
385
  success: true
227
386
  });
228
387
  }
229
- async publishPortalProduct(productId, preview = false) {
388
+ /**
389
+ * Prepare publication metadata and URL for a portal product and page.
390
+ * Fetches product, portal, and section details step-by-step, gracefully handling failures.
391
+ * Product and portal details are essential for URL generation; sections are optional.
392
+ * @param productId - ID of the product to publish
393
+ * @param preview - Whether this is a preview publish
394
+ * @param tableOfContentsId - Optional table of contents UUID or identifier
395
+ * @returns Publication metadata with optional fields based on what could be fetched
396
+ */
397
+ async preparePublicationMetadata(productId, preview, tableOfContentsId) {
398
+ let productDetails = null;
399
+ try {
400
+ productDetails = await this.getPortalProduct(productId);
401
+ } catch (error) {
402
+ console.warn("Failed to fetch product details:", error);
403
+ return {
404
+ publicationUrl: null,
405
+ warning: {
406
+ step: "product",
407
+ message: `Product published ${preview ? "in preview mode" : "in live mode"} successfully, but failed to fetch product details for URL generation`
408
+ }
409
+ };
410
+ }
411
+ let portalDetails = null;
412
+ if (productDetails?.portalId) {
413
+ try {
414
+ portalDetails = await this.getPortal(String(productDetails.portalId));
415
+ } catch (error) {
416
+ console.warn("Failed to fetch portal details:", error);
417
+ }
418
+ }
419
+ if (!portalDetails) {
420
+ return {
421
+ publicationUrl: null,
422
+ product: productDetails ? {
423
+ id: productDetails.id,
424
+ name: productDetails.name,
425
+ slug: productDetails.slug
426
+ } : void 0,
427
+ warning: {
428
+ step: "portal",
429
+ message: `Product published ${preview ? "in preview mode" : "in live mode"} successfully, but failed to fetch portal details for URL generation`
430
+ }
431
+ };
432
+ }
433
+ let sections = null;
434
+ try {
435
+ sections = await this.getPortalProductSections(productId, {
436
+ embed: ["tableOfContents", "tableOfContents.swaggerhubApi"]
437
+ });
438
+ } catch (error) {
439
+ console.warn("Failed to fetch sections:", error);
440
+ }
441
+ const targetSection = sections?.items[0] ?? null;
442
+ const targetTocItem = tableOfContentsId && targetSection ? findTableOfContentsItem(
443
+ targetSection.tableOfContents ?? [],
444
+ tableOfContentsId
445
+ ) : null;
446
+ const publicationUrl = buildPortalLiveUrl(
447
+ this.config,
448
+ portalDetails,
449
+ productDetails.slug,
450
+ targetSection,
451
+ targetTocItem,
452
+ preview
453
+ );
454
+ return {
455
+ publicationUrl,
456
+ product: {
457
+ id: productDetails.id,
458
+ name: productDetails.name,
459
+ slug: productDetails.slug
460
+ },
461
+ portal: {
462
+ id: portalDetails.id,
463
+ name: portalDetails.name,
464
+ subdomain: portalDetails.subdomain,
465
+ customDomain: portalDetails.customDomain
466
+ },
467
+ ...targetTocItem ? {
468
+ tableOfContentsItem: {
469
+ id: targetTocItem.id,
470
+ slug: targetTocItem.slug,
471
+ title: targetTocItem.title,
472
+ order: targetTocItem.order,
473
+ parentId: targetTocItem.parentId
474
+ }
475
+ } : {}
476
+ };
477
+ }
478
+ /**
479
+ * Publish a portal product and generate a published URL with environment-specific domain.
480
+ * The publish operation always succeeds if the API call succeeds. URL generation is done separately and may fail gracefully.
481
+ * Returns `liveUrl` for live publishes and `previewUrl` for preview publishes (null if URL building fails).
482
+ * When metadata/URL building fails, response includes `success: true` (publish succeeded), `liveUrl/previewUrl: null`,
483
+ * and a `warning` object explaining which step failed (product/portal fetch or URL building).
484
+ * @param productId - ID of the product to publish
485
+ * @param preview - Whether to publish in preview mode (default: false)
486
+ * @param tableOfContentsId - Optional table of contents UUID, or identifier in the format 'portal-subdomain:product-slug:section-slug:table-of-contents-slug'
487
+ * @returns Complete publish response with `success: true`, optional URL/metadata, and optional warning details
488
+ */
489
+ async publishPortalProduct(productId, preview = false, tableOfContentsId = null) {
230
490
  const response = await fetch(
231
491
  `${this.config.portalBasePath}/products/${productId}/published-content?preview=${preview}`,
232
492
  {
@@ -234,9 +494,46 @@ class SwaggerAPI {
234
494
  headers: this.headers
235
495
  }
236
496
  );
237
- return this.handleResponse(response, {
497
+ const result = await this.handleResponse(response, {
238
498
  success: true
239
499
  });
500
+ try {
501
+ const metadata = await this.preparePublicationMetadata(
502
+ productId,
503
+ preview,
504
+ tableOfContentsId
505
+ );
506
+ return {
507
+ ...result,
508
+ preview,
509
+ [preview ? "previewUrl" : "liveUrl"]: metadata.publicationUrl,
510
+ ...metadata.product ? { product: metadata.product } : {},
511
+ ...metadata.portal ? { portal: metadata.portal } : {},
512
+ ...metadata.tableOfContentsItem ? { tableOfContentsItem: metadata.tableOfContentsItem } : {},
513
+ ...metadata.warning ? {
514
+ warning: {
515
+ code: "METADATA_FETCH_FAILED",
516
+ step: metadata.warning.step,
517
+ message: metadata.warning.message
518
+ }
519
+ } : {}
520
+ };
521
+ } catch (error) {
522
+ console.warn(
523
+ "Failed to build publication metadata — returning publish success with empty URL:",
524
+ error
525
+ );
526
+ return {
527
+ ...result,
528
+ preview,
529
+ [preview ? "previewUrl" : "liveUrl"]: null,
530
+ warning: {
531
+ code: "METADATA_BUILD_FAILED",
532
+ step: "url_build",
533
+ message: `Product published ${preview ? "in preview mode" : "in live mode"} successfully, but failed to build publication URL`
534
+ }
535
+ };
536
+ }
240
537
  }
241
538
  async getPortalProductSections(productId, params) {
242
539
  const queryParameters = new URLSearchParams();
@@ -256,9 +553,18 @@ class SwaggerAPI {
256
553
  method: "GET",
257
554
  headers: this.headers
258
555
  });
556
+ const defaultResponse = {
557
+ page: {
558
+ number: 0,
559
+ size: 0,
560
+ totalElements: 0,
561
+ totalPages: 0
562
+ },
563
+ items: []
564
+ };
259
565
  const result = await this.handleResponse(
260
566
  response,
261
- []
567
+ defaultResponse
262
568
  );
263
569
  return result;
264
570
  }
@@ -315,7 +621,10 @@ class SwaggerAPI {
315
621
  );
316
622
  }
317
623
  const result = await response.json();
318
- return result.items;
624
+ if (Array.isArray(result)) {
625
+ return result;
626
+ }
627
+ return result.items ?? [];
319
628
  }
320
629
  /**
321
630
  * Get document content and metadata
@@ -359,6 +668,73 @@ class SwaggerAPI {
359
668
  }
360
669
  return { success: true };
361
670
  }
671
+ async createDocumentationPage(args) {
672
+ const {
673
+ portalId,
674
+ productId,
675
+ pageTitle,
676
+ pageContent,
677
+ contentType = "markdown",
678
+ source = "internal",
679
+ order = 0,
680
+ parentId = null
681
+ } = args;
682
+ if (contentType === "html" && source === "internal" && pageContent !== void 0) {
683
+ throw new ToolError(
684
+ "Cannot create an html + internal page with content via API. Use 'external' source for html, or 'markdown' with 'internal' source."
685
+ );
686
+ }
687
+ const portal = await this.getPortal(portalId);
688
+ const product = await this.getPortalProduct(productId);
689
+ const productSlug = product?.slug;
690
+ const sections = await this.getPortalProductSections(productId, {});
691
+ if (sections.items.length === 0) {
692
+ throw new ToolError(
693
+ `Product ${productId} has no sections. Create a section first before adding documentation pages.`
694
+ );
695
+ }
696
+ const section = sections.items[0];
697
+ const pageSlug = normalizeSlug(pageTitle);
698
+ const normalizedTitle = pageTitle.slice(0, 255);
699
+ const tocItem = await this.createTableOfContents(section.id, {
700
+ type: "new",
701
+ title: normalizedTitle,
702
+ slug: pageSlug,
703
+ order,
704
+ parentId,
705
+ content: {
706
+ type: contentType,
707
+ source
708
+ }
709
+ });
710
+ const documentId = tocItem.documentId;
711
+ if (pageContent !== void 0) {
712
+ await this.updateDocument({
713
+ documentId,
714
+ content: pageContent,
715
+ type: contentType
716
+ });
717
+ }
718
+ const host = portal?.customDomain ?? portal?.subdomain;
719
+ const portalUiDomain = portal?.customDomain ? "" : this.config.getPortalUiDomainSuffix();
720
+ const draftUrl = `https://${host}${portalUiDomain}/sp-admin/products/${productSlug}/edit/content/${tocItem.id}`;
721
+ return {
722
+ productId,
723
+ sectionId: section.id,
724
+ sectionSlug: section.slug,
725
+ pageDetails: {
726
+ tableOfContentsId: tocItem.id,
727
+ slug: pageSlug,
728
+ title: normalizedTitle,
729
+ content: {
730
+ type: contentType,
731
+ source,
732
+ documentId
733
+ }
734
+ },
735
+ draftUrl
736
+ };
737
+ }
362
738
  /**
363
739
  * Delete table of contents entry
364
740
  * @param args - Parameters for deleting table of contents entry
@@ -457,9 +833,10 @@ class SwaggerAPI {
457
833
  /**
458
834
  * Get API definition from SwaggerHub Registry
459
835
  * @param params Parameters including owner, api name, version, and options
836
+ * @param options Optional transport options
460
837
  * @returns API definition (OpenAPI/Swagger specification)
461
838
  */
462
- async getApiDefinition(params) {
839
+ async getApiDefinition(params, options) {
463
840
  const searchParams = new URLSearchParams();
464
841
  if (params.resolved !== void 0)
465
842
  searchParams.append("resolved", params.resolved.toString());
@@ -468,7 +845,7 @@ class SwaggerAPI {
468
845
  const url = `${this.config.registryBasePath}/apis/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.api)}/${encodeURIComponent(params.version)}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
469
846
  const response = await fetch(url, {
470
847
  method: "GET",
471
- headers: this.headers
848
+ headers: options?.accept ? { ...this.headers, Accept: options.accept } : this.headers
472
849
  });
473
850
  if (!response.ok) {
474
851
  throw new ToolError(
@@ -628,7 +1005,43 @@ class SwaggerAPI {
628
1005
  `SwaggerHub Registry API scanStandardization failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}. URL: ${url}`
629
1006
  );
630
1007
  }
631
- return await this.handleResponse(response);
1008
+ const results = await this.handleResponse(response);
1009
+ if (isStandardizationResult(results)) {
1010
+ const errors = results.validation ?? [];
1011
+ const countsBySeverity = {};
1012
+ for (const error of errors) {
1013
+ const severity = error.severity ?? "Unknown";
1014
+ countsBySeverity[severity] = (countsBySeverity[severity] ?? 0) + 1;
1015
+ }
1016
+ return { ...results, count: errors.length, countsBySeverity };
1017
+ }
1018
+ return results;
1019
+ }
1020
+ /**
1021
+ * Fetch an API definition from the registry and run a standardization scan
1022
+ * @param params Parameters including organization name, API name, and version
1023
+ * @returns Scan results with total count and counts grouped by severity
1024
+ */
1025
+ async scanApiStandardizationFromRegistry(params) {
1026
+ const definition = await this.getApiDefinition(
1027
+ {
1028
+ owner: params.orgName,
1029
+ api: params.apiName,
1030
+ version: params.version
1031
+ },
1032
+ { accept: "text/plain" }
1033
+ );
1034
+ const results = await this.scanStandardization({
1035
+ orgName: params.orgName,
1036
+ definition
1037
+ });
1038
+ if (isStandardizationResult(results)) {
1039
+ return {
1040
+ ...results,
1041
+ url: `${this.config.uiBasePath}/apis/${params.orgName}/${params.apiName}/${params.version}`
1042
+ };
1043
+ }
1044
+ return results;
632
1045
  }
633
1046
  /**
634
1047
  * Standardize and fix an API definition using AI
@@ -34,6 +34,15 @@ class SwaggerConfiguration {
34
34
  "User-Agent": userAgent
35
35
  };
36
36
  }
37
+ getPortalUiDomainSuffix() {
38
+ const match = this.portalBasePath.match(
39
+ /https?:\/\/api(\..*\.portal\.swaggerhub\.com)/
40
+ );
41
+ if (match?.[1]) {
42
+ return match[1];
43
+ }
44
+ return ".portal.swaggerhub.com";
45
+ }
37
46
  }
38
47
  export {
39
48
  SwaggerConfiguration
@@ -2,10 +2,12 @@ import { ToolError } from "../../common/tools.js";
2
2
  const API_HOSTNAME = "api.reflect.run";
3
3
  const FUNCTIONAL_TESTING_API_KEY_HEADER = "X-API-KEY";
4
4
  class FunctionalTestingAPI {
5
- constructor(getToken, userAgent) {
5
+ constructor(getToken, userAgent, baseUrl) {
6
6
  this.getToken = getToken;
7
7
  this.userAgent = userAgent;
8
+ this.baseUrl = baseUrl || `https://${API_HOSTNAME}/v1`;
8
9
  }
10
+ baseUrl;
9
11
  getFtHeaders() {
10
12
  const token = this.getToken();
11
13
  if (!token) {
@@ -18,7 +20,7 @@ class FunctionalTestingAPI {
18
20
  };
19
21
  }
20
22
  async listTests() {
21
- const response = await fetch(`https://${API_HOSTNAME}/v1/tests`, {
23
+ const response = await fetch(`${this.baseUrl}/tests`, {
22
24
  method: "GET",
23
25
  headers: this.getFtHeaders()
24
26
  });
@@ -29,6 +31,40 @@ class FunctionalTestingAPI {
29
31
  }
30
32
  return response.json();
31
33
  }
34
+ async runTest(args) {
35
+ if (!args.testId) throw new ToolError("testId argument is required");
36
+ const response = await fetch(
37
+ `${this.baseUrl}/tests/${args.testId}/executions`,
38
+ {
39
+ method: "POST",
40
+ headers: this.getFtHeaders()
41
+ }
42
+ );
43
+ if (!response.ok) {
44
+ throw new ToolError(
45
+ `Failed to run test: ${response.status} ${response.statusText}`
46
+ );
47
+ }
48
+ return response.json();
49
+ }
50
+ async getTestExecution(args) {
51
+ if (!args.executionId) {
52
+ throw new ToolError("executionId argument is required");
53
+ }
54
+ const response = await fetch(
55
+ `${this.baseUrl}/executions/${args.executionId}`,
56
+ {
57
+ method: "GET",
58
+ headers: this.getFtHeaders()
59
+ }
60
+ );
61
+ if (!response.ok) {
62
+ throw new ToolError(
63
+ `Failed to get test status: ${response.status} ${response.statusText}`
64
+ );
65
+ }
66
+ return response.json();
67
+ }
32
68
  }
33
69
  export {
34
70
  FUNCTIONAL_TESTING_API_KEY_HEADER,
@@ -1,9 +1,27 @@
1
+ import { RunFunctionalTestingTestParamsSchema, GetFunctionalTestingExecutionTestSchema } from "./functional-testing-types.js";
1
2
  const FUNCTIONAL_TESTING_TOOLS = [
2
3
  {
3
4
  title: "List Tests",
4
5
  toolset: "Functional Testing",
5
6
  summary: "Lists all API tests available in your Swagger Functional Testing account. Use this tool when you need to discover available tests before running them or checking their status. Do not use this tool to retrieve test execution results or history.",
6
7
  handler: "listFunctionalTestingTests"
8
+ },
9
+ {
10
+ title: "Run Test",
11
+ toolset: "Functional Testing",
12
+ summary: "Runs a specific API test in your Swagger Functional Testing workspace. The execution is asynchronous — it returns an executionId, not the result directly. Use swagger_get_test_status with that executionId to track progress and retrieve the final result.",
13
+ inputSchema: RunFunctionalTestingTestParamsSchema,
14
+ handler: "runFunctionalTestingTest",
15
+ idempotent: false,
16
+ readOnly: false
17
+ },
18
+ {
19
+ title: "Get Test Status",
20
+ toolset: "Functional Testing",
21
+ summary: "Get the status of a Swagger Functional Testing test execution. It returns information about the execution such as its status (running, passed or failed), run time, as well as the break down of the status of each test step.",
22
+ inputSchema: GetFunctionalTestingExecutionTestSchema,
23
+ handler: "getFunctionalTestingExecution",
24
+ idempotent: false
7
25
  }
8
26
  ];
9
27
  export {
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ const RunFunctionalTestingTestParamsSchema = z.object({
3
+ testId: z.string().describe("ID of the Functional Testing test to run").trim().min(1)
4
+ });
5
+ const GetFunctionalTestingExecutionTestSchema = z.object({
6
+ executionId: z.string().describe("ID of the Functional Testing execution").trim().min(1)
7
+ });
8
+ export {
9
+ GetFunctionalTestingExecutionTestSchema,
10
+ RunFunctionalTestingTestParamsSchema
11
+ };