@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.
- package/README.md +149 -63
- package/dist/common/templates.js +4 -1
- package/dist/insight-hub/client/api/CurrentUser.js +9 -3
- package/dist/insight-hub/client/api/Error.js +81 -0
- package/dist/insight-hub/client/api/Project.js +19 -1
- package/dist/insight-hub/client/api/base.js +10 -2
- package/dist/insight-hub/client.js +300 -131
- package/dist/package.json +13 -4
- package/dist/tests/unit/common/templates.test.js +149 -0
- package/dist/tests/unit/insight-hub/api-utilities.test.js +31 -0
- package/dist/tests/unit/insight-hub/client.test.js +920 -0
- package/dist/tests/unit/insight-hub/filters.test.js +93 -0
- package/dist/vitest.config.js +57 -0
- package/package.json +13 -4
- package/api-hub/README.md +0 -29
- package/assets/smartbear-logo-dark.svg +0 -17
- package/assets/smartbear-logo-light.svg +0 -17
- package/insight-hub/README.md +0 -42
- package/reflect/README.md +0 -25
|
@@ -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
|
|
12
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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:
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
73
|
-
return this.
|
|
103
|
+
async getDashboardUrl(project) {
|
|
104
|
+
return `${this.appEndpoint}/${(await this.getOrganization()).slug}/${project.slug}`;
|
|
74
105
|
}
|
|
75
|
-
async
|
|
76
|
-
|
|
106
|
+
async getErrorUrl(project, errorId, queryString = '') {
|
|
107
|
+
const dashboardUrl = await this.getDashboardUrl(project);
|
|
108
|
+
return `${dashboardUrl}/errors/${errorId}${queryString}`;
|
|
77
109
|
}
|
|
78
|
-
async
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
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
|
|
105
|
-
|
|
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
|
|
108
|
-
|
|
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
|
|
111
|
-
|
|
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
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
119
|
-
|
|
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(
|
|
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
|
|
182
|
-
purpose: "Retrieve error
|
|
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
|
-
...(
|
|
193
|
-
createParameter("projectId", "string",
|
|
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
|
|
213
|
-
if (!
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
424
|
-
if (!
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
}
|