@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.
- package/README.md +10 -2
- package/dist/package.json.js +1 -1
- package/dist/qmetry/client/api/client-api.js +13 -1
- package/dist/qmetry/client/auto-resolve.js +23 -1
- package/dist/qmetry/client/handlers.js +10 -2
- package/dist/qmetry/client/issues.js +113 -1
- package/dist/qmetry/client/testcase.js +82 -1
- package/dist/qmetry/client/testsuite.js +20 -8
- package/dist/qmetry/client/tools/index.js +5 -2
- package/dist/qmetry/client/tools/issue-tools.js +163 -1
- package/dist/qmetry/client/tools/testcase-tools.js +81 -7
- package/dist/qmetry/client/tools/testsuite-tools.js +291 -26
- package/dist/qmetry/client/tools/udf-tools.js +294 -0
- package/dist/qmetry/client/udf.js +376 -0
- package/dist/qmetry/client.js +30 -3
- package/dist/qmetry/config/constants.js +63 -2
- package/dist/qmetry/config/rest-endpoints.js +7 -1
- package/dist/qmetry/types/common.js +85 -47
- package/dist/qmetry/types/issues.js +5 -0
- package/dist/qmetry/types/udf.js +77 -0
- package/dist/swagger/client/api.js +421 -8
- package/dist/swagger/client/configuration.js +9 -0
- package/dist/swagger/client/functional-testing-api.js +38 -2
- package/dist/swagger/client/functional-testing-tools.js +18 -0
- package/dist/swagger/client/functional-testing-types.js +11 -0
- package/dist/swagger/client/portal-types.js +35 -3
- package/dist/swagger/client/portal-utils.js +50 -0
- package/dist/swagger/client/registry-types.js +8 -0
- package/dist/swagger/client/tools.js +25 -4
- package/dist/swagger/client/utils.js +37 -0
- package/dist/swagger/client.js +35 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
};
|