Skip to content
Open
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
95 changes: 37 additions & 58 deletions src/bugsnag/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,26 @@ import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info";
import type { SmartBearMcpServer } from "../common/server";
import { ToolError } from "../common/tools";
import type {
Client,
GetInputFunction,
RegisterResourceFunction,
RegisterToolsFunction,
} from "../common/types";
import { CurrentUserAPI } from "./client/api/CurrentUser";
import { Configuration } from "./client/api/configuration";
import { ErrorAPI } from "./client/api/Error";
import { Client } from "../common/types";
import type {
Build,
EventField,
Organization,
Project,
Release,
TraceField,
} from "./client/api/index";
import { ProjectAPI } from "./client/api/Project";
} from "./client/api";
import {
Configuration,
CurrentUserAPI,
ErrorAPI,
ProjectAPI,
} from "./client/api";
import type { FilterObject } from "./client/filters";
import { GetError } from "./tool/error/get-error";
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 { GetNetworkEndpointGroupings } from "./tool/performance/get-network-endpoint-groupings";
import { GetSpanGroup } from "./tool/performance/get-span-group";
import { GetTrace } from "./tool/performance/get-trace";
import { ListSpanGroups } from "./tool/performance/list-span-groups";
import { ListSpans } from "./tool/performance/list-spans";
import { ListTraceFields } from "./tool/performance/list-trace-fields";
import { SetNetworkEndpointGroupings } from "./tool/performance/set-network-endpoint-groupings";
import { GetCurrentProject } from "./tool/project/get-current-project";
import { ListProjectEventFilters } from "./tool/project/list-project-event-filters";
import { ListProjects } from "./tool/project/list-projects";
import { GetBuild } from "./tool/release/get-build";
import { GetRelease } from "./tool/release/get-release";
import { ListReleases } from "./tool/release/list-releases";

const HUB_PREFIX = "00000";
const DEFAULT_DOMAIN = "bugsnag.com";
Expand Down Expand Up @@ -74,7 +58,7 @@ const ConfigurationSchema = z.object({
endpoint: z.string().url().describe("BugSnag endpoint URL").optional(),
});

export class BugsnagClient implements Client {
export class BugsnagClient extends Client {
private cache?: CacheService;
private _projectApiKey?: string;
private _isConfigured: boolean = false;
Expand Down Expand Up @@ -271,7 +255,7 @@ export class BugsnagClient implements Client {
return projectFiltersCache[project.id];
}

async getEvent(eventId: string, projectId?: string): Promise<any> {
async getEvent(eventId: string, projectId?: string) {
const projectIds = projectId
? [projectId]
: (await this.getProjects()).map((p) => p.id);
Expand Down Expand Up @@ -367,42 +351,37 @@ export class BugsnagClient implements Client {
register: RegisterToolsFunction,
getInput: GetInputFunction,
): Promise<void> {
const tools = [
new GetCurrentProject(this),
new ListProjects(this),
new ListProjectEventFilters(this),
new GetError(this),
new ListProjectErrors(this),
new UpdateError(this, getInput),
new GetEvent(this),
new GetEventDetailsFromDashboardUrl(this),
new ListReleases(this),
new GetRelease(this),
new GetBuild(this),
new ListSpanGroups(this),
new GetSpanGroup(this),
new ListSpans(this),
new GetTrace(this),
new ListTraceFields(this),
new GetNetworkEndpointGroupings(this),
new SetNetworkEndpointGroupings(this),
];
const tools = await Promise.all([
import("./tool/error/get-error"),
import("./tool/error/list-project-errors"),
import("./tool/error/update-error"),
import("./tool/event/get-event"),
import("./tool/event/get-event-details-from-dashboard-url"),
import("./tool/performance/get-network-endpoint-groupings"),
import("./tool/performance/get-span-group"),
import("./tool/performance/get-trace"),
import("./tool/performance/list-span-groups"),
import("./tool/performance/list-spans"),
import("./tool/performance/list-trace-fields"),
import("./tool/performance/set-network-endpoint-groupings"),
import("./tool/project/get-current-project"),
import("./tool/project/list-project-event-filters"),
import("./tool/project/list-projects"),
import("./tool/release/get-build"),
import("./tool/release/get-release"),
import("./tool/release/list-releases"),
]);

for (const tool of tools) {
register(tool.specification, tool.handle);
tool.default.register(this, register, getInput);
}
}

registerResources(register: RegisterResourceFunction): void {
register("event", "{id}", async (uri, variables, _extra) => {
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(await this.getEvent(variables.id as string)),
},
],
};
});
async registerResources(register: RegisterResourceFunction): Promise<void> {
const resources = [await import("./resource/event-resource")];

for (const resource of resources) {
resource.default.register(this, register);
}
}
}
18 changes: 18 additions & 0 deletions src/bugsnag/resource/event-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BugsnagClient } from "../client";

export default BugsnagClient.createResource(
{
name: "event",
path: "{id}",
},
async ({ client, uri, variables }) => {
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(await client.getEvent(variables.id)),
},
],
};
},
);
43 changes: 19 additions & 24 deletions src/bugsnag/tool/error/get-error.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ZodRawShape } from "zod";
import { z } from "zod";
import { Tool, ToolError } from "../../../common/tools";
import type { ToolParams } from "../../../common/types";
import type { BugsnagClient } from "../../client";
import { ToolError } from "../../../common/tools";
import { BugsnagClient } from "../../client";
import { type FilterObject, toUrlSearchParams } from "../../client/filters";
import { toolInputParameters } from "../../input-schemas";

Expand All @@ -19,8 +16,8 @@ const inputSchema = z.object({
});

// Fetches full details for a single error including aggregated stats, the latest event, pivots, and a dashboard URL.
export class GetError extends Tool<BugsnagClient> {
specification: ToolParams = {
export default BugsnagClient.createTool(
{
title: "Get Error",
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.",
Expand Down Expand Up @@ -56,32 +53,30 @@ export class GetError extends Tool<BugsnagClient> {
"If you used a filter to get this error, you can pass the same filters here to restrict the results or apply further filters",
"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",
],
};

handle: ToolCallback<ZodRawShape> = async (args, _extra) => {
const params = inputSchema.parse(args);
const project = await this.client.getInputProject(params.projectId);
},
async ({ client, args }) => {
const project = await client.getInputProject(args.projectId);
const errorDetails = (
await this.client.errorsApi.viewErrorOnProject(project.id, params.errorId)
await client.errorsApi.viewErrorOnProject(project.id, args.errorId)
).body;
if (!errorDetails) {
throw new ToolError(
`Error with ID ${params.errorId} not found in project ${project.id}.`,
`Error with ID ${args.errorId} not found in project ${project.id}.`,
);
}

const filters: FilterObject = {
error: [{ type: "eq", value: params.errorId }],
...params.filters,
error: [{ type: "eq", value: args.errorId }],
...args.filters,
};

await this.client.validateEventFields(project, filters);
await client.validateEventFields(project, filters);

// Get the latest event for this error using the events endpoint with filters
let latestEvent = null;
try {
const latestEvents = (
await this.client.errorsApi.listEventsOnProject(
await client.errorsApi.listEventsOnProject(
project.id,
null,
"timestamp",
Expand All @@ -105,21 +100,21 @@ export class GetError extends Tool<BugsnagClient> {
latest_event: latestEvent,
pivots:
(
await this.client.errorsApi.getPivotValuesOnAnError(
await client.errorsApi.getPivotValuesOnAnError(
project.id,
params.errorId,
args.errorId,
filters,
5,
)
).body || [],
url: await this.client.getErrorUrl(
url: await client.getErrorUrl(
project,
params.errorId,
args.errorId,
toUrlSearchParams(filters).toString(),
),
};
return {
content: [{ type: "text", text: JSON.stringify(content) }],
};
};
}
},
);
36 changes: 15 additions & 21 deletions src/bugsnag/tool/error/list-project-errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
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 { BugsnagClient } from "../../client";
import type { FilterObject } from "../../client/filters";
import { toolInputParameters } from "../../input-schemas";

Expand All @@ -20,8 +16,8 @@ const inputSchema = z.object({
});

// Lists errors in a project with optional filters, sorting, and pagination.
export class ListProjectErrors extends Tool<BugsnagClient> {
specification: ToolParams = {
export default BugsnagClient.createTool(
{
title: "List Project Errors",
summary:
"List and search errors in a project using customizable filters and pagination",
Expand Down Expand Up @@ -84,29 +80,27 @@ export class ListProjectErrors extends Tool<BugsnagClient> {
"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.",
"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.",
],
};

handle: ToolCallback<ZodRawShape> = async (args, _extra) => {
const params = inputSchema.parse(args);
const project = await this.client.getInputProject(params.projectId);
},
async ({ client, args }) => {
const project = await client.getInputProject(args.projectId);

const filters: FilterObject = {
"event.since": [{ type: "eq", value: "30d" }],
"error.status": [{ type: "eq", value: "open" }],
...params.filters,
...args.filters,
};

// Validate filter keys against cached event fields
await this.client.validateEventFields(project, filters);
await client.validateEventFields(project, filters);

const response = await this.client.errorsApi.listProjectErrors(
const response = await client.errorsApi.listProjectErrors(
project.id,
null,
params.sort,
params.direction,
params.perPage,
args.sort,
args.direction,
args.perPage,
filters,
params.nextUrl,
args.nextUrl,
);

const result = {
Expand All @@ -118,5 +112,5 @@ export class ListProjectErrors extends Tool<BugsnagClient> {
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
};
}
},
);
Loading
Loading