@smartbear/mcp 0.8.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 (44) hide show
  1. package/dist/api-hub/client/api.js +51 -10
  2. package/dist/api-hub/client/registry-types.js +8 -0
  3. package/dist/api-hub/client/tools.js +7 -1
  4. package/dist/api-hub/client.js +3 -0
  5. package/dist/bugsnag/client/api/CurrentUser.js +12 -49
  6. package/dist/bugsnag/client/api/Error.js +29 -142
  7. package/dist/bugsnag/client/api/Project.js +52 -113
  8. package/dist/bugsnag/client/api/api.js +3743 -0
  9. package/dist/bugsnag/client/api/base.js +97 -34
  10. package/dist/bugsnag/client/api/configuration.js +26 -0
  11. package/dist/bugsnag/client/api/index.js +2 -0
  12. package/dist/bugsnag/client/filters.js +28 -0
  13. package/dist/bugsnag/client.js +100 -151
  14. package/dist/common/server.js +25 -3
  15. package/dist/common/types.js +6 -1
  16. package/dist/pactflow/client/prompt-utils.js +2 -1
  17. package/dist/pactflow/client/utils.js +5 -4
  18. package/dist/pactflow/client.js +10 -9
  19. package/dist/qmetry/client/api/client-api.js +21 -16
  20. package/dist/qmetry/client/api/error-handler.js +329 -0
  21. package/dist/qmetry/client/auto-resolve.js +74 -0
  22. package/dist/qmetry/client/handlers.js +19 -2
  23. package/dist/qmetry/client/issues.js +26 -0
  24. package/dist/qmetry/client/project.js +56 -0
  25. package/dist/qmetry/client/requirement.js +76 -0
  26. package/dist/qmetry/client/testcase.js +46 -8
  27. package/dist/qmetry/client/testsuite.js +117 -0
  28. package/dist/qmetry/client/tools.js +1455 -4
  29. package/dist/qmetry/client/utils.js +16 -0
  30. package/dist/qmetry/client.js +19 -16
  31. package/dist/qmetry/config/constants.js +14 -0
  32. package/dist/qmetry/config/rest-endpoints.js +20 -0
  33. package/dist/qmetry/types/common.js +313 -8
  34. package/dist/qmetry/types/issues.js +6 -0
  35. package/dist/qmetry/types/project.js +10 -0
  36. package/dist/qmetry/types/requirements.js +19 -0
  37. package/dist/qmetry/types/testcase.js +14 -0
  38. package/dist/qmetry/types/testsuite.js +26 -0
  39. package/dist/reflect/client.js +7 -6
  40. package/dist/zephyr/common/auth-service.js +1 -0
  41. package/package.json +1 -1
  42. package/dist/bugsnag/client/api/filters.js +0 -167
  43. package/dist/bugsnag/client/configuration.js +0 -10
  44. package/dist/bugsnag/client/index.js +0 -2
@@ -1,9 +1,9 @@
1
1
  import NodeCache from "node-cache";
2
2
  import { z } from "zod";
3
3
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
4
- import { FilterObjectSchema, toQueryString, } from "./client/api/filters.js";
5
- import { ProjectAPI, } from "./client/api/Project.js";
6
- import { Configuration, CurrentUserAPI, ErrorAPI } from "./client/index.js";
4
+ import { ToolError, } from "../common/types.js";
5
+ import { Configuration, CurrentUserAPI, ErrorAPI, ProjectAPI, } from "./client/api/index.js";
6
+ import { FilterObjectSchema, toUrlSearchParams, } from "./client/filters.js";
7
7
  const HUB_PREFIX = "00000";
8
8
  const DEFAULT_DOMAIN = "bugsnag.com";
9
9
  const HUB_DOMAIN = "bugsnag.smartbear.com";
@@ -39,7 +39,7 @@ export class BugsnagClient {
39
39
  this.apiEndpoint = this.getEndpoint("api", projectApiKey, endpoint);
40
40
  this.appEndpoint = this.getEndpoint("app", projectApiKey, endpoint);
41
41
  const config = new Configuration({
42
- authToken: token,
42
+ apiKey: `token ${token}`,
43
43
  headers: {
44
44
  "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
45
45
  "Content-Type": "application/json",
@@ -121,15 +121,15 @@ export class BugsnagClient {
121
121
  async getDashboardUrl(project) {
122
122
  return `${this.appEndpoint}/${(await this.getOrganization()).slug}/${project.slug}`;
123
123
  }
124
- async getErrorUrl(project, errorId, queryString = "") {
124
+ async getErrorUrl(project, errorId, queryString) {
125
125
  const dashboardUrl = await this.getDashboardUrl(project);
126
- return `${dashboardUrl}/errors/${errorId}${queryString}`;
126
+ return `${dashboardUrl}/errors/${errorId}${queryString ? `?${queryString}` : ""}`;
127
127
  }
128
128
  async getOrganization() {
129
129
  let org = this.cache.get(cacheKeys.ORG);
130
130
  if (!org) {
131
131
  const response = await this.currentUserApi.listUserOrganizations();
132
- const orgs = response.body || [];
132
+ const orgs = response.body;
133
133
  if (!orgs || orgs.length === 0) {
134
134
  throw new Error("No organizations found for the current user.");
135
135
  }
@@ -147,7 +147,7 @@ export class BugsnagClient {
147
147
  if (!projects) {
148
148
  const org = await this.getOrganization();
149
149
  const response = await this.currentUserApi.getOrganizationProjects(org.id);
150
- projects = response.body || [];
150
+ projects = response.body;
151
151
  this.cache.set(cacheKeys.PROJECTS, projects);
152
152
  }
153
153
  return projects;
@@ -160,9 +160,10 @@ export class BugsnagClient {
160
160
  let project = this.cache.get(cacheKeys.CURRENT_PROJECT) ?? null;
161
161
  if (!project && this.projectApiKey) {
162
162
  const projects = await this.getProjects();
163
- project = projects.find((p) => p.api_key === this.projectApiKey) ?? null;
163
+ project =
164
+ projects.find((p) => p.apiKey === this.projectApiKey) ?? null;
164
165
  if (!project) {
165
- throw new Error("Unable to find project with the configured API key.");
166
+ throw new ToolError("Unable to find project with the configured API key.");
166
167
  }
167
168
  this.cache.set(cacheKeys.CURRENT_PROJECT, project);
168
169
  if (project) {
@@ -174,9 +175,9 @@ export class BugsnagClient {
174
175
  async getProjectEventFilters(project) {
175
176
  let filtersResponse = (await this.projectApi.listProjectEventFields(project.id)).body;
176
177
  if (!filtersResponse || filtersResponse.length === 0) {
177
- throw new Error(`No event fields found for project ${project.name}.`);
178
+ throw new ToolError(`No event fields found for project ${project.name}.`);
178
179
  }
179
- filtersResponse = filtersResponse.filter((field) => !EXCLUDED_EVENT_FIELDS.has(field.display_id));
180
+ filtersResponse = filtersResponse.filter((field) => field.displayId && !EXCLUDED_EVENT_FIELDS.has(field.displayId));
180
181
  return filtersResponse;
181
182
  }
182
183
  async getEvent(eventId, projectId) {
@@ -186,111 +187,48 @@ export class BugsnagClient {
186
187
  const projectEvents = await Promise.all(projectIds.map((projectId) => this.errorsApi.viewEventById(projectId, eventId).catch((_e) => null)));
187
188
  return projectEvents.find((event) => event && !!event.body)?.body || null;
188
189
  }
189
- async updateError(projectId, errorId, operation, options) {
190
- const errorUpdateRequest = {
191
- operation: operation,
192
- ...options,
193
- };
194
- const response = await this.errorsApi.updateErrorOnProject(projectId, errorId, errorUpdateRequest);
195
- return response.status === 200 || response.status === 204;
196
- }
197
190
  async getInputProject(projectId) {
198
191
  if (typeof projectId === "string") {
199
192
  const maybeProject = await this.getProject(projectId);
200
193
  if (!maybeProject) {
201
- throw new Error(`Project with ID ${projectId} not found.`);
194
+ throw new ToolError(`Project with ID ${projectId} not found.`);
202
195
  }
203
196
  return maybeProject;
204
197
  }
205
198
  else {
206
199
  const currentProject = await this.getCurrentProject();
207
200
  if (!currentProject) {
208
- throw new Error("No current project found. Please provide a projectId or configure a project API key.");
201
+ throw new ToolError("No current project found. Please provide a projectId or configure a project API key.");
209
202
  }
210
203
  return currentProject;
211
204
  }
212
205
  }
213
- async listBuilds(projectId, opts) {
214
- const response = await this.projectApi.listBuilds(projectId, opts);
215
- if (!response.body || response.body.length === 0) {
216
- return { ...response, body: [] };
217
- }
218
- const project = await this.getProject(projectId);
219
- if (!project)
220
- throw new Error(`Project with ID ${projectId} not found.`);
221
- return {
222
- ...response,
223
- body: response.body.map((b) => this.addStabilityData(b, project)),
224
- };
225
- }
226
- async getBuild(projectId, buildId) {
227
- const response = await this.projectApi.getBuild(projectId, buildId);
228
- if (!response.body)
229
- throw new Error(`No build for ${buildId} found.`);
230
- const project = await this.getProject(projectId);
231
- if (!project)
232
- throw new Error(`Project with ID ${projectId} not found.`);
233
- return { ...response, body: this.addStabilityData(response.body, project) };
234
- }
235
- async listReleases(projectId, opts) {
236
- const response = await this.projectApi.listReleases(projectId, opts);
237
- if (!response.body || response.body.length === 0) {
238
- return { ...response, body: [] };
239
- }
240
- const project = await this.getProject(projectId);
241
- if (!project)
242
- throw new Error(`Project with ID ${projectId} not found.`);
243
- return {
244
- ...response,
245
- body: response.body.map((r) => this.addStabilityData(r, project)),
246
- };
247
- }
248
- async getRelease(projectId, releaseId) {
249
- const response = await this.projectApi.getRelease(releaseId);
250
- if (!response.body)
251
- throw new Error(`No release for ${releaseId} found.`);
252
- const project = await this.getProject(projectId);
253
- if (!project)
254
- throw new Error(`Project with ID ${projectId} not found.`);
255
- return { ...response, body: this.addStabilityData(response.body, project) };
256
- }
257
- async listBuildsInRelease(projectId, releaseId) {
258
- const response = await this.projectApi.listBuildsInRelease(releaseId);
259
- if (!response.body || response.body.length === 0) {
260
- return { ...response, body: [] };
261
- }
262
- const project = await this.getProject(projectId);
263
- if (!project)
264
- throw new Error(`Project with ID ${projectId} not found.`);
265
- return {
266
- ...response,
267
- body: response.body.map((b) => this.addStabilityData(b, project)),
268
- };
269
- }
270
206
  addStabilityData(source, project) {
271
- const user_stability = source.accumulative_daily_users_seen === 0 // avoid division by zero
207
+ const accumulativeDailyUsersSeen = source.accumulativeDailyUsersSeen || 0;
208
+ const accumulativeDailyUsersWithUnhandled = source.accumulativeDailyUsersWithUnhandled || 0;
209
+ const userStability = accumulativeDailyUsersSeen === 0 // avoid division by zero
272
210
  ? 0
273
- : (source.accumulative_daily_users_seen -
274
- source.accumulative_daily_users_with_unhandled) /
275
- source.accumulative_daily_users_seen;
276
- const session_stability = source.total_sessions_count === 0 // avoid division by zero
211
+ : (accumulativeDailyUsersSeen - accumulativeDailyUsersWithUnhandled) /
212
+ accumulativeDailyUsersSeen;
213
+ const totalSessionsCount = source.totalSessionsCount || 0;
214
+ const unhandledSessionsCount = source.unhandledSessionsCount || 0;
215
+ const sessionStability = totalSessionsCount === 0 // avoid division by zero
277
216
  ? 0
278
- : (source.total_sessions_count - source.unhandled_sessions_count) /
279
- source.total_sessions_count;
280
- const stabilityMetric = project.stability_target_type === "user"
281
- ? user_stability
282
- : session_stability;
283
- const meets_target_stability = stabilityMetric >= project.target_stability.value;
284
- const meets_critical_stability = stabilityMetric >= project.critical_stability.value;
217
+ : (totalSessionsCount - unhandledSessionsCount) / totalSessionsCount;
218
+ const stabilityMetric = project.stabilityTargetType === "user" ? userStability : sessionStability;
219
+ const targetStability = project.targetStability?.value || 0;
220
+ const criticalStability = project.criticalStability?.value || 0;
221
+ const meetsTargetStability = stabilityMetric >= targetStability;
222
+ const meetsCriticalStability = stabilityMetric >= criticalStability;
285
223
  return {
286
224
  ...source,
287
- user_stability,
288
- session_stability,
289
- stability_target_type: project.stability_target_type,
290
- target_stability: project.target_stability.value,
291
- critical_stability: project.critical_stability.value,
292
- meets_target_stability,
293
- meets_critical_stability,
225
+ userStability,
226
+ sessionStability,
227
+ stabilityTargetType: project.stabilityTargetType || "user",
228
+ targetStability,
229
+ criticalStability,
230
+ meetsTargetStability,
231
+ meetsCriticalStability,
294
232
  };
295
233
  }
296
234
  registerTools(register, getInput) {
@@ -429,28 +367,22 @@ export class BugsnagClient {
429
367
  }, async (args, _extra) => {
430
368
  const project = await this.getInputProject(args.projectId);
431
369
  if (!args.errorId)
432
- throw new Error("Both projectId and errorId arguments are required");
370
+ throw new ToolError("Both projectId and errorId arguments are required");
433
371
  const errorDetails = (await this.errorsApi.viewErrorOnProject(project.id, args.errorId)).body;
434
372
  if (!errorDetails) {
435
- throw new Error(`Error with ID ${args.errorId} not found in project ${project.id}.`);
373
+ throw new ToolError(`Error with ID ${args.errorId} not found in project ${project.id}.`);
436
374
  }
437
- // Build query parameters
438
- const params = new URLSearchParams();
439
- // Add sorting and pagination parameters to get the latest event
440
- params.append("sort", "timestamp");
441
- params.append("direction", "desc");
442
- params.append("per_page", "1");
443
- params.append("full_reports", "true");
444
375
  const filters = {
445
376
  error: [{ type: "eq", value: args.errorId }],
446
377
  ...args.filters,
447
378
  };
448
- const filtersQueryString = toQueryString(filters);
449
- const listEventsQueryString = `?${params}&${filtersQueryString}`;
450
379
  // Get the latest event for this error using the events endpoint with filters
451
380
  let latestEvent = null;
452
381
  try {
453
- latestEvent = (await this.errorsApi.getLatestEventOnProject(project.id, listEventsQueryString)).body;
382
+ const latestEvents = (await this.errorsApi.listEventsOnProject(project.id, null, "timestamp", "desc", 1, filters, true)).body;
383
+ if (latestEvents && latestEvents.length > 0) {
384
+ latestEvent = latestEvents[0];
385
+ }
454
386
  }
455
387
  catch (e) {
456
388
  console.warn("Failed to fetch latest event:", e);
@@ -459,9 +391,8 @@ export class BugsnagClient {
459
391
  const content = {
460
392
  error_details: errorDetails,
461
393
  latest_event: latestEvent,
462
- pivots: (await this.errorsApi.listErrorPivots(project.id, args.errorId))
463
- .body || [],
464
- url: await this.getErrorUrl(project, args.errorId, `?${filtersQueryString}`),
394
+ pivots: (await this.errorsApi.getPivotValuesOnAnError(project.id, args.errorId, filters, 5)).body || [],
395
+ url: await this.getErrorUrl(project, args.errorId, toUrlSearchParams(filters).toString()),
465
396
  };
466
397
  return {
467
398
  content: [{ type: "text", text: JSON.stringify(content) }],
@@ -505,17 +436,17 @@ export class BugsnagClient {
505
436
  ],
506
437
  }, async (args, _extra) => {
507
438
  if (!args.link)
508
- throw new Error("link argument is required");
439
+ throw new ToolError("link argument is required");
509
440
  const url = new URL(args.link);
510
441
  const eventId = url.searchParams.get("event_id");
511
442
  const projectSlug = url.pathname.split("/")[2];
512
443
  if (!projectSlug || !eventId)
513
- throw new Error("Both projectSlug and eventId must be present in the link");
444
+ throw new ToolError("Both projectSlug and eventId must be present in the link");
514
445
  // get the project id from list of projects
515
446
  const projects = await this.getProjects();
516
447
  const projectId = projects.find((p) => p.slug === projectSlug)?.id;
517
448
  if (!projectId) {
518
- throw new Error("Project with the specified slug not found.");
449
+ throw new ToolError("Project with the specified slug not found.");
519
450
  }
520
451
  const response = await this.getEvent(eventId, projectId);
521
452
  return {
@@ -578,7 +509,7 @@ export class BugsnagClient {
578
509
  },
579
510
  {
580
511
  name: "nextUrl",
581
- type: z.string().url(),
512
+ type: z.string(),
582
513
  description: "URL for retrieving the next page of results. Use the value in the previous response to get the next page when more results are available.",
583
514
  required: false,
584
515
  examples: [
@@ -648,29 +579,19 @@ export class BugsnagClient {
648
579
  // Validate filter keys against cached event fields
649
580
  if (args.filters) {
650
581
  const eventFields = this.cache.get(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS) || [];
651
- const validKeys = new Set(eventFields.map((f) => f.display_id));
582
+ const validKeys = new Set(eventFields.map((f) => f.displayId));
652
583
  for (const key of Object.keys(args.filters)) {
653
584
  if (!validKeys.has(key)) {
654
- throw new Error(`Invalid filter key: ${key}`);
585
+ throw new ToolError(`Invalid filter key: ${key}`);
655
586
  }
656
587
  }
657
588
  }
658
- const defaultFilters = {
589
+ const filters = {
659
590
  "event.since": [{ type: "eq", value: "30d" }],
660
591
  "error.status": [{ type: "eq", value: "open" }],
592
+ ...args.filters,
661
593
  };
662
- const options = {
663
- filters: { ...defaultFilters, ...args.filters },
664
- };
665
- if (args.sort !== undefined)
666
- options.sort = args.sort;
667
- if (args.direction !== undefined)
668
- options.direction = args.direction;
669
- if (args.perPage !== undefined)
670
- options.per_page = args.perPage;
671
- if (args.nextUrl !== undefined)
672
- options.next_url = args.nextUrl;
673
- const response = await this.errorsApi.listProjectErrors(project.id, options);
594
+ const response = await this.errorsApi.listProjectErrors(project.id, null, args.sort || "last_seen", args.direction || "desc", args.perPage || 30, filters, args.nextUrl);
674
595
  const result = {
675
596
  data: response.body,
676
597
  next_url: response.nextUrl ?? undefined,
@@ -705,7 +626,7 @@ export class BugsnagClient {
705
626
  }, async (_args, _extra) => {
706
627
  const projectFields = this.cache.get(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS);
707
628
  if (!projectFields)
708
- throw new Error("No event filters found in cache.");
629
+ throw new ToolError("No event filters found in cache.");
709
630
  return {
710
631
  content: [{ type: "text", text: JSON.stringify(projectFields) }],
711
632
  };
@@ -784,12 +705,18 @@ export class BugsnagClient {
784
705
  severity = result.content.severity;
785
706
  }
786
707
  }
787
- const result = await this.updateError(project.id, errorId, operation, {
788
- severity,
708
+ const result = await this.errorsApi.updateErrorOnProject(project.id, errorId, {
709
+ operation: operation,
710
+ severity: severity,
789
711
  });
790
712
  return {
791
713
  content: [
792
- { type: "text", text: JSON.stringify({ success: result }) },
714
+ {
715
+ type: "text",
716
+ text: JSON.stringify({
717
+ success: result.status === 200 || result.status === 204,
718
+ }),
719
+ },
793
720
  ],
794
721
  };
795
722
  });
@@ -826,6 +753,13 @@ export class BugsnagClient {
826
753
  required: false,
827
754
  examples: ["true", "false"],
828
755
  },
756
+ {
757
+ name: "perPage",
758
+ type: z.number().min(1).max(100).default(30),
759
+ description: "How many results to return per page.",
760
+ required: false,
761
+ examples: ["30", "50", "100"],
762
+ },
829
763
  {
830
764
  name: "nextUrl",
831
765
  type: z.string(),
@@ -866,19 +800,21 @@ export class BugsnagClient {
866
800
  idempotent: true,
867
801
  outputFormat: "JSON array of release summary objects with metadata, with a URL to the next page if more results are available",
868
802
  }, async (args, _extra) => {
869
- const response = await this.listReleases((await this.getInputProject(args.projectId)).id, {
870
- release_stage_name: args.releaseStage,
871
- visible_only: args.visibleOnly,
872
- next_url: args.nextUrl,
873
- });
803
+ const project = await this.getInputProject(args.projectId);
804
+ const response = await this.projectApi.listProjectReleaseGroups(project.id, args.releaseStage || "production", false, // Not top-only
805
+ args.visibleOnly || false, args.perPage || 30, args.nextUrl);
806
+ let releases = [];
807
+ if (response.body) {
808
+ releases = response.body.map((r) => this.addStabilityData(r, project));
809
+ }
874
810
  return {
875
811
  content: [
876
812
  {
877
813
  type: "text",
878
814
  text: JSON.stringify({
879
- data: response.body,
815
+ data: releases,
880
816
  next_url: response.nextUrl ?? undefined,
881
- data_count: response.body?.length,
817
+ data_count: releases.length,
882
818
  total_count: response.totalCount ?? undefined,
883
819
  }),
884
820
  },
@@ -928,17 +864,26 @@ export class BugsnagClient {
928
864
  outputFormat: "JSON object containing release details along with stability metrics such as user and session stability, and whether it meets project targets",
929
865
  }, async (args, _extra) => {
930
866
  if (!args.releaseId)
931
- throw new Error("releaseId argument is required");
867
+ throw new ToolError("releaseId argument is required");
932
868
  const project = await this.getInputProject(args.projectId);
933
- const releaseResponse = await this.getRelease(project.id, args.releaseId);
934
- const buildsResponse = await this.listBuildsInRelease(project.id, args.releaseId);
869
+ const releaseResponse = await this.projectApi.getReleaseGroup(args.releaseId);
870
+ if (!releaseResponse.body)
871
+ throw new ToolError(`No release for ${args.releaseId} found.`);
872
+ const release = this.addStabilityData(releaseResponse.body, project);
873
+ let builds = [];
874
+ if (releaseResponse.body) {
875
+ const buildsResponse = await this.projectApi.listBuildsInRelease(args.releaseId);
876
+ if (buildsResponse.body) {
877
+ builds = buildsResponse.body.map((b) => this.addStabilityData(b, project));
878
+ }
879
+ }
935
880
  return {
936
881
  content: [
937
882
  {
938
883
  type: "text",
939
884
  text: JSON.stringify({
940
- release: releaseResponse.body,
941
- builds: buildsResponse.body,
885
+ release: release,
886
+ builds: builds,
942
887
  }),
943
888
  },
944
889
  ],
@@ -987,10 +932,14 @@ export class BugsnagClient {
987
932
  outputFormat: "JSON object containing build details along with stability metrics such as user and session stability, and whether it meets project targets",
988
933
  }, async (args, _extra) => {
989
934
  if (!args.buildId)
990
- throw new Error("buildId argument is required");
991
- const response = await this.getBuild((await this.getInputProject(args.projectId)).id, args.buildId);
935
+ throw new ToolError("buildId argument is required");
936
+ const project = await this.getInputProject(args.projectId);
937
+ const response = await this.projectApi.getProjectReleaseById(project.id, args.buildId);
938
+ if (!response.body)
939
+ throw new ToolError(`No build for ${args.buildId} found.`);
940
+ const build = this.addStabilityData(response.body, project);
992
941
  return {
993
- content: [{ type: "text", text: JSON.stringify(response.body) }],
942
+ content: [{ type: "text", text: JSON.stringify(build) }],
994
943
  };
995
944
  });
996
945
  }
@@ -2,6 +2,7 @@ import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/m
2
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
  });
@@ -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
+ }
@@ -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
  /**
@@ -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,7 +94,7 @@ 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");
@@ -116,7 +117,7 @@ export class PactflowClient {
116
117
  });
117
118
  if (!response.ok) {
118
119
  const errorText = await response.text().catch(() => "");
119
- throw new Error(`PactFlow AI Entitlements 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}` : ""}`);
120
121
  }
121
122
  return (await response.json());
122
123
  }
@@ -145,7 +146,7 @@ export class PactflowClient {
145
146
  });
146
147
  // Check if the response is OK (status 200)
147
148
  if (!response.ok) {
148
- throw new Error(`HTTP error! status: ${response.status}`);
149
+ throw new ToolError(`HTTP error! status: ${response.status}`);
149
150
  }
150
151
  return response.json();
151
152
  }
@@ -161,12 +162,12 @@ export class PactflowClient {
161
162
  return await this.getResult(status_response.result_url);
162
163
  }
163
164
  if (statusCheck.status !== 202) {
164
- throw new Error(`${operationName} failed with status: ${statusCheck.status}`);
165
+ throw new ToolError(`${operationName} failed with status: ${statusCheck.status}`);
165
166
  }
166
167
  // Wait before next poll
167
168
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
168
169
  }
169
- throw new Error(`${operationName} timed out after ${timeout / 1000} seconds`);
170
+ throw new ToolError(`${operationName} timed out after ${timeout / 1000} seconds`);
170
171
  }
171
172
  // PactFlow / Pact_Broker client methods
172
173
  async getProviderStates({ provider, }) {
@@ -176,7 +177,7 @@ export class PactflowClient {
176
177
  headers: this.headers,
177
178
  });
178
179
  if (!response.ok) {
179
- throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
180
+ throw new ToolError(`HTTP error! status: ${response.status} - ${await response.text()}`);
180
181
  }
181
182
  return response.json();
182
183
  }
@@ -206,7 +207,7 @@ export class PactflowClient {
206
207
  });
207
208
  if (!response.ok) {
208
209
  const errorText = await response.text().catch(() => "");
209
- 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}` : ""}`);
210
211
  }
211
212
  return (await response.json());
212
213
  }
@@ -265,7 +266,7 @@ export class PactflowClient {
265
266
  });
266
267
  if (!response.ok) {
267
268
  const errorText = await response.text().catch(() => "");
268
- 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}` : ""}`);
269
270
  }
270
271
  return (await response.json());
271
272
  }