@smartbear/mcp 0.7.0 → 0.9.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 (52) hide show
  1. package/README.md +19 -2
  2. package/dist/api-hub/client/api.js +198 -23
  3. package/dist/api-hub/client/registry-types.js +22 -0
  4. package/dist/api-hub/client/tools.js +19 -1
  5. package/dist/api-hub/client.js +9 -0
  6. package/dist/bugsnag/client/api/CurrentUser.js +13 -50
  7. package/dist/bugsnag/client/api/Error.js +30 -155
  8. package/dist/bugsnag/client/api/Project.js +56 -124
  9. package/dist/bugsnag/client/api/api.js +3743 -0
  10. package/dist/bugsnag/client/api/base.js +107 -32
  11. package/dist/bugsnag/client/api/configuration.js +26 -0
  12. package/dist/bugsnag/client/api/index.js +2 -0
  13. package/dist/bugsnag/client/filters.js +28 -0
  14. package/dist/bugsnag/client.js +157 -325
  15. package/dist/common/server.js +29 -4
  16. package/dist/common/types.js +6 -1
  17. package/dist/index.js +8 -1
  18. package/dist/pactflow/client/prompt-utils.js +2 -1
  19. package/dist/pactflow/client/tools.js +9 -9
  20. package/dist/pactflow/client/utils.js +5 -4
  21. package/dist/pactflow/client.js +16 -14
  22. package/dist/qmetry/client/api/client-api.js +21 -16
  23. package/dist/qmetry/client/api/error-handler.js +329 -0
  24. package/dist/qmetry/client/auto-resolve.js +74 -0
  25. package/dist/qmetry/client/handlers.js +19 -2
  26. package/dist/qmetry/client/issues.js +26 -0
  27. package/dist/qmetry/client/project.js +56 -0
  28. package/dist/qmetry/client/requirement.js +76 -0
  29. package/dist/qmetry/client/testcase.js +46 -8
  30. package/dist/qmetry/client/testsuite.js +117 -0
  31. package/dist/qmetry/client/tools.js +1455 -4
  32. package/dist/qmetry/client/utils.js +16 -0
  33. package/dist/qmetry/client.js +19 -16
  34. package/dist/qmetry/config/constants.js +14 -0
  35. package/dist/qmetry/config/rest-endpoints.js +20 -0
  36. package/dist/qmetry/types/common.js +313 -8
  37. package/dist/qmetry/types/issues.js +6 -0
  38. package/dist/qmetry/types/project.js +10 -0
  39. package/dist/qmetry/types/requirements.js +19 -0
  40. package/dist/qmetry/types/testcase.js +14 -0
  41. package/dist/qmetry/types/testsuite.js +26 -0
  42. package/dist/reflect/client.js +7 -6
  43. package/dist/zephyr/client.js +16 -0
  44. package/dist/zephyr/common/api-client.js +27 -0
  45. package/dist/zephyr/common/auth-service.js +15 -0
  46. package/dist/zephyr/common/types.js +35 -0
  47. package/dist/zephyr/tool/project/get-projects.js +54 -0
  48. package/dist/zephyr/tool/zephyr-tool.js +1 -0
  49. package/package.json +3 -2
  50. package/dist/bugsnag/client/api/filters.js +0 -167
  51. package/dist/bugsnag/client/configuration.js +0 -10
  52. package/dist/bugsnag/client/index.js +0 -2
@@ -1,7 +1,8 @@
1
1
  import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodLiteral, ZodNumber, ZodObject, ZodString, ZodUnion, } from "zod";
2
+ import { ZodAny, ZodArray, ZodBoolean, ZodEnum, ZodLiteral, ZodNumber, ZodObject, ZodOptional, ZodString, ZodUnion, } from "zod";
3
3
  import Bugsnag from "../common/bugsnag.js";
4
4
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
5
+ import { ToolError } from "./types.js";
5
6
  export class SmartBearMcpServer extends McpServer {
6
7
  constructor() {
7
8
  super({
@@ -32,7 +33,24 @@ export class SmartBearMcpServer extends McpServer {
32
33
  return await cb(args, extra);
33
34
  }
34
35
  catch (e) {
35
- Bugsnag.notify(e);
36
+ // ToolErrors should not be reported to BugSnag
37
+ if (e instanceof ToolError) {
38
+ return {
39
+ isError: true,
40
+ content: [
41
+ {
42
+ type: "text",
43
+ text: `Error executing ${toolTitle}: ${e.message}`,
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ else {
49
+ Bugsnag.notify(e, (event) => {
50
+ event.addMetadata("app", { tool: toolName });
51
+ event.unhandled = true;
52
+ });
53
+ }
36
54
  throw e;
37
55
  }
38
56
  });
@@ -41,14 +59,18 @@ export class SmartBearMcpServer extends McpServer {
41
59
  });
42
60
  if (client.registerResources) {
43
61
  client.registerResources((name, path, cb) => {
44
- return super.registerResource(name, new ResourceTemplate(`${client.prefix}://${name}/${path}`, {
62
+ const url = `${client.prefix}://${name}/${path}`;
63
+ return super.registerResource(name, new ResourceTemplate(url, {
45
64
  list: undefined,
46
65
  }), {}, async (url, variables, extra) => {
47
66
  try {
48
67
  return await cb(url, variables, extra);
49
68
  }
50
69
  catch (e) {
51
- Bugsnag.notify(e);
70
+ Bugsnag.notify(e, (event) => {
71
+ event.addMetadata("app", { resource: name, url: url });
72
+ event.unhandled = true;
73
+ });
52
74
  throw e;
53
75
  }
54
76
  });
@@ -142,6 +164,9 @@ export class SmartBearMcpServer extends McpServer {
142
164
  `${key === "constraints" && field instanceof ZodEnum ? `\n - ${Object.keys(field.enum).join("\n - ")}` : ""}`);
143
165
  }
144
166
  getReadableTypeName(zodType) {
167
+ if (zodType instanceof ZodOptional) {
168
+ zodType = zodType._def.innerType;
169
+ }
145
170
  if (zodType instanceof ZodString)
146
171
  return "string";
147
172
  if (zodType instanceof ZodNumber)
@@ -1 +1,6 @@
1
- export {};
1
+ /**
2
+ * Error class for tool-specific errors – these result in a response to the LLM with `isError: true`
3
+ * and are not reported to BugSnag
4
+ */
5
+ export class ToolError extends Error {
6
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { SmartBearMcpServer } from "./common/server.js";
7
7
  import { PactflowClient } from "./pactflow/client.js";
8
8
  import { QmetryClient } from "./qmetry/client.js";
9
9
  import { ReflectClient } from "./reflect/client.js";
10
+ import { ZephyrClient } from "./zephyr/client.js";
10
11
  // This is used to report errors in the MCP server itself
11
12
  // If you want to use your own BugSnag API key, set the MCP_SERVER_BUGSNAG_API_KEY environment variable
12
13
  const McpServerBugsnagAPIKey = process.env.MCP_SERVER_BUGSNAG_API_KEY;
@@ -24,6 +25,8 @@ async function main() {
24
25
  const pactBrokerPassword = process.env.PACT_BROKER_PASSWORD;
25
26
  const qmetryToken = process.env.QMETRY_API_KEY;
26
27
  const qmetryBaseUrl = process.env.QMETRY_BASE_URL;
28
+ const zephyrToken = process.env.ZEPHYR_API_TOKEN;
29
+ const zephyrBaseUrl = process.env.ZEPHYR_BASE_URL;
27
30
  let client_defined = false;
28
31
  if (reflectToken) {
29
32
  server.addClient(new ReflectClient(reflectToken));
@@ -56,8 +59,12 @@ async function main() {
56
59
  server.addClient(new QmetryClient(qmetryToken, qmetryBaseUrl));
57
60
  client_defined = true;
58
61
  }
62
+ if (zephyrToken) {
63
+ server.addClient(new ZephyrClient(zephyrToken, zephyrBaseUrl));
64
+ client_defined = true;
65
+ }
59
66
  if (!client_defined) {
60
- console.error("Please set one of REFLECT_API_TOKEN, BUGSNAG_AUTH_TOKEN, API_HUB_API_KEY, QMETRY_API_KEY or PACT_BROKER_BASE_URL / (and relevant Pact auth) environment variables");
67
+ console.error("Please set one of REFLECT_API_TOKEN, BUGSNAG_AUTH_TOKEN, API_HUB_API_KEY, QMETRY_API_KEY, ZEPHYR_API_TOKEN, or PACT_BROKER_BASE_URL / (and relevant Pact auth) environment variables");
61
68
  process.exit(1);
62
69
  }
63
70
  const transport = new StdioServerTransport();
@@ -1,3 +1,4 @@
1
+ import { ToolError } from "../../common/types.js";
1
2
  import { EndpointMatcherSchema, MatcherRecommendationInputSchema, } from "./ai.js";
2
3
  import { OADMatcherPrompt } from "./prompts.js";
3
4
  /**
@@ -30,7 +31,7 @@ export async function getOADMatcherRecommendations(openAPI, server) {
30
31
  return matcherRecommendations;
31
32
  }
32
33
  else {
33
- throw new Error("Unable to parse recommendations please provide OpenAPI matchers manually.");
34
+ throw new ToolError("Unable to parse recommendations please provide OpenAPI matchers manually.");
34
35
  }
35
36
  }
36
37
  /**
@@ -71,17 +71,17 @@ export const TOOLS = [
71
71
  clients: ["pactflow", "pact_broker"],
72
72
  },
73
73
  {
74
- title: "PactFlow AI Status",
75
- summary: "Check PactFlow AI usage status, remaining credits, and eligibility",
76
- purpose: "Retrieve the AI feature status for the PactFlow account, including whether AI is enabled, the number of remaining and consumed AI credits, and entitlement or permission issues preventing usage.",
74
+ title: "Check PactFlow AI Entitlements",
75
+ summary: "Check your PactFlow AI entitlements and credit balance if you encounter 401 Unauthorized errors or permission/credit issues when using PactFlow AI features.",
76
+ purpose: "Retrieve AI entitlement information when PactFlow AI operations fail with 401 unauthorized errors. Use this to diagnose permission issues, check remaining credits, and verify account eligibility for AI features.",
77
77
  useCases: [
78
- "Verify if AI functionality is enabled for the account before attempting to use AI-powered features",
79
- "Monitor remaining and consumed AI credits to manage usage and avoid unexpected disruptions",
80
- "Detect entitlement or permission issues when a user tries to access AI features and guide corrective actions",
81
- "Integrate into deployment pipelines to ensure the environment is correctly configured with necessary entitlements and sufficient credits before executing AI-driven tasks",
82
- "Fetches usage and entitlement reports for auditing, budgeting, and compliance purposes",
78
+ "Diagnose 401 unauthorized errors when attempting to use PactFlow AI features",
79
+ "Check remaining AI credits when PactFlow AI operations are rejected due to insufficient credits",
80
+ "Verify account entitlements when users receive permission denied errors for PactFlow AI functionality",
81
+ "Troubleshoot PactFlow AI access issues by retrieving current entitlement status and credit balance",
82
+ "Provide detailed error context when PactFlow AI features are unavailable due to account limitations",
83
83
  ],
84
- handler: "getAIStatus",
84
+ handler: "checkAIEntitlements",
85
85
  clients: ["pactflow"],
86
86
  },
87
87
  ];
@@ -1,6 +1,7 @@
1
1
  import yaml from "js-yaml";
2
2
  // @ts-expect-error missing type declarations
3
3
  import Swagger from "swagger-client";
4
+ import { ToolError } from "../../common/types.js";
4
5
  import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
5
6
  /**
6
7
  * Resolve the OpenAPI specification from the provided input.
@@ -12,12 +13,12 @@ import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
12
13
  export async function resolveOpenAPISpec(remoteOpenAPIDocument) {
13
14
  const openAPISchema = RemoteOpenAPIDocumentSchema.safeParse(remoteOpenAPIDocument);
14
15
  if (openAPISchema.error || !remoteOpenAPIDocument) {
15
- throw new Error(`Invalid RemoteOpenAPIDocument: ${JSON.stringify(openAPISchema.error?.issues)}`);
16
+ throw new ToolError(`Invalid RemoteOpenAPIDocument: ${JSON.stringify(openAPISchema.error?.issues)}`);
16
17
  }
17
18
  const unresolvedSpec = await getRemoteSpecContents(openAPISchema.data);
18
19
  const resolvedSpec = await Swagger.resolve({ spec: unresolvedSpec });
19
20
  if (resolvedSpec.errors?.length) {
20
- throw new Error(`Failed to resolve OpenAPI document: ${resolvedSpec.errors?.join(", ")}`);
21
+ throw new ToolError(`Failed to resolve OpenAPI document: ${resolvedSpec.errors?.join(", ")}`);
21
22
  }
22
23
  return resolvedSpec.spec;
23
24
  }
@@ -30,7 +31,7 @@ export async function resolveOpenAPISpec(remoteOpenAPIDocument) {
30
31
  */
31
32
  export async function getRemoteSpecContents(openAPISchema) {
32
33
  if (!openAPISchema.url) {
33
- throw new Error("'url' must be provided.");
34
+ throw new ToolError("'url' must be provided.");
34
35
  }
35
36
  let headers = {};
36
37
  if (openAPISchema.authToken) {
@@ -51,7 +52,7 @@ export async function getRemoteSpecContents(openAPISchema) {
51
52
  return yaml.load(specRawBody);
52
53
  }
53
54
  catch (yamlError) {
54
- throw new Error(`Unsupported Content-Type: ${remoteSpec.headers.get("Content-Type")} for remote OpenAPI document. Found following parse errors:-\nJSON parse error: ${jsonError}\nYAML parse error: ${yamlError}`);
55
+ throw new ToolError(`Unsupported Content-Type: ${remoteSpec.headers.get("Content-Type")} for remote OpenAPI document. Found following parse errors:-\nJSON parse error: ${jsonError}\nYAML parse error: ${yamlError}`);
55
56
  }
56
57
  }
57
58
  }
@@ -1,4 +1,5 @@
1
1
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
+ import { ToolError, } from "../common/types.js";
2
3
  import { getOADMatcherRecommendations, getUserMatcherSelection, } from "./client/prompt-utils.js";
3
4
  import { PROMPTS } from "./client/prompts.js";
4
5
  import { TOOLS } from "./client/tools.js";
@@ -65,7 +66,7 @@ export class PactflowClient {
65
66
  body: JSON.stringify(toolInput),
66
67
  });
67
68
  if (!response.ok) {
68
- throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
69
+ throw new ToolError(`HTTP error! status: ${response.status} - ${await response.text()}`);
69
70
  }
70
71
  const status_response = await response.json();
71
72
  return await this.pollForCompletion(status_response, "Generation");
@@ -93,20 +94,21 @@ export class PactflowClient {
93
94
  body: JSON.stringify(toolInput),
94
95
  });
95
96
  if (!response.ok) {
96
- throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
97
+ throw new ToolError(`HTTP error! status: ${response.status} - ${await response.text()}`);
97
98
  }
98
99
  const status_response = await response.json();
99
100
  return await this.pollForCompletion(status_response, "Review Pacts");
100
101
  }
101
102
  /**
102
- * Retrieves AI status information for the current user
103
- * and organization.
103
+ * Retrieve PactFlow AI entitlement information for the current user
104
+ * and organization when encountering 401 unauthorized errors.
105
+ * Use this to check AI entitlements and credits when AI operations fail.
104
106
  *
105
- * @returns Entitlement containing AI status information, organization
107
+ * @returns Entitlement containing permissions, organization
106
108
  * entitlements, and user entitlements.
107
109
  * @throws Error if the request fails or returns a non-OK response.
108
110
  */
109
- async getAIStatus() {
111
+ async checkAIEntitlements() {
110
112
  const url = `${this.aiBaseUrl}/entitlement`;
111
113
  try {
112
114
  const response = await fetch(url, {
@@ -115,12 +117,12 @@ export class PactflowClient {
115
117
  });
116
118
  if (!response.ok) {
117
119
  const errorText = await response.text().catch(() => "");
118
- throw new Error(`PactFlow AI Status Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
120
+ throw new ToolError(`PactFlow AI Entitlements Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
119
121
  }
120
122
  return (await response.json());
121
123
  }
122
124
  catch (error) {
123
- process.stderr.write(`[GetAICredits] Unexpected error: ${error}\n`);
125
+ process.stderr.write(`[CheckAIEntitlements] Unexpected error: ${error}\n`);
124
126
  throw error;
125
127
  }
126
128
  }
@@ -144,7 +146,7 @@ export class PactflowClient {
144
146
  });
145
147
  // Check if the response is OK (status 200)
146
148
  if (!response.ok) {
147
- throw new Error(`HTTP error! status: ${response.status}`);
149
+ throw new ToolError(`HTTP error! status: ${response.status}`);
148
150
  }
149
151
  return response.json();
150
152
  }
@@ -160,12 +162,12 @@ export class PactflowClient {
160
162
  return await this.getResult(status_response.result_url);
161
163
  }
162
164
  if (statusCheck.status !== 202) {
163
- throw new Error(`${operationName} failed with status: ${statusCheck.status}`);
165
+ throw new ToolError(`${operationName} failed with status: ${statusCheck.status}`);
164
166
  }
165
167
  // Wait before next poll
166
168
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
167
169
  }
168
- throw new Error(`${operationName} timed out after ${timeout / 1000} seconds`);
170
+ throw new ToolError(`${operationName} timed out after ${timeout / 1000} seconds`);
169
171
  }
170
172
  // PactFlow / Pact_Broker client methods
171
173
  async getProviderStates({ provider, }) {
@@ -175,7 +177,7 @@ export class PactflowClient {
175
177
  headers: this.headers,
176
178
  });
177
179
  if (!response.ok) {
178
- throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
180
+ throw new ToolError(`HTTP error! status: ${response.status} - ${await response.text()}`);
179
181
  }
180
182
  return response.json();
181
183
  }
@@ -205,7 +207,7 @@ export class PactflowClient {
205
207
  });
206
208
  if (!response.ok) {
207
209
  const errorText = await response.text().catch(() => "");
208
- throw new Error(`Can-I-Deploy Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
210
+ throw new ToolError(`Can-I-Deploy Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
209
211
  }
210
212
  return (await response.json());
211
213
  }
@@ -264,7 +266,7 @@ export class PactflowClient {
264
266
  });
265
267
  if (!response.ok) {
266
268
  const errorText = await response.text().catch(() => "");
267
- throw new Error(`Matrix Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
269
+ throw new ToolError(`Matrix Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
268
270
  }
269
271
  return (await response.json());
270
272
  }
@@ -1,5 +1,16 @@
1
1
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../../../common/info.js";
2
2
  import { QMETRY_DEFAULTS } from "../../config/constants.js";
3
+ import { handleQMetryApiError, handleQMetryFetchError, } from "./error-handler.js";
4
+ /**
5
+ * QMetry API request function with centralized error handling.
6
+ *
7
+ * Handles authentication, project access, CORS, and generic API errors with
8
+ * user-friendly messages and troubleshooting guidance.
9
+ *
10
+ * @param options Request configuration including authentication token
11
+ * @returns Parsed JSON response from QMetry API
12
+ * @throws User-friendly errors with detailed troubleshooting steps for various scenarios
13
+ */
3
14
  export async function qmetryRequest({ method = "GET", path, token, project, baseUrl, body, }) {
4
15
  const url = `${baseUrl}${path}`;
5
16
  const headers = {
@@ -17,23 +28,17 @@ export async function qmetryRequest({ method = "GET", path, token, project, base
17
28
  if (body && ["POST", "PUT", "PATCH"].includes(method)) {
18
29
  init.body = JSON.stringify(body);
19
30
  }
20
- const res = await fetch(url, init);
31
+ let res;
32
+ try {
33
+ res = await fetch(url, init);
34
+ }
35
+ catch (error) {
36
+ // Handle fetch errors (CORS, network issues, SSL certificate issues, etc.)
37
+ handleQMetryFetchError(error instanceof Error ? error : new Error(String(error)), baseUrl, project, path);
38
+ }
21
39
  if (!res.ok) {
22
- let errorText;
23
- try {
24
- const contentType = res.headers.get("content-type");
25
- if (contentType?.includes("application/json")) {
26
- const json = await res.json();
27
- errorText = JSON.stringify(json);
28
- }
29
- else {
30
- errorText = await res.text();
31
- }
32
- }
33
- catch {
34
- errorText = res.statusText;
35
- }
36
- throw new Error(`QMetry API request failed (${res.status}): ${errorText}`);
40
+ // Use centralized error handling
41
+ await handleQMetryApiError(res, baseUrl, project, path);
37
42
  }
38
43
  return (await res.json());
39
44
  }