@smartbear/mcp 0.2.2 → 0.3.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.
@@ -5,32 +5,40 @@ import { z } from "zod";
5
5
  import Bugsnag from "../common/bugsnag.js";
6
6
  import NodeCache from "node-cache";
7
7
  import { ProjectAPI } from "./client/api/Project.js";
8
- import { FilterObjectSchema } from "./client/api/filters.js";
8
+ import { FilterObjectSchema, toQueryString } from "./client/api/filters.js";
9
9
  import { toolDescriptionTemplate, createParameter, createExample } from "../common/templates.js";
10
10
  const HUB_PREFIX = "00000";
11
- const HUB_API_ENDPOINT = "https://api.insighthub.smartbear.com";
12
- const DEFAULT_API_ENDPOINT = "https://api.bugsnag.com";
11
+ const DEFAULT_DOMAIN = "bugsnag.com";
12
+ const HUB_DOMAIN = "insighthub.smartbear.com";
13
13
  const cacheKeys = {
14
14
  ORG: "insight_hub_org",
15
15
  PROJECTS: "insight_hub_projects",
16
16
  CURRENT_PROJECT: "insight_hub_current_project",
17
- PROJECT_EVENT_FILTERS: "insight_hub_project_event_filters",
17
+ CURRENT_PROJECT_EVENT_FILTERS: "insight_hub_current_project_event_filters",
18
18
  };
19
+ // Exclude certain event fields from the project event filters to improve agent usage
20
+ const EXCLUDED_EVENT_FIELDS = new Set([
21
+ "search" // This is searches multiple fields and is more a convenience for humans, we're removing to avoid over-matching
22
+ ]);
23
+ const PERMITTED_UPDATE_OPERATIONS = [
24
+ "override_severity",
25
+ "open",
26
+ "fix",
27
+ "ignore",
28
+ "discard",
29
+ "undiscard"
30
+ ];
19
31
  export class InsightHubClient {
20
32
  currentUserApi;
21
33
  errorsApi;
22
34
  cache;
23
35
  projectApi;
24
36
  projectApiKey;
37
+ apiEndpoint;
38
+ appEndpoint;
25
39
  constructor(token, projectApiKey, endpoint) {
26
- if (!endpoint) {
27
- if (projectApiKey && projectApiKey.startsWith(HUB_PREFIX)) {
28
- endpoint = HUB_API_ENDPOINT;
29
- }
30
- else {
31
- endpoint = DEFAULT_API_ENDPOINT;
32
- }
33
- }
40
+ this.apiEndpoint = this.getEndpoint("api", projectApiKey, endpoint);
41
+ this.appEndpoint = this.getEndpoint("app", projectApiKey, endpoint);
34
42
  const config = new Configuration({
35
43
  authToken: token,
36
44
  headers: {
@@ -39,7 +47,7 @@ export class InsightHubClient {
39
47
  "X-Bugsnag-API": "true",
40
48
  "X-Version": "2",
41
49
  },
42
- basePath: endpoint,
50
+ basePath: this.apiEndpoint,
43
51
  });
44
52
  this.currentUserApi = new CurrentUserAPI(config);
45
53
  this.errorsApi = new ErrorAPI(config);
@@ -48,39 +56,69 @@ export class InsightHubClient {
48
56
  this.projectApiKey = projectApiKey;
49
57
  }
50
58
  async initialize() {
51
- const orgs = await this.listOrgs();
52
- if (!orgs || orgs.length === 0) {
53
- throw new Error("No organizations found for the current user.");
59
+ // Trigger caching of org and projects
60
+ await this.getProjects();
61
+ await this.getCurrentProject();
62
+ }
63
+ getHost(apiKey, subdomain) {
64
+ if (apiKey && apiKey.startsWith(HUB_PREFIX)) {
65
+ return `https://${subdomain}.${HUB_DOMAIN}`;
54
66
  }
55
- // We should only have one org
56
- this.cache.set(cacheKeys.ORG, orgs[0]);
57
- const projects = await this.listProjects(orgs[0].id);
58
- this.cache.set(cacheKeys.PROJECTS, projects);
59
- if (this.projectApiKey) {
60
- const project = projects.find((project) => project.api_key === this.projectApiKey);
61
- if (!project) {
62
- throw new Error(`Project with API key ${this.projectApiKey} not found in organization ${orgs[0].name}.`);
67
+ else {
68
+ return `https://${subdomain}.${DEFAULT_DOMAIN}`;
69
+ }
70
+ }
71
+ // If the endpoint is not provided, it will use the default API endpoint based on the project API key.
72
+ // if the project api key is not provided, the endpoint will be the default API endpoint.
73
+ // if the endpoint is provided, it will be used as is for custom domains, or normalized for known domains.
74
+ getEndpoint(subdomain, apiKey, endpoint) {
75
+ let subDomainEndpoint;
76
+ if (!endpoint) {
77
+ if (apiKey && apiKey.startsWith(HUB_PREFIX)) {
78
+ subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
63
79
  }
64
- this.cache.set(cacheKeys.CURRENT_PROJECT, project);
65
- const projectFields = await this.listProjectEventFields(project.id);
66
- if (!projectFields || projectFields.length === 0) {
67
- throw new Error(`No event fields found for project ${project.name}.`);
80
+ else {
81
+ subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
82
+ }
83
+ }
84
+ else {
85
+ // check if the endpoint matches either the HUB_DOMAIN or DEFAULT_DOMAIN
86
+ const url = new URL(endpoint);
87
+ if (url.hostname.endsWith(HUB_DOMAIN) || url.hostname.endsWith(DEFAULT_DOMAIN)) {
88
+ // For known domains (Hub or Bugsnag), always use HTTPS and standard format
89
+ if (url.hostname.endsWith(HUB_DOMAIN)) {
90
+ subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
91
+ }
92
+ else {
93
+ subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
94
+ }
95
+ }
96
+ else {
97
+ // For custom domains, use the endpoint exactly as provided
98
+ subDomainEndpoint = endpoint;
68
99
  }
69
- this.cache.set(cacheKeys.PROJECT_EVENT_FILTERS, projectFields);
70
100
  }
101
+ return subDomainEndpoint;
71
102
  }
72
- async listProjectEventFields(projectId) {
73
- return this.projectApi.listProjectEventFields(projectId);
103
+ async getDashboardUrl(project) {
104
+ return `${this.appEndpoint}/${(await this.getOrganization()).slug}/${project.slug}`;
74
105
  }
75
- async listOrgs() {
76
- return this.currentUserApi.listUserOrganizations();
106
+ async getErrorUrl(project, errorId, queryString = '') {
107
+ const dashboardUrl = await this.getDashboardUrl(project);
108
+ return `${dashboardUrl}/errors/${errorId}${queryString}`;
77
109
  }
78
- async listProjects(orgId, options) {
79
- options = {
80
- ...options,
81
- paginate: true
82
- };
83
- return this.currentUserApi.getOrganizationProjects(orgId, options);
110
+ async getOrganization() {
111
+ let org = this.cache.get(cacheKeys.ORG);
112
+ if (!org) {
113
+ const response = await this.currentUserApi.listUserOrganizations();
114
+ const orgs = response.body || [];
115
+ if (!orgs || orgs.length === 0) {
116
+ throw new Error("No organizations found for the current user.");
117
+ }
118
+ org = orgs[0];
119
+ this.cache.set(cacheKeys.ORG, org);
120
+ }
121
+ return org;
84
122
  }
85
123
  // This method retrieves all projects for the organization stored in the cache.
86
124
  // If no projects are found in the cache, it fetches them from the API and
@@ -89,34 +127,71 @@ export class InsightHubClient {
89
127
  async getProjects() {
90
128
  let projects = this.cache.get(cacheKeys.PROJECTS);
91
129
  if (!projects) {
92
- const org = this.cache.get(cacheKeys.ORG);
93
- if (!org) {
94
- throw new Error("No organization found in cache.");
95
- }
96
- projects = await this.listProjects(org.id);
130
+ const org = await this.getOrganization();
131
+ const options = {
132
+ paginate: true
133
+ };
134
+ const response = await this.currentUserApi.getOrganizationProjects(org.id, options);
135
+ projects = response.body || [];
97
136
  this.cache.set(cacheKeys.PROJECTS, projects);
98
137
  }
99
- if (!projects) {
100
- throw new Error("No projects found.");
101
- }
102
138
  return projects;
103
139
  }
104
- async getErrorDetails(projectId, errorId) {
105
- return this.errorsApi.viewErrorOnProject(projectId, errorId);
140
+ async getProject(projectId) {
141
+ const projects = await this.getProjects();
142
+ return projects.find((p) => p.id === projectId) || null;
143
+ }
144
+ async getCurrentProject() {
145
+ let project = this.cache.get(cacheKeys.CURRENT_PROJECT) ?? null;
146
+ if (!project && this.projectApiKey) {
147
+ const projects = await this.getProjects();
148
+ project = projects.find((p) => p.api_key === this.projectApiKey) ?? null;
149
+ if (!project) {
150
+ throw new Error(`Unable to find project with API key ${this.projectApiKey} in organization.`);
151
+ }
152
+ this.cache.set(cacheKeys.CURRENT_PROJECT, project);
153
+ if (project) {
154
+ this.cache.set(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS, await this.getProjectEventFilters(project));
155
+ }
156
+ }
157
+ return project;
106
158
  }
107
- async getLatestErrorEvent(errorId) {
108
- return this.errorsApi.viewLatestEventOnError(errorId);
159
+ async getProjectEventFilters(project) {
160
+ let filtersResponse = (await this.projectApi.listProjectEventFields(project.id)).body;
161
+ if (!filtersResponse || filtersResponse.length === 0) {
162
+ throw new Error(`No event fields found for project ${project.name}.`);
163
+ }
164
+ filtersResponse = filtersResponse.filter(field => !EXCLUDED_EVENT_FIELDS.has(field.display_id));
165
+ return filtersResponse;
109
166
  }
110
- async getProjectEvent(projectId, eventId) {
111
- return this.errorsApi.viewEventById(projectId, eventId);
167
+ async getEvent(eventId, projectId) {
168
+ const projectIds = projectId ? [projectId] : (await this.getProjects()).map((p) => p.id);
169
+ const projectEvents = await Promise.all(projectIds.map((projectId) => this.errorsApi.viewEventById(projectId, eventId).catch(_e => null)));
170
+ return projectEvents.find(event => event && !!event.body)?.body || null;
112
171
  }
113
- async findEventById(eventId) {
114
- const projects = await this.listOrgs().then(([org]) => this.listProjects(org.id));
115
- const eventDetails = await Promise.all(projects.map((p) => this.getProjectEvent(p.id, eventId).catch(_e => null)));
116
- return eventDetails.find(event => !!event);
172
+ async updateError(projectId, errorId, operation, options) {
173
+ const errorUpdateRequest = {
174
+ operation: operation,
175
+ ...options
176
+ };
177
+ const response = await this.errorsApi.updateErrorOnProject(projectId, errorId, errorUpdateRequest);
178
+ return response.status === 200 || response.status === 204;
117
179
  }
118
- async listProjectErrors(projectId, filters) {
119
- return this.errorsApi.listProjectErrors(projectId, { filters });
180
+ async getInputProject(projectId) {
181
+ if (typeof projectId === 'string') {
182
+ const maybeProject = await this.getProject(projectId);
183
+ if (!maybeProject) {
184
+ throw new Error(`Project with ID ${projectId} not found.`);
185
+ }
186
+ return maybeProject;
187
+ }
188
+ else {
189
+ const currentProject = await this.getCurrentProject();
190
+ if (!currentProject) {
191
+ throw new Error('No current project found. Please provide a projectId or configure a project API key.');
192
+ }
193
+ return currentProject;
194
+ }
120
195
  }
121
196
  registerTools(server) {
122
197
  if (!this.projectApiKey) {
@@ -166,8 +241,12 @@ export class InsightHubClient {
166
241
  const page = args.page || 1;
167
242
  projects = projects.slice((page - 1) * pageSize, page * pageSize);
168
243
  }
244
+ const result = {
245
+ data: projects,
246
+ count: projects.length,
247
+ };
169
248
  return {
170
- content: [{ type: "text", text: JSON.stringify(projects) }],
249
+ content: [{ type: "text", text: JSON.stringify(result) }],
171
250
  };
172
251
  }
173
252
  catch (e) {
@@ -178,10 +257,11 @@ export class InsightHubClient {
178
257
  }
179
258
  server.registerTool("get_insight_hub_error", {
180
259
  description: toolDescriptionTemplate({
181
- summary: "Get detailed information about a specific error from a project",
182
- purpose: "Retrieve error details including metadata, events, and context for debugging",
260
+ summary: "Get full details on an error, including aggregated and summarised data across all events (occurrences) and details of the latest event (occurrence), such as breadcrumbs, metadata and the stacktrace. Use the filters parameter to narrow down the summaries further.",
261
+ purpose: "Retrieve all the information required on a specified error to understand who it is affecting and why.",
183
262
  useCases: [
184
263
  "Investigate a specific error found through list_insight_hub_project_errors",
264
+ "Understand which types of user are affected by the error using summarised event data",
185
265
  "Get error details for debugging and root cause analysis",
186
266
  "Retrieve error metadata for incident reports and documentation"
187
267
  ],
@@ -189,10 +269,28 @@ export class InsightHubClient {
189
269
  createParameter("errorId", "string", true, "Unique identifier of the error to retrieve", {
190
270
  examples: ["6863e2af8c857c0a5023b411"]
191
271
  }),
192
- ...(!this.projectApiKey ? [
193
- createParameter("projectId", "string", false, "ID of the project containing the error (optional if project API key is configured)")
194
- ] : [])
272
+ ...(this.projectApiKey ? [] : [
273
+ createParameter("projectId", "string", true, "ID of the project containing the error")
274
+ ]),
275
+ createParameter("filters", "FilterObject", false, "Apply filters to narrow down the error list. Use get_project_event_filters to discover available filter fields", {
276
+ examples: [
277
+ '{"error.status": [{"type": "eq", "value": "open"}]}',
278
+ '{"event.since": [{"type": "eq", "value": "7d"}]} // Relative time: last 7 days',
279
+ '{"event.since": [{"type": "eq", "value": "2018-05-20T00:00:00Z"}]} // ISO 8601 UTC format',
280
+ '{"user.email": [{"type": "eq", "value": "user@example.com"}]}'
281
+ ],
282
+ constraints: [
283
+ "Time filters support ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)",
284
+ "ISO 8601 times must be in UTC and use extended format",
285
+ "Relative time periods: h (hours), d (days)"
286
+ ]
287
+ })
195
288
  ],
289
+ outputFormat: "JSON object containing: " +
290
+ " - error_details: Aggregated data about the error, including first and last seen occurrence" +
291
+ " - latest_event: Detailed information about the most recent occurrence of the error, including stacktrace, breadcrumbs, user and context" +
292
+ " - pivots: List of pivots (summaries) for the error, which can be used to analyze patterns in occurrences" +
293
+ " - url: A link to the error in the Insight Hub dashboard - this should be shown to the user for them to perform further analysis",
196
294
  examples: [
197
295
  createExample("Get details for a specific error", {
198
296
  errorId: "6863e2af8c857c0a5023b411"
@@ -200,62 +298,58 @@ export class InsightHubClient {
200
298
  ],
201
299
  hints: [
202
300
  "Error IDs can be found using the list_insight_hub_project_errors tool",
203
- "Use this after filtering errors to get detailed information about specific errors"
301
+ "Use this after filtering errors to get detailed information about specific errors",
302
+ "If you used a filter to get this error, you can pass the same filters here to restrict the results or apply further filters",
303
+ "The response also includes the latest event for the error, do not call get_insight_hub_error_latest_event afterwards as this will be redundant",
304
+ "The URL provided in the response points should be shown to the user in all cases as it allows them to view the error in the Insight Hub dashboard and perform further analysis",
204
305
  ]
205
306
  }),
206
307
  inputSchema: {
207
308
  errorId: z.string().describe("ID of the error to fetch"),
208
309
  ...(!this.projectApiKey ? { projectId: z.string().optional().describe("ID of the project") } : {}),
310
+ filters: FilterObjectSchema.optional().describe("Filters to apply to the error list. Valid filters for a project can be found in the `get_project_event_filters` tool.")
209
311
  }
210
312
  }, async (args, _extra) => {
211
313
  try {
212
- const projectId = typeof args.projectId === 'string' ? args.projectId : this.cache.get(cacheKeys.CURRENT_PROJECT)?.id;
213
- if (!projectId || !args.errorId)
314
+ const project = await this.getInputProject(args.projectId);
315
+ if (!args.errorId)
214
316
  throw new Error("Both projectId and errorId arguments are required");
215
- const response = await this.getErrorDetails(projectId, args.errorId);
216
- return {
217
- content: [{ type: "text", text: JSON.stringify(response) }],
317
+ const errorDetails = (await this.errorsApi.viewErrorOnProject(project.id, args.errorId)).body;
318
+ if (!errorDetails) {
319
+ throw new Error(`Error with ID ${args.errorId} not found in project ${project.id}.`);
320
+ }
321
+ // Build query parameters
322
+ const params = new URLSearchParams();
323
+ // Add sorting and pagination parameters to get the latest event
324
+ params.append('sort', 'timestamp');
325
+ params.append('direction', 'desc');
326
+ params.append('per_page', '1');
327
+ params.append('full_reports', 'true');
328
+ const filters = {
329
+ "error": [{ type: "eq", value: args.errorId }],
330
+ ...args.filters
331
+ };
332
+ const filtersQueryString = toQueryString(filters);
333
+ const listEventsQueryString = `?${params}&${filtersQueryString}`;
334
+ // Get the latest event for this error using the events endpoint with filters
335
+ let latestEvent = null;
336
+ try {
337
+ const eventsResponse = await this.errorsApi.listEventsOnProject(project.id, listEventsQueryString);
338
+ const events = eventsResponse.body || [];
339
+ latestEvent = events[0] || null;
340
+ }
341
+ catch (e) {
342
+ console.warn("Failed to fetch latest event:", e);
343
+ // Continue without latest event rather than failing the entire request
344
+ }
345
+ const content = {
346
+ error_details: errorDetails,
347
+ latest_event: latestEvent,
348
+ pivots: (await this.errorsApi.listErrorPivots(project.id, args.errorId)).body || [],
349
+ url: await this.getErrorUrl(project, args.errorId, `?${filtersQueryString}`),
218
350
  };
219
- }
220
- catch (e) {
221
- Bugsnag.notify(e);
222
- throw e;
223
- }
224
- });
225
- server.registerTool("get_insight_hub_error_latest_event", {
226
- description: toolDescriptionTemplate({
227
- summary: "Get the most recent event of a specific error",
228
- purpose: "Retrieve the latest event (occurrence) of an error to understand when it last happened and its details",
229
- useCases: [
230
- "Get the most recent occurrence of an error for immediate debugging",
231
- "Understand the latest context and stack trace for an ongoing issue",
232
- "Check when an error last occurred and with what parameters"
233
- ],
234
- parameters: [
235
- createParameter("errorId", "string", true, "Unique identifier of the error to get the latest event for", {
236
- examples: ["6863e2af8c857c0a5023b411"]
237
- })
238
- ],
239
- examples: [
240
- createExample("Get the latest event for an error", {
241
- errorId: "6863e2af8c857c0a5023b411"
242
- }, "JSON object with the most recent event details including timestamp, stack trace, and context")
243
- ],
244
- hints: [
245
- "This shows the most recent occurrence - use get_insight_hub_error for aggregated details of all events grouped into the error",
246
- "The event includes detailed context like user information, request data, and environment details"
247
- ]
248
- }),
249
- inputSchema: {
250
- errorId: z.string().describe("ID of the error to get the latest event for"),
251
- }
252
- }, async (args, _extra) => {
253
- try {
254
- if (!args.errorId)
255
- throw new Error("errorId argument is required");
256
- const response = await this.getLatestErrorEvent(args.errorId);
257
351
  return {
258
- content: [{ type: "text", text: JSON.stringify(response) }],
352
+ content: [{ type: "text", text: JSON.stringify(content) }]
259
353
  };
260
354
  }
261
355
  catch (e) {
@@ -305,15 +399,12 @@ export class InsightHubClient {
305
399
  if (!projectSlug || !eventId)
306
400
  throw new Error("Both projectSlug and eventId must be present in the link");
307
401
  // get the project id from list of projects
308
- const projects = this.cache.get("insight_hub_projects");
309
- if (!projects) {
310
- throw new Error("No projects found in cache.");
311
- }
402
+ const projects = await this.getProjects();
312
403
  const projectId = projects.find((p) => p.slug === projectSlug)?.id;
313
404
  if (!projectId) {
314
405
  throw new Error("Project with the specified slug not found.");
315
406
  }
316
- const response = await this.getProjectEvent(projectId, eventId);
407
+ const response = await this.getEvent(eventId, projectId);
317
408
  return {
318
409
  content: [{ type: "text", text: JSON.stringify(response) }],
319
410
  };
@@ -358,29 +449,25 @@ export class InsightHubClient {
358
449
  "user.email": [{ "type": "eq", "value": "user@example.com" }],
359
450
  "event.since": [{ "type": "eq", "value": "24h" }]
360
451
  }
361
- })
452
+ }, "JSON object with a list of errors in the 'data' field, and an error count in the 'count' field")
362
453
  ],
363
454
  hints: [
364
455
  "Use get_project_event_filters tool first to discover valid filter field names for your project",
365
456
  "Combine multiple filters to narrow results - filters are applied with AND logic",
366
457
  "For time filters: use relative format (7d, 24h) for recent periods or ISO 8601 UTC format (2018-05-20T00:00:00Z) for specific dates",
367
- "Common time filters: event.since (from this time), event.before (until this time)"
458
+ "Common time filters: event.since (from this time), event.before (until this time)",
459
+ "There may not be any errors matching the filters - this is not a problem with the tool, in fact it might be a good thing that the user's application had no errors"
368
460
  ]
369
461
  }),
370
462
  inputSchema: {
371
463
  filters: FilterObjectSchema.optional().describe("Filters to apply to the error list. Valid filters for a project can be found in the `get_project_event_filters` tool."),
372
- // Conditionally add projectId only when no project API key is configured
373
- ...(this.projectApiKey ? {} : {
374
- projectId: z.string().describe("ID of the project"),
375
- }),
464
+ ...(!this.projectApiKey ? { projectId: z.string().optional().describe("ID of the project containing the error") } : {}),
376
465
  }
377
466
  }, async (args, _extra) => {
378
467
  try {
379
- const projectId = typeof args.projectId === 'string' ? args.projectId : this.cache.get(cacheKeys.CURRENT_PROJECT)?.id;
380
- if (!projectId)
381
- throw new Error("projectId argument is required");
468
+ const project = await this.getInputProject(args.projectId);
382
469
  // Optionally, validate filter keys against cached event fields
383
- const eventFields = this.cache.get(cacheKeys.PROJECT_EVENT_FILTERS) || [];
470
+ const eventFields = this.cache.get(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS) || [];
384
471
  if (args.filters) {
385
472
  const validKeys = new Set(eventFields.map(f => f.display_id));
386
473
  for (const key of Object.keys(args.filters)) {
@@ -389,9 +476,14 @@ export class InsightHubClient {
389
476
  }
390
477
  }
391
478
  }
392
- const response = await this.listProjectErrors(projectId, args.filters);
479
+ const response = await this.errorsApi.listProjectErrors(project.id, { filters: args.filters });
480
+ const errors = response.body || [];
481
+ const result = {
482
+ data: errors,
483
+ count: errors.length,
484
+ };
393
485
  return {
394
- content: [{ type: "text", text: JSON.stringify(response) }],
486
+ content: [{ type: "text", text: JSON.stringify(result) }],
395
487
  };
396
488
  }
397
489
  catch (e) {
@@ -420,11 +512,88 @@ export class InsightHubClient {
420
512
  inputSchema: {}
421
513
  }, async (_args, _extra) => {
422
514
  try {
423
- const eventFields = this.cache.get(cacheKeys.PROJECT_EVENT_FILTERS);
424
- if (!eventFields)
515
+ const projectFields = this.cache.get(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS);
516
+ if (!projectFields)
425
517
  throw new Error("No event filters found in cache.");
426
518
  return {
427
- content: [{ type: "text", text: JSON.stringify(eventFields) }],
519
+ content: [{ type: "text", text: JSON.stringify(projectFields) }],
520
+ };
521
+ }
522
+ catch (e) {
523
+ Bugsnag.notify(e);
524
+ throw e;
525
+ }
526
+ });
527
+ server.registerTool("update_error", {
528
+ description: toolDescriptionTemplate({
529
+ summary: "Update the status of an error in Insight Hub",
530
+ purpose: "Change an error's workflow state, such as marking it as resolved or ignored",
531
+ useCases: [
532
+ "Mark an error as open, fixed or ignored",
533
+ "Discard or undiscard an error",
534
+ "Update the severity of an error"
535
+ ],
536
+ parameters: [
537
+ ...(this.projectApiKey ? [] : [
538
+ createParameter("projectId", "string", true, "ID of the project that contains the error to be updated")
539
+ ]),
540
+ createParameter("errorId", "string", true, "ID of the error to update", {
541
+ examples: ["6863e2af8c857c0a5023b411"]
542
+ }),
543
+ createParameter("operation", "string", true, "The operation to apply to the error", {
544
+ examples: ["fix", "open", "ignore", "discard", "undiscard"]
545
+ })
546
+ ],
547
+ examples: [
548
+ createExample("Mark an error as fixed", {
549
+ errorId: "6863e2af8c857c0a5023b411",
550
+ operation: "fix"
551
+ }, "Success response indicating the error was marked as fixed")
552
+ ],
553
+ hints: [
554
+ "Only use valid operations - Insight Hub may reject invalid values"
555
+ ]
556
+ }),
557
+ inputSchema: {
558
+ errorId: z.string().describe("ID of the error to update"),
559
+ ...(!this.projectApiKey ? { projectId: z.string().optional().describe("ID of the project containing the error") } : {}),
560
+ operation: z.enum(PERMITTED_UPDATE_OPERATIONS).describe("The operation to apply to the error"),
561
+ },
562
+ annotations: {
563
+ title: "Update an Insight Hub Error",
564
+ readOnlyHint: false,
565
+ destructiveHint: true,
566
+ idempotentHint: false,
567
+ openWorldHint: true
568
+ }
569
+ }, async (args, _extra) => {
570
+ const { errorId, operation } = args;
571
+ try {
572
+ const project = await this.getInputProject(args.projectId);
573
+ let severity = undefined;
574
+ if (operation === 'override_severity') {
575
+ // illicit the severity from the user
576
+ const result = await server.server.elicitInput({
577
+ message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
578
+ requestedSchema: {
579
+ type: "object",
580
+ properties: {
581
+ severity: {
582
+ type: "string",
583
+ enum: ['info', 'warning', 'error'],
584
+ description: "The new severity level for the error"
585
+ }
586
+ }
587
+ },
588
+ required: ["severity"]
589
+ });
590
+ if (result.action === "accept" && result.content?.severity) {
591
+ severity = result.content.severity;
592
+ }
593
+ }
594
+ const result = await this.updateError(project.id, errorId, operation, { severity });
595
+ return {
596
+ content: [{ type: "text", text: JSON.stringify({ success: result }) }],
428
597
  };
429
598
  }
430
599
  catch (e) {
@@ -439,7 +608,7 @@ export class InsightHubClient {
439
608
  return {
440
609
  contents: [{
441
610
  uri: uri.href,
442
- text: JSON.stringify(await this.findEventById(id))
611
+ text: JSON.stringify(await this.getEvent(id))
443
612
  }]
444
613
  };
445
614
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",
@@ -31,20 +31,29 @@
31
31
  "build": "tsc && shx chmod +x dist/*.js",
32
32
  "lint": "eslint . --ext .ts",
33
33
  "prepare": "npm run build",
34
- "watch": "tsc --watch"
34
+ "watch": "tsc --watch",
35
+ "test": "vitest",
36
+ "test:watch": "vitest --watch",
37
+ "test:coverage": "vitest --coverage",
38
+ "test:coverage:ci": "vitest --coverage --reporter=verbose",
39
+ "test:run": "vitest run",
40
+ "coverage:check": "vitest --coverage --reporter=verbose --config vitest.config.coverage.ts"
35
41
  },
36
42
  "dependencies": {
37
43
  "@bugsnag/js": "^8.2.0",
38
44
  "@modelcontextprotocol/sdk": "^1.15.0",
39
- "node-cache": "^5.1.2"
45
+ "node-cache": "^5.1.2",
46
+ "zod": "^3"
40
47
  },
41
48
  "devDependencies": {
42
49
  "@eslint/js": "^9.29.0",
43
50
  "@types/node": "^22",
51
+ "@vitest/coverage-v8": "^3.2.4",
44
52
  "eslint": "^9.29.0",
45
53
  "globals": "^16.2.0",
46
54
  "shx": "^0.3.4",
47
55
  "typescript": "^5.6.2",
48
- "typescript-eslint": "^8.34.1"
56
+ "typescript-eslint": "^8.34.1",
57
+ "vitest": "^3.2.4"
49
58
  }
50
59
  }