Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- [Zephyr] Added a tool `update-test-steps` for updating test execution test steps [#386](https://github.com/SmartBear/smartbear-mcp/pull/386)
- [BugSnag] Added "Get Events on an Error" tool for listing the events that have grouped into a specified error [#406](https://github.com/SmartBear/smartbear-mcp/pull/406)

### Changed

Expand Down
5 changes: 5 additions & 0 deletions docs/products/SmartBear MCP Server/bugsnag-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ If you wish to interact with only one BugSnag project, we also recommend setting
- Link to the error on the dashboard.
- This tool also takes filter parameters in the same format as List Errors to specialize the results returned in the summaries/pivots.

### Get Events on an Error

- Retrieves a list of events (occurrences) for a specified error, with complete details for each event.
- This tool also takes filter parameters in the same format as List Errors to filter the events returned.

### Get Event Details

- Retrieve event (occurrence) details of a specific event by ID.
Expand Down
2 changes: 2 additions & 0 deletions src/bugsnag/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ListProjectErrors } from "./tool/error/list-project-errors";
import { UpdateError } from "./tool/error/update-error";
import { GetEvent } from "./tool/event/get-event";
import { GetEventDetailsFromDashboardUrl } from "./tool/event/get-event-details-from-dashboard-url";
import { ListErrorEvents } from "./tool/event/list-error-events";
import { GetNetworkEndpointGroupings } from "./tool/performance/get-network-endpoint-groupings";
import { GetSpanGroup } from "./tool/performance/get-span-group";
import { GetTrace } from "./tool/performance/get-trace";
Expand Down Expand Up @@ -376,6 +377,7 @@ export class BugsnagClient implements Client {
new UpdateError(this, getInput),
new GetEvent(this),
new GetEventDetailsFromDashboardUrl(this),
new ListErrorEvents(this),
new ListReleases(this),
new GetRelease(this),
new GetBuild(this),
Expand Down
55 changes: 55 additions & 0 deletions src/bugsnag/client/api/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,61 @@ export class ErrorAPI extends BaseAPI {
);
}

/**
* List the Events on an Error
* GET /projects/{project_id}/errors/{error_id}/events
*/
async listErrorEvents(
projectId: string,
errorId: string,
base?: Date | null,
sort?: string,
direction?: string,
perPage?: number,
filters?: FilterObject,
nextUrl?: string,
): Promise<ApiResponse<EventApiView[]>> {
if (nextUrl) {
// Don't allow override of these params when using nextUrl
direction = undefined;
sort = undefined;
base = undefined;
}
const localVarFetchArgs = ErrorsApiFetchParamCreator(
this.configuration,
).listEventsOnError(
projectId,
errorId,
base ?? undefined,
sort,
direction,
undefined,
undefined, // Filters are encoded separately below
undefined,
undefined,
);

const url = new URL(
nextUrl ?? localVarFetchArgs.url,
this.configuration.basePath,
);
if (perPage) {
// Allow override of per page, even with nextUrl
url.searchParams.set("per_page", perPage.toString());
}
if (!nextUrl && filters) {
// Apply our own encoding of filters
toUrlSearchParams(filters).forEach((value, key) => {
url.searchParams.append(key, value);
});
}
return await this.requestArray<ErrorApiView>(
url.toString(),
localVarFetchArgs.options,
false, // Paginate results
);
}

/**
* Update an Error on a Project
* PATCH /projects/{project_id}/errors/{error_id}
Expand Down
69 changes: 69 additions & 0 deletions src/bugsnag/tool/event/list-error-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ZodRawShape } from "zod";
import { z } from "zod";
import { Tool } from "../../../common/tools";
import type { ToolParams } from "../../../common/types";
import type { BugsnagClient } from "../../client";
import { toolInputParameters } from "../../input-schemas";

const inputSchema = z.object({
projectId: toolInputParameters.projectId,
errorId: toolInputParameters.errorId,
filters: toolInputParameters.filters.describe(
"Apply filters to narrow down the event 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).",
),
direction: toolInputParameters.direction,
perPage: toolInputParameters.perPage,
nextUrl: toolInputParameters.nextUrl,
});

// Fetches full details for a single event by its ID, including stack trace and metadata.
export class ListErrorEvents extends Tool<BugsnagClient> {
specification: ToolParams = {
title: "Get Events on an Error",
summary: "Gets a list of events that have grouped into the specified error",
purpose:
"Show the events that make up an error to see individual occurrences of the error for detailed analysis",
useCases: [
"Retrieving all the events for comparison to find commonalities or differences in stack traces, breadcrumbs and metadata",
],
inputSchema,
examples: [
{
description: "Get events of an error",
parameters: {
projectId: "1234567890abcdef12345678",
errorId: "6863e2af012caf1d5c320000",
},
expectedOutput:
"A list of events, ordered by timestamp, with complete details including stack trace, breadcrumbs, metadata, and context",
},
],
};

handle: ToolCallback<ZodRawShape> = async (args, _extra) => {
const params = inputSchema.parse(args);
const project = await this.client.getInputProject(params.projectId);
const response = await this.client.errorsApi.listErrorEvents(
project.id,
params.errorId,
undefined, // base
"timestamp", // sort (the only available option)
params.direction,
params.perPage,
params.filters,
params.nextUrl,
);

const result = {
data: response.body,
next_url: response.nextUrl ?? undefined,
data_count: response.body?.length,
total_count: response.totalCount ?? undefined,
};
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
};
}
53 changes: 52 additions & 1 deletion src/tests/unit/bugsnag/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const mockErrorAPI = {
listEventsOnProject: vi.fn(),
viewEventById: vi.fn(),
listProjectErrors: vi.fn(),
listErrorEvents: vi.fn(),
updateErrorOnProject: vi.fn(),
getPivotValuesOnAnError: vi.fn(),
} satisfies Omit<ErrorAPI, keyof BaseAPI>;
Expand Down Expand Up @@ -729,6 +730,7 @@ describe("BugsnagClient", () => {
expect(registeredTools).toContain("Get Event");
expect(registeredTools).toContain("Get Event Details From Dashboard URL");
expect(registeredTools).toContain("List Project Errors");
expect(registeredTools).toContain("Get Events on an Error");
expect(registeredTools).toContain("List Project Event Filters");
expect(registeredTools).toContain("Update Error");
expect(registeredTools).toContain("Get Build");
Expand All @@ -741,7 +743,7 @@ describe("BugsnagClient", () => {
expect(registeredTools).toContain("List Trace Fields");
expect(registeredTools).toContain("Get Network Endpoint Groupings");
expect(registeredTools).toContain("Set Network Endpoint Groupings");
expect(registeredTools.length).toBe(18);
expect(registeredTools.length).toBe(19);
});
});

Expand Down Expand Up @@ -1271,6 +1273,55 @@ describe("BugsnagClient", () => {
});
});

describe("Get Events on an Error tool handler", () => {
const mockProject = getMockProject("proj-1", "Project 1");

it("should list error events with supplied parameters", async () => {
const mockEvents: EventApiView[] = [
getMockEvent("event-1"),
getMockEvent("event-2"),
];
const filters = {
"event.since": [{ type: "eq" as const, value: "7d" }],
};

mockCache.get.mockReturnValueOnce(mockProject); // current project
mockErrorAPI.listErrorEvents.mockResolvedValue({
body: mockEvents,
totalCount: 2,
});

client.registerTools(registerToolsSpy, getInputFunctionSpy);
const toolHandler = registerToolsSpy.mock.calls.find(
(call: any) => call[0].title === "Get Events on an Error",
)[1];

const result = await toolHandler({
errorId: "error-1",
filters,
direction: "desc",
perPage: 50,
});

expect(mockErrorAPI.listErrorEvents).toHaveBeenCalledWith(
"proj-1",
"error-1",
undefined,
"timestamp",
"desc",
50,
filters,
undefined,
);
const expectedResult = {
data: mockEvents,
data_count: 2,
total_count: 2,
};
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
});
});

describe("List Project Event Filters tool handler", () => {
const mockProject = getMockProject("proj-1", "Project 1");

Expand Down
Loading