@smartbear/mcp 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +30 -6
  2. package/dist/bugsnag/client/api/CurrentUser.js +50 -26
  3. package/dist/bugsnag/client/api/Error.js +156 -93
  4. package/dist/bugsnag/client/api/Project.js +398 -276
  5. package/dist/bugsnag/client/api/api.js +4087 -3837
  6. package/dist/bugsnag/client/api/base.js +155 -173
  7. package/dist/bugsnag/client/api/configuration.js +28 -25
  8. package/dist/bugsnag/client/filters.js +11 -20
  9. package/dist/bugsnag/client.js +1398 -1281
  10. package/dist/bugsnag/input-schemas.js +39 -57
  11. package/dist/collaborator/client.js +335 -371
  12. package/dist/common/bugsnag.js +5 -3
  13. package/dist/common/cache.js +50 -57
  14. package/dist/common/client-registry.js +106 -119
  15. package/dist/common/info.js +7 -3
  16. package/dist/common/register-clients.js +0 -16
  17. package/dist/common/server.js +270 -228
  18. package/dist/common/tools.js +19 -0
  19. package/dist/common/transport-http.js +252 -343
  20. package/dist/common/transport-stdio.js +40 -37
  21. package/dist/common/zod-utils.js +20 -0
  22. package/dist/index.js +18 -23
  23. package/dist/package.json.js +11 -0
  24. package/dist/pactflow/client/ai.js +142 -169
  25. package/dist/pactflow/client/base.js +41 -51
  26. package/dist/pactflow/client/prompt-utils.js +93 -84
  27. package/dist/pactflow/client/prompts.js +95 -92
  28. package/dist/pactflow/client/tools.js +94 -83
  29. package/dist/pactflow/client/utils.js +60 -64
  30. package/dist/pactflow/client.js +399 -320
  31. package/dist/qmetry/client/api/client-api.js +43 -41
  32. package/dist/qmetry/client/api/error-handler.js +264 -310
  33. package/dist/qmetry/client/auto-resolve.js +78 -99
  34. package/dist/qmetry/client/automation.js +139 -162
  35. package/dist/qmetry/client/handlers.js +49 -46
  36. package/dist/qmetry/client/issues.js +133 -115
  37. package/dist/qmetry/client/project.js +153 -174
  38. package/dist/qmetry/client/requirement.js +82 -70
  39. package/dist/qmetry/client/testcase.js +240 -208
  40. package/dist/qmetry/client/testsuite.js +332 -293
  41. package/dist/qmetry/client/tools/automation-tools.js +291 -288
  42. package/dist/qmetry/client/tools/index.js +16 -13
  43. package/dist/qmetry/client/tools/issue-tools.js +534 -543
  44. package/dist/qmetry/client/tools/project-tools.js +635 -656
  45. package/dist/qmetry/client/tools/requirement-tools.js +525 -528
  46. package/dist/qmetry/client/tools/testcase-tools.js +773 -786
  47. package/dist/qmetry/client/tools/testsuite-tools.js +1069 -1083
  48. package/dist/qmetry/client/utils.js +8 -14
  49. package/dist/qmetry/client.js +111 -109
  50. package/dist/qmetry/config/constants.js +48 -44
  51. package/dist/qmetry/config/rest-endpoints.js +51 -48
  52. package/dist/qmetry/types/automation.js +7 -7
  53. package/dist/qmetry/types/common.js +763 -1049
  54. package/dist/qmetry/types/issues.js +26 -19
  55. package/dist/qmetry/types/project.js +32 -25
  56. package/dist/qmetry/types/requirements.js +26 -21
  57. package/dist/qmetry/types/testcase.js +55 -44
  58. package/dist/qmetry/types/testsuite.js +66 -52
  59. package/dist/reflect/client.js +284 -226
  60. package/dist/swagger/client/api.js +645 -662
  61. package/dist/swagger/client/configuration.js +31 -33
  62. package/dist/swagger/client/portal-types.js +204 -244
  63. package/dist/swagger/client/registry-types.js +62 -96
  64. package/dist/swagger/client/tools.js +148 -158
  65. package/dist/swagger/client/user-management-types.js +11 -22
  66. package/dist/swagger/client.js +143 -135
  67. package/dist/swagger/config-utils.js +10 -16
  68. package/dist/zephyr/client.js +43 -42
  69. package/dist/zephyr/common/api-client.js +35 -30
  70. package/dist/zephyr/common/auth-service.js +16 -13
  71. package/dist/zephyr/common/rest-api-schemas.js +3173 -5146
  72. package/dist/zephyr/tool/environment/get-environments.js +66 -66
  73. package/dist/zephyr/tool/priority/get-priorities.js +41 -41
  74. package/dist/zephyr/tool/project/get-project.js +37 -37
  75. package/dist/zephyr/tool/project/get-projects.js +46 -46
  76. package/dist/zephyr/tool/status/get-statuses.js +47 -47
  77. package/dist/zephyr/tool/test-case/get-test-case.js +37 -37
  78. package/dist/zephyr/tool/test-case/get-test-cases.js +62 -62
  79. package/dist/zephyr/tool/test-cycle/get-test-cycle.js +37 -37
  80. package/dist/zephyr/tool/test-cycle/get-test-cycles.js +70 -70
  81. package/dist/zephyr/tool/test-execution/get-test-execution.js +37 -37
  82. package/dist/zephyr/tool/test-execution/get-test-executions.js +43 -43
  83. package/package.json +5 -5
  84. package/dist/bugsnag/client/api/index.js +0 -6
  85. package/dist/common/types.js +0 -6
  86. package/dist/qmetry/client/tools/types.js +0 -1
  87. package/dist/swagger/client/index.js +0 -6
  88. package/dist/tests/unit/bugsnag/utils/factories.js +0 -86
  89. package/dist/zephyr/tool/zephyr-tool.js +0 -1
@@ -1,1323 +1,1440 @@
1
1
  import { z } from "zod";
2
2
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
3
- import { ToolError, } from "../common/types.js";
4
- import { Configuration, CurrentUserAPI, ErrorAPI, ErrorUpdateRequest, ProjectAPI, } from "./client/api/index.js";
3
+ import { ToolError } from "../common/tools.js";
4
+ import { ErrorUpdateRequest } from "./client/api/api.js";
5
+ import { CurrentUserAPI } from "./client/api/CurrentUser.js";
6
+ import { Configuration } from "./client/api/configuration.js";
7
+ import { ErrorAPI } from "./client/api/Error.js";
8
+ import { ProjectAPI } from "./client/api/Project.js";
5
9
  import { toUrlSearchParams } from "./client/filters.js";
6
10
  import { toolInputParameters } from "./input-schemas.js";
7
11
  const HUB_PREFIX = "00000";
8
12
  const DEFAULT_DOMAIN = "bugsnag.com";
9
13
  const HUB_DOMAIN = "bugsnag.smartbear.com";
10
14
  const cacheKeys = {
11
- ORG: "bugsnag_org",
12
- PROJECTS: "bugsnag_projects",
13
- PROJECT_EVENT_FIELDS: "bugsnag_project_event_fields",
14
- PROJECT_TRACE_FIELDS: "bugsnag_project_trace_fields",
15
- CURRENT_PROJECT: "bugsnag_current_project",
15
+ ORG: "bugsnag_org",
16
+ PROJECTS: "bugsnag_projects",
17
+ PROJECT_EVENT_FIELDS: "bugsnag_project_event_fields",
18
+ PROJECT_TRACE_FIELDS: "bugsnag_project_trace_fields",
19
+ CURRENT_PROJECT: "bugsnag_current_project"
16
20
  };
17
- // Exclude certain event fields from the project event filters to improve agent usage
18
- const EXCLUDED_EVENT_FIELDS = new Set([
19
- "search", // This is searches multiple fields and is more a convenience for humans, we're removing to avoid over-matching
21
+ const EXCLUDED_EVENT_FIELDS = /* @__PURE__ */ new Set([
22
+ "search"
23
+ // This is searches multiple fields and is more a convenience for humans, we're removing to avoid over-matching
20
24
  ]);
21
25
  const PERMITTED_UPDATE_OPERATIONS = [
22
- "override_severity",
23
- "open",
24
- "fix",
25
- "ignore",
26
- "discard",
27
- "undiscard",
26
+ "override_severity",
27
+ "open",
28
+ "fix",
29
+ "ignore",
30
+ "discard",
31
+ "undiscard"
28
32
  ];
29
33
  const ConfigurationSchema = z.object({
30
- auth_token: z.string().describe("BugSnag personal authentication token"),
31
- project_api_key: z.string().describe("BugSnag project API key").optional(),
32
- endpoint: z.string().url().describe("BugSnag endpoint URL").optional(),
34
+ auth_token: z.string().describe("BugSnag personal authentication token"),
35
+ project_api_key: z.string().describe("BugSnag project API key").optional(),
36
+ endpoint: z.url().describe("BugSnag endpoint URL").optional()
33
37
  });
34
- export class BugsnagClient {
35
- cache;
36
- projectApiKey;
37
- configuredProjectApiKey;
38
- _currentUserApi;
39
- _errorsApi;
40
- _projectApi;
41
- _appEndpoint;
42
- get currentUserApi() {
43
- if (!this._currentUserApi)
44
- throw new Error("Client not configured");
45
- return this._currentUserApi;
46
- }
47
- get errorsApi() {
48
- if (!this._errorsApi)
49
- throw new Error("Client not configured");
50
- return this._errorsApi;
51
- }
52
- get projectApi() {
53
- if (!this._projectApi)
54
- throw new Error("Client not configured");
55
- return this._projectApi;
56
- }
57
- get appEndpoint() {
58
- if (!this._appEndpoint)
59
- throw new Error("Client not configured");
60
- return this._appEndpoint;
61
- }
62
- name = "BugSnag";
63
- toolPrefix = "bugsnag";
64
- configPrefix = "Bugsnag";
65
- config = ConfigurationSchema;
66
- async configure(server, config) {
67
- this.cache = server.getCache();
68
- this._appEndpoint = this.getEndpoint("app", config.project_api_key, config.endpoint);
69
- const apiConfig = new Configuration({
70
- apiKey: `token ${config.auth_token}`,
71
- headers: {
72
- "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
73
- "Content-Type": "application/json",
74
- "X-Bugsnag-API": "true",
75
- "X-Version": "2",
76
- },
77
- basePath: this.getEndpoint("api", config.project_api_key, config.endpoint),
78
- });
79
- this._currentUserApi = new CurrentUserAPI(apiConfig);
80
- this._errorsApi = new ErrorAPI(apiConfig);
81
- this._projectApi = new ProjectAPI(apiConfig);
82
- this.projectApiKey = config.project_api_key;
83
- // Trigger caching of org and projects
84
- try {
85
- const projects = await this.getProjects();
86
- // If there's just one project, make this the current project
87
- if (projects.length === 1 && !this.projectApiKey) {
88
- this.projectApiKey = projects[0].apiKey;
89
- }
90
- }
91
- catch (error) {
92
- // Swallow auth errors here to allow the tools to be registered for visibility, even if the token is invalid
93
- console.error("Unable to connect to BugSnag APIs, the BugSnag tools will not work. Check your configured BugSnag auth token.", error);
38
+ class BugsnagClient {
39
+ cache;
40
+ projectApiKey;
41
+ configuredProjectApiKey;
42
+ _isConfigured = false;
43
+ _currentUserApi;
44
+ _errorsApi;
45
+ _projectApi;
46
+ _appEndpoint;
47
+ get currentUserApi() {
48
+ if (!this._currentUserApi) throw new Error("Client not configured");
49
+ return this._currentUserApi;
50
+ }
51
+ get errorsApi() {
52
+ if (!this._errorsApi) throw new Error("Client not configured");
53
+ return this._errorsApi;
54
+ }
55
+ get projectApi() {
56
+ if (!this._projectApi) throw new Error("Client not configured");
57
+ return this._projectApi;
58
+ }
59
+ get appEndpoint() {
60
+ if (!this._appEndpoint) throw new Error("Client not configured");
61
+ return this._appEndpoint;
62
+ }
63
+ name = "BugSnag";
64
+ toolPrefix = "bugsnag";
65
+ configPrefix = "Bugsnag";
66
+ config = ConfigurationSchema;
67
+ async configure(server, config) {
68
+ this.cache = server.getCache();
69
+ this._appEndpoint = this.getEndpoint(
70
+ "app",
71
+ config.project_api_key,
72
+ config.endpoint
73
+ );
74
+ const apiConfig = new Configuration({
75
+ apiKey: `token ${config.auth_token}`,
76
+ headers: {
77
+ "User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
78
+ "Content-Type": "application/json",
79
+ "X-Bugsnag-API": "true",
80
+ "X-Version": "2"
81
+ },
82
+ basePath: this.getEndpoint(
83
+ "api",
84
+ config.project_api_key,
85
+ config.endpoint
86
+ )
87
+ });
88
+ this._currentUserApi = new CurrentUserAPI(apiConfig);
89
+ this._errorsApi = new ErrorAPI(apiConfig);
90
+ this._projectApi = new ProjectAPI(apiConfig);
91
+ this.projectApiKey = config.project_api_key;
92
+ try {
93
+ const projects = await this.getProjects();
94
+ if (projects.length === 1 && !this.projectApiKey) {
95
+ this.projectApiKey = projects[0].api_key;
96
+ }
97
+ if (this.projectApiKey) {
98
+ this.configuredProjectApiKey = this.projectApiKey;
99
+ const currentProject = await this.getCurrentProject();
100
+ if (currentProject) {
101
+ await this.getProjectEventFields(currentProject);
102
+ } else {
103
+ this.projectApiKey = void 0;
104
+ console.error(
105
+ "Unable to find your configured BugSnag project, the BugSnag tools will continue to work across all projects in your organization. Check your configured BugSnag project API key."
106
+ );
94
107
  }
95
- if (this.projectApiKey) {
96
- this.configuredProjectApiKey = this.projectApiKey; // Store the originally configured API key
97
- let currentProject = null;
98
- try {
99
- currentProject = await this.getCurrentProject();
100
- }
101
- catch (error) {
102
- console.error("An error occurred while fetching project information", error);
103
- }
104
- if (currentProject) {
105
- await this.getProjectEventFields(currentProject);
106
- }
107
- else {
108
- // Clear the project API key to allow tools to work across all projects
109
- this.projectApiKey = undefined;
110
- console.error("Unable to find your configured BugSnag project, the BugSnag tools will continue to work across all projects in your organization. " +
111
- "Check your configured BugSnag project API key.");
112
- }
113
- }
114
- return true;
108
+ }
109
+ } catch (error) {
110
+ console.error(
111
+ "Unable to connect to BugSnag APIs, the BugSnag tools will not work. Check your configured BugSnag auth token.",
112
+ error
113
+ );
114
+ return;
115
115
  }
116
- getHost(apiKey, subdomain) {
117
- if (apiKey?.startsWith(HUB_PREFIX)) {
118
- return `https://${subdomain}.${HUB_DOMAIN}`;
119
- }
120
- else {
121
- return `https://${subdomain}.${DEFAULT_DOMAIN}`;
116
+ this._isConfigured = true;
117
+ return;
118
+ }
119
+ isConfigured() {
120
+ return this._isConfigured;
121
+ }
122
+ // If the endpoint is not provided, it will use the default API endpoint based on the project API key.
123
+ // if the project api key is not provided, the endpoint will be the default API endpoint.
124
+ // if the endpoint is provided, it will be used as is for custom domains, or normalized for known domains.
125
+ getEndpoint(subdomain, apiKey, endpoint) {
126
+ let subDomainEndpoint;
127
+ if (!endpoint) {
128
+ if (apiKey?.startsWith(HUB_PREFIX)) {
129
+ subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
130
+ } else {
131
+ subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
132
+ }
133
+ } else {
134
+ const url = new URL(endpoint);
135
+ if (url.hostname.endsWith(HUB_DOMAIN) || url.hostname.endsWith(DEFAULT_DOMAIN)) {
136
+ if (url.hostname.endsWith(HUB_DOMAIN)) {
137
+ subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
138
+ } else {
139
+ subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
122
140
  }
141
+ } else {
142
+ subDomainEndpoint = endpoint;
143
+ }
123
144
  }
124
- // If the endpoint is not provided, it will use the default API endpoint based on the project API key.
125
- // if the project api key is not provided, the endpoint will be the default API endpoint.
126
- // if the endpoint is provided, it will be used as is for custom domains, or normalized for known domains.
127
- getEndpoint(subdomain, apiKey, endpoint) {
128
- let subDomainEndpoint;
129
- if (!endpoint) {
130
- if (apiKey?.startsWith(HUB_PREFIX)) {
131
- subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
132
- }
133
- else {
134
- subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
135
- }
136
- }
137
- else {
138
- // check if the endpoint matches either the HUB_DOMAIN or DEFAULT_DOMAIN
139
- const url = new URL(endpoint);
140
- if (url.hostname.endsWith(HUB_DOMAIN) ||
141
- url.hostname.endsWith(DEFAULT_DOMAIN)) {
142
- // For known domains (Hub or Bugsnag), always use HTTPS and standard format
143
- if (url.hostname.endsWith(HUB_DOMAIN)) {
144
- subDomainEndpoint = `https://${subdomain}.${HUB_DOMAIN}`;
145
- }
146
- else {
147
- subDomainEndpoint = `https://${subdomain}.${DEFAULT_DOMAIN}`;
148
- }
149
- }
150
- else {
151
- // For custom domains, use the endpoint exactly as provided
152
- subDomainEndpoint = endpoint;
153
- }
154
- }
155
- return subDomainEndpoint;
145
+ return subDomainEndpoint;
146
+ }
147
+ async getDashboardUrl(project) {
148
+ return `${this.appEndpoint}/${(await this.getOrganization()).slug}/${project.slug}`;
149
+ }
150
+ async getErrorUrl(project, errorId, queryString) {
151
+ const dashboardUrl = await this.getDashboardUrl(project);
152
+ return `${dashboardUrl}/errors/${errorId}${queryString ? `?${queryString}` : ""}`;
153
+ }
154
+ async getOrganization() {
155
+ let org = this.cache?.get(cacheKeys.ORG);
156
+ if (!org) {
157
+ const response = await this.currentUserApi.listUserOrganizations();
158
+ const orgs = response.body;
159
+ if (!orgs || orgs.length === 0) {
160
+ throw new Error("No organizations found for the current user.");
161
+ }
162
+ org = orgs[0];
163
+ this.cache?.set(cacheKeys.ORG, org);
156
164
  }
157
- async getDashboardUrl(project) {
158
- return `${this.appEndpoint}/${(await this.getOrganization()).slug}/${project.slug}`;
165
+ return org;
166
+ }
167
+ // This method retrieves all projects for the organization stored in the cache.
168
+ // If no projects are found in the cache, it fetches them from the API and
169
+ // stores them in the cache for future use.
170
+ // It throws an error if no organizations are found in the cache.
171
+ async getProjects() {
172
+ let projects = this.cache?.get(cacheKeys.PROJECTS);
173
+ if (!projects) {
174
+ const org = await this.getOrganization();
175
+ const response = await this.currentUserApi.getOrganizationProjects(
176
+ org.id
177
+ );
178
+ projects = response.body;
179
+ this.cache?.set(cacheKeys.PROJECTS, projects);
159
180
  }
160
- async getErrorUrl(project, errorId, queryString) {
161
- const dashboardUrl = await this.getDashboardUrl(project);
162
- return `${dashboardUrl}/errors/${errorId}${queryString ? `?${queryString}` : ""}`;
181
+ return projects;
182
+ }
183
+ async getProject(projectId) {
184
+ const projects = await this.getProjects();
185
+ return projects.find((p) => p.id === projectId) || null;
186
+ }
187
+ async getCurrentProject() {
188
+ let project = this.cache?.get(cacheKeys.CURRENT_PROJECT) ?? null;
189
+ if (!project && this.projectApiKey) {
190
+ const projects = await this.getProjects();
191
+ project = projects.find((p) => p.api_key === this.projectApiKey) ?? null;
192
+ this.cache?.set(cacheKeys.CURRENT_PROJECT, project);
163
193
  }
164
- async getOrganization() {
165
- let org = this.cache?.get(cacheKeys.ORG);
166
- if (!org) {
167
- const response = await this.currentUserApi.listUserOrganizations();
168
- const orgs = response.body;
169
- if (!orgs || orgs.length === 0) {
170
- throw new Error("No organizations found for the current user.");
171
- }
172
- org = orgs[0];
173
- this.cache?.set(cacheKeys.ORG, org);
174
- }
175
- return org;
194
+ return project;
195
+ }
196
+ async getProjectEventFields(project) {
197
+ const projectFiltersCache = this.cache?.get(
198
+ cacheKeys.PROJECT_EVENT_FIELDS
199
+ ) || {};
200
+ if (!projectFiltersCache[project.id]) {
201
+ let filtersResponse = (await this.projectApi.listProjectEventFields(project.id)).body;
202
+ filtersResponse = filtersResponse.filter(
203
+ (field) => field.display_id && !EXCLUDED_EVENT_FIELDS.has(field.display_id)
204
+ );
205
+ projectFiltersCache[project.id] = filtersResponse;
206
+ this.cache?.set(cacheKeys.PROJECT_EVENT_FIELDS, projectFiltersCache);
207
+ }
208
+ return projectFiltersCache[project.id];
209
+ }
210
+ async getProjectTraceFields(project) {
211
+ const projectFiltersCache = this.cache?.get(
212
+ cacheKeys.PROJECT_TRACE_FIELDS
213
+ ) || {};
214
+ if (!projectFiltersCache[project.id]) {
215
+ const filtersResponse = (await this.projectApi.listProjectTraceFields(project.id)).body;
216
+ projectFiltersCache[project.id] = filtersResponse;
217
+ this.cache?.set(cacheKeys.PROJECT_TRACE_FIELDS, projectFiltersCache);
176
218
  }
177
- // This method retrieves all projects for the organization stored in the cache.
178
- // If no projects are found in the cache, it fetches them from the API and
179
- // stores them in the cache for future use.
180
- // It throws an error if no organizations are found in the cache.
181
- async getProjects() {
182
- let projects = this.cache?.get(cacheKeys.PROJECTS);
183
- if (!projects) {
184
- const org = await this.getOrganization();
185
- const response = await this.currentUserApi.getOrganizationProjects(org.id);
186
- projects = response.body;
187
- this.cache?.set(cacheKeys.PROJECTS, projects);
219
+ return projectFiltersCache[project.id];
220
+ }
221
+ async getEvent(eventId, projectId) {
222
+ const projectIds = projectId ? [projectId] : (await this.getProjects()).map((p) => p.id);
223
+ const projectEvents = await Promise.all(
224
+ projectIds.map(
225
+ (projectId2) => this.errorsApi.viewEventById(projectId2, eventId).catch((_e) => null)
226
+ )
227
+ );
228
+ return projectEvents.find((event) => event && !!event.body)?.body || null;
229
+ }
230
+ async validateEventFields(project, fields) {
231
+ if (fields) {
232
+ const eventFields = await this.getProjectEventFields(project);
233
+ const validKeys = new Set(eventFields.map((f) => f.display_id));
234
+ for (const key of Object.keys(fields)) {
235
+ if (!validKeys.has(key)) {
236
+ throw new ToolError(`Invalid filter key: ${key}`);
188
237
  }
189
- return projects;
238
+ }
190
239
  }
191
- async getProject(projectId) {
192
- const projects = await this.getProjects();
193
- return projects.find((p) => p.id === projectId) || null;
240
+ }
241
+ async getInputProject(projectId) {
242
+ if (typeof projectId === "string") {
243
+ const maybeProject = await this.getProject(projectId);
244
+ if (!maybeProject) {
245
+ throw new ToolError(`Project with ID ${projectId} not found.`);
246
+ }
247
+ if (!this.configuredProjectApiKey) {
248
+ this.cache?.set(cacheKeys.CURRENT_PROJECT, maybeProject);
249
+ }
250
+ return maybeProject;
251
+ } else {
252
+ const currentProject = await this.getCurrentProject();
253
+ if (!currentProject) {
254
+ throw new ToolError(
255
+ "No current project found. Please provide a projectId or configure a project API key."
256
+ );
257
+ }
258
+ return currentProject;
194
259
  }
195
- async getCurrentProject() {
196
- let project = this.cache?.get(cacheKeys.CURRENT_PROJECT) ?? null;
197
- if (!project && this.projectApiKey) {
198
- const projects = await this.getProjects();
199
- project =
200
- projects.find((p) => p.apiKey === this.projectApiKey) ?? null;
201
- this.cache?.set(cacheKeys.CURRENT_PROJECT, project);
260
+ }
261
+ addStabilityData(source, project) {
262
+ const accumulativeDailyUsersSeen = source.accumulative_daily_users_seen || 0;
263
+ const accumulativeDailyUsersWithUnhandled = source.accumulative_daily_users_with_unhandled || 0;
264
+ const userStability = accumulativeDailyUsersSeen === 0 ? 0 : (accumulativeDailyUsersSeen - accumulativeDailyUsersWithUnhandled) / accumulativeDailyUsersSeen;
265
+ const totalSessionsCount = source.total_sessions_count || 0;
266
+ const unhandledSessionsCount = source.unhandled_sessions_count || 0;
267
+ const sessionStability = totalSessionsCount === 0 ? 0 : (totalSessionsCount - unhandledSessionsCount) / totalSessionsCount;
268
+ const stabilityMetric = project.stability_target_type === "user" ? userStability : sessionStability;
269
+ const targetStability = project.target_stability?.value || 0;
270
+ const criticalStability = project.critical_stability?.value || 0;
271
+ const meetsTargetStability = stabilityMetric >= targetStability;
272
+ const meetsCriticalStability = stabilityMetric >= criticalStability;
273
+ return {
274
+ ...source,
275
+ user_stability: userStability,
276
+ session_stability: sessionStability,
277
+ stability_target_type: project.stability_target_type || "user",
278
+ target_stability: targetStability,
279
+ critical_stability: criticalStability,
280
+ meets_target_stability: meetsTargetStability,
281
+ meets_critical_stability: meetsCriticalStability
282
+ };
283
+ }
284
+ async registerTools(register, getInput) {
285
+ register(
286
+ {
287
+ title: "Get Current Project",
288
+ summary: "Retrieve the 'current' project on which tools should operate by default. This allows BugSnag tools to be called with no projectId parameter.",
289
+ purpose: "Gets information about the 'current' BugSnag project, including ID and API key",
290
+ useCases: ["Understand if a current project has been set"],
291
+ inputSchema: toolInputParameters.empty,
292
+ hints: [
293
+ "If a project is returned, it can be assumed that the user expects interactions with BugSnag tools to refer to this project",
294
+ "If this tool returns no current project then other BugSnag tools will require an explicit project ID parameter",
295
+ "Call the List Projects tool to see all projects that the user has access to. Get the project ID from this list either by asking the user for the project name or slug",
296
+ "You might find a BugSnag API key in the user's code where they configure the BugSnag SDK that can be matched to a project 'apiKey' field from the project list"
297
+ ]
298
+ },
299
+ async (_args, _extra) => {
300
+ const project = await this.getCurrentProject();
301
+ if (!project) {
302
+ throw new ToolError(
303
+ "No current project is configured in the MCP server - use List Projects to see the available projects and use the project ID as a parameter to other BugSnag tools. You can ask the user to select the project based on the name or slug, or use the apiKey field and see if there's a BugSnag API key set in the user's code when they configure the BugSnag SDK"
304
+ );
202
305
  }
203
- return project;
204
- }
205
- async getProjectEventFields(project) {
206
- const projectFiltersCache = this.cache?.get(cacheKeys.PROJECT_EVENT_FIELDS) || {};
207
- if (!projectFiltersCache[project.id]) {
208
- let filtersResponse = (await this.projectApi.listProjectEventFields(project.id)).body;
209
- if (!filtersResponse || filtersResponse.length === 0) {
210
- throw new ToolError(`No event fields found for project ${project.name}.`);
211
- }
212
- filtersResponse = filtersResponse.filter((field) => field.displayId && !EXCLUDED_EVENT_FIELDS.has(field.displayId));
213
- projectFiltersCache[project.id] = filtersResponse;
214
- this.cache?.set(cacheKeys.PROJECT_EVENT_FIELDS, projectFiltersCache);
306
+ return {
307
+ content: [{ type: "text", text: JSON.stringify(project) }]
308
+ };
309
+ }
310
+ );
311
+ const listProjectsInputSchema = z.object({
312
+ apiKey: z.string().optional().describe("The API key of the BugSnag project, if known.")
313
+ });
314
+ register(
315
+ {
316
+ title: "List Projects",
317
+ summary: "List all projects in the organization that the current user has access to, or find a project matching an API key.",
318
+ purpose: "Retrieve available projects for browsing and selecting which project to analyze.",
319
+ useCases: [
320
+ "Get an overview of all projects in the organization",
321
+ "Locate a project by its API key if known from the user's code"
322
+ ],
323
+ inputSchema: listProjectsInputSchema,
324
+ hints: [
325
+ "Project IDs from this list can be used with other tools when no project API key is configured"
326
+ ]
327
+ },
328
+ async (args, _extra) => {
329
+ const params = listProjectsInputSchema.parse(args);
330
+ let projects = await this.getProjects();
331
+ if (!projects || projects.length === 0) {
332
+ throw new ToolError(
333
+ "No BugSnag projects found for the current user."
334
+ );
215
335
  }
216
- return projectFiltersCache[project.id];
217
- }
218
- async getProjectTraceFields(project) {
219
- const projectFiltersCache = this.cache?.get(cacheKeys.PROJECT_TRACE_FIELDS) || {};
220
- if (!projectFiltersCache[project.id]) {
221
- const filtersResponse = (await this.projectApi.listProjectTraceFields(project.id)).body;
222
- if (!filtersResponse || filtersResponse.length === 0) {
223
- throw new ToolError(`No trace fields found for project ${project.name}.`);
224
- }
225
- projectFiltersCache[project.id] = filtersResponse;
226
- this.cache?.set(cacheKeys.PROJECT_TRACE_FIELDS, projectFiltersCache);
336
+ if (params.apiKey) {
337
+ const matchedProject = projects.find(
338
+ (p) => p.api_key === params.apiKey
339
+ );
340
+ projects = matchedProject ? [matchedProject] : [];
227
341
  }
228
- return projectFiltersCache[project.id];
229
- }
230
- async getEvent(eventId, projectId) {
231
- const projectIds = projectId
232
- ? [projectId]
233
- : (await this.getProjects()).map((p) => p.id);
234
- const projectEvents = await Promise.all(projectIds.map((projectId) => this.errorsApi.viewEventById(projectId, eventId).catch((_e) => null)));
235
- return projectEvents.find((event) => event && !!event.body)?.body || null;
236
- }
237
- async validateEventFields(project, fields) {
238
- if (fields) {
239
- const eventFields = await this.getProjectEventFields(project);
240
- const validKeys = new Set(eventFields.map((f) => f.displayId));
241
- for (const key of Object.keys(fields)) {
242
- if (!validKeys.has(key)) {
243
- throw new ToolError(`Invalid filter key: ${key}`);
244
- }
245
- }
342
+ const content = {
343
+ data: projects,
344
+ count: projects.length
345
+ };
346
+ return {
347
+ content: [{ type: "text", text: JSON.stringify(content) }]
348
+ };
349
+ }
350
+ );
351
+ const getErrorInputSchema = z.object({
352
+ projectId: toolInputParameters.projectId,
353
+ errorId: toolInputParameters.errorId.describe(
354
+ "Unique identifier of the error to retrieve"
355
+ ),
356
+ filters: toolInputParameters.filters.describe(
357
+ "Apply filters to narrow down the error list. Use the List Project Event Filters tool to discover available filter fields. Time filters support extended ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)."
358
+ )
359
+ });
360
+ register(
361
+ {
362
+ title: "Get Error",
363
+ 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.",
364
+ purpose: "Retrieve all the information required on a specified error to understand who it is affecting and why.",
365
+ useCases: [
366
+ "Investigate a specific error found through the List Project Errors tool",
367
+ "Understand which types of user are affected by the error using summarized event data",
368
+ "Get error details for debugging and root cause analysis",
369
+ "Retrieve error metadata for incident reports and documentation"
370
+ ],
371
+ inputSchema: getErrorInputSchema,
372
+ outputDescription: "JSON object containing: - error_details: Aggregated data about the error, including first and last seen occurrence - latest_event: Detailed information about the most recent occurrence of the error, including stacktrace, breadcrumbs, user and context - pivots: List of pivots (summaries) for the error, which can be used to analyze patterns in occurrences - url: A link to the error in the dashboard - this should be shown to the user for them to perform further analysis",
373
+ examples: [
374
+ {
375
+ description: "Get details for a specific error",
376
+ parameters: {
377
+ errorId: "6863e2af8c857c0a5023b411"
378
+ },
379
+ expectedOutput: "JSON object with error details including message, stack trace, occurrence count, and metadata"
380
+ }
381
+ ],
382
+ hints: [
383
+ "Error IDs can be found using the List Project Errors tool",
384
+ "Use this after filtering errors to get detailed information about specific errors",
385
+ "Use Get Event Details tool if you need detailed information about a specific event (occurrence) rather than the aggregated error",
386
+ "If you used a filter to get this error, you can pass the same filters here to restrict the results or apply further filters",
387
+ "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"
388
+ ]
389
+ },
390
+ async (args, _extra) => {
391
+ const params = getErrorInputSchema.parse(args);
392
+ const project = await this.getInputProject(params.projectId);
393
+ const errorDetails = (await this.errorsApi.viewErrorOnProject(project.id, params.errorId)).body;
394
+ if (!errorDetails) {
395
+ throw new ToolError(
396
+ `Error with ID ${params.errorId} not found in project ${project.id}.`
397
+ );
246
398
  }
247
- }
248
- async getInputProject(projectId) {
249
- if (typeof projectId === "string") {
250
- const maybeProject = await this.getProject(projectId);
251
- if (!maybeProject) {
252
- throw new ToolError(`Project with ID ${projectId} not found.`);
253
- }
254
- // If this hasn't been configured at startup, set this to the current project for future tool calls
255
- if (!this.configuredProjectApiKey) {
256
- this.cache?.set(cacheKeys.CURRENT_PROJECT, maybeProject);
257
- }
258
- return maybeProject;
399
+ const filters = {
400
+ error: [{ type: "eq", value: params.errorId }],
401
+ ...params.filters
402
+ };
403
+ await this.validateEventFields(project, filters);
404
+ let latestEvent = null;
405
+ try {
406
+ const latestEvents = (await this.errorsApi.listEventsOnProject(
407
+ project.id,
408
+ null,
409
+ "timestamp",
410
+ "desc",
411
+ 1,
412
+ filters,
413
+ true
414
+ )).body;
415
+ if (latestEvents && latestEvents.length > 0) {
416
+ latestEvent = latestEvents[0];
417
+ latestEvent.threads = void 0;
418
+ }
419
+ } catch (e) {
420
+ console.warn("Failed to fetch latest event:", e);
259
421
  }
260
- else {
261
- const currentProject = await this.getCurrentProject();
262
- if (!currentProject) {
263
- throw new ToolError("No current project found. Please provide a projectId or configure a project API key.");
264
- }
265
- return currentProject;
422
+ const content = {
423
+ error_details: errorDetails,
424
+ latest_event: latestEvent,
425
+ pivots: (await this.errorsApi.getPivotValuesOnAnError(
426
+ project.id,
427
+ params.errorId,
428
+ filters,
429
+ 5
430
+ )).body || [],
431
+ url: await this.getErrorUrl(
432
+ project,
433
+ params.errorId,
434
+ toUrlSearchParams(filters).toString()
435
+ )
436
+ };
437
+ return {
438
+ content: [{ type: "text", text: JSON.stringify(content) }]
439
+ };
440
+ }
441
+ );
442
+ const getEventInputSchema = z.object({
443
+ projectId: toolInputParameters.projectId,
444
+ eventId: toolInputParameters.eventId
445
+ });
446
+ register(
447
+ {
448
+ title: "Get Event",
449
+ summary: "Get detailed information about a specific event",
450
+ purpose: "Retrieve event details directly from its ID",
451
+ useCases: [
452
+ "Get the full details of an event, including any thread stack traces"
453
+ ],
454
+ inputSchema: getEventInputSchema,
455
+ examples: [
456
+ {
457
+ description: "Get event details of an event",
458
+ parameters: {
459
+ eventId: "6863e2af012caf1d5c320000"
460
+ },
461
+ expectedOutput: "JSON object with complete event details including stack trace (error trace and other threads, if present), metadata, and context"
462
+ }
463
+ ]
464
+ },
465
+ async (args, _extra) => {
466
+ const params = getEventInputSchema.parse(args);
467
+ const project = await this.getInputProject(params.projectId);
468
+ const response = await this.getEvent(params.eventId, project.id);
469
+ return {
470
+ content: [{ type: "text", text: JSON.stringify(response) }]
471
+ };
472
+ }
473
+ );
474
+ const getEventDetailsFromDashboardUrlInputSchema = z.object({
475
+ link: z.string().describe(
476
+ "Full URL to the event details page in the BugSnag dashboard (web interface), containing project slug and event_id parameter."
477
+ )
478
+ });
479
+ register(
480
+ {
481
+ title: "Get Event Details From Dashboard URL",
482
+ summary: "Get detailed information about a specific event using its dashboard URL",
483
+ purpose: "Retrieve event details directly from a dashboard URL for quick debugging",
484
+ useCases: [
485
+ "Get event details when given a dashboard URL from a user or notification",
486
+ "Extract event information from shared links or browser URLs",
487
+ "Quick lookup of event details without needing separate project and event IDs"
488
+ ],
489
+ inputSchema: getEventDetailsFromDashboardUrlInputSchema,
490
+ examples: [
491
+ {
492
+ description: "Get event details from a dashboard URL",
493
+ parameters: {
494
+ link: "https://app.bugsnag.com/my-org/my-project/errors/6863e2af8c857c0a5023b411?event_id=6863e2af012caf1d5c320000"
495
+ },
496
+ expectedOutput: "JSON object with complete event details including stack trace, metadata, and context"
497
+ }
498
+ ],
499
+ hints: [
500
+ "The URL must contain both project slug in the path and event_id in query parameters",
501
+ "This is useful when users share BugSnag dashboard URLs and you need to extract the event data"
502
+ ]
503
+ },
504
+ async (args, _extra) => {
505
+ const params = getEventDetailsFromDashboardUrlInputSchema.parse(args);
506
+ const url = new URL(params.link);
507
+ const eventId = url.searchParams.get("event_id");
508
+ const projectSlug = url.pathname.split("/")[2];
509
+ if (!projectSlug || !eventId)
510
+ throw new ToolError(
511
+ "Both projectSlug and eventId must be present in the link"
512
+ );
513
+ const projects = await this.getProjects();
514
+ const projectId = projects.find((p) => p.slug === projectSlug)?.id;
515
+ if (!projectId) {
516
+ throw new ToolError("Project with the specified slug not found.");
266
517
  }
267
- }
268
- addStabilityData(source, project) {
269
- const accumulativeDailyUsersSeen = source.accumulativeDailyUsersSeen || 0;
270
- const accumulativeDailyUsersWithUnhandled = source.accumulativeDailyUsersWithUnhandled || 0;
271
- const userStability = accumulativeDailyUsersSeen === 0 // avoid division by zero
272
- ? 0
273
- : (accumulativeDailyUsersSeen - accumulativeDailyUsersWithUnhandled) /
274
- accumulativeDailyUsersSeen;
275
- const totalSessionsCount = source.totalSessionsCount || 0;
276
- const unhandledSessionsCount = source.unhandledSessionsCount || 0;
277
- const sessionStability = totalSessionsCount === 0 // avoid division by zero
278
- ? 0
279
- : (totalSessionsCount - unhandledSessionsCount) / totalSessionsCount;
280
- const stabilityMetric = project.stabilityTargetType === "user" ? userStability : sessionStability;
281
- const targetStability = project.targetStability?.value || 0;
282
- const criticalStability = project.criticalStability?.value || 0;
283
- const meetsTargetStability = stabilityMetric >= targetStability;
284
- const meetsCriticalStability = stabilityMetric >= criticalStability;
518
+ const response = await this.getEvent(eventId, projectId);
285
519
  return {
286
- ...source,
287
- userStability,
288
- sessionStability,
289
- stabilityTargetType: project.stabilityTargetType || "user",
290
- targetStability,
291
- criticalStability,
292
- meetsTargetStability,
293
- meetsCriticalStability,
520
+ content: [{ type: "text", text: JSON.stringify(response) }]
294
521
  };
295
- }
296
- registerTools(register, getInput) {
297
- register({
298
- title: "Get Current Project",
299
- summary: "Retrieve the 'current' project on which tools should operate by default. This allows BugSnag tools to be called with no projectId parameter.",
300
- purpose: "Gets information about the 'current' BugSnag project, including ID and API key",
301
- useCases: ["Understand if a current project has been set"],
302
- inputSchema: toolInputParameters.empty,
303
- hints: [
304
- "If a project is returned, it can be assumed that the user expects interactions with BugSnag tools to refer to this project",
305
- "If this tool returns no current project then other BugSnag tools will require an explicit project ID parameter",
306
- "Call the List Projects tool to see all projects that the user has access to. Get the project ID from this list either by asking the user for the project name or slug",
307
- "You might find a BugSnag API key in the user's code where they configure the BugSnag SDK that can be matched to a project 'apiKey' field from the project list",
308
- ],
309
- }, async (_args, _extra) => {
310
- const project = await this.getCurrentProject();
311
- if (!project) {
312
- throw new ToolError("No current project is configured in the MCP server - use List Projects to see the available projects and use the project ID as a parameter to other BugSnag tools. You can ask the user to select the project based on the name or slug, or use the apiKey field and see if there's a BugSnag API key set in the user's code when they configure the BugSnag SDK");
313
- }
314
- return {
315
- content: [{ type: "text", text: JSON.stringify(project) }],
316
- };
317
- });
318
- const listProjectsInputSchema = z.object({
319
- apiKey: z
320
- .string()
321
- .optional()
322
- .describe("The API key of the BugSnag project, if known."),
323
- });
324
- register({
325
- title: "List Projects",
326
- summary: "List all projects in the organization that the current user has access to, or find a project matching an API key.",
327
- purpose: "Retrieve available projects for browsing and selecting which project to analyze.",
328
- useCases: [
329
- "Get an overview of all projects in the organization",
330
- "Locate a project by its API key if known from the user's code",
331
- ],
332
- inputSchema: listProjectsInputSchema,
333
- hints: [
334
- "Project IDs from this list can be used with other tools when no project API key is configured",
335
- ],
336
- }, async (args, _extra) => {
337
- const params = listProjectsInputSchema.parse(args);
338
- let projects = await this.getProjects();
339
- if (!projects || projects.length === 0) {
340
- throw new ToolError("No BugSnag projects found for the current user.");
341
- }
342
- if (params.apiKey) {
343
- const matchedProject = projects.find((p) => p.apiKey === params.apiKey);
344
- projects = matchedProject ? [matchedProject] : [];
345
- }
346
- const content = {
347
- data: projects,
348
- count: projects.length,
349
- };
350
- return {
351
- content: [{ type: "text", text: JSON.stringify(content) }],
352
- };
353
- });
354
- const getErrorInputSchema = z.object({
355
- projectId: toolInputParameters.projectId,
356
- errorId: toolInputParameters.errorId.describe("Unique identifier of the error to retrieve"),
357
- filters: toolInputParameters.filters.describe("Apply filters to narrow down the error list. Use the List Project Event Filters tool to discover available filter fields. " +
358
- "Time filters support extended ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)."),
359
- });
360
- register({
361
- title: "Get Error",
362
- 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.",
363
- purpose: "Retrieve all the information required on a specified error to understand who it is affecting and why.",
364
- useCases: [
365
- "Investigate a specific error found through the List Project Errors tool",
366
- "Understand which types of user are affected by the error using summarized event data",
367
- "Get error details for debugging and root cause analysis",
368
- "Retrieve error metadata for incident reports and documentation",
369
- ],
370
- inputSchema: getErrorInputSchema,
371
- outputDescription: "JSON object containing: " +
372
- " - error_details: Aggregated data about the error, including first and last seen occurrence" +
373
- " - latest_event: Detailed information about the most recent occurrence of the error, including stacktrace, breadcrumbs, user and context" +
374
- " - pivots: List of pivots (summaries) for the error, which can be used to analyze patterns in occurrences" +
375
- " - url: A link to the error in the dashboard - this should be shown to the user for them to perform further analysis",
376
- examples: [
377
- {
378
- description: "Get details for a specific error",
379
- parameters: {
380
- errorId: "6863e2af8c857c0a5023b411",
381
- },
382
- expectedOutput: "JSON object with error details including message, stack trace, occurrence count, and metadata",
383
- },
384
- ],
385
- hints: [
386
- "Error IDs can be found using the List Project Errors tool",
387
- "Use this after filtering errors to get detailed information about specific errors",
388
- "Use Get Event Details tool if you need detailed information about a specific event (occurrence) rather than the aggregated error",
389
- "If you used a filter to get this error, you can pass the same filters here to restrict the results or apply further filters",
390
- "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",
391
- ],
392
- }, async (args, _extra) => {
393
- const params = getErrorInputSchema.parse(args);
394
- const project = await this.getInputProject(params.projectId);
395
- const errorDetails = (await this.errorsApi.viewErrorOnProject(project.id, params.errorId)).body;
396
- if (!errorDetails) {
397
- throw new ToolError(`Error with ID ${params.errorId} not found in project ${project.id}.`);
398
- }
399
- const filters = {
400
- error: [{ type: "eq", value: params.errorId }],
401
- ...args.filters,
402
- };
403
- await this.validateEventFields(project, filters);
404
- // Get the latest event for this error using the events endpoint with filters
405
- let latestEvent = null;
406
- try {
407
- const latestEvents = (await this.errorsApi.listEventsOnProject(project.id, null, "timestamp", "desc", 1, filters, true)).body;
408
- if (latestEvents && latestEvents.length > 0) {
409
- latestEvent = latestEvents[0];
410
- latestEvent.threads = undefined; // Remove threads to reduce payload size
522
+ }
523
+ );
524
+ const listProjectErrorsInputSchema = z.object({
525
+ projectId: toolInputParameters.projectId,
526
+ filters: toolInputParameters.filters.describe(
527
+ "Apply filters to narrow down the error list. Use the List Project Event Filters tool to discover available filter fields. Time filters support extended ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)."
528
+ ),
529
+ sort: toolInputParameters.sort,
530
+ direction: toolInputParameters.direction,
531
+ perPage: toolInputParameters.perPage,
532
+ nextUrl: toolInputParameters.nextUrl
533
+ });
534
+ register(
535
+ {
536
+ title: "List Project Errors",
537
+ summary: "List and search errors in a project using customizable filters and pagination",
538
+ purpose: "Retrieve filtered list of errors from a project for analysis, debugging, and reporting",
539
+ useCases: [
540
+ "Debug recent application errors by filtering for open errors in the last 7 days",
541
+ "Generate error reports for stakeholders by filtering specific error types or severity levels",
542
+ "Monitor error trends over time using date range filters",
543
+ "Find errors affecting specific users or environments using metadata filters"
544
+ ],
545
+ inputSchema: listProjectErrorsInputSchema,
546
+ examples: [
547
+ {
548
+ description: "Find errors affecting a specific user in the last 24 hours",
549
+ parameters: {
550
+ filters: {
551
+ "user.email": [{ type: "eq", value: "user@example.com" }],
552
+ "event.since": [{ type: "eq", value: "24h" }]
553
+ }
554
+ },
555
+ expectedOutput: "JSON object with a list of errors in the 'data' field, a count of the current page of results in the 'count' field, and a total count of all results in the 'total' field"
556
+ },
557
+ {
558
+ description: "Get the 10 open errors with the most users affected in the last 30 days",
559
+ parameters: {
560
+ filters: {
561
+ "event.since": [{ type: "eq", value: "30d" }],
562
+ "error.status": [{ type: "eq", value: "open" }]
563
+ },
564
+ sort: "users",
565
+ direction: "desc",
566
+ perPage: 10
567
+ },
568
+ expectedOutput: "JSON object with a list of errors in the 'data' field, a count of the current page of results in the 'count' field, and a total count of all results in the 'total' field"
569
+ },
570
+ {
571
+ description: "Get the next 50 results",
572
+ parameters: {
573
+ nextUrl: "https://api.bugsnag.com/projects/515fb9337c1074f6fd000003/errors?base=2025-08-29T13%3A11%3A37Z&direction=desc&filters%5Berror.status%5D%5B%5D%5Btype%5D=eq&filters%5Berror.status%5D%5B%5D%5Bvalue%5D=open&offset=10&per_page=10&sort=users",
574
+ perPage: 50
575
+ },
576
+ expectedOutput: "JSON object with a list of errors, with a URL to the next page if more results are available and a total count of all errors matched"
577
+ }
578
+ ],
579
+ hints: [
580
+ "Use List Project Event Filters tool first to discover valid filter field names for your project",
581
+ "Combine multiple filters to narrow results - filters are applied with AND logic",
582
+ "For time filters: use relative format (7d, 24h) for recent periods or ISO 8601 UTC format (2018-05-20T00:00:00Z) for specific dates",
583
+ "Common time filters: event.since (from this time), event.before (until this time)",
584
+ "The 'event.since' filter and 'error.status' filters are always applied and if not specified are set to '30d' and 'open' respectively",
585
+ "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",
586
+ "This tool returns paged results. The 'page_error_count' field indicates the number of results returned in the current page, and the 'total_error_count' field indicates the total number of results across all pages.",
587
+ "If the output contains a 'next_url' value, there are more results available - call this tool again supplying the next URL as a parameter to retrieve the next page.",
588
+ "Do not modify the next URL as this can cause incorrect results. The only other parameter that can be used with 'next' is 'per_page' to control the page size."
589
+ ]
590
+ },
591
+ async (args, _extra) => {
592
+ const params = listProjectErrorsInputSchema.parse(args);
593
+ const project = await this.getInputProject(params.projectId);
594
+ const filters = {
595
+ "event.since": [{ type: "eq", value: "30d" }],
596
+ "error.status": [{ type: "eq", value: "open" }],
597
+ ...params.filters
598
+ };
599
+ await this.validateEventFields(project, filters);
600
+ const response = await this.errorsApi.listProjectErrors(
601
+ project.id,
602
+ null,
603
+ params.sort,
604
+ params.direction,
605
+ params.perPage,
606
+ filters,
607
+ params.nextUrl
608
+ );
609
+ const result = {
610
+ data: response.body,
611
+ next_url: response.nextUrl ?? void 0,
612
+ data_count: response.body?.length,
613
+ total_count: response.totalCount ?? void 0
614
+ };
615
+ return {
616
+ content: [{ type: "text", text: JSON.stringify(result) }]
617
+ };
618
+ }
619
+ );
620
+ const listProjectEventFiltersInputSchema = z.object({
621
+ projectId: toolInputParameters.projectId
622
+ });
623
+ register(
624
+ {
625
+ title: "List Project Event Filters",
626
+ summary: "Get available event filter fields for a project",
627
+ purpose: "Discover valid filter field names and options that can be used with the List Errors or Get Error tools",
628
+ useCases: [
629
+ "Discover what filter fields are available before searching for errors",
630
+ "Find the correct field names for filtering by user, environment, or custom metadata",
631
+ "Understand filter options and data types for building complex queries"
632
+ ],
633
+ inputSchema: listProjectEventFiltersInputSchema,
634
+ examples: [
635
+ {
636
+ description: "Get all available filter fields",
637
+ parameters: {},
638
+ expectedOutput: "JSON array of EventField objects containing display_id, custom flag, and filter/pivot options"
639
+ }
640
+ ],
641
+ hints: [
642
+ "Use this tool before the List Errors or Get Error tools to understand available filters",
643
+ "Look for display_id field in the response - these are the field names to use in filters"
644
+ ]
645
+ },
646
+ async (args, _extra) => {
647
+ const params = listProjectEventFiltersInputSchema.parse(args);
648
+ const eventFilters = await this.getProjectEventFields(
649
+ await this.getInputProject(params.projectId)
650
+ );
651
+ return {
652
+ content: [{ type: "text", text: JSON.stringify(eventFilters) }]
653
+ };
654
+ }
655
+ );
656
+ const updateErrorInputSchema = z.object({
657
+ projectId: toolInputParameters.projectId,
658
+ errorId: toolInputParameters.errorId,
659
+ operation: z.enum(PERMITTED_UPDATE_OPERATIONS).describe("The operation to apply to the error")
660
+ });
661
+ register(
662
+ {
663
+ title: "Update Error",
664
+ summary: "Update the status of an error",
665
+ purpose: "Change an error's workflow state, such as marking it as resolved or ignored",
666
+ useCases: [
667
+ "Mark an error as open, fixed or ignored",
668
+ "Discard or un-discard an error",
669
+ "Update the severity of an error"
670
+ ],
671
+ inputSchema: updateErrorInputSchema,
672
+ examples: [
673
+ {
674
+ description: "Mark an error as fixed",
675
+ parameters: {
676
+ errorId: "6863e2af8c857c0a5023b411",
677
+ operation: "fix"
678
+ },
679
+ expectedOutput: "Success response indicating the error was marked as fixed"
680
+ }
681
+ ],
682
+ hints: [
683
+ "Only use valid operations - BugSnag may reject invalid values"
684
+ ],
685
+ readOnly: false,
686
+ idempotent: false
687
+ },
688
+ async (args, _extra) => {
689
+ const params = updateErrorInputSchema.parse(args);
690
+ const project = await this.getInputProject(params.projectId);
691
+ let severity;
692
+ if (params.operation === "override_severity") {
693
+ const result2 = await getInput({
694
+ message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
695
+ requestedSchema: {
696
+ type: "object",
697
+ properties: {
698
+ severity: {
699
+ type: "string",
700
+ enum: ["info", "warning", "error"],
701
+ description: "The new severity level for the error"
411
702
  }
703
+ },
704
+ required: ["severity"]
412
705
  }
413
- catch (e) {
414
- console.warn("Failed to fetch latest event:", e);
415
- // Continue without latest event rather than failing the entire request
706
+ });
707
+ if (result2.action === "accept" && result2.content?.severity) {
708
+ severity = result2.content.severity;
709
+ }
710
+ }
711
+ const result = await this.errorsApi.updateErrorOnProject(
712
+ project.id,
713
+ params.errorId,
714
+ {
715
+ operation: Object.values(ErrorUpdateRequest.OperationEnum).find(
716
+ (value) => value === params.operation
717
+ ),
718
+ severity
719
+ }
720
+ );
721
+ return {
722
+ content: [
723
+ {
724
+ type: "text",
725
+ text: JSON.stringify({
726
+ success: result.status === 200 || result.status === 204
727
+ })
416
728
  }
417
- const content = {
418
- error_details: errorDetails,
419
- latest_event: latestEvent,
420
- pivots: (await this.errorsApi.getPivotValuesOnAnError(project.id, args.errorId, filters, 5)).body || [],
421
- url: await this.getErrorUrl(project, args.errorId, toUrlSearchParams(filters).toString()),
422
- };
423
- return {
424
- content: [{ type: "text", text: JSON.stringify(content) }],
425
- };
426
- });
427
- const getEventInputSchema = z.object({
428
- projectId: toolInputParameters.projectId,
429
- eventId: toolInputParameters.eventId,
430
- });
431
- register({
432
- title: "Get Event",
433
- summary: "Get detailed information about a specific event",
434
- purpose: "Retrieve event details directly from its ID",
435
- useCases: [
436
- "Get the full details of an event, including any thread stack traces",
437
- ],
438
- inputSchema: getEventInputSchema,
439
- examples: [
440
- {
441
- description: "Get event details of an event",
442
- parameters: {
443
- eventId: "6863e2af012caf1d5c320000",
444
- },
445
- expectedOutput: "JSON object with complete event details including stack trace (error trace and other threads, if present), metadata, and context",
446
- },
447
- ],
448
- }, async (args, _extra) => {
449
- const params = getEventInputSchema.parse(args);
450
- const project = await this.getInputProject(params.projectId);
451
- const response = await this.getEvent(params.eventId, project.id);
452
- return {
453
- content: [{ type: "text", text: JSON.stringify(response) }],
454
- };
455
- });
456
- const getEventDetailsFromDashboardUrlInputSchema = z.object({
457
- link: z
458
- .string()
459
- .describe("Full URL to the event details page in the BugSnag dashboard (web interface), containing project slug and event_id parameter."),
460
- });
461
- register({
462
- title: "Get Event Details From Dashboard URL",
463
- summary: "Get detailed information about a specific event using its dashboard URL",
464
- purpose: "Retrieve event details directly from a dashboard URL for quick debugging",
465
- useCases: [
466
- "Get event details when given a dashboard URL from a user or notification",
467
- "Extract event information from shared links or browser URLs",
468
- "Quick lookup of event details without needing separate project and event IDs",
469
- ],
470
- inputSchema: getEventDetailsFromDashboardUrlInputSchema,
471
- examples: [
472
- {
473
- description: "Get event details from a dashboard URL",
474
- parameters: {
475
- link: "https://app.bugsnag.com/my-org/my-project/errors/6863e2af8c857c0a5023b411?event_id=6863e2af012caf1d5c320000",
476
- },
477
- expectedOutput: "JSON object with complete event details including stack trace, metadata, and context",
478
- },
479
- ],
480
- hints: [
481
- "The URL must contain both project slug in the path and event_id in query parameters",
482
- "This is useful when users share BugSnag dashboard URLs and you need to extract the event data",
483
- ],
484
- }, async (args, _extra) => {
485
- const params = getEventDetailsFromDashboardUrlInputSchema.parse(args);
486
- const url = new URL(params.link);
487
- const eventId = url.searchParams.get("event_id");
488
- const projectSlug = url.pathname.split("/")[2];
489
- if (!projectSlug || !eventId)
490
- throw new ToolError("Both projectSlug and eventId must be present in the link");
491
- // get the project id from list of projects
492
- const projects = await this.getProjects();
493
- const projectId = projects.find((p) => p.slug === projectSlug)?.id;
494
- if (!projectId) {
495
- throw new ToolError("Project with the specified slug not found.");
729
+ ]
730
+ };
731
+ }
732
+ );
733
+ const listReleasesInputSchema = z.object({
734
+ projectId: toolInputParameters.projectId,
735
+ releaseStage: toolInputParameters.releaseStage,
736
+ visibleOnly: z.boolean().describe(
737
+ "Whether to only include releases that are marked as visible in the dashboard"
738
+ ).default(false),
739
+ perPage: toolInputParameters.perPage,
740
+ nextUrl: toolInputParameters.nextUrl
741
+ });
742
+ register(
743
+ {
744
+ title: "List Releases",
745
+ summary: "List releases for a project",
746
+ purpose: "Retrieve a list of release summaries to analyze deployment history and associated errors",
747
+ useCases: [
748
+ "View recent releases to correlate with error spikes",
749
+ "Filter releases by stage (e.g. production, staging) for targeted analysis"
750
+ ],
751
+ inputSchema: listReleasesInputSchema,
752
+ examples: [
753
+ {
754
+ description: "List production releases for a project",
755
+ parameters: {},
756
+ expectedOutput: "JSON array of release objects in the production stage"
757
+ },
758
+ {
759
+ description: "List staging releases for a project",
760
+ parameters: {
761
+ releaseStage: "staging"
762
+ },
763
+ expectedOutput: "JSON array of release objects in the staging stage"
764
+ },
765
+ {
766
+ description: "Get the next page of results",
767
+ parameters: {
768
+ nextUrl: "/projects/515fb9337c1074f6fd000003/releases?offset=30&per_page=30"
769
+ },
770
+ expectedOutput: "JSON array of release objects with metadata from the next page"
771
+ }
772
+ ],
773
+ hints: [
774
+ "Use the Get Release tool to get more details on a specific release, including the builds it contains",
775
+ "The release stage defaults to 'production' if not specified",
776
+ "Use visibleOnly to filter out releases that have been marked as hidden in the dashboard"
777
+ ],
778
+ readOnly: true,
779
+ idempotent: true,
780
+ outputDescription: "JSON array of release summary objects with metadata, with a URL to the next page if more results are available"
781
+ },
782
+ async (args, _extra) => {
783
+ const params = listReleasesInputSchema.parse(args);
784
+ const project = await this.getInputProject(params.projectId);
785
+ const response = await this.projectApi.listProjectReleaseGroups(
786
+ project.id,
787
+ params.releaseStage,
788
+ false,
789
+ // Not top-only
790
+ params.visibleOnly,
791
+ params.perPage,
792
+ params.nextUrl
793
+ );
794
+ let releases = [];
795
+ if (response.body) {
796
+ releases = response.body.map(
797
+ (r) => this.addStabilityData(r, project)
798
+ );
799
+ }
800
+ return {
801
+ content: [
802
+ {
803
+ type: "text",
804
+ text: JSON.stringify({
805
+ data: releases,
806
+ next_url: response.nextUrl ?? void 0,
807
+ data_count: releases.length,
808
+ total_count: response.totalCount ?? void 0
809
+ })
496
810
  }
497
- const response = await this.getEvent(eventId, projectId);
498
- return {
499
- content: [{ type: "text", text: JSON.stringify(response) }],
500
- };
501
- });
502
- const listProjectErrorsInputSchema = z.object({
503
- projectId: toolInputParameters.projectId,
504
- filters: toolInputParameters.filters.describe("Apply filters to narrow down the error list. Use the List Project Event Filters tool to discover available filter fields. " +
505
- "Time filters support extended ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h)."),
506
- sort: toolInputParameters.sort,
507
- direction: toolInputParameters.direction,
508
- perPage: toolInputParameters.perPage,
509
- nextUrl: toolInputParameters.nextUrl,
510
- });
511
- register({
512
- title: "List Project Errors",
513
- summary: "List and search errors in a project using customizable filters and pagination",
514
- purpose: "Retrieve filtered list of errors from a project for analysis, debugging, and reporting",
515
- useCases: [
516
- "Debug recent application errors by filtering for open errors in the last 7 days",
517
- "Generate error reports for stakeholders by filtering specific error types or severity levels",
518
- "Monitor error trends over time using date range filters",
519
- "Find errors affecting specific users or environments using metadata filters",
520
- ],
521
- inputSchema: listProjectErrorsInputSchema,
522
- examples: [
523
- {
524
- description: "Find errors affecting a specific user in the last 24 hours",
525
- parameters: {
526
- filters: {
527
- "user.email": [{ type: "eq", value: "user@example.com" }],
528
- "event.since": [{ type: "eq", value: "24h" }],
529
- },
530
- },
531
- expectedOutput: "JSON object with a list of errors in the 'data' field, a count of the current page of results in the 'count' field, and a total count of all results in the 'total' field",
532
- },
533
- {
534
- description: "Get the 10 open errors with the most users affected in the last 30 days",
535
- parameters: {
536
- filters: {
537
- "event.since": [{ type: "eq", value: "30d" }],
538
- "error.status": [{ type: "eq", value: "open" }],
539
- },
540
- sort: "users",
541
- direction: "desc",
542
- perPage: 10,
543
- },
544
- expectedOutput: "JSON object with a list of errors in the 'data' field, a count of the current page of results in the 'count' field, and a total count of all results in the 'total' field",
545
- },
546
- {
547
- description: "Get the next 50 results",
548
- parameters: {
549
- nextUrl: "https://api.bugsnag.com/projects/515fb9337c1074f6fd000003/errors?base=2025-08-29T13%3A11%3A37Z&direction=desc&filters%5Berror.status%5D%5B%5D%5Btype%5D=eq&filters%5Berror.status%5D%5B%5D%5Bvalue%5D=open&offset=10&per_page=10&sort=users",
550
- perPage: 50,
551
- },
552
- expectedOutput: "JSON object with a list of errors, with a URL to the next page if more results are available and a total count of all errors matched",
553
- },
554
- ],
555
- hints: [
556
- "Use List Project Event Filters tool first to discover valid filter field names for your project",
557
- "Combine multiple filters to narrow results - filters are applied with AND logic",
558
- "For time filters: use relative format (7d, 24h) for recent periods or ISO 8601 UTC format (2018-05-20T00:00:00Z) for specific dates",
559
- "Common time filters: event.since (from this time), event.before (until this time)",
560
- "The 'event.since' filter and 'error.status' filters are always applied and if not specified are set to '30d' and 'open' respectively",
561
- "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",
562
- "This tool returns paged results. The 'page_error_count' field indicates the number of results returned in the current page, and the 'total_error_count' field indicates the total number of results across all pages.",
563
- "If the output contains a 'next_url' value, there are more results available - call this tool again supplying the next URL as a parameter to retrieve the next page.",
564
- "Do not modify the next URL as this can cause incorrect results. The only other parameter that can be used with 'next' is 'per_page' to control the page size.",
565
- ],
566
- }, async (args, _extra) => {
567
- const params = listProjectErrorsInputSchema.parse(args);
568
- const project = await this.getInputProject(params.projectId);
569
- const filters = {
570
- "event.since": [{ type: "eq", value: "30d" }],
571
- "error.status": [{ type: "eq", value: "open" }],
572
- ...params.filters,
573
- };
574
- // Validate filter keys against cached event fields
575
- await this.validateEventFields(project, filters);
576
- const response = await this.errorsApi.listProjectErrors(project.id, null, params.sort, params.direction, params.perPage, filters, params.nextUrl);
577
- const result = {
578
- data: response.body,
579
- next_url: response.nextUrl ?? undefined,
580
- data_count: response.body?.length,
581
- total_count: response.totalCount ?? undefined,
582
- };
583
- return {
584
- content: [{ type: "text", text: JSON.stringify(result) }],
585
- };
586
- });
587
- const listProjectEventFiltersInputSchema = z.object({
588
- projectId: toolInputParameters.projectId,
589
- });
590
- register({
591
- title: "List Project Event Filters",
592
- summary: "Get available event filter fields for a project",
593
- purpose: "Discover valid filter field names and options that can be used with the List Errors or Get Error tools",
594
- useCases: [
595
- "Discover what filter fields are available before searching for errors",
596
- "Find the correct field names for filtering by user, environment, or custom metadata",
597
- "Understand filter options and data types for building complex queries",
598
- ],
599
- inputSchema: listProjectEventFiltersInputSchema,
600
- examples: [
601
- {
602
- description: "Get all available filter fields",
603
- parameters: {},
604
- expectedOutput: "JSON array of EventField objects containing display_id, custom flag, and filter/pivot options",
605
- },
606
- ],
607
- hints: [
608
- "Use this tool before the List Errors or Get Error tools to understand available filters",
609
- "Look for display_id field in the response - these are the field names to use in filters",
610
- ],
611
- }, async (args, _extra) => {
612
- const params = listProjectEventFiltersInputSchema.parse(args);
613
- const eventFilters = await this.getProjectEventFields(await this.getInputProject(params.projectId));
614
- return {
615
- content: [{ type: "text", text: JSON.stringify(eventFilters) }],
616
- };
617
- });
618
- const updateErrorInputSchema = z.object({
619
- projectId: toolInputParameters.projectId,
620
- errorId: toolInputParameters.errorId,
621
- operation: z
622
- .enum(PERMITTED_UPDATE_OPERATIONS)
623
- .describe("The operation to apply to the error"),
624
- });
625
- register({
626
- title: "Update Error",
627
- summary: "Update the status of an error",
628
- purpose: "Change an error's workflow state, such as marking it as resolved or ignored",
629
- useCases: [
630
- "Mark an error as open, fixed or ignored",
631
- "Discard or un-discard an error",
632
- "Update the severity of an error",
633
- ],
634
- inputSchema: updateErrorInputSchema,
635
- examples: [
636
- {
637
- description: "Mark an error as fixed",
638
- parameters: {
639
- errorId: "6863e2af8c857c0a5023b411",
640
- operation: "fix",
641
- },
642
- expectedOutput: "Success response indicating the error was marked as fixed",
643
- },
644
- ],
645
- hints: [
646
- "Only use valid operations - BugSnag may reject invalid values",
647
- ],
648
- readOnly: false,
649
- idempotent: false,
650
- }, async (args, _extra) => {
651
- const params = updateErrorInputSchema.parse(args);
652
- const project = await this.getInputProject(params.projectId);
653
- let severity;
654
- if (params.operation === "override_severity") {
655
- // illicit the severity from the user
656
- const result = await getInput({
657
- message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
658
- requestedSchema: {
659
- type: "object",
660
- properties: {
661
- severity: {
662
- type: "string",
663
- enum: ["info", "warning", "error"],
664
- description: "The new severity level for the error",
665
- },
666
- },
667
- },
668
- required: ["severity"],
669
- });
670
- if (result.action === "accept" && result.content?.severity) {
671
- severity = result.content.severity;
672
- }
811
+ ]
812
+ };
813
+ }
814
+ );
815
+ const getReleaseInputSchema = z.object({
816
+ projectId: toolInputParameters.projectId,
817
+ releaseId: toolInputParameters.releaseId
818
+ });
819
+ register(
820
+ {
821
+ title: "Get Release",
822
+ summary: "Get more details for a specific release by its ID, including source control information and associated builds",
823
+ purpose: "Retrieve detailed information about a release for analysis and debugging",
824
+ useCases: [
825
+ "View release metadata such as version, source control info, and error counts",
826
+ "Analyze the stability data and targets for a release",
827
+ "See the builds that make up the release"
828
+ ],
829
+ inputSchema: getReleaseInputSchema,
830
+ examples: [
831
+ {
832
+ description: "Get details for a specific release",
833
+ parameters: {
834
+ releaseId: "5f8d0d55c9e77c0017a1b2c3"
835
+ },
836
+ expectedOutput: "JSON object with release details including version, source control info, error counts and stability data."
837
+ }
838
+ ],
839
+ hints: ["Release IDs can be found using the List releases tool"],
840
+ readOnly: true,
841
+ idempotent: true,
842
+ outputDescription: "JSON object containing release details along with stability metrics such as user and session stability, and whether it meets project targets"
843
+ },
844
+ async (args, _extra) => {
845
+ const params = getReleaseInputSchema.parse(args);
846
+ const project = await this.getInputProject(params.projectId);
847
+ const releaseResponse = await this.projectApi.getReleaseGroup(
848
+ params.releaseId
849
+ );
850
+ if (!releaseResponse.body)
851
+ throw new ToolError(`No release for ${params.releaseId} found.`);
852
+ const release = this.addStabilityData(releaseResponse.body, project);
853
+ let builds = [];
854
+ if (releaseResponse.body) {
855
+ const buildsResponse = await this.projectApi.listBuildsInRelease(
856
+ params.releaseId
857
+ );
858
+ if (buildsResponse.body) {
859
+ builds = buildsResponse.body.map(
860
+ (b) => this.addStabilityData(b, project)
861
+ );
862
+ }
863
+ }
864
+ return {
865
+ content: [
866
+ {
867
+ type: "text",
868
+ text: JSON.stringify({
869
+ release,
870
+ builds
871
+ })
673
872
  }
674
- const result = await this.errorsApi.updateErrorOnProject(project.id, params.errorId, {
675
- operation: Object.values(ErrorUpdateRequest.OperationEnum).find((value) => value === params.operation),
676
- severity: severity,
677
- });
678
- return {
679
- content: [
680
- {
681
- type: "text",
682
- text: JSON.stringify({
683
- success: result.status === 200 || result.status === 204,
684
- }),
685
- },
686
- ],
687
- };
688
- });
689
- const listReleasesInputSchema = z.object({
690
- projectId: toolInputParameters.projectId,
691
- releaseStage: toolInputParameters.releaseStage,
692
- visibleOnly: z
693
- .boolean()
694
- .describe("Whether to only include releases that are marked as visible in the dashboard")
695
- .default(false),
696
- perPage: toolInputParameters.perPage,
697
- nextUrl: toolInputParameters.nextUrl,
698
- });
699
- register({
700
- title: "List Releases",
701
- summary: "List releases for a project",
702
- purpose: "Retrieve a list of release summaries to analyze deployment history and associated errors",
703
- useCases: [
704
- "View recent releases to correlate with error spikes",
705
- "Filter releases by stage (e.g. production, staging) for targeted analysis",
706
- ],
707
- inputSchema: listReleasesInputSchema,
708
- examples: [
709
- {
710
- description: "List production releases for a project",
711
- parameters: {},
712
- expectedOutput: "JSON array of release objects in the production stage",
713
- },
714
- {
715
- description: "List staging releases for a project",
716
- parameters: {
717
- releaseStage: "staging",
718
- },
719
- expectedOutput: "JSON array of release objects in the staging stage",
720
- },
721
- {
722
- description: "Get the next page of results",
723
- parameters: {
724
- nextUrl: "/projects/515fb9337c1074f6fd000003/releases?offset=30&per_page=30",
725
- },
726
- expectedOutput: "JSON array of release objects with metadata from the next page",
727
- },
728
- ],
729
- hints: [
730
- "Use the Get Release tool to get more details on a specific release, including the builds it contains",
731
- "The release stage defaults to 'production' if not specified",
732
- "Use visibleOnly to filter out releases that have been marked as hidden in the dashboard",
733
- ],
734
- readOnly: true,
735
- idempotent: true,
736
- outputDescription: "JSON array of release summary objects with metadata, with a URL to the next page if more results are available",
737
- }, async (args, _extra) => {
738
- const params = listReleasesInputSchema.parse(args);
739
- const project = await this.getInputProject(params.projectId);
740
- const response = await this.projectApi.listProjectReleaseGroups(project.id, params.releaseStage, false, // Not top-only
741
- params.visibleOnly, params.perPage, params.nextUrl);
742
- let releases = [];
743
- if (response.body) {
744
- releases = response.body.map((r) => this.addStabilityData(r, project));
873
+ ]
874
+ };
875
+ }
876
+ );
877
+ const getBuildInputSchema = z.object({
878
+ projectId: toolInputParameters.projectId,
879
+ buildId: toolInputParameters.buildId
880
+ });
881
+ register(
882
+ {
883
+ title: "Get Build",
884
+ summary: "Get more details for a specific build by its ID",
885
+ purpose: "Retrieve detailed information about a build for analysis and debugging",
886
+ useCases: [
887
+ "View build metadata such as version, source control info, and error counts",
888
+ "Analyze a specific build to correlate with error spikes or deployments",
889
+ "See the stability targets for a project and if the build meets them"
890
+ ],
891
+ inputSchema: getBuildInputSchema,
892
+ examples: [
893
+ {
894
+ description: "Get details for a specific build",
895
+ parameters: {
896
+ buildId: "5f8d0d55c9e77c0017a1b2c3"
897
+ },
898
+ expectedOutput: "JSON object with build details including version, source control info, error counts and stability data."
899
+ }
900
+ ],
901
+ hints: ["Build IDs can be found using the List builds tool"],
902
+ readOnly: true,
903
+ idempotent: true,
904
+ outputDescription: "JSON object containing build details along with stability metrics such as user and session stability, and whether it meets project targets"
905
+ },
906
+ async (args, _extra) => {
907
+ const params = getBuildInputSchema.parse(args);
908
+ const project = await this.getInputProject(params.projectId);
909
+ const response = await this.projectApi.getProjectReleaseById(
910
+ project.id,
911
+ params.buildId
912
+ );
913
+ if (!response.body)
914
+ throw new ToolError(`No build for ${params.buildId} found.`);
915
+ const build = this.addStabilityData(response.body, project);
916
+ return {
917
+ content: [{ type: "text", text: JSON.stringify(build) }]
918
+ };
919
+ }
920
+ );
921
+ const listSpanGroupsInputSchema = z.object({
922
+ projectId: toolInputParameters.projectId,
923
+ sort: z.enum([
924
+ "total_spans",
925
+ "last_seen",
926
+ "name",
927
+ "display_name",
928
+ "network_http_method",
929
+ "rendering_slow_frame_span_percentage",
930
+ "rendering_frozen_frame_span_percentage",
931
+ "duration_p50",
932
+ "duration_p75",
933
+ "duration_p90",
934
+ "duration_p95",
935
+ "duration_p99",
936
+ "system_metrics_cpu_total_mean_p50",
937
+ "system_metrics_cpu_total_mean_p75",
938
+ "system_metrics_cpu_total_mean_p90",
939
+ "system_metrics_cpu_total_mean_p95",
940
+ "system_metrics_cpu_total_mean_p99",
941
+ "system_metrics_memory_device_mean_p50",
942
+ "system_metrics_memory_device_mean_p75",
943
+ "system_metrics_memory_device_mean_p90",
944
+ "system_metrics_memory_device_mean_p95",
945
+ "system_metrics_memory_device_mean_p99",
946
+ "rendering_metrics_fps_mean_p50",
947
+ "rendering_metrics_fps_mean_p75",
948
+ "rendering_metrics_fps_mean_p90",
949
+ "rendering_metrics_fps_mean_p95",
950
+ "rendering_metrics_fps_mean_p99",
951
+ "http_response_4xx_percentage",
952
+ "http_response_5xx_percentage"
953
+ ]).optional().describe("Field to sort by"),
954
+ direction: toolInputParameters.direction,
955
+ perPage: toolInputParameters.perPage,
956
+ starredOnly: z.boolean().optional().describe("Show only starred span groups"),
957
+ nextUrl: toolInputParameters.nextUrl,
958
+ filters: toolInputParameters.performanceFilters
959
+ });
960
+ register(
961
+ {
962
+ title: "List Span Groups",
963
+ summary: "List span groups (operations) tracked for performance monitoring",
964
+ purpose: "Discover and analyze different operations being monitored",
965
+ useCases: [
966
+ "View all operations being tracked for performance",
967
+ "Find slow operations by sorting by duration metrics",
968
+ "Filter to starred/important span groups"
969
+ ],
970
+ inputSchema: listSpanGroupsInputSchema,
971
+ examples: [
972
+ {
973
+ description: "List slowest operations",
974
+ parameters: {
975
+ sort: "duration_p95",
976
+ direction: "desc",
977
+ perPage: 10
978
+ },
979
+ expectedOutput: "Array of span groups sorted by 95th percentile duration"
980
+ },
981
+ {
982
+ description: "List starred span groups with filtering",
983
+ parameters: {
984
+ starredOnly: true,
985
+ filters: {
986
+ "span_group.category": [
987
+ { type: "eq", value: "full_page_load" }
988
+ ]
989
+ }
990
+ },
991
+ expectedOutput: "Array of starred span groups filtered by category"
992
+ }
993
+ ],
994
+ hints: [
995
+ "Span groups represent different operation types (page loads, API calls, etc.)",
996
+ "Use sort by duration_p95 or duration_p99 to find the slowest operations",
997
+ "Star important span groups for quick access",
998
+ "Use nextUrl for pagination"
999
+ ]
1000
+ },
1001
+ async (args, _extra) => {
1002
+ const params = listSpanGroupsInputSchema.parse(args);
1003
+ const project = await this.getInputProject(params.projectId);
1004
+ const result = await this.projectApi.listProjectSpanGroups(
1005
+ project.id,
1006
+ params.sort,
1007
+ params.direction,
1008
+ params.perPage,
1009
+ void 0,
1010
+ params.filters,
1011
+ params.starredOnly,
1012
+ params.nextUrl
1013
+ );
1014
+ return {
1015
+ content: [
1016
+ {
1017
+ type: "text",
1018
+ text: JSON.stringify({
1019
+ data: result.body,
1020
+ next_url: result.nextUrl,
1021
+ count: result.body?.length
1022
+ })
745
1023
  }
746
- return {
747
- content: [
748
- {
749
- type: "text",
750
- text: JSON.stringify({
751
- data: releases,
752
- next_url: response.nextUrl ?? undefined,
753
- data_count: releases.length,
754
- total_count: response.totalCount ?? undefined,
755
- }),
756
- },
757
- ],
758
- };
759
- });
760
- const getReleaseInputSchema = z.object({
761
- projectId: toolInputParameters.projectId,
762
- releaseId: toolInputParameters.releaseId,
763
- });
764
- register({
765
- title: "Get Release",
766
- summary: "Get more details for a specific release by its ID, including source control information and associated builds",
767
- purpose: "Retrieve detailed information about a release for analysis and debugging",
768
- useCases: [
769
- "View release metadata such as version, source control info, and error counts",
770
- "Analyze the stability data and targets for a release",
771
- "See the builds that make up the release",
772
- ],
773
- inputSchema: getReleaseInputSchema,
774
- examples: [
775
- {
776
- description: "Get details for a specific release",
777
- parameters: {
778
- releaseId: "5f8d0d55c9e77c0017a1b2c3",
779
- },
780
- expectedOutput: "JSON object with release details including version, source control info, error counts and stability data.",
781
- },
782
- ],
783
- hints: ["Release IDs can be found using the List releases tool"],
784
- readOnly: true,
785
- idempotent: true,
786
- outputDescription: "JSON object containing release details along with stability metrics such as user and session stability, and whether it meets project targets",
787
- }, async (args, _extra) => {
788
- const params = getReleaseInputSchema.parse(args);
789
- const project = await this.getInputProject(params.projectId);
790
- const releaseResponse = await this.projectApi.getReleaseGroup(params.releaseId);
791
- if (!releaseResponse.body)
792
- throw new ToolError(`No release for ${params.releaseId} found.`);
793
- const release = this.addStabilityData(releaseResponse.body, project);
794
- let builds = [];
795
- if (releaseResponse.body) {
796
- const buildsResponse = await this.projectApi.listBuildsInRelease(params.releaseId);
797
- if (buildsResponse.body) {
798
- builds = buildsResponse.body.map((b) => this.addStabilityData(b, project));
799
- }
1024
+ ]
1025
+ };
1026
+ }
1027
+ );
1028
+ const getSpanGroupInputSchema = z.object({
1029
+ projectId: toolInputParameters.projectId,
1030
+ spanGroupId: toolInputParameters.spanGroupId,
1031
+ filters: toolInputParameters.performanceFilters
1032
+ });
1033
+ register(
1034
+ {
1035
+ title: "Get Span Group",
1036
+ summary: "Get detailed performance metrics for a specific span group",
1037
+ purpose: "Analyze performance characteristics of a specific operation",
1038
+ useCases: [
1039
+ "View detailed statistics (p50, p75, p90, p95, p99) for an operation",
1040
+ "Check if performance targets are configured",
1041
+ "Monitor span count to understand operation volume"
1042
+ ],
1043
+ inputSchema: getSpanGroupInputSchema,
1044
+ examples: [
1045
+ {
1046
+ description: "Get details for an API endpoint span group",
1047
+ parameters: { spanGroupId: "[HttpClient]GET-api.example.com" },
1048
+ expectedOutput: "Statistics, category, and performance target info"
1049
+ },
1050
+ {
1051
+ description: "Get span group details with device filtering",
1052
+ parameters: {
1053
+ spanGroupId: "[HttpClient]GET-api.example.com",
1054
+ filters: {
1055
+ "device.browser_name": [{ type: "eq", value: "Chrome" }]
1056
+ }
1057
+ },
1058
+ expectedOutput: "Statistics filtered for Chrome browser only"
1059
+ }
1060
+ ],
1061
+ hints: [
1062
+ "Use List Span Groups first to discover available span group IDs",
1063
+ "IDs are automatically URL-encoded - provide the raw ID",
1064
+ "Statistics include p50, p75, p90, p95, p99 percentiles"
1065
+ ]
1066
+ },
1067
+ async (args, _extra) => {
1068
+ const params = getSpanGroupInputSchema.parse(args);
1069
+ const project = await this.getInputProject(params.projectId);
1070
+ const spanGroupResults = await this.projectApi.getProjectSpanGroup(
1071
+ project.id,
1072
+ params.spanGroupId,
1073
+ params.filters
1074
+ );
1075
+ const spanGroupTimelineResult = await this.projectApi.getProjectSpanGroupTimeline(
1076
+ project.id,
1077
+ params.spanGroupId,
1078
+ params.filters
1079
+ );
1080
+ const spanGroupDistributionResult = await this.projectApi.getProjectSpanGroupDistribution(
1081
+ project.id,
1082
+ params.spanGroupId,
1083
+ params.filters
1084
+ );
1085
+ const result = {
1086
+ ...spanGroupResults.body,
1087
+ timeline: spanGroupTimelineResult.body,
1088
+ distribution: spanGroupDistributionResult.body
1089
+ };
1090
+ return {
1091
+ content: [{ type: "text", text: JSON.stringify(result) }]
1092
+ };
1093
+ }
1094
+ );
1095
+ const listSpansInputSchema = z.object({
1096
+ projectId: toolInputParameters.projectId,
1097
+ spanGroupId: toolInputParameters.spanGroupId,
1098
+ sort: z.enum([
1099
+ "duration",
1100
+ "timestamp",
1101
+ "full_page_load_lcp",
1102
+ "full_page_load_fid",
1103
+ "full_page_load_cls",
1104
+ "full_page_load_ttfb",
1105
+ "full_page_load_fcp",
1106
+ "rendering_slow_frame_percentage",
1107
+ "rendering_frozen_frame_percentage",
1108
+ "system_metrics_cpu_total_mean",
1109
+ "system_metrics_memory_device_mean",
1110
+ "rendering_metrics_fps_mean",
1111
+ "rendering_metrics_fps_minimum",
1112
+ "rendering_metrics_fps_maximum",
1113
+ "http_response_code"
1114
+ ]).optional().describe("Field to sort by"),
1115
+ direction: toolInputParameters.direction,
1116
+ perPage: toolInputParameters.perPage,
1117
+ nextUrl: toolInputParameters.nextUrl,
1118
+ filters: toolInputParameters.performanceFilters
1119
+ });
1120
+ register(
1121
+ {
1122
+ title: "List Spans",
1123
+ summary: "Get individual spans belonging to a span group",
1124
+ purpose: "Examine individual operation instances within a span group",
1125
+ useCases: [
1126
+ "Analyze individual slow operations",
1127
+ "Debug performance issues by examining specific traces",
1128
+ "Find patterns in operation attributes"
1129
+ ],
1130
+ inputSchema: listSpansInputSchema,
1131
+ examples: [
1132
+ {
1133
+ description: "Get slowest spans for an operation",
1134
+ parameters: {
1135
+ spanGroupId: "[HttpClient]GET-api.example.com",
1136
+ sort: "duration",
1137
+ direction: "desc",
1138
+ perPage: 10
1139
+ },
1140
+ expectedOutput: "Array of the 10 slowest span instances"
1141
+ },
1142
+ {
1143
+ description: "Get spans filtered by OS with pagination",
1144
+ parameters: {
1145
+ spanGroupId: "[HttpClient]GET-api.example.com",
1146
+ sort: "timestamp",
1147
+ filters: {
1148
+ "os.name": [{ type: "eq", value: "iOS" }]
1149
+ },
1150
+ nextUrl: "/projects/123/spans?offset=30&per_page=30"
1151
+ },
1152
+ expectedOutput: "Array of spans from iOS devices with next page navigation"
1153
+ }
1154
+ ],
1155
+ hints: [
1156
+ "Sort by duration descending to find the slowest instances",
1157
+ "Each span includes trace ID for further investigation"
1158
+ ]
1159
+ },
1160
+ async (args, _extra) => {
1161
+ const params = listSpansInputSchema.parse(args);
1162
+ const project = await this.getInputProject(params.projectId);
1163
+ const result = await this.projectApi.listSpansBySpanGroupId(
1164
+ project.id,
1165
+ params.spanGroupId,
1166
+ params.filters,
1167
+ params.sort,
1168
+ params.direction,
1169
+ params.perPage,
1170
+ params.nextUrl
1171
+ );
1172
+ return {
1173
+ content: [
1174
+ {
1175
+ type: "text",
1176
+ text: JSON.stringify({
1177
+ data: result.body,
1178
+ next_url: result.nextUrl,
1179
+ count: result.body?.length
1180
+ })
800
1181
  }
801
- return {
802
- content: [
803
- {
804
- type: "text",
805
- text: JSON.stringify({
806
- release: release,
807
- builds: builds,
808
- }),
809
- },
810
- ],
811
- };
812
- });
813
- const getBuildInputSchema = z.object({
814
- projectId: toolInputParameters.projectId,
815
- buildId: toolInputParameters.buildId,
816
- });
817
- register({
818
- title: "Get Build",
819
- summary: "Get more details for a specific build by its ID",
820
- purpose: "Retrieve detailed information about a build for analysis and debugging",
821
- useCases: [
822
- "View build metadata such as version, source control info, and error counts",
823
- "Analyze a specific build to correlate with error spikes or deployments",
824
- "See the stability targets for a project and if the build meets them",
825
- ],
826
- inputSchema: getBuildInputSchema,
827
- examples: [
828
- {
829
- description: "Get details for a specific build",
830
- parameters: {
831
- buildId: "5f8d0d55c9e77c0017a1b2c3",
832
- },
833
- expectedOutput: "JSON object with build details including version, source control info, error counts and stability data.",
834
- },
835
- ],
836
- hints: ["Build IDs can be found using the List builds tool"],
837
- readOnly: true,
838
- idempotent: true,
839
- outputDescription: "JSON object containing build details along with stability metrics such as user and session stability, and whether it meets project targets",
840
- }, async (args, _extra) => {
841
- const params = getBuildInputSchema.parse(args);
842
- const project = await this.getInputProject(params.projectId);
843
- const response = await this.projectApi.getProjectReleaseById(project.id, params.buildId);
844
- if (!response.body)
845
- throw new ToolError(`No build for ${params.buildId} found.`);
846
- const build = this.addStabilityData(response.body, project);
847
- return {
848
- content: [{ type: "text", text: JSON.stringify(build) }],
849
- };
850
- });
851
- // ============================================================
852
- // Performance Monitoring Tools
853
- // ============================================================
854
- const listSpanGroupsInputSchema = z.object({
855
- projectId: toolInputParameters.projectId,
856
- sort: z
857
- .enum([
858
- "total_spans",
859
- "last_seen",
860
- "name",
861
- "display_name",
862
- "network_http_method",
863
- "rendering_slow_frame_span_percentage",
864
- "rendering_frozen_frame_span_percentage",
865
- "duration_p50",
866
- "duration_p75",
867
- "duration_p90",
868
- "duration_p95",
869
- "duration_p99",
870
- "system_metrics_cpu_total_mean_p50",
871
- "system_metrics_cpu_total_mean_p75",
872
- "system_metrics_cpu_total_mean_p90",
873
- "system_metrics_cpu_total_mean_p95",
874
- "system_metrics_cpu_total_mean_p99",
875
- "system_metrics_memory_device_mean_p50",
876
- "system_metrics_memory_device_mean_p75",
877
- "system_metrics_memory_device_mean_p90",
878
- "system_metrics_memory_device_mean_p95",
879
- "system_metrics_memory_device_mean_p99",
880
- "rendering_metrics_fps_mean_p50",
881
- "rendering_metrics_fps_mean_p75",
882
- "rendering_metrics_fps_mean_p90",
883
- "rendering_metrics_fps_mean_p95",
884
- "rendering_metrics_fps_mean_p99",
885
- "http_response_4xx_percentage",
886
- "http_response_5xx_percentage",
887
- ])
888
- .optional()
889
- .describe("Field to sort by"),
890
- direction: toolInputParameters.direction,
891
- perPage: toolInputParameters.perPage,
892
- starredOnly: z
893
- .boolean()
894
- .optional()
895
- .describe("Show only starred span groups"),
896
- nextUrl: toolInputParameters.nextUrl,
897
- filters: toolInputParameters.performanceFilters,
898
- });
899
- register({
900
- title: "List Span Groups",
901
- summary: "List span groups (operations) tracked for performance monitoring",
902
- purpose: "Discover and analyze different operations being monitored",
903
- useCases: [
904
- "View all operations being tracked for performance",
905
- "Find slow operations by sorting by duration metrics",
906
- "Filter to starred/important span groups",
907
- ],
908
- inputSchema: listSpanGroupsInputSchema,
909
- examples: [
910
- {
911
- description: "List slowest operations",
912
- parameters: {
913
- sort: "duration_p95",
914
- direction: "desc",
915
- perPage: 10,
916
- },
917
- expectedOutput: "Array of span groups sorted by 95th percentile duration",
918
- },
919
- {
920
- description: "List starred span groups with filtering",
921
- parameters: {
922
- starredOnly: true,
923
- filters: {
924
- "span_group.category": [
925
- { type: "eq", value: "full_page_load" },
926
- ],
927
- },
928
- },
929
- expectedOutput: "Array of starred span groups filtered by category",
930
- },
931
- ],
932
- hints: [
933
- "Span groups represent different operation types (page loads, API calls, etc.)",
934
- "Use sort by duration_p95 or duration_p99 to find the slowest operations",
935
- "Star important span groups for quick access",
936
- "Use nextUrl for pagination",
937
- ],
938
- }, async (args, _extra) => {
939
- const params = listSpanGroupsInputSchema.parse(args);
940
- const project = await this.getInputProject(params.projectId);
941
- const result = await this.projectApi.listProjectSpanGroups(project.id, params.sort, params.direction, params.perPage, undefined, params.filters, params.starredOnly, params.nextUrl);
942
- return {
943
- content: [
944
- {
945
- type: "text",
946
- text: JSON.stringify({
947
- data: result.body,
948
- next_url: result.nextUrl,
949
- count: result.body?.length,
950
- }),
951
- },
952
- ],
953
- };
954
- });
955
- const getSpanGroupInputSchema = z.object({
956
- projectId: toolInputParameters.projectId,
957
- spanGroupId: toolInputParameters.spanGroupId,
958
- filters: toolInputParameters.performanceFilters,
959
- });
960
- register({
961
- title: "Get Span Group",
962
- summary: "Get detailed performance metrics for a specific span group",
963
- purpose: "Analyze performance characteristics of a specific operation",
964
- useCases: [
965
- "View detailed statistics (p50, p75, p90, p95, p99) for an operation",
966
- "Check if performance targets are configured",
967
- "Monitor span count to understand operation volume",
968
- ],
969
- inputSchema: getSpanGroupInputSchema,
970
- examples: [
971
- {
972
- description: "Get details for an API endpoint span group",
973
- parameters: { spanGroupId: "[HttpClient]GET-api.example.com" },
974
- expectedOutput: "Statistics, category, and performance target info",
975
- },
976
- {
977
- description: "Get span group details with device filtering",
978
- parameters: {
979
- spanGroupId: "[HttpClient]GET-api.example.com",
980
- filters: {
981
- "device.browser_name": [{ type: "eq", value: "Chrome" }],
982
- },
983
- },
984
- expectedOutput: "Statistics filtered for Chrome browser only",
985
- },
986
- ],
987
- hints: [
988
- "Use List Span Groups first to discover available span group IDs",
989
- "IDs are automatically URL-encoded - provide the raw ID",
990
- "Statistics include p50, p75, p90, p95, p99 percentiles",
991
- ],
992
- }, async (args, _extra) => {
993
- const params = getSpanGroupInputSchema.parse(args);
994
- const project = await this.getInputProject(params.projectId);
995
- const spanGroupResults = await this.projectApi.getProjectSpanGroup(project.id, params.spanGroupId, params.filters);
996
- const spanGroupTimelineResult = await this.projectApi.getProjectSpanGroupTimeline(project.id, params.spanGroupId, params.filters);
997
- const spanGroupDistributionResult = await this.projectApi.getProjectSpanGroupDistribution(project.id, params.spanGroupId, params.filters);
998
- const result = {
999
- ...spanGroupResults.body,
1000
- timeline: spanGroupTimelineResult.body,
1001
- distribution: spanGroupDistributionResult.body,
1002
- };
1003
- return {
1004
- content: [{ type: "text", text: JSON.stringify(result) }],
1005
- };
1006
- });
1007
- const listSpansInputSchema = z.object({
1008
- projectId: toolInputParameters.projectId,
1009
- spanGroupId: toolInputParameters.spanGroupId,
1010
- sort: z
1011
- .enum([
1012
- "duration",
1013
- "timestamp",
1014
- "full_page_load_lcp",
1015
- "full_page_load_fid",
1016
- "full_page_load_cls",
1017
- "full_page_load_ttfb",
1018
- "full_page_load_fcp",
1019
- "rendering_slow_frame_percentage",
1020
- "rendering_frozen_frame_percentage",
1021
- "system_metrics_cpu_total_mean",
1022
- "system_metrics_memory_device_mean",
1023
- "rendering_metrics_fps_mean",
1024
- "rendering_metrics_fps_minimum",
1025
- "rendering_metrics_fps_maximum",
1026
- "http_response_code",
1027
- ])
1028
- .optional()
1029
- .describe("Field to sort by"),
1030
- direction: toolInputParameters.direction,
1031
- perPage: toolInputParameters.perPage,
1032
- nextUrl: toolInputParameters.nextUrl,
1033
- filters: toolInputParameters.performanceFilters,
1034
- });
1035
- register({
1036
- title: "List Spans",
1037
- summary: "Get individual spans belonging to a span group",
1038
- purpose: "Examine individual operation instances within a span group",
1039
- useCases: [
1040
- "Analyze individual slow operations",
1041
- "Debug performance issues by examining specific traces",
1042
- "Find patterns in operation attributes",
1043
- ],
1044
- inputSchema: listSpansInputSchema,
1045
- examples: [
1046
- {
1047
- description: "Get slowest spans for an operation",
1048
- parameters: {
1049
- spanGroupId: "[HttpClient]GET-api.example.com",
1050
- sort: "duration",
1051
- direction: "desc",
1052
- perPage: 10,
1053
- },
1054
- expectedOutput: "Array of the 10 slowest span instances",
1055
- },
1056
- {
1057
- description: "Get spans filtered by OS with pagination",
1058
- parameters: {
1059
- spanGroupId: "[HttpClient]GET-api.example.com",
1060
- sort: "timestamp",
1061
- filters: {
1062
- "os.name": [{ type: "eq", value: "iOS" }],
1063
- },
1064
- nextUrl: "/projects/123/spans?offset=30&per_page=30",
1065
- },
1066
- expectedOutput: "Array of spans from iOS devices with next page navigation",
1067
- },
1068
- ],
1069
- hints: [
1070
- "Sort by duration descending to find the slowest instances",
1071
- "Each span includes trace ID for further investigation",
1072
- ],
1073
- }, async (args, _extra) => {
1074
- const params = listSpansInputSchema.parse(args);
1075
- const project = await this.getInputProject(params.projectId);
1076
- const result = await this.projectApi.listSpansBySpanGroupId(project.id, params.spanGroupId, params.filters, params.sort, params.direction, params.perPage, params.nextUrl);
1077
- return {
1078
- content: [
1079
- {
1080
- type: "text",
1081
- text: JSON.stringify({
1082
- data: result.body,
1083
- next_url: result.nextUrl,
1084
- count: result.body?.length,
1085
- }),
1086
- },
1087
- ],
1088
- };
1089
- });
1090
- const getTraceInputSchema = z.object({
1091
- projectId: toolInputParameters.projectId,
1092
- traceId: z.string().describe("Trace ID"),
1093
- from: z.string().describe("Start time (ISO 8601 format)"),
1094
- to: z.string().describe("End time (ISO 8601 format)"),
1095
- targetSpanId: z
1096
- .string()
1097
- .optional()
1098
- .describe("Optional target span ID to focus on"),
1099
- perPage: toolInputParameters.perPage,
1100
- nextUrl: toolInputParameters.nextUrl,
1101
- });
1102
- register({
1103
- title: "Get Trace",
1104
- summary: "Get all spans within a specific trace",
1105
- purpose: "View the complete trace of operations for a request/transaction",
1106
- useCases: [
1107
- "Debug slow requests by viewing all operations in the trace",
1108
- "Understand the flow of a request through the system",
1109
- "Identify bottlenecks in distributed systems",
1110
- ],
1111
- inputSchema: getTraceInputSchema,
1112
- examples: [
1113
- {
1114
- description: "Get all spans for a trace",
1115
- parameters: {
1116
- traceId: "abc123",
1117
- from: "2024-01-01T00:00:00Z",
1118
- to: "2024-01-01T23:59:59Z",
1119
- },
1120
- expectedOutput: "Array of all spans in the trace with timing and hierarchy",
1121
- },
1122
- {
1123
- description: "Get spans for a trace with pagination and target span",
1124
- parameters: {
1125
- traceId: "def456",
1126
- from: "2024-01-01T00:00:00Z",
1127
- to: "2024-01-01T23:59:59Z",
1128
- targetSpanId: "span-789",
1129
- perPage: 50,
1130
- },
1131
- expectedOutput: "Array of up to 50 spans focused around the target span",
1132
- },
1133
- ],
1134
- hints: [
1135
- "Traces show the complete execution path of a request",
1136
- "Use from/to parameters to narrow the time window",
1137
- "targetSpanId can be used to focus on a specific span in the trace",
1138
- ],
1139
- }, async (args, _extra) => {
1140
- const params = getTraceInputSchema.parse(args);
1141
- const project = await this.getInputProject(params.projectId);
1142
- if (!params.traceId || !params.from || !params.to) {
1143
- throw new ToolError("traceId, from, and to are required");
1182
+ ]
1183
+ };
1184
+ }
1185
+ );
1186
+ const getTraceInputSchema = z.object({
1187
+ projectId: toolInputParameters.projectId,
1188
+ traceId: z.string().describe("Trace ID"),
1189
+ from: z.string().describe("Start time (ISO 8601 format)"),
1190
+ to: z.string().describe("End time (ISO 8601 format)"),
1191
+ targetSpanId: z.string().optional().describe("Optional target span ID to focus on"),
1192
+ perPage: toolInputParameters.perPage,
1193
+ nextUrl: toolInputParameters.nextUrl
1194
+ });
1195
+ register(
1196
+ {
1197
+ title: "Get Trace",
1198
+ summary: "Get all spans within a specific trace",
1199
+ purpose: "View the complete trace of operations for a request/transaction",
1200
+ useCases: [
1201
+ "Debug slow requests by viewing all operations in the trace",
1202
+ "Understand the flow of a request through the system",
1203
+ "Identify bottlenecks in distributed systems"
1204
+ ],
1205
+ inputSchema: getTraceInputSchema,
1206
+ examples: [
1207
+ {
1208
+ description: "Get all spans for a trace",
1209
+ parameters: {
1210
+ traceId: "abc123",
1211
+ from: "2024-01-01T00:00:00Z",
1212
+ to: "2024-01-01T23:59:59Z"
1213
+ },
1214
+ expectedOutput: "Array of all spans in the trace with timing and hierarchy"
1215
+ },
1216
+ {
1217
+ description: "Get spans for a trace with pagination and target span",
1218
+ parameters: {
1219
+ traceId: "def456",
1220
+ from: "2024-01-01T00:00:00Z",
1221
+ to: "2024-01-01T23:59:59Z",
1222
+ targetSpanId: "span-789",
1223
+ perPage: 50
1224
+ },
1225
+ expectedOutput: "Array of up to 50 spans focused around the target span"
1226
+ }
1227
+ ],
1228
+ hints: [
1229
+ "Traces show the complete execution path of a request",
1230
+ "Use from/to parameters to narrow the time window",
1231
+ "targetSpanId can be used to focus on a specific span in the trace"
1232
+ ]
1233
+ },
1234
+ async (args, _extra) => {
1235
+ const params = getTraceInputSchema.parse(args);
1236
+ const project = await this.getInputProject(params.projectId);
1237
+ if (!params.traceId || !params.from || !params.to) {
1238
+ throw new ToolError("traceId, from, and to are required");
1239
+ }
1240
+ const result = await this.projectApi.listSpansByTraceId(
1241
+ project.id,
1242
+ params.traceId,
1243
+ params.from,
1244
+ params.to,
1245
+ params.targetSpanId,
1246
+ params.perPage,
1247
+ params.nextUrl
1248
+ );
1249
+ return {
1250
+ content: [
1251
+ {
1252
+ type: "text",
1253
+ text: JSON.stringify({
1254
+ data: result.body,
1255
+ next_url: result.nextUrl,
1256
+ count: result.body?.length
1257
+ })
1144
1258
  }
1145
- const result = await this.projectApi.listSpansByTraceId(project.id, params.traceId, params.from, params.to, params.targetSpanId, params.perPage, params.nextUrl);
1146
- return {
1147
- content: [
1148
- {
1149
- type: "text",
1150
- text: JSON.stringify({
1151
- data: result.body,
1152
- next_url: result.nextUrl,
1153
- count: result.body?.length,
1154
- }),
1155
- },
1156
- ],
1157
- };
1158
- });
1159
- const listTraceFieldsInputSchema = z.object({
1160
- projectId: toolInputParameters.projectId,
1161
- });
1162
- // Similar to event filters, consider caching
1163
- register({
1164
- title: "List Trace Fields",
1165
- summary: "Get available trace fields/attributes for filtering",
1166
- purpose: "Discover what custom attributes are available for filtering",
1167
- useCases: [
1168
- "Find available custom attributes for performance filtering",
1169
- "Understand what metadata is attached to traces",
1170
- "Build dynamic filters based on available fields",
1171
- ],
1172
- inputSchema: listTraceFieldsInputSchema,
1173
- examples: [
1174
- {
1175
- description: "Get all trace fields",
1176
- parameters: {},
1177
- expectedOutput: "Array of field names and types available for filtering",
1178
- },
1179
- ],
1180
- hints: [
1181
- "Trace fields are custom attributes added to spans",
1182
- "Use these fields for filtering other performance queries",
1183
- ],
1184
- }, async (args, _extra) => {
1185
- const params = listTraceFieldsInputSchema.parse(args);
1186
- const project = await this.getInputProject(params.projectId);
1187
- const traceFields = await this.getProjectTraceFields(project);
1188
- return {
1189
- content: [{ type: "text", text: JSON.stringify(traceFields) }],
1190
- };
1191
- });
1192
- const getNetworkGroupingInputSchema = z.object({
1193
- projectId: toolInputParameters.projectId,
1194
- });
1195
- register({
1196
- title: "Get Network Endpoint Groupings",
1197
- summary: "Get the network endpoint grouping rules for a project",
1198
- purpose: "Retrieve the URL patterns used to group network spans for performance monitoring",
1199
- useCases: [
1200
- "View current network endpoint grouping configuration",
1201
- "Understand how network requests are being grouped in performance monitoring",
1202
- "Check grouping patterns before making updates",
1203
- ],
1204
- inputSchema: getNetworkGroupingInputSchema,
1205
- examples: [
1206
- {
1207
- description: "Get network grouping rules for a project",
1208
- parameters: {},
1209
- expectedOutput: "Array of endpoint URL patterns",
1210
- },
1211
- ],
1212
- hints: [
1213
- "Network grouping patterns help consolidate similar requests into single span groups",
1214
- "Patterns use OpenAPI path templating syntax with curly braces for path parameters (e.g., /users/{userId})",
1215
- "Wildcards (*) can be used in domains to match multiple subdomains (e.g., https://*.example.com)",
1216
- ],
1217
- readOnly: true,
1218
- idempotent: true,
1219
- }, async (args, _extra) => {
1220
- const params = getNetworkGroupingInputSchema.parse(args);
1221
- const project = await this.getInputProject(params.projectId);
1222
- const result = await this.projectApi.getProjectNetworkGroupingRuleset(project.id);
1223
- return {
1224
- content: [
1225
- { type: "text", text: JSON.stringify(result.body.endpoints || []) },
1226
- ],
1227
- };
1228
- });
1229
- const setNetworkGroupingInputSchema = z.object({
1230
- projectId: toolInputParameters.projectId,
1231
- endpoints: z
1232
- .array(z.string())
1233
- .describe("Array of URL patterns by which network spans are grouped. " +
1234
- "Endpoints follow OpenAPI path templating syntax (https://swagger.io/specification/#path-templating) where path parameters use curly braces (e.g., /users/{id}). " +
1235
- "If you encounter colon-prefixed parameters (e.g., :userId from Express/React Router), convert them to curly braces (e.g., {userId}). " +
1236
- "Wildcards (*) can be used in domains (e.g., https://*.example.com) to match multiple subdomains."),
1237
- });
1238
- register({
1239
- title: "Set Network Endpoint Groupings",
1240
- summary: "Set the network endpoint grouping rules for a project",
1241
- purpose: "Configure URL patterns to control how network spans are grouped in performance monitoring",
1242
- useCases: [
1243
- "Consolidate similar API endpoints into single span groups",
1244
- "Group dynamic URLs using path parameters (e.g., /api/users/{userId} groups /api/users/123, /api/users/456)",
1245
- "Match multiple subdomains using wildcards (e.g., https://*.example.com groups api.example.com, cdn.example.com)",
1246
- "Simplify performance monitoring by reducing span group clutter",
1247
- ],
1248
- inputSchema: setNetworkGroupingInputSchema,
1249
- examples: [
1250
- {
1251
- description: "Group API endpoints with path parameters",
1252
- parameters: {
1253
- endpoints: [
1254
- "/api/users/{userId}",
1255
- "/api/products/{productId}",
1256
- "/api/orders/{orderId}/items/{itemId}",
1257
- ],
1258
- },
1259
- expectedOutput: "Success response confirming the update",
1260
- },
1261
- {
1262
- description: "Group endpoints with domain wildcards and path parameters",
1263
- parameters: {
1264
- endpoints: [
1265
- "https://*.example.com/api/v1/{resourceId}",
1266
- "https://api.example.com/v2/users/{userId}",
1267
- "/graphql",
1268
- ],
1269
- },
1270
- expectedOutput: "Success response confirming the update",
1271
- },
1272
- {
1273
- description: "Convert colon-prefixed parameters to curly braces (e.g., from Express/React Router)",
1274
- parameters: {
1275
- endpoints: [
1276
- "/{organizationSlug}/{projectSlug}/performance/view-load",
1277
- "/api/{version}/items/{itemId}",
1278
- ],
1279
- },
1280
- expectedOutput: "Success response confirming the update",
1281
- },
1282
- ],
1283
- hints: [
1284
- "Use Get Network Grouping first to see current patterns",
1285
- "Use OpenAPI path templating with curly braces for path parameters: /users/{userId}, /orders/{orderId}/items/{itemId}",
1286
- "Convert colon-prefixed parameters to curly braces: :organizationSlug becomes {organizationSlug}, :projectSlug becomes {projectSlug}",
1287
- "Wildcards (*) can be used in domains to match subdomains: https://*.example.com/api",
1288
- "This replaces all existing patterns - include all patterns you want to keep",
1289
- "Well-designed patterns reduce noise in performance monitoring",
1290
- ],
1291
- readOnly: false,
1292
- idempotent: true,
1293
- }, async (args, _extra) => {
1294
- const params = setNetworkGroupingInputSchema.parse(args);
1295
- const project = await this.getInputProject(params.projectId);
1296
- const result = await this.projectApi.updateProjectNetworkGroupingRuleset(project.id, params.endpoints);
1297
- return {
1298
- content: [
1299
- {
1300
- type: "text",
1301
- text: JSON.stringify({
1302
- success: result.status === 200 || result.status === 204,
1303
- projectId: project.id,
1304
- endpoints: params.endpoints,
1305
- }),
1306
- },
1307
- ],
1308
- };
1309
- });
1310
- }
1311
- registerResources(register) {
1312
- register("event", "{id}", async (uri, variables, _extra) => {
1313
- return {
1314
- contents: [
1315
- {
1316
- uri: uri.href,
1317
- text: JSON.stringify(await this.getEvent(variables.id)),
1318
- },
1319
- ],
1320
- };
1321
- });
1322
- }
1259
+ ]
1260
+ };
1261
+ }
1262
+ );
1263
+ const listTraceFieldsInputSchema = z.object({
1264
+ projectId: toolInputParameters.projectId
1265
+ });
1266
+ register(
1267
+ {
1268
+ title: "List Trace Fields",
1269
+ summary: "Get available trace fields/attributes for filtering",
1270
+ purpose: "Discover what custom attributes are available for filtering",
1271
+ useCases: [
1272
+ "Find available custom attributes for performance filtering",
1273
+ "Understand what metadata is attached to traces",
1274
+ "Build dynamic filters based on available fields"
1275
+ ],
1276
+ inputSchema: listTraceFieldsInputSchema,
1277
+ examples: [
1278
+ {
1279
+ description: "Get all trace fields",
1280
+ parameters: {},
1281
+ expectedOutput: "Array of field names and types available for filtering"
1282
+ }
1283
+ ],
1284
+ hints: [
1285
+ "Trace fields are custom attributes added to spans",
1286
+ "Use these fields for filtering other performance queries"
1287
+ ]
1288
+ },
1289
+ async (args, _extra) => {
1290
+ const params = listTraceFieldsInputSchema.parse(args);
1291
+ const project = await this.getInputProject(params.projectId);
1292
+ const traceFields = await this.getProjectTraceFields(project);
1293
+ return {
1294
+ content: [{ type: "text", text: JSON.stringify(traceFields) }]
1295
+ };
1296
+ }
1297
+ );
1298
+ const getNetworkGroupingInputSchema = z.object({
1299
+ projectId: toolInputParameters.projectId
1300
+ });
1301
+ register(
1302
+ {
1303
+ title: "Get Network Endpoint Groupings",
1304
+ summary: "Get the network endpoint grouping rules for a project",
1305
+ purpose: "Retrieve the URL patterns used to group network spans for performance monitoring",
1306
+ useCases: [
1307
+ "View current network endpoint grouping configuration",
1308
+ "Understand how network requests are being grouped in performance monitoring",
1309
+ "Check grouping patterns before making updates"
1310
+ ],
1311
+ inputSchema: getNetworkGroupingInputSchema,
1312
+ examples: [
1313
+ {
1314
+ description: "Get network grouping rules for a project",
1315
+ parameters: {},
1316
+ expectedOutput: "Array of endpoint URL patterns"
1317
+ }
1318
+ ],
1319
+ hints: [
1320
+ "Network grouping patterns help consolidate similar requests into single span groups",
1321
+ "Patterns use OpenAPI path templating syntax with curly braces for path parameters (e.g., /users/{userId})",
1322
+ "Wildcards (*) can be used in domains to match multiple subdomains (e.g., https://*.example.com)"
1323
+ ],
1324
+ readOnly: true,
1325
+ idempotent: true
1326
+ },
1327
+ async (args, _extra) => {
1328
+ const params = getNetworkGroupingInputSchema.parse(args);
1329
+ const project = await this.getInputProject(params.projectId);
1330
+ const result = await this.projectApi.getProjectNetworkGroupingRuleset(
1331
+ project.id
1332
+ );
1333
+ return {
1334
+ content: [
1335
+ { type: "text", text: JSON.stringify(result.body.endpoints || []) }
1336
+ ]
1337
+ };
1338
+ }
1339
+ );
1340
+ const setNetworkGroupingInputSchema = z.object({
1341
+ projectId: toolInputParameters.projectId,
1342
+ endpoints: z.array(z.string()).describe(
1343
+ "Array of URL patterns by which network spans are grouped. Endpoints follow OpenAPI path templating syntax (https://swagger.io/specification/#path-templating) where path parameters use curly braces (e.g., /users/{id}). If you encounter colon-prefixed parameters (e.g., :userId from Express/React Router), convert them to curly braces (e.g., {userId}). Wildcards (*) can be used in domains (e.g., https://*.example.com) to match multiple subdomains."
1344
+ )
1345
+ });
1346
+ register(
1347
+ {
1348
+ title: "Set Network Endpoint Groupings",
1349
+ summary: "Set the network endpoint grouping rules for a project",
1350
+ purpose: "Configure URL patterns to control how network spans are grouped in performance monitoring",
1351
+ useCases: [
1352
+ "Consolidate similar API endpoints into single span groups",
1353
+ "Group dynamic URLs using path parameters (e.g., /api/users/{userId} groups /api/users/123, /api/users/456)",
1354
+ "Match multiple subdomains using wildcards (e.g., https://*.example.com groups api.example.com, cdn.example.com)",
1355
+ "Simplify performance monitoring by reducing span group clutter"
1356
+ ],
1357
+ inputSchema: setNetworkGroupingInputSchema,
1358
+ examples: [
1359
+ {
1360
+ description: "Group API endpoints with path parameters",
1361
+ parameters: {
1362
+ endpoints: [
1363
+ "/api/users/{userId}",
1364
+ "/api/products/{productId}",
1365
+ "/api/orders/{orderId}/items/{itemId}"
1366
+ ]
1367
+ },
1368
+ expectedOutput: "Success response confirming the update"
1369
+ },
1370
+ {
1371
+ description: "Group endpoints with domain wildcards and path parameters",
1372
+ parameters: {
1373
+ endpoints: [
1374
+ "https://*.example.com/api/v1/{resourceId}",
1375
+ "https://api.example.com/v2/users/{userId}",
1376
+ "/graphql"
1377
+ ]
1378
+ },
1379
+ expectedOutput: "Success response confirming the update"
1380
+ },
1381
+ {
1382
+ description: "Convert colon-prefixed parameters to curly braces (e.g., from Express/React Router)",
1383
+ parameters: {
1384
+ endpoints: [
1385
+ "/{organizationSlug}/{projectSlug}/performance/view-load",
1386
+ "/api/{version}/items/{itemId}"
1387
+ ]
1388
+ },
1389
+ expectedOutput: "Success response confirming the update"
1390
+ }
1391
+ ],
1392
+ hints: [
1393
+ "Use Get Network Grouping first to see current patterns",
1394
+ "Use OpenAPI path templating with curly braces for path parameters: /users/{userId}, /orders/{orderId}/items/{itemId}",
1395
+ "Convert colon-prefixed parameters to curly braces: :organizationSlug becomes {organizationSlug}, :projectSlug becomes {projectSlug}",
1396
+ "Wildcards (*) can be used in domains to match subdomains: https://*.example.com/api",
1397
+ "This replaces all existing patterns - include all patterns you want to keep",
1398
+ "Well-designed patterns reduce noise in performance monitoring"
1399
+ ],
1400
+ readOnly: false,
1401
+ idempotent: true
1402
+ },
1403
+ async (args, _extra) => {
1404
+ const params = setNetworkGroupingInputSchema.parse(args);
1405
+ const project = await this.getInputProject(params.projectId);
1406
+ const result = await this.projectApi.updateProjectNetworkGroupingRuleset(
1407
+ project.id,
1408
+ params.endpoints
1409
+ );
1410
+ return {
1411
+ content: [
1412
+ {
1413
+ type: "text",
1414
+ text: JSON.stringify({
1415
+ success: result.status === 200 || result.status === 204,
1416
+ projectId: project.id,
1417
+ endpoints: params.endpoints
1418
+ })
1419
+ }
1420
+ ]
1421
+ };
1422
+ }
1423
+ );
1424
+ }
1425
+ registerResources(register) {
1426
+ register("event", "{id}", async (uri, variables, _extra) => {
1427
+ return {
1428
+ contents: [
1429
+ {
1430
+ uri: uri.href,
1431
+ text: JSON.stringify(await this.getEvent(variables.id))
1432
+ }
1433
+ ]
1434
+ };
1435
+ });
1436
+ }
1323
1437
  }
1438
+ export {
1439
+ BugsnagClient
1440
+ };