@smartbear/mcp 0.26.0 → 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 +37 -5
  32. package/package.json +1 -1
@@ -20,7 +20,7 @@ const GetProductSectionsArgsSchema = z.object({
20
20
  "Page number for paginated results - specifies which page of results to retrieve (default is 1)"
21
21
  ),
22
22
  size: z.number().optional().describe(
23
- "Number of items per page for pagination - controls how many results are returned per page (default is 20)"
23
+ "Number of items per page for pagination - controls how many results are returned per page (default is 10)"
24
24
  )
25
25
  });
26
26
  const GetTableOfContentsArgsSchema = z.object({
@@ -176,7 +176,15 @@ const UpdateProductArgsSchema = ProductArgsSchema.extend({
176
176
  "Change navigation visibility - true hides from portal landing page menus while keeping the product accessible via direct links"
177
177
  )
178
178
  });
179
+ const ResolveOrganizationPortalArgsSchema = z.object({
180
+ organizationId: z.string().uuid().describe(
181
+ "Swagger organization UUID - the organization to resolve portal details for"
182
+ )
183
+ });
179
184
  const PublishProductArgsSchema = ProductArgsSchema.extend({
185
+ tableOfContentsId: z.string().optional().describe(
186
+ "Optional table of contents UUID, or identifier in the format 'portal-subdomain:product-slug:section-slug:table-of-contents-slug'. When provided, publishPortalProduct uses it to resolve the published URL path for the returned preview/live link."
187
+ ),
180
188
  preview: z.boolean().optional().default(false).describe(
181
189
  "Whether to publish as preview (true) or live (false). Preview allows testing before going live. Defaults to false (live publication)"
182
190
  )
@@ -190,10 +198,10 @@ const UpdateDocumentArgsSchema = z.object({
190
198
  "The document content to update (HTML or Markdown based on document type)"
191
199
  ),
192
200
  type: z.enum(["html", "markdown"]).optional().describe(
193
- "Content type - 'html' for HTML content or 'markdown' for Markdown content"
201
+ "Content type of the document. Note: documents with type 'html' and source 'internal' cannot be edited via API — only 'html' + 'external' and all 'markdown' combinations are supported."
194
202
  ),
195
203
  source: z.enum(["internal", "external"]).optional().describe(
196
- "Source of the document content - 'internal' allows to edit content in both UI and API, 'external' enables editing only via API."
204
+ "Where the document content is managed. 'internal': editable in both portal UI and API. 'external': editable via API only. Note: 'html' + 'internal' documents cannot be updated via API."
197
205
  )
198
206
  });
199
207
  const DeleteTableOfContentsArgsSchema = z.object({
@@ -204,7 +212,30 @@ const DeleteTableOfContentsArgsSchema = z.object({
204
212
  "Flag to include all the nested tables of contents (default: false)"
205
213
  )
206
214
  });
215
+ const CreateDocumentationPageArgsSchema = z.object({
216
+ portalId: z.string().describe("Portal UUID or subdomain - unique identifier for the portal"),
217
+ productId: z.string().uuid().describe("Product UUID - unique identifier for the product"),
218
+ pageTitle: z.string().describe(
219
+ "Title of the documentation page - will be displayed in navigation (3-255 characters, used to generate the page slug)"
220
+ ),
221
+ pageContent: z.string().optional().describe(
222
+ "Content of the documentation page. Provide HTML when contentType is 'html', Markdown when contentType is 'markdown'."
223
+ ),
224
+ contentType: z.enum(["markdown", "html"]).default("markdown").optional().describe(
225
+ "Content type of the documentation page. 'markdown' works with both 'internal' and 'external' source. 'html' only works with 'external' source — html + internal is not supported by the API and will return an error."
226
+ ),
227
+ source: z.enum(["internal", "external"]).default("internal").optional().describe(
228
+ "Where the document content is managed. 'internal': editable in both the portal UI and via API. 'external': editable via API only, not in the portal UI. Constraint: 'html' content type only supports 'external' source."
229
+ ),
230
+ order: z.number().optional().default(0).describe(
231
+ "Order position of the documentation page within its parent section or item"
232
+ ),
233
+ parentId: z.string().nullable().optional().default(null).describe(
234
+ "Parent table of contents item ID - null for top-level pages, or ID of parent item for nested structure"
235
+ )
236
+ });
207
237
  export {
238
+ CreateDocumentationPageArgsSchema,
208
239
  CreatePortalArgsSchema,
209
240
  CreateProductArgsSchema,
210
241
  CreateTableOfContentsArgsSchema,
@@ -215,6 +246,7 @@ export {
215
246
  PortalArgsSchema,
216
247
  ProductArgsSchema,
217
248
  PublishProductArgsSchema,
249
+ ResolveOrganizationPortalArgsSchema,
218
250
  UpdateDocumentArgsSchema,
219
251
  UpdatePortalArgsSchema,
220
252
  UpdateProductArgsSchema
@@ -0,0 +1,50 @@
1
+ import { ToolError } from "../../common/tools.js";
2
+ const SUBDOMAIN_MIN_LENGTH = 3;
3
+ const SUBDOMAIN_MAX_LENGTH = 20;
4
+ const PORTAL_NAME_MIN_LENGTH = 3;
5
+ const PORTAL_NAME_MAX_LENGTH = 40;
6
+ function slugify(value, maxLength) {
7
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
8
+ }
9
+ function buildSubdomainCandidate(organizationId, organizationName) {
10
+ const fallback = `portal-${organizationId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 8)}`;
11
+ if (!organizationName) {
12
+ return fallback;
13
+ }
14
+ const sanitized = slugify(organizationName, SUBDOMAIN_MAX_LENGTH);
15
+ return sanitized.length >= SUBDOMAIN_MIN_LENGTH ? sanitized : fallback;
16
+ }
17
+ function buildSuffixedSubdomain(baseSubdomain, organizationId) {
18
+ const idSuffix = organizationId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 4);
19
+ const trimmedBase = baseSubdomain.slice(0, SUBDOMAIN_MAX_LENGTH - idSuffix.length - 1).replace(/-+$/, "");
20
+ return `${trimmedBase}-${idSuffix}`;
21
+ }
22
+ function buildPortalName(organizationName) {
23
+ const trimmed = organizationName?.trim();
24
+ if (!trimmed || trimmed.length < PORTAL_NAME_MIN_LENGTH) {
25
+ return void 0;
26
+ }
27
+ return trimmed.slice(0, PORTAL_NAME_MAX_LENGTH);
28
+ }
29
+ function isConflictError(error) {
30
+ return error instanceof ToolError && (error.metadata?.get("status") === 409 || error.message.startsWith("HTTP 409"));
31
+ }
32
+ function isOrganizationPortalConflict(error) {
33
+ if (!(error instanceof ToolError)) {
34
+ return false;
35
+ }
36
+ const message = error.message.toLowerCase();
37
+ return message.includes("already exists") && message.includes("organization");
38
+ }
39
+ export {
40
+ PORTAL_NAME_MAX_LENGTH,
41
+ PORTAL_NAME_MIN_LENGTH,
42
+ SUBDOMAIN_MAX_LENGTH,
43
+ SUBDOMAIN_MIN_LENGTH,
44
+ buildPortalName,
45
+ buildSubdomainCandidate,
46
+ buildSuffixedSubdomain,
47
+ isConflictError,
48
+ isOrganizationPortalConflict,
49
+ slugify
50
+ };
@@ -38,6 +38,13 @@ const ScanStandardizationParamsSchema = z.object({
38
38
  "API definition content (OpenAPI/AsyncAPI specification in JSON or YAML format) to scan for standardization errors"
39
39
  )
40
40
  });
41
+ const ScanApiStandardizationFromRegistryParamsSchema = z.object({
42
+ orgName: z.string().describe(
43
+ "The organization name that owns the API and provides the standardization rules (case-sensitive)"
44
+ ),
45
+ apiName: z.string().describe("API name (case-sensitive)"),
46
+ version: z.string().describe("Version identifier")
47
+ });
41
48
  const CreateApiFromPromptParamsSchema = z.object({
42
49
  owner: z.string().describe("API owner (organization or user, case-sensitive)"),
43
50
  apiName: z.string().describe("API name"),
@@ -67,6 +74,7 @@ export {
67
74
  ApiSearchParamsSchema,
68
75
  CreateApiFromPromptParamsSchema,
69
76
  CreateApiParamsSchema,
77
+ ScanApiStandardizationFromRegistryParamsSchema,
70
78
  ScanStandardizationParamsSchema,
71
79
  StandardizeApiParamsSchema
72
80
  };
@@ -1,6 +1,6 @@
1
1
  import { FUNCTIONAL_TESTING_TOOLS } from "./functional-testing-tools.js";
2
- import { CreatePortalArgsSchema, PortalArgsSchema, UpdatePortalArgsSchema, CreateProductArgsSchema, ProductArgsSchema, UpdateProductArgsSchema, PublishProductArgsSchema, GetProductSectionsArgsSchema, CreateTableOfContentsArgsSchema, GetTableOfContentsArgsSchema, DeleteTableOfContentsArgsSchema, GetDocumentArgsSchema, UpdateDocumentArgsSchema } from "./portal-types.js";
3
- import { ApiSearchParamsSchema, ApiDefinitionParamsSchema, CreateApiParamsSchema, ScanStandardizationParamsSchema, CreateApiFromPromptParamsSchema, StandardizeApiParamsSchema } from "./registry-types.js";
2
+ import { CreatePortalArgsSchema, PortalArgsSchema, UpdatePortalArgsSchema, ResolveOrganizationPortalArgsSchema, CreateProductArgsSchema, ProductArgsSchema, UpdateProductArgsSchema, PublishProductArgsSchema, GetProductSectionsArgsSchema, CreateTableOfContentsArgsSchema, GetTableOfContentsArgsSchema, DeleteTableOfContentsArgsSchema, CreateDocumentationPageArgsSchema, GetDocumentArgsSchema, UpdateDocumentArgsSchema } from "./portal-types.js";
3
+ import { ApiSearchParamsSchema, ApiDefinitionParamsSchema, CreateApiParamsSchema, ScanStandardizationParamsSchema, ScanApiStandardizationFromRegistryParamsSchema, CreateApiFromPromptParamsSchema, StandardizeApiParamsSchema } from "./registry-types.js";
4
4
  import { OrganizationsQuerySchema } from "./user-management-types.js";
5
5
  const TOOLS = [
6
6
  {
@@ -30,6 +30,13 @@ const TOOLS = [
30
30
  inputSchema: UpdatePortalArgsSchema,
31
31
  handler: "updatePortal"
32
32
  },
33
+ {
34
+ title: "Resolve Organization Portal",
35
+ toolset: "Portals",
36
+ summary: "Resolve portal details for a Swagger organization in a single step. Given an organization UUID, returns the portal ID, subdomain, customDomain (when configured), and the list of products (with productId, productSlug, and productName) for the organization's portal. If the organization has no portal yet, a new portal is created automatically. Use this tool to obtain all portal context needed for subsequent portal and product operations.",
37
+ inputSchema: ResolveOrganizationPortalArgsSchema,
38
+ handler: "resolveOrganizationPortal"
39
+ },
33
40
  {
34
41
  title: "List Portal Products",
35
42
  toolset: "Products",
@@ -69,7 +76,7 @@ const TOOLS = [
69
76
  {
70
77
  title: "Publish Portal Product",
71
78
  toolset: "Products",
72
- summary: "Publish a product's content to make it live or as preview. This endpoint publishes the current content of a product, making it visible to portal visitors. Use preview mode to test before going live.",
79
+ summary: "Publish a product's content to make it live or as preview. This endpoint publishes the current content of a product, making it visible to portal visitors. Use preview mode to test before going live. Optionally provide `tableOfContentsId` to get a page-specific URL. Returns publication status, a live or preview URL (null if URL building fails), product and portal metadata, and an optional `warning` when metadata/URL building failed — a warning does NOT mean the publish failed.",
73
80
  inputSchema: PublishProductArgsSchema,
74
81
  handler: "publishPortalProduct"
75
82
  },
@@ -102,6 +109,13 @@ const TOOLS = [
102
109
  handler: "deleteTableOfContents"
103
110
  },
104
111
  // Document management tools
112
+ {
113
+ title: "Create Documentation Page",
114
+ toolset: "Documents",
115
+ summary: "Create a documentation page in a portal product in a single tool call. Supports markdown and html content types. Returns the page location details (productId, sectionId, slug) and a draftUrl to edit it in the portal.",
116
+ inputSchema: CreateDocumentationPageArgsSchema,
117
+ handler: "createDocumentationPage"
118
+ },
105
119
  {
106
120
  title: "Get Document",
107
121
  toolset: "Documents",
@@ -149,10 +163,17 @@ const TOOLS = [
149
163
  {
150
164
  title: "Scan API Standardization",
151
165
  toolset: "Registry API",
152
- summary: "Run a standardization scan against an API definition using the organization's governance and standardization rules. Accepts a YAML or JSON OpenAPI/AsyncAPI definition and returns a list of standardization errors and validation issues. Use this tool when users ask to validate, scan, or check API governance or standardization.",
166
+ summary: "Run a standardization scan against an API definition using the organization's governance and standardization rules. Accepts a raw YAML or JSON OpenAPI/AsyncAPI definition and returns a list of validation errors, the total issue count, and counts grouped by severity. Use this tool when the user provides the API definition content directly (as raw YAML or JSON) and asks to validate, scan, or check the governance or standardization of the API.",
153
167
  inputSchema: ScanStandardizationParamsSchema,
154
168
  handler: "scanStandardization"
155
169
  },
170
+ {
171
+ title: "Scan API Standardization from Registry",
172
+ toolset: "Registry API",
173
+ summary: "Run a standardization scan on an API that already exists in SwaggerHub Registry, identified by organization name, API name, and version. Fetches the API definition from the registry internally and scans it against the organization's governance and standardization rules. Returns a list of validation errors, total issue count, counts grouped by severity, and a SwaggerHub UI URL for the scanned API. Use this tool when the user identifies the API by org name, API name, and version and asks to validate, scan, or check the governance or standardization of an existing API.",
174
+ inputSchema: ScanApiStandardizationFromRegistryParamsSchema,
175
+ handler: "scanApiStandardizationFromRegistry"
176
+ },
156
177
  {
157
178
  title: "Create API from Prompt",
158
179
  toolset: "Registry API",
@@ -0,0 +1,37 @@
1
+ function normalizeSlug(value) {
2
+ const slug = value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3
+ if (slug.length < 3) {
4
+ throw new Error(`Slug "${slug}" is too short (minimum 3 characters).`);
5
+ }
6
+ return slug;
7
+ }
8
+ function findTableOfContentsItem(items, targetId) {
9
+ for (const item of items) {
10
+ if (item.id === targetId) {
11
+ return item;
12
+ }
13
+ const nestedMatch = findTableOfContentsItem(item.children ?? [], targetId);
14
+ if (nestedMatch) {
15
+ return nestedMatch;
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ function buildPortalLiveUrl(config, portal, productSlug, section, tocItem, preview = false) {
21
+ const host = portal?.customDomain ?? portal?.subdomain;
22
+ const portalUiDomain = portal?.customDomain ? "" : config.getPortalUiDomainSuffix();
23
+ if (!host || !productSlug) {
24
+ return "";
25
+ }
26
+ const baseUrl = `https://${host}${portalUiDomain}/${productSlug}`;
27
+ const previewSuffix = preview ? "?preview=product" : "";
28
+ if (section?.slug && tocItem?.slug) {
29
+ return `${baseUrl}/${section.slug}/${tocItem.slug}${previewSuffix}`;
30
+ }
31
+ return `${baseUrl}${previewSuffix}`;
32
+ }
33
+ export {
34
+ buildPortalLiveUrl,
35
+ findTableOfContentsItem,
36
+ normalizeSlug
37
+ };
@@ -17,6 +17,9 @@ const ConfigurationSchema = z.object({
17
17
  ui_base_path: z.string().optional().describe("Base URL for the SwaggerHub UI (optional)"),
18
18
  functional_testing_api_token: z.string().optional().describe(
19
19
  "Swagger Functional Testing API token. Leave empty to disable Functional Testing tools."
20
+ ),
21
+ functional_testing_base_path: z.string().optional().describe(
22
+ "Base URL for Swagger Functional Testing API requests (optional)"
20
23
  )
21
24
  });
22
25
  class SwaggerClient {
@@ -29,7 +32,8 @@ class SwaggerClient {
29
32
  configPrefix = "Swagger";
30
33
  config = ConfigurationSchema;
31
34
  async configure(_server, config, _cache) {
32
- if (config.api_key) {
35
+ const hasSwaggerAuth = !!config.api_key || !!getRequestHeader("Swagger-Api-Key") || !!getRequestHeader("Authorization");
36
+ if (hasSwaggerAuth) {
33
37
  this._apiKey = config.api_key;
34
38
  this.api = new SwaggerAPI(
35
39
  new SwaggerConfiguration({
@@ -45,7 +49,8 @@ class SwaggerClient {
45
49
  this._ftApiToken = config.functional_testing_api_token;
46
50
  this.ftApi = new FunctionalTestingAPI(
47
51
  () => this.getFtAuthToken(),
48
- USER_AGENT
52
+ USER_AGENT,
53
+ config.functional_testing_base_path
49
54
  );
50
55
  }
51
56
  }
@@ -89,6 +94,9 @@ class SwaggerClient {
89
94
  const { portalId, ...body } = args;
90
95
  return this.getApi().updatePortal(portalId, body);
91
96
  }
97
+ async resolveOrganizationPortal(args) {
98
+ return this.getApi().resolveOrganizationPortal(args);
99
+ }
92
100
  async getPortalProducts(args) {
93
101
  return this.getApi().getPortalProducts(args.portalId);
94
102
  }
@@ -107,8 +115,12 @@ class SwaggerClient {
107
115
  return this.getApi().updatePortalProduct(productId, body);
108
116
  }
109
117
  async publishPortalProduct(args) {
110
- const { productId, preview } = args;
111
- return this.getApi().publishPortalProduct(productId, preview);
118
+ const { productId, preview, tableOfContentsId } = args;
119
+ return this.getApi().publishPortalProduct(
120
+ productId,
121
+ preview,
122
+ tableOfContentsId ?? null
123
+ );
112
124
  }
113
125
  async getPortalProductSections(args) {
114
126
  const { productId, ...params } = args;
@@ -130,6 +142,9 @@ class SwaggerClient {
130
142
  async deleteTableOfContents(args) {
131
143
  return this.getApi().deleteTableOfContents(args);
132
144
  }
145
+ async createDocumentationPage(args) {
146
+ return this.getApi().createDocumentationPage(args);
147
+ }
133
148
  // Registry API methods for SwaggerHub Design functionality
134
149
  async searchApis(args = {}) {
135
150
  return this.getApi().searchApis(args);
@@ -150,14 +165,31 @@ class SwaggerClient {
150
165
  async scanStandardization(args) {
151
166
  return this.getApi().scanStandardization(args);
152
167
  }
168
+ async scanApiStandardizationFromRegistry(args) {
169
+ return this.getApi().scanApiStandardizationFromRegistry(args);
170
+ }
153
171
  async standardizeApi(args) {
154
172
  return this.getApi().standardizeApi(args);
155
173
  }
174
+ // Functional Testing methods
156
175
  async listFunctionalTestingTests() {
176
+ return this.withFunctionalTesting((ftApi) => ftApi.listTests());
177
+ }
178
+ async runFunctionalTestingTest(args) {
179
+ return this.withFunctionalTesting((ftApi) => ftApi.runTest(args));
180
+ }
181
+ async getFunctionalTestingExecution(args) {
182
+ return this.withFunctionalTesting((ftApi) => ftApi.getTestExecution(args));
183
+ }
184
+ /**
185
+ * Perform an operation with the Functional Testing API.
186
+ * Throws a ToolError if Functional Testing is not configured
187
+ */
188
+ async withFunctionalTesting(fn) {
157
189
  if (!this.ftApi) {
158
190
  throw new ToolError("Functional Testing API not configured");
159
191
  }
160
- return this.ftApi.listTests();
192
+ return fn(this.ftApi);
161
193
  }
162
194
  async registerTools(register, _getInput) {
163
195
  TOOLS.forEach((tool) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",