@smartbear/mcp 0.2.2 → 0.4.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 +207 -60
- package/dist/api-hub/client.js +298 -52
- package/dist/common/server.js +145 -0
- package/dist/index.js +31 -22
- 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 +524 -357
- package/dist/package.json +15 -5
- package/dist/pactflow/client/ai.js +127 -0
- package/dist/pactflow/client/base.js +1 -0
- package/dist/pactflow/client/tools.js +46 -0
- package/dist/pactflow/client.js +132 -0
- package/dist/reflect/client.js +100 -18
- package/dist/tests/unit/common/server.test.js +319 -0
- package/dist/tests/unit/insight-hub/api-utilities.test.js +31 -0
- package/dist/tests/unit/insight-hub/client.test.js +852 -0
- package/dist/tests/unit/insight-hub/filters.test.js +93 -0
- package/dist/tests/unit/pactflow/ai.test.js +21 -0
- package/dist/tests/unit/pactflow/client.test.js +67 -0
- package/dist/tests/unit/pactflow/tools.test.js +34 -0
- package/dist/vitest.config.js +57 -0
- package/package.json +15 -5
- package/api-hub/README.md +0 -29
- package/assets/smartbear-logo-dark.svg +0 -17
- package/assets/smartbear-logo-light.svg +0 -17
- package/dist/common/templates.js +0 -54
- package/insight-hub/README.md +0 -42
- package/reflect/README.md +0 -25
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
import
|
|
1
|
+
import NodeCache from "node-cache";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
4
|
import { CurrentUserAPI, ErrorAPI, Configuration } from "./client/index.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import Bugsnag from "../common/bugsnag.js";
|
|
6
|
-
import NodeCache from "node-cache";
|
|
7
5
|
import { ProjectAPI } from "./client/api/Project.js";
|
|
8
|
-
import { FilterObjectSchema } from "./client/api/filters.js";
|
|
9
|
-
import { toolDescriptionTemplate, createParameter, createExample } from "../common/templates.js";
|
|
6
|
+
import { FilterObjectSchema, toQueryString } from "./client/api/filters.js";
|
|
10
7
|
const HUB_PREFIX = "00000";
|
|
11
|
-
const
|
|
12
|
-
const
|
|
8
|
+
const DEFAULT_DOMAIN = "bugsnag.com";
|
|
9
|
+
const HUB_DOMAIN = "insighthub.smartbear.com";
|
|
13
10
|
const cacheKeys = {
|
|
14
11
|
ORG: "insight_hub_org",
|
|
15
12
|
PROJECTS: "insight_hub_projects",
|
|
16
13
|
CURRENT_PROJECT: "insight_hub_current_project",
|
|
17
|
-
|
|
14
|
+
CURRENT_PROJECT_EVENT_FILTERS: "insight_hub_current_project_event_filters",
|
|
18
15
|
};
|
|
16
|
+
// Exclude certain event fields from the project event filters to improve agent usage
|
|
17
|
+
const EXCLUDED_EVENT_FIELDS = new Set([
|
|
18
|
+
"search" // This is searches multiple fields and is more a convenience for humans, we're removing to avoid over-matching
|
|
19
|
+
]);
|
|
20
|
+
const PERMITTED_UPDATE_OPERATIONS = [
|
|
21
|
+
"override_severity",
|
|
22
|
+
"open",
|
|
23
|
+
"fix",
|
|
24
|
+
"ignore",
|
|
25
|
+
"discard",
|
|
26
|
+
"undiscard"
|
|
27
|
+
];
|
|
19
28
|
export class InsightHubClient {
|
|
20
29
|
currentUserApi;
|
|
21
30
|
errorsApi;
|
|
22
31
|
cache;
|
|
23
32
|
projectApi;
|
|
24
33
|
projectApiKey;
|
|
34
|
+
apiEndpoint;
|
|
35
|
+
appEndpoint;
|
|
36
|
+
name = "Insight Hub";
|
|
37
|
+
prefix = "insight_hub";
|
|
25
38
|
constructor(token, projectApiKey, endpoint) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
endpoint = HUB_API_ENDPOINT;
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
endpoint = DEFAULT_API_ENDPOINT;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
39
|
+
this.apiEndpoint = this.getEndpoint("api", projectApiKey, endpoint);
|
|
40
|
+
this.appEndpoint = this.getEndpoint("app", projectApiKey, endpoint);
|
|
34
41
|
const config = new Configuration({
|
|
35
42
|
authToken: token,
|
|
36
43
|
headers: {
|
|
@@ -39,7 +46,7 @@ export class InsightHubClient {
|
|
|
39
46
|
"X-Bugsnag-API": "true",
|
|
40
47
|
"X-Version": "2",
|
|
41
48
|
},
|
|
42
|
-
basePath:
|
|
49
|
+
basePath: this.apiEndpoint,
|
|
43
50
|
});
|
|
44
51
|
this.currentUserApi = new CurrentUserAPI(config);
|
|
45
52
|
this.errorsApi = new ErrorAPI(config);
|
|
@@ -48,39 +55,69 @@ export class InsightHubClient {
|
|
|
48
55
|
this.projectApiKey = projectApiKey;
|
|
49
56
|
}
|
|
50
57
|
async initialize() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
// Trigger caching of org and projects
|
|
59
|
+
await this.getProjects();
|
|
60
|
+
await this.getCurrentProject();
|
|
61
|
+
}
|
|
62
|
+
getHost(apiKey, subdomain) {
|
|
63
|
+
if (apiKey && apiKey.startsWith(HUB_PREFIX)) {
|
|
64
|
+
return `https://${subdomain}.${HUB_DOMAIN}`;
|
|
54
65
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
else {
|
|
67
|
+
return `https://${subdomain}.${DEFAULT_DOMAIN}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// If the endpoint is not provided, it will use the default API endpoint based on the project API key.
|
|
71
|
+
// if the project api key is not provided, the endpoint will be the default API endpoint.
|
|
72
|
+
// if the endpoint is provided, it will be used as is for custom domains, or normalized for known domains.
|
|
73
|
+
getEndpoint(subdomain, apiKey, endpoint) {
|
|
74
|
+
let subDomainEndpoint;
|
|
75
|
+
if (!endpoint) {
|
|
76
|
+
if (apiKey && apiKey.startsWith(HUB_PREFIX)) {
|
|
77
|
+
subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
|
|
63
78
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!projectFields || projectFields.length === 0) {
|
|
67
|
-
throw new Error(`No event fields found for project ${project.name}.`);
|
|
79
|
+
else {
|
|
80
|
+
subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
|
|
68
81
|
}
|
|
69
|
-
this.cache.set(cacheKeys.PROJECT_EVENT_FILTERS, projectFields);
|
|
70
82
|
}
|
|
83
|
+
else {
|
|
84
|
+
// check if the endpoint matches either the HUB_DOMAIN or DEFAULT_DOMAIN
|
|
85
|
+
const url = new URL(endpoint);
|
|
86
|
+
if (url.hostname.endsWith(HUB_DOMAIN) || url.hostname.endsWith(DEFAULT_DOMAIN)) {
|
|
87
|
+
// For known domains (Hub or Bugsnag), always use HTTPS and standard format
|
|
88
|
+
if (url.hostname.endsWith(HUB_DOMAIN)) {
|
|
89
|
+
subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// For custom domains, use the endpoint exactly as provided
|
|
97
|
+
subDomainEndpoint = endpoint;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return subDomainEndpoint;
|
|
71
101
|
}
|
|
72
|
-
async
|
|
73
|
-
return this.
|
|
102
|
+
async getDashboardUrl(project) {
|
|
103
|
+
return `${this.appEndpoint}/${(await this.getOrganization()).slug}/${project.slug}`;
|
|
74
104
|
}
|
|
75
|
-
async
|
|
76
|
-
|
|
105
|
+
async getErrorUrl(project, errorId, queryString = '') {
|
|
106
|
+
const dashboardUrl = await this.getDashboardUrl(project);
|
|
107
|
+
return `${dashboardUrl}/errors/${errorId}${queryString}`;
|
|
77
108
|
}
|
|
78
|
-
async
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
async getOrganization() {
|
|
110
|
+
let org = this.cache.get(cacheKeys.ORG);
|
|
111
|
+
if (!org) {
|
|
112
|
+
const response = await this.currentUserApi.listUserOrganizations();
|
|
113
|
+
const orgs = response.body || [];
|
|
114
|
+
if (!orgs || orgs.length === 0) {
|
|
115
|
+
throw new Error("No organizations found for the current user.");
|
|
116
|
+
}
|
|
117
|
+
org = orgs[0];
|
|
118
|
+
this.cache.set(cacheKeys.ORG, org);
|
|
119
|
+
}
|
|
120
|
+
return org;
|
|
84
121
|
}
|
|
85
122
|
// This method retrieves all projects for the organization stored in the cache.
|
|
86
123
|
// If no projects are found in the cache, it fetches them from the API and
|
|
@@ -89,364 +126,494 @@ export class InsightHubClient {
|
|
|
89
126
|
async getProjects() {
|
|
90
127
|
let projects = this.cache.get(cacheKeys.PROJECTS);
|
|
91
128
|
if (!projects) {
|
|
92
|
-
const org = this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
129
|
+
const org = await this.getOrganization();
|
|
130
|
+
const options = {
|
|
131
|
+
paginate: true
|
|
132
|
+
};
|
|
133
|
+
const response = await this.currentUserApi.getOrganizationProjects(org.id, options);
|
|
134
|
+
projects = response.body || [];
|
|
97
135
|
this.cache.set(cacheKeys.PROJECTS, projects);
|
|
98
136
|
}
|
|
99
|
-
if (!projects) {
|
|
100
|
-
throw new Error("No projects found.");
|
|
101
|
-
}
|
|
102
137
|
return projects;
|
|
103
138
|
}
|
|
104
|
-
async
|
|
105
|
-
|
|
139
|
+
async getProject(projectId) {
|
|
140
|
+
const projects = await this.getProjects();
|
|
141
|
+
return projects.find((p) => p.id === projectId) || null;
|
|
106
142
|
}
|
|
107
|
-
async
|
|
108
|
-
|
|
143
|
+
async getCurrentProject() {
|
|
144
|
+
let project = this.cache.get(cacheKeys.CURRENT_PROJECT) ?? null;
|
|
145
|
+
if (!project && this.projectApiKey) {
|
|
146
|
+
const projects = await this.getProjects();
|
|
147
|
+
project = projects.find((p) => p.api_key === this.projectApiKey) ?? null;
|
|
148
|
+
if (!project) {
|
|
149
|
+
throw new Error(`Unable to find project with API key ${this.projectApiKey} in organization.`);
|
|
150
|
+
}
|
|
151
|
+
this.cache.set(cacheKeys.CURRENT_PROJECT, project);
|
|
152
|
+
if (project) {
|
|
153
|
+
this.cache.set(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS, await this.getProjectEventFilters(project));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return project;
|
|
109
157
|
}
|
|
110
|
-
async
|
|
111
|
-
|
|
158
|
+
async getProjectEventFilters(project) {
|
|
159
|
+
let filtersResponse = (await this.projectApi.listProjectEventFields(project.id)).body;
|
|
160
|
+
if (!filtersResponse || filtersResponse.length === 0) {
|
|
161
|
+
throw new Error(`No event fields found for project ${project.name}.`);
|
|
162
|
+
}
|
|
163
|
+
filtersResponse = filtersResponse.filter(field => !EXCLUDED_EVENT_FIELDS.has(field.display_id));
|
|
164
|
+
return filtersResponse;
|
|
112
165
|
}
|
|
113
|
-
async
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
return
|
|
166
|
+
async getEvent(eventId, projectId) {
|
|
167
|
+
const projectIds = projectId ? [projectId] : (await this.getProjects()).map((p) => p.id);
|
|
168
|
+
const projectEvents = await Promise.all(projectIds.map((projectId) => this.errorsApi.viewEventById(projectId, eventId).catch(_e => null)));
|
|
169
|
+
return projectEvents.find(event => event && !!event.body)?.body || null;
|
|
117
170
|
}
|
|
118
|
-
async
|
|
119
|
-
|
|
171
|
+
async updateError(projectId, errorId, operation, options) {
|
|
172
|
+
const errorUpdateRequest = {
|
|
173
|
+
operation: operation,
|
|
174
|
+
...options
|
|
175
|
+
};
|
|
176
|
+
const response = await this.errorsApi.updateErrorOnProject(projectId, errorId, errorUpdateRequest);
|
|
177
|
+
return response.status === 200 || response.status === 204;
|
|
120
178
|
}
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"Browse available projects when no specific project API key is configured",
|
|
129
|
-
"Find project IDs needed for other tools",
|
|
130
|
-
"Get an overview of all projects in the organization"
|
|
131
|
-
],
|
|
132
|
-
parameters: [
|
|
133
|
-
createParameter("page_size", "number", false, "Number of projects to return per page for pagination", {
|
|
134
|
-
examples: ["10", "25", "50"]
|
|
135
|
-
}),
|
|
136
|
-
createParameter("page", "number", false, "Page number to return (starts from 1)", {
|
|
137
|
-
examples: ["1", "2", "3"]
|
|
138
|
-
})
|
|
139
|
-
],
|
|
140
|
-
examples: [
|
|
141
|
-
createExample("Get first 10 projects", {
|
|
142
|
-
page_size: 10,
|
|
143
|
-
page: 1
|
|
144
|
-
}, "JSON array of project objects with IDs, names, and metadata"),
|
|
145
|
-
createExample("Get all projects (no pagination)", {}, "JSON array of all available projects")
|
|
146
|
-
],
|
|
147
|
-
hints: [
|
|
148
|
-
"Use pagination for organizations with many projects to avoid large responses",
|
|
149
|
-
"Project IDs from this list can be used with other tools when no project API key is configured"
|
|
150
|
-
]
|
|
151
|
-
}),
|
|
152
|
-
inputSchema: {
|
|
153
|
-
page_size: z.number().optional().describe("Number of projects to return per page"),
|
|
154
|
-
page: z.number().optional().describe("Page number to return"),
|
|
155
|
-
}
|
|
156
|
-
}, async (args, _extra) => {
|
|
157
|
-
try {
|
|
158
|
-
let projects = await this.getProjects();
|
|
159
|
-
if (!projects || projects.length === 0) {
|
|
160
|
-
return {
|
|
161
|
-
content: [{ type: "text", text: "No projects found." }],
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (args.page_size || args.page) {
|
|
165
|
-
const pageSize = args.page_size || 10;
|
|
166
|
-
const page = args.page || 1;
|
|
167
|
-
projects = projects.slice((page - 1) * pageSize, page * pageSize);
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text", text: JSON.stringify(projects) }],
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
catch (e) {
|
|
174
|
-
Bugsnag.notify(e);
|
|
175
|
-
throw e;
|
|
176
|
-
}
|
|
177
|
-
});
|
|
179
|
+
async getInputProject(projectId) {
|
|
180
|
+
if (typeof projectId === 'string') {
|
|
181
|
+
const maybeProject = await this.getProject(projectId);
|
|
182
|
+
if (!maybeProject) {
|
|
183
|
+
throw new Error(`Project with ID ${projectId} not found.`);
|
|
184
|
+
}
|
|
185
|
+
return maybeProject;
|
|
178
186
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
else {
|
|
188
|
+
const currentProject = await this.getCurrentProject();
|
|
189
|
+
if (!currentProject) {
|
|
190
|
+
throw new Error('No current project found. Please provide a projectId or configure a project API key.');
|
|
191
|
+
}
|
|
192
|
+
return currentProject;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
registerTools(register, getInput) {
|
|
196
|
+
if (!this.projectApiKey) {
|
|
197
|
+
register({
|
|
198
|
+
title: "List Projects",
|
|
199
|
+
summary: "List all projects in the organization with optional pagination",
|
|
200
|
+
purpose: "Retrieve available projects for browsing and selecting which project to analyze",
|
|
183
201
|
useCases: [
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
202
|
+
"Browse available projects when no specific project API key is configured",
|
|
203
|
+
"Find project IDs needed for other tools",
|
|
204
|
+
"Get an overview of all projects in the organization"
|
|
187
205
|
],
|
|
188
206
|
parameters: [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
207
|
+
{
|
|
208
|
+
name: "page_size",
|
|
209
|
+
type: z.number(),
|
|
210
|
+
description: "Number of projects to return per page for pagination",
|
|
211
|
+
required: false,
|
|
212
|
+
examples: ["10", "25", "50"]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "page",
|
|
216
|
+
type: z.number(),
|
|
217
|
+
description: "Page number to return (starts from 1)",
|
|
218
|
+
required: false,
|
|
219
|
+
examples: ["1", "2", "3"]
|
|
220
|
+
}
|
|
195
221
|
],
|
|
196
222
|
examples: [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
{
|
|
224
|
+
description: "Get first 10 projects",
|
|
225
|
+
parameters: {
|
|
226
|
+
page_size: 10,
|
|
227
|
+
page: 1
|
|
228
|
+
},
|
|
229
|
+
expectedOutput: "JSON array of project objects with IDs, names, and metadata",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
description: "Get all projects (no pagination)",
|
|
233
|
+
parameters: {},
|
|
234
|
+
expectedOutput: "JSON array of all available projects"
|
|
235
|
+
}
|
|
200
236
|
],
|
|
201
237
|
hints: [
|
|
202
|
-
"
|
|
203
|
-
"
|
|
238
|
+
"Use pagination for organizations with many projects to avoid large responses",
|
|
239
|
+
"Project IDs from this list can be used with other tools when no project API key is configured"
|
|
204
240
|
]
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
241
|
+
}, async (args, _extra) => {
|
|
242
|
+
let projects = await this.getProjects();
|
|
243
|
+
if (!projects || projects.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: "No projects found." }],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (args.page_size || args.page) {
|
|
249
|
+
const pageSize = args.page_size || 10;
|
|
250
|
+
const page = args.page || 1;
|
|
251
|
+
projects = projects.slice((page - 1) * pageSize, page * pageSize);
|
|
252
|
+
}
|
|
253
|
+
const result = {
|
|
254
|
+
data: projects,
|
|
255
|
+
count: projects.length,
|
|
256
|
+
};
|
|
216
257
|
return {
|
|
217
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
258
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
218
259
|
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
register({
|
|
263
|
+
title: "Get Error",
|
|
264
|
+
summary: "Get full details on an error, including aggregated and summarized 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.",
|
|
265
|
+
purpose: "Retrieve all the information required on a specified error to understand who it is affecting and why.",
|
|
266
|
+
useCases: [
|
|
267
|
+
"Investigate a specific error found through the List Project Errors tool",
|
|
268
|
+
"Understand which types of user are affected by the error using summarized event data",
|
|
269
|
+
"Get error details for debugging and root cause analysis",
|
|
270
|
+
"Retrieve error metadata for incident reports and documentation"
|
|
271
|
+
],
|
|
272
|
+
parameters: [
|
|
273
|
+
{
|
|
274
|
+
name: "errorId",
|
|
275
|
+
type: z.string(),
|
|
276
|
+
required: true,
|
|
277
|
+
description: "Unique identifier of the error to retrieve",
|
|
278
|
+
examples: ["6863e2af8c857c0a5023b411"]
|
|
279
|
+
},
|
|
280
|
+
...(this.projectApiKey ? [] : [
|
|
281
|
+
{
|
|
282
|
+
name: "projectId",
|
|
283
|
+
type: z.string(),
|
|
284
|
+
required: true,
|
|
285
|
+
description: "ID of the project containing the error",
|
|
286
|
+
}
|
|
287
|
+
]),
|
|
288
|
+
{
|
|
289
|
+
name: "filters",
|
|
290
|
+
type: FilterObjectSchema,
|
|
291
|
+
required: false,
|
|
292
|
+
description: "Apply filters to narrow down the error list. Use the List Project Event Filters tool to discover available filter fields",
|
|
293
|
+
examples: [
|
|
294
|
+
'{"error.status": [{"type": "eq", "value": "open"}]}',
|
|
295
|
+
'{"event.since": [{"type": "eq", "value": "7d"}]} // Relative time: last 7 days',
|
|
296
|
+
'{"event.since": [{"type": "eq", "value": "2018-05-20T00:00:00Z"}]} // ISO 8601 UTC format',
|
|
297
|
+
'{"user.email": [{"type": "eq", "value": "user@example.com"}]}'
|
|
298
|
+
],
|
|
299
|
+
constraints: [
|
|
300
|
+
"Time filters support ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)",
|
|
301
|
+
"ISO 8601 times must be in UTC and use extended format",
|
|
302
|
+
"Relative time periods: h (hours), d (days)"
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
outputFormat: "JSON object containing: " +
|
|
307
|
+
" - error_details: Aggregated data about the error, including first and last seen occurrence" +
|
|
308
|
+
" - latest_event: Detailed information about the most recent occurrence of the error, including stacktrace, breadcrumbs, user and context" +
|
|
309
|
+
" - pivots: List of pivots (summaries) for the error, which can be used to analyze patterns in occurrences" +
|
|
310
|
+
" - url: A link to the error in the dashboard - this should be shown to the user for them to perform further analysis",
|
|
311
|
+
examples: [
|
|
312
|
+
{
|
|
313
|
+
description: "Get details for a specific error",
|
|
314
|
+
parameters: {
|
|
241
315
|
errorId: "6863e2af8c857c0a5023b411"
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
316
|
+
},
|
|
317
|
+
expectedOutput: "JSON object with error details including message, stack trace, occurrence count, and metadata"
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
hints: [
|
|
321
|
+
"Error IDs can be found using the List Errors tool",
|
|
322
|
+
"Use this after filtering errors to get detailed information about specific errors",
|
|
323
|
+
"If you used a filter to get this error, you can pass the same filters here to restrict the results or apply further filters",
|
|
324
|
+
"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 dashboard and perform further analysis",
|
|
325
|
+
],
|
|
252
326
|
}, async (args, _extra) => {
|
|
327
|
+
const project = await this.getInputProject(args.projectId);
|
|
328
|
+
if (!args.errorId)
|
|
329
|
+
throw new Error("Both projectId and errorId arguments are required");
|
|
330
|
+
const errorDetails = (await this.errorsApi.viewErrorOnProject(project.id, args.errorId)).body;
|
|
331
|
+
if (!errorDetails) {
|
|
332
|
+
throw new Error(`Error with ID ${args.errorId} not found in project ${project.id}.`);
|
|
333
|
+
}
|
|
334
|
+
// Build query parameters
|
|
335
|
+
const params = new URLSearchParams();
|
|
336
|
+
// Add sorting and pagination parameters to get the latest event
|
|
337
|
+
params.append('sort', 'timestamp');
|
|
338
|
+
params.append('direction', 'desc');
|
|
339
|
+
params.append('per_page', '1');
|
|
340
|
+
params.append('full_reports', 'true');
|
|
341
|
+
const filters = {
|
|
342
|
+
"error": [{ type: "eq", value: args.errorId }],
|
|
343
|
+
...args.filters
|
|
344
|
+
};
|
|
345
|
+
const filtersQueryString = toQueryString(filters);
|
|
346
|
+
const listEventsQueryString = `?${params}&${filtersQueryString}`;
|
|
347
|
+
// Get the latest event for this error using the events endpoint with filters
|
|
348
|
+
let latestEvent = null;
|
|
253
349
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
259
|
-
};
|
|
350
|
+
const eventsResponse = await this.errorsApi.listEventsOnProject(project.id, listEventsQueryString);
|
|
351
|
+
const events = eventsResponse.body || [];
|
|
352
|
+
latestEvent = events[0] || null;
|
|
260
353
|
}
|
|
261
354
|
catch (e) {
|
|
262
|
-
|
|
263
|
-
|
|
355
|
+
console.warn("Failed to fetch latest event:", e);
|
|
356
|
+
// Continue without latest event rather than failing the entire request
|
|
264
357
|
}
|
|
358
|
+
const content = {
|
|
359
|
+
error_details: errorDetails,
|
|
360
|
+
latest_event: latestEvent,
|
|
361
|
+
pivots: (await this.errorsApi.listErrorPivots(project.id, args.errorId)).body || [],
|
|
362
|
+
url: await this.getErrorUrl(project, args.errorId, `?${filtersQueryString}`),
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: "text", text: JSON.stringify(content) }]
|
|
366
|
+
};
|
|
265
367
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
link: "https://app.bugsnag.com/my-org/my-project/errors/6863e2af8c857c0a5023b411?event_id=6863e2af012caf1d5c320000"
|
|
288
|
-
}, "JSON object with complete event details including stack trace, metadata, and context")
|
|
289
|
-
],
|
|
290
|
-
hints: [
|
|
291
|
-
"The URL must contain both project slug in the path and event_id in query parameters",
|
|
292
|
-
"This is useful when users share Insight Hub dashboard URLs and you need to extract the event data"
|
|
293
|
-
]
|
|
294
|
-
}),
|
|
295
|
-
inputSchema: {
|
|
296
|
-
link: z.string().describe("Link to the event details"),
|
|
297
|
-
}
|
|
298
|
-
}, async (args, _extra) => {
|
|
299
|
-
try {
|
|
300
|
-
if (!args.link)
|
|
301
|
-
throw new Error("link argument is required");
|
|
302
|
-
const url = new URL(args.link);
|
|
303
|
-
const eventId = url.searchParams.get("event_id");
|
|
304
|
-
const projectSlug = url.pathname.split('/')[2];
|
|
305
|
-
if (!projectSlug || !eventId)
|
|
306
|
-
throw new Error("Both projectSlug and eventId must be present in the link");
|
|
307
|
-
// 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.");
|
|
368
|
+
register({
|
|
369
|
+
title: "Get Event Details",
|
|
370
|
+
summary: "Get detailed information about a specific event using its dashboard URL",
|
|
371
|
+
purpose: "Retrieve event details directly from a dashboard URL for quick debugging",
|
|
372
|
+
useCases: [
|
|
373
|
+
"Get event details when given a dashboard URL from a user or notification",
|
|
374
|
+
"Extract event information from shared links or browser URLs",
|
|
375
|
+
"Quick lookup of event details without needing separate project and event IDs"
|
|
376
|
+
],
|
|
377
|
+
parameters: [
|
|
378
|
+
{
|
|
379
|
+
name: "link",
|
|
380
|
+
type: z.string(),
|
|
381
|
+
description: "Full URL to the event details page in the Insight Hub dashboard (web interface)",
|
|
382
|
+
required: true,
|
|
383
|
+
examples: [
|
|
384
|
+
"https://app.bugsnag.com/my-org/my-project/errors/6863e2af8c857c0a5023b411?event_id=6863e2af012caf1d5c320000"
|
|
385
|
+
],
|
|
386
|
+
constraints: [
|
|
387
|
+
"Must be a valid dashboard URL containing project slug and event_id parameter"
|
|
388
|
+
]
|
|
311
389
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
390
|
+
],
|
|
391
|
+
examples: [
|
|
392
|
+
{
|
|
393
|
+
description: "Get event details from a dashboard URL",
|
|
394
|
+
parameters: {
|
|
395
|
+
link: "https://app.bugsnag.com/my-org/my-project/errors/6863e2af8c857c0a5023b411?event_id=6863e2af012caf1d5c320000"
|
|
396
|
+
},
|
|
397
|
+
expectedOutput: "JSON object with complete event details including stack trace, metadata, and context"
|
|
315
398
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
throw
|
|
399
|
+
],
|
|
400
|
+
hints: [
|
|
401
|
+
"The URL must contain both project slug in the path and event_id in query parameters",
|
|
402
|
+
"This is useful when users share Insight Hub dashboard URLs and you need to extract the event data"
|
|
403
|
+
]
|
|
404
|
+
}, async (args, _extra) => {
|
|
405
|
+
if (!args.link)
|
|
406
|
+
throw new Error("link argument is required");
|
|
407
|
+
const url = new URL(args.link);
|
|
408
|
+
const eventId = url.searchParams.get("event_id");
|
|
409
|
+
const projectSlug = url.pathname.split('/')[2];
|
|
410
|
+
if (!projectSlug || !eventId)
|
|
411
|
+
throw new Error("Both projectSlug and eventId must be present in the link");
|
|
412
|
+
// get the project id from list of projects
|
|
413
|
+
const projects = await this.getProjects();
|
|
414
|
+
const projectId = projects.find((p) => p.slug === projectSlug)?.id;
|
|
415
|
+
if (!projectId) {
|
|
416
|
+
throw new Error("Project with the specified slug not found.");
|
|
324
417
|
}
|
|
418
|
+
const response = await this.getEvent(eventId, projectId);
|
|
419
|
+
return {
|
|
420
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
421
|
+
};
|
|
325
422
|
});
|
|
326
423
|
// Dynamically infer the filters schema from cached project event fields
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
],
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
]
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
424
|
+
register({
|
|
425
|
+
title: "List Project Errors",
|
|
426
|
+
summary: "List and search errors in a project using customizable filters",
|
|
427
|
+
purpose: "Retrieve filtered list of errors from a project for analysis, debugging, and reporting",
|
|
428
|
+
useCases: [
|
|
429
|
+
"Debug recent application errors by filtering for open errors in the last 7 days",
|
|
430
|
+
"Generate error reports for stakeholders by filtering specific error types or severity levels",
|
|
431
|
+
"Monitor error trends over time using date range filters",
|
|
432
|
+
"Find errors affecting specific users or environments using metadata filters"
|
|
433
|
+
],
|
|
434
|
+
parameters: [
|
|
435
|
+
{
|
|
436
|
+
name: "filters",
|
|
437
|
+
type: FilterObjectSchema,
|
|
438
|
+
description: "Apply filters to narrow down the error list. Use the List Errors or Get Error tools to discover available filter fields",
|
|
439
|
+
required: false,
|
|
440
|
+
examples: [
|
|
441
|
+
'{"error.status": [{"type": "eq", "value": "open"}]}',
|
|
442
|
+
'{"event.since": [{"type": "eq", "value": "7d"}]} // Relative time: last 7 days',
|
|
443
|
+
'{"event.since": [{"type": "eq", "value": "2018-05-20T00:00:00Z"}]} // ISO 8601 UTC format',
|
|
444
|
+
'{"user.email": [{"type": "eq", "value": "user@example.com"}]}'
|
|
445
|
+
],
|
|
446
|
+
constraints: [
|
|
447
|
+
"Time filters support ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)",
|
|
448
|
+
"ISO 8601 times must be in UTC and use extended format",
|
|
449
|
+
"Relative time periods: h (hours), d (days)"
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
...(this.projectApiKey ? [] : [
|
|
453
|
+
{
|
|
454
|
+
name: "projectId",
|
|
455
|
+
type: z.string(),
|
|
456
|
+
description: "ID of the project to query for errors",
|
|
457
|
+
required: true,
|
|
458
|
+
}
|
|
459
|
+
])
|
|
460
|
+
],
|
|
461
|
+
examples: [
|
|
462
|
+
{
|
|
463
|
+
description: "Find errors affecting a specific user in the last 24 hours",
|
|
464
|
+
parameters: {
|
|
357
465
|
filters: {
|
|
358
466
|
"user.email": [{ "type": "eq", "value": "user@example.com" }],
|
|
359
467
|
"event.since": [{ "type": "eq", "value": "24h" }]
|
|
360
468
|
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
}),
|
|
376
|
-
}
|
|
469
|
+
},
|
|
470
|
+
expectedOutput: "JSON object with a list of errors in the 'data' field, and an error count in the 'count' field"
|
|
471
|
+
}
|
|
472
|
+
],
|
|
473
|
+
hints: [
|
|
474
|
+
"Use list_project_event_filters tool first to discover valid filter field names for your project",
|
|
475
|
+
"Combine multiple filters to narrow results - filters are applied with AND logic",
|
|
476
|
+
"For time filters: use relative format (7d, 24h) for recent periods or ISO 8601 UTC format (2018-05-20T00:00:00Z) for specific dates",
|
|
477
|
+
"Common time filters: event.since (from this time), event.before (until this time)",
|
|
478
|
+
"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"
|
|
479
|
+
]
|
|
377
480
|
}, async (args, _extra) => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
for (const key of Object.keys(args.filters)) {
|
|
387
|
-
if (!validKeys.has(key)) {
|
|
388
|
-
throw new Error(`Invalid filter key: ${key}`);
|
|
389
|
-
}
|
|
481
|
+
const project = await this.getInputProject(args.projectId);
|
|
482
|
+
// Optionally, validate filter keys against cached event fields
|
|
483
|
+
const eventFields = this.cache.get(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS) || [];
|
|
484
|
+
if (args.filters) {
|
|
485
|
+
const validKeys = new Set(eventFields.map(f => f.display_id));
|
|
486
|
+
for (const key of Object.keys(args.filters)) {
|
|
487
|
+
if (!validKeys.has(key)) {
|
|
488
|
+
throw new Error(`Invalid filter key: ${key}`);
|
|
390
489
|
}
|
|
391
490
|
}
|
|
392
|
-
const response = await this.listProjectErrors(projectId, args.filters);
|
|
393
|
-
return {
|
|
394
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
catch (e) {
|
|
398
|
-
Bugsnag.notify(e);
|
|
399
|
-
throw e;
|
|
400
491
|
}
|
|
492
|
+
const response = await this.errorsApi.listProjectErrors(project.id, { filters: args.filters });
|
|
493
|
+
const errors = response.body || [];
|
|
494
|
+
const result = {
|
|
495
|
+
data: errors,
|
|
496
|
+
count: errors.length,
|
|
497
|
+
};
|
|
498
|
+
return {
|
|
499
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
500
|
+
};
|
|
401
501
|
});
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
"
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
502
|
+
register({
|
|
503
|
+
title: "List Project Event Filters",
|
|
504
|
+
summary: "Get available event filter fields for the current project",
|
|
505
|
+
purpose: "Discover valid filter field names and options that can be used with the List Errors or Get Error tools",
|
|
506
|
+
useCases: [
|
|
507
|
+
"Discover what filter fields are available before searching for errors",
|
|
508
|
+
"Find the correct field names for filtering by user, environment, or custom metadata",
|
|
509
|
+
"Understand filter options and data types for building complex queries"
|
|
510
|
+
],
|
|
511
|
+
parameters: [],
|
|
512
|
+
examples: [
|
|
513
|
+
{
|
|
514
|
+
description: "Get all available filter fields",
|
|
515
|
+
parameters: {},
|
|
516
|
+
expectedOutput: "JSON array of EventField objects containing display_id, custom flag, and filter/pivot options"
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
hints: [
|
|
520
|
+
"Use this tool before the List Errors or Get Error tools to understand available filters",
|
|
521
|
+
"Look for display_id field in the response - these are the field names to use in filters"
|
|
522
|
+
]
|
|
421
523
|
}, async (_args, _extra) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
524
|
+
const projectFields = this.cache.get(cacheKeys.CURRENT_PROJECT_EVENT_FILTERS);
|
|
525
|
+
if (!projectFields)
|
|
526
|
+
throw new Error("No event filters found in cache.");
|
|
527
|
+
return {
|
|
528
|
+
content: [{ type: "text", text: JSON.stringify(projectFields) }],
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
register({
|
|
532
|
+
title: "Update Error",
|
|
533
|
+
summary: "Update the status of an error",
|
|
534
|
+
purpose: "Change an error's workflow state, such as marking it as resolved or ignored",
|
|
535
|
+
useCases: [
|
|
536
|
+
"Mark an error as open, fixed or ignored",
|
|
537
|
+
"Discard or un-discard an error",
|
|
538
|
+
"Update the severity of an error"
|
|
539
|
+
],
|
|
540
|
+
parameters: [
|
|
541
|
+
...(this.projectApiKey ? [] : [
|
|
542
|
+
{
|
|
543
|
+
name: "projectId",
|
|
544
|
+
type: z.string(),
|
|
545
|
+
description: "ID of the project that contains the error to be updated",
|
|
546
|
+
required: true,
|
|
547
|
+
}
|
|
548
|
+
]),
|
|
549
|
+
{
|
|
550
|
+
name: "errorId",
|
|
551
|
+
type: z.string(),
|
|
552
|
+
description: "ID of the error to update",
|
|
553
|
+
required: true,
|
|
554
|
+
examples: ["6863e2af8c857c0a5023b411"]
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "operation",
|
|
558
|
+
type: z.enum(PERMITTED_UPDATE_OPERATIONS),
|
|
559
|
+
description: "The operation to apply to the error",
|
|
560
|
+
required: true,
|
|
561
|
+
examples: ["fix", "open", "ignore", "discard", "undiscard"]
|
|
562
|
+
}
|
|
563
|
+
],
|
|
564
|
+
examples: [
|
|
565
|
+
{
|
|
566
|
+
description: "Mark an error as fixed",
|
|
567
|
+
parameters: {
|
|
568
|
+
errorId: "6863e2af8c857c0a5023b411",
|
|
569
|
+
operation: "fix"
|
|
570
|
+
},
|
|
571
|
+
expectedOutput: "Success response indicating the error was marked as fixed"
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
hints: [
|
|
575
|
+
"Only use valid operations - Insight Hub may reject invalid values"
|
|
576
|
+
],
|
|
577
|
+
readOnly: false,
|
|
578
|
+
idempotent: false,
|
|
579
|
+
}, async (args, _extra) => {
|
|
580
|
+
const { errorId, operation } = args;
|
|
581
|
+
const project = await this.getInputProject(args.projectId);
|
|
582
|
+
let severity = undefined;
|
|
583
|
+
if (operation === 'override_severity') {
|
|
584
|
+
// illicit the severity from the user
|
|
585
|
+
const result = await getInput({
|
|
586
|
+
message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
|
|
587
|
+
requestedSchema: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: {
|
|
590
|
+
severity: {
|
|
591
|
+
type: "string",
|
|
592
|
+
enum: ['info', 'warning', 'error'],
|
|
593
|
+
description: "The new severity level for the error"
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
required: ["severity"]
|
|
598
|
+
});
|
|
599
|
+
if (result.action === "accept" && result.content?.severity) {
|
|
600
|
+
severity = result.content.severity;
|
|
601
|
+
}
|
|
433
602
|
}
|
|
603
|
+
const result = await this.updateError(project.id, errorId, operation, { severity });
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: JSON.stringify({ success: result }) }],
|
|
606
|
+
};
|
|
434
607
|
});
|
|
435
608
|
}
|
|
436
|
-
registerResources(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
catch (e) {
|
|
447
|
-
Bugsnag.notify(e);
|
|
448
|
-
throw e;
|
|
449
|
-
}
|
|
609
|
+
registerResources(register) {
|
|
610
|
+
register("event", "{id}", async (uri, variables, _extra) => {
|
|
611
|
+
return {
|
|
612
|
+
contents: [{
|
|
613
|
+
uri: uri.href,
|
|
614
|
+
text: JSON.stringify(await this.getEvent(variables.id))
|
|
615
|
+
}]
|
|
616
|
+
};
|
|
450
617
|
});
|
|
451
618
|
}
|
|
452
619
|
}
|