@smartbear/mcp 0.10.0 → 0.12.0

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