diff --git a/src/bugsnag/client.ts b/src/bugsnag/client.ts index 0ef48055..3893af3d 100644 --- a/src/bugsnag/client.ts +++ b/src/bugsnag/client.ts @@ -4,14 +4,11 @@ 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, @@ -19,27 +16,14 @@ import type { 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"; @@ -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; @@ -271,7 +255,7 @@ export class BugsnagClient implements Client { return projectFiltersCache[project.id]; } - async getEvent(eventId: string, projectId?: string): Promise { + async getEvent(eventId: string, projectId?: string) { const projectIds = projectId ? [projectId] : (await this.getProjects()).map((p) => p.id); @@ -367,42 +351,37 @@ export class BugsnagClient implements Client { register: RegisterToolsFunction, getInput: GetInputFunction, ): Promise { - 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 { + const resources = [await import("./resource/event-resource")]; + + for (const resource of resources) { + resource.default.register(this, register); + } } } diff --git a/src/bugsnag/resource/event-resource.ts b/src/bugsnag/resource/event-resource.ts new file mode 100644 index 00000000..0f4ea9f4 --- /dev/null +++ b/src/bugsnag/resource/event-resource.ts @@ -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)), + }, + ], + }; + }, +); diff --git a/src/bugsnag/tool/error/get-error.ts b/src/bugsnag/tool/error/get-error.ts index 2b3337fe..ad2da904 100644 --- a/src/bugsnag/tool/error/get-error.ts +++ b/src/bugsnag/tool/error/get-error.ts @@ -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"; @@ -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 { - 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.", @@ -56,32 +53,30 @@ export class GetError extends Tool { "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 = 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", @@ -105,21 +100,21 @@ export class GetError extends Tool { 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) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/error/list-project-errors.ts b/src/bugsnag/tool/error/list-project-errors.ts index c9e9762c..b2de3600 100644 --- a/src/bugsnag/tool/error/list-project-errors.ts +++ b/src/bugsnag/tool/error/list-project-errors.ts @@ -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"; @@ -20,8 +16,8 @@ const inputSchema = z.object({ }); // Lists errors in a project with optional filters, sorting, and pagination. -export class ListProjectErrors extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Project Errors", summary: "List and search errors in a project using customizable filters and pagination", @@ -84,29 +80,27 @@ export class ListProjectErrors extends Tool { "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 = 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 = { @@ -118,5 +112,5 @@ export class ListProjectErrors extends Tool { return { content: [{ type: "text", text: JSON.stringify(result) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/error/update-error.ts b/src/bugsnag/tool/error/update-error.ts index 068a4440..affac820 100644 --- a/src/bugsnag/tool/error/update-error.ts +++ b/src/bugsnag/tool/error/update-error.ts @@ -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 { GetInputFunction, ToolParams } from "../../../common/types"; -import type { BugsnagClient } from "../../client"; +import { ToolError } from "../../../common/tools"; +import { BugsnagClient } from "../../client"; import { ErrorUpdateRequest } from "../../client/api/index"; import { toolInputParameters } from "../../input-schemas"; @@ -87,10 +84,8 @@ const inputSchema = z.object({ }); // Updates an error's workflow state (e.g. fix, ignore, snooze, link/unlink issue). Prompts for severity when overriding it. -export class UpdateError extends Tool { - private getInput: GetInputFunction; - - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Update Error", summary: "Update the status of an error", purpose: @@ -185,42 +180,35 @@ export class UpdateError extends Tool { ], readOnly: false, idempotent: false, - }; - - constructor(client: BugsnagClient, getInput: GetInputFunction) { - super(client); - this.getInput = getInput; - } - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); + }, + async ({ client, args, getInput }) => { + const project = await client.getInputProject(args.projectId); // Validate snooze operation requirements - if (params.operation === "snooze" && !params.reopenRules) { + if (args.operation === "snooze" && !args.reopenRules) { throw new ToolError( "reopenRules parameter is required when using 'snooze' operation", ); } // Validate link_issue operation requirements - if (params.operation === "link_issue" && !params.issue_url) { + if (args.operation === "link_issue" && !args.issue_url) { throw new ToolError( "'issue_url' parameter is required for 'link_issue' operation", ); } // Validate reopen rule parameters based on reopenIf type - if (params.reopenRules) { - const { reopenIf } = params.reopenRules; - if (reopenIf === "occurs_after" && !params.reopenRules.seconds) { + if (args.reopenRules) { + const { reopenIf } = args.reopenRules; + if (reopenIf === "occurs_after" && !args.reopenRules.seconds) { throw new ToolError( "'seconds' parameter is required for 'occurs_after' reopen rules", ); } if ( reopenIf === "n_additional_users" && - !params.reopenRules.additionalUsers + !args.reopenRules.additionalUsers ) { throw new ToolError( "'additionalUsers' parameter is required for 'n_additional_users' reopen rules", @@ -228,7 +216,7 @@ export class UpdateError extends Tool { } if ( reopenIf === "n_occurrences_in_m_hours" && - (!params.reopenRules.occurrences || !params.reopenRules.hours) + (!args.reopenRules.occurrences || !args.reopenRules.hours) ) { throw new ToolError( "Both 'occurrences' and 'hours' parameters are required for 'n_occurrences_in_m_hours' reopen rules", @@ -236,7 +224,7 @@ export class UpdateError extends Tool { } if ( reopenIf === "n_additional_occurrences" && - !params.reopenRules.additionalOccurrences + !args.reopenRules.additionalOccurrences ) { throw new ToolError( "'additionalOccurrences' parameter is required for 'n_additional_occurrences' reopen rules", @@ -245,9 +233,9 @@ export class UpdateError extends Tool { } let severity: any; - if (params.operation === "override_severity") { + if (args.operation === "override_severity") { // illicit the severity from the user - const result = await this.getInput({ + const result = await getInput({ message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')", requestedSchema: { @@ -270,31 +258,31 @@ export class UpdateError extends Tool { // Prepare reopen rules for API call let reopenRules: any; - if (params.reopenRules) { + if (args.reopenRules) { reopenRules = { - reopen_if: params.reopenRules.reopenIf, + reopen_if: args.reopenRules.reopenIf, }; - if (params.reopenRules.additionalUsers !== undefined) { - reopenRules.additional_users = params.reopenRules.additionalUsers; + if (args.reopenRules.additionalUsers !== undefined) { + reopenRules.additional_users = args.reopenRules.additionalUsers; } - if (params.reopenRules.seconds !== undefined) { - reopenRules.seconds = params.reopenRules.seconds; + if (args.reopenRules.seconds !== undefined) { + reopenRules.seconds = args.reopenRules.seconds; } - if (params.reopenRules.occurrences !== undefined) { - reopenRules.occurrences = params.reopenRules.occurrences; + if (args.reopenRules.occurrences !== undefined) { + reopenRules.occurrences = args.reopenRules.occurrences; } - if (params.reopenRules.hours !== undefined) { - reopenRules.hours = params.reopenRules.hours; + if (args.reopenRules.hours !== undefined) { + reopenRules.hours = args.reopenRules.hours; } - if (params.reopenRules.additionalOccurrences !== undefined) { + if (args.reopenRules.additionalOccurrences !== undefined) { reopenRules.additional_occurrences = - params.reopenRules.additionalOccurrences; + args.reopenRules.additionalOccurrences; } } const errorUpdateRequestBody: any = { operation: Object.values(ErrorUpdateRequest.OperationEnum).find( - (value) => value === params.operation, + (value) => value === args.operation, ) as ErrorUpdateRequest.OperationEnum, }; if (severity !== undefined) { @@ -303,14 +291,14 @@ export class UpdateError extends Tool { if (reopenRules !== undefined) { errorUpdateRequestBody.reopen_rules = reopenRules; } - if (params.operation === "link_issue" && params.issue_url) { - errorUpdateRequestBody.issue_url = params.issue_url; + if (args.operation === "link_issue" && args.issue_url) { + errorUpdateRequestBody.issue_url = args.issue_url; errorUpdateRequestBody.verify_issue_url = true; } - const result = await this.client.errorsApi.updateErrorOnProject( + const result = await client.errorsApi.updateErrorOnProject( project.id, - params.errorId, + args.errorId, errorUpdateRequestBody, ); return { @@ -322,6 +310,9 @@ export class UpdateError extends Tool { }), }, ], + structuredContent: { + success: result.status === 200 || result.status === 204, + }, }; - }; -} + }, +); diff --git a/src/bugsnag/tool/event/get-event-details-from-dashboard-url.ts b/src/bugsnag/tool/event/get-event-details-from-dashboard-url.ts index ee57d36a..c18d4f0c 100644 --- a/src/bugsnag/tool/event/get-event-details-from-dashboard-url.ts +++ b/src/bugsnag/tool/event/get-event-details-from-dashboard-url.ts @@ -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"; const inputSchema = z.object({ link: z @@ -14,8 +11,8 @@ const inputSchema = z.object({ }); // Parses a BugSnag dashboard URL to extract the project slug and event ID, then fetches the event details. -export class GetEventDetailsFromDashboardUrl extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Event Details From Dashboard URL", summary: "Get detailed information about a specific event using its dashboard URL", @@ -41,11 +38,9 @@ export class GetEventDetailsFromDashboardUrl extends Tool { "The URL must contain both project slug in the path and event_id in query parameters", "This is useful when users share BugSnag dashboard URLs and you need to extract the event data", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const url = new URL(params.link); + }, + async ({ client, args }) => { + const url = new URL(args.link); const eventId = url.searchParams.get("event_id"); const projectSlug = url.pathname.split("/")[2]; if (!projectSlug || !eventId) @@ -54,15 +49,16 @@ export class GetEventDetailsFromDashboardUrl extends Tool { ); // get the project id from list of projects - const projects = await this.client.getProjects(); + const projects = await client.getProjects(); const projectId = projects.find((p: any) => p.slug === projectSlug)?.id; if (!projectId) { throw new ToolError("Project with the specified slug not found."); } - const response = await this.client.getEvent(eventId, projectId); + const response = await client.getEvent(eventId, projectId); return { content: [{ type: "text", text: JSON.stringify(response) }], + structuredContent: response, }; - }; -} + }, +); diff --git a/src/bugsnag/tool/event/get-event.ts b/src/bugsnag/tool/event/get-event.ts index 0d994cbd..6c622216 100644 --- a/src/bugsnag/tool/event/get-event.ts +++ b/src/bugsnag/tool/event/get-event.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -12,8 +8,8 @@ const inputSchema = z.object({ }); // Fetches full details for a single event by its ID, including stack trace and metadata. -export class GetEvent extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Event", summary: "Get detailed information about a specific event", purpose: "Retrieve event details directly from its ID", @@ -31,14 +27,12 @@ export class GetEvent extends Tool { "JSON object with complete event details including stack trace (error trace and other threads, if present), metadata, and context", }, ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const response = await this.client.getEvent(params.eventId, project.id); + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const response = await client.getEvent(args.eventId, project.id); return { content: [{ type: "text", text: JSON.stringify(response) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/get-network-endpoint-groupings.ts b/src/bugsnag/tool/performance/get-network-endpoint-groupings.ts index 60ed4eb4..381657d0 100644 --- a/src/bugsnag/tool/performance/get-network-endpoint-groupings.ts +++ b/src/bugsnag/tool/performance/get-network-endpoint-groupings.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -11,8 +7,8 @@ const inputSchema = z.object({ }); // Returns the current network endpoint grouping rules (URL patterns) configured for a project. -export class GetNetworkEndpointGroupings extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Network Endpoint Groupings", summary: "Get the network endpoint grouping rules for a project", purpose: @@ -37,17 +33,16 @@ export class GetNetworkEndpointGroupings extends Tool { ], readOnly: true, idempotent: true, - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const result = - await this.client.projectApi.getProjectNetworkGroupingRuleset(project.id); + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const result = await client.projectApi.getProjectNetworkGroupingRuleset( + project.id, + ); return { content: [ { type: "text", text: JSON.stringify(result.body.endpoints || []) }, ], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/get-span-group.ts b/src/bugsnag/tool/performance/get-span-group.ts index 847ce466..83c2abcf 100644 --- a/src/bugsnag/tool/performance/get-span-group.ts +++ b/src/bugsnag/tool/performance/get-span-group.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -13,8 +9,8 @@ const inputSchema = z.object({ }); // Fetches detailed performance metrics for a span group, including timeline and duration distribution. -export class GetSpanGroup extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Span Group", summary: "Get detailed performance metrics for a specific span group", purpose: "Analyze performance characteristics of a specific operation", @@ -46,30 +42,28 @@ export class GetSpanGroup extends Tool { "IDs are automatically URL-encoded - provide the raw ID", "Statistics include p50, p75, p90, p95, p99 percentiles", ], - }; + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - - const spanGroupResults = await this.client.projectApi.getProjectSpanGroup( + const spanGroupResults = await client.projectApi.getProjectSpanGroup( project.id, - params.spanGroupId, - params.filters, + args.spanGroupId, + args.filters, ); const spanGroupTimelineResult = - await this.client.projectApi.getProjectSpanGroupTimeline( + await client.projectApi.getProjectSpanGroupTimeline( project.id, - params.spanGroupId, - params.filters, + args.spanGroupId, + args.filters, ); const spanGroupDistributionResult = - await this.client.projectApi.getProjectSpanGroupDistribution( + await client.projectApi.getProjectSpanGroupDistribution( project.id, - params.spanGroupId, - params.filters, + args.spanGroupId, + args.filters, ); const result = { @@ -81,5 +75,5 @@ export class GetSpanGroup extends Tool { return { content: [{ type: "text", text: JSON.stringify(result) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/get-trace.ts b/src/bugsnag/tool/performance/get-trace.ts index ec670119..a2bf51b5 100644 --- a/src/bugsnag/tool/performance/get-trace.ts +++ b/src/bugsnag/tool/performance/get-trace.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -20,8 +17,8 @@ const inputSchema = z.object({ }); // Fetches all spans within a trace by trace ID and time window, optionally focused on a target span. -export class GetTrace extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Trace", summary: "Get all spans within a specific trace", purpose: "View the complete trace of operations for a request/transaction", @@ -60,22 +57,20 @@ export class GetTrace extends Tool { "Use from/to parameters to narrow the time window", "targetSpanId can be used to focus on a specific span in the trace", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - if (!params.traceId || !params.from || !params.to) { + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + if (!args.traceId || !args.from || !args.to) { throw new ToolError("traceId, from, and to are required"); } - const result = await this.client.projectApi.listSpansByTraceId( + const result = await client.projectApi.listSpansByTraceId( project.id, - params.traceId, - params.from, - params.to, - params.targetSpanId, - params.perPage, - params.nextUrl, + args.traceId, + args.from, + args.to, + args.targetSpanId, + args.perPage, + args.nextUrl, ); return { content: [ @@ -89,5 +84,5 @@ export class GetTrace extends Tool { }, ], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/list-span-groups.ts b/src/bugsnag/tool/performance/list-span-groups.ts index 2e923f4d..81f930c6 100644 --- a/src/bugsnag/tool/performance/list-span-groups.ts +++ b/src/bugsnag/tool/performance/list-span-groups.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -50,8 +46,8 @@ const inputSchema = z.object({ }); // Lists span groups (operation types) being tracked for performance, with support for sorting and filtering. -export class ListSpanGroups extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Span Groups", summary: "List span groups (operations) tracked for performance monitoring", purpose: "Discover and analyze different operations being monitored", @@ -89,20 +85,18 @@ export class ListSpanGroups extends Tool { "Star important span groups for quick access", "Use nextUrl for pagination", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const result = await this.client.projectApi.listProjectSpanGroups( + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const result = await client.projectApi.listProjectSpanGroups( project.id, - params.sort, - params.direction, - params.perPage, + args.sort, + args.direction, + args.perPage, undefined, - params.filters, - params.starredOnly, - params.nextUrl, + args.filters, + args.starredOnly, + args.nextUrl, ); return { content: [ @@ -116,5 +110,5 @@ export class ListSpanGroups extends Tool { }, ], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/list-spans.ts b/src/bugsnag/tool/performance/list-spans.ts index 84f3472a..278348fe 100644 --- a/src/bugsnag/tool/performance/list-spans.ts +++ b/src/bugsnag/tool/performance/list-spans.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -36,8 +32,8 @@ const inputSchema = z.object({ }); // Lists individual span instances within a span group, with sorting and filtering support. -export class ListSpans extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Spans", summary: "Get individual spans belonging to a span group", purpose: "Examine individual operation instances within a span group", @@ -76,19 +72,17 @@ export class ListSpans extends Tool { "Sort by duration descending to find the slowest instances", "Each span includes trace ID for further investigation", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const result = await this.client.projectApi.listSpansBySpanGroupId( + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const result = await client.projectApi.listSpansBySpanGroupId( project.id, - params.spanGroupId, - params.filters, - params.sort, - params.direction, - params.perPage, - params.nextUrl, + args.spanGroupId, + args.filters, + args.sort, + args.direction, + args.perPage, + args.nextUrl, ); return { content: [ @@ -102,5 +96,5 @@ export class ListSpans extends Tool { }, ], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/list-trace-fields.ts b/src/bugsnag/tool/performance/list-trace-fields.ts index 9e227f81..827ea788 100644 --- a/src/bugsnag/tool/performance/list-trace-fields.ts +++ b/src/bugsnag/tool/performance/list-trace-fields.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -11,8 +7,8 @@ const inputSchema = z.object({ }); // Returns the available custom trace attribute fields for a project, used to build performance filters. -export class ListTraceFields extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Trace Fields", summary: "Get available trace fields/attributes for filtering", purpose: "Discover what custom attributes are available for filtering", @@ -34,15 +30,13 @@ export class ListTraceFields extends Tool { "Trace fields are custom attributes added to spans", "Use these fields for filtering other performance queries", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const traceFields = await this.client.getProjectTraceFields(project); + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const traceFields = await client.getProjectTraceFields(project); return { content: [{ type: "text", text: JSON.stringify(traceFields) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/performance/set-network-endpoint-groupings.ts b/src/bugsnag/tool/performance/set-network-endpoint-groupings.ts index 7ab30cfd..cde8f5b3 100644 --- a/src/bugsnag/tool/performance/set-network-endpoint-groupings.ts +++ b/src/bugsnag/tool/performance/set-network-endpoint-groupings.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -19,8 +15,8 @@ const inputSchema = z.object({ }); // Replaces all network endpoint grouping rules for a project with the provided URL patterns. -export class SetNetworkEndpointGroupings extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Set Network Endpoint Groupings", summary: "Set the network endpoint grouping rules for a project", purpose: @@ -78,16 +74,13 @@ export class SetNetworkEndpointGroupings extends Tool { ], readOnly: false, idempotent: true, - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const result = - await this.client.projectApi.updateProjectNetworkGroupingRuleset( - project.id, - params.endpoints, - ); + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const result = await client.projectApi.updateProjectNetworkGroupingRuleset( + project.id, + args.endpoints, + ); return { content: [ { @@ -95,10 +88,10 @@ export class SetNetworkEndpointGroupings extends Tool { text: JSON.stringify({ success: result.status === 200 || result.status === 204, projectId: project.id, - endpoints: params.endpoints, + endpoints: args.endpoints, }), }, ], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/project/get-current-project.ts b/src/bugsnag/tool/project/get-current-project.ts index 4732cfa4..637c3de6 100644 --- a/src/bugsnag/tool/project/get-current-project.ts +++ b/src/bugsnag/tool/project/get-current-project.ts @@ -1,17 +1,10 @@ -import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ZodRawShape } 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 { toolInputParameters } from "../../input-schemas"; -// import { -// listEnvironmentsQueryParams, -// listEnvironments200Response as listEnvironmentsResponse, -// } from "../../common/rest-api-schemas"; // Returns the currently configured project, or throws if no project is set. -export class GetCurrentProject extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Current Project", summary: "Retrieve the 'current' project on which tools should operate by default. This allows BugSnag tools to be called with no projectId parameter.", @@ -25,10 +18,9 @@ export class GetCurrentProject extends Tool { "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", "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", ], - }; - - handle: ToolCallback = async (_args, _extra) => { - const project = await this.client.getCurrentProject(); + }, + async ({ client }) => { + const project = await client.getCurrentProject(); if (!project) { 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", @@ -37,5 +29,5 @@ export class GetCurrentProject extends Tool { return { content: [{ type: "text", text: JSON.stringify(project) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/project/list-project-event-filters.ts b/src/bugsnag/tool/project/list-project-event-filters.ts index de007b9a..94ae6f7e 100644 --- a/src/bugsnag/tool/project/list-project-event-filters.ts +++ b/src/bugsnag/tool/project/list-project-event-filters.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -11,8 +7,8 @@ const inputSchema = z.object({ }); // Returns the available event filter fields for a project, used to build filter queries for errors and events. -export class ListProjectEventFilters extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Project Event Filters", summary: "Get available event filter fields for a project", purpose: @@ -35,15 +31,13 @@ export class ListProjectEventFilters extends Tool { "Use this tool before the List Errors or Get Error tools to understand available filters", "Look for display_id field in the response - these are the field names to use in filters", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const eventFilters = await this.client.getProjectEventFields( - await this.client.getInputProject(params.projectId), + }, + async ({ client, args }) => { + const eventFilters = await client.getProjectEventFields( + await client.getInputProject(args.projectId), ); return { content: [{ type: "text", text: JSON.stringify(eventFilters) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/project/list-projects.ts b/src/bugsnag/tool/project/list-projects.ts index cd8bb540..7e2ab6e6 100644 --- a/src/bugsnag/tool/project/list-projects.ts +++ b/src/bugsnag/tool/project/list-projects.ts @@ -1,10 +1,7 @@ -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 type { Project } from "../../client/api/index"; +import { ToolError } from "../../../common/tools"; +import { BugsnagClient } from "../../client"; +import type { Project } from "../../client/api"; const inputSchema = z.object({ apiKey: z @@ -14,8 +11,8 @@ const inputSchema = z.object({ }); // Lists all projects the user has access to, optionally filtered by API key. -export class ListProjects extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Projects", summary: "List all projects in the organization that the current user has access to, or find a project matching an API key.", @@ -29,17 +26,15 @@ export class ListProjects extends Tool { hints: [ "Project IDs from this list can be used with other tools when no project API key is configured", ], - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - let projects = await this.client.getProjects(); + }, + async ({ client, args }) => { + let projects = await client.getProjects(); if (!projects || projects.length === 0) { throw new ToolError("No BugSnag projects found for the current user."); } - if (params.apiKey) { + if (args.apiKey) { const matchedProject = projects.find( - (p: Project) => p.api_key === params.apiKey, + (p: Project) => p.api_key === args.apiKey, ); projects = matchedProject ? [matchedProject] : []; } @@ -49,6 +44,7 @@ export class ListProjects extends Tool { }; return { content: [{ type: "text", text: JSON.stringify(content) }], + structuredContent: content, }; - }; -} + }, +); diff --git a/src/bugsnag/tool/release/get-build.ts b/src/bugsnag/tool/release/get-build.ts index 4f030d41..9ae15cb0 100644 --- a/src/bugsnag/tool/release/get-build.ts +++ b/src/bugsnag/tool/release/get-build.ts @@ -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 { toolInputParameters } from "../../input-schemas"; const inputSchema = z.object({ @@ -12,8 +9,8 @@ const inputSchema = z.object({ }); // Fetches a single build by ID with stability metrics appended. -export class GetBuild extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Build", summary: "Get more details for a specific build by its ID", purpose: @@ -39,21 +36,19 @@ export class GetBuild extends Tool { idempotent: true, outputDescription: "JSON object containing build details along with stability metrics such as user and session stability, and whether it meets project targets", - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const response = await this.client.projectApi.getProjectReleaseById( + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const response = await client.projectApi.getProjectReleaseById( project.id, - params.buildId, + args.buildId, ); if (!response.body) - throw new ToolError(`No build for ${params.buildId} found.`); - const build = this.client.addStabilityData(response.body, project); + throw new ToolError(`No build for ${args.buildId} found.`); + const build = client.addStabilityData(response.body, project); return { content: [{ type: "text", text: JSON.stringify(build) }], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/release/get-release.ts b/src/bugsnag/tool/release/get-release.ts index 393627b1..54f1ed32 100644 --- a/src/bugsnag/tool/release/get-release.ts +++ b/src/bugsnag/tool/release/get-release.ts @@ -1,10 +1,7 @@ -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 type { Build } from "../../client/api/index"; +import { ToolError } from "../../../common/tools"; +import { BugsnagClient } from "../../client"; +import type { Build } from "../../client/api"; import { toolInputParameters } from "../../input-schemas"; interface StabilityData { @@ -23,8 +20,8 @@ const inputSchema = z.object({ }); // Fetches a release by ID including its builds, with stability metrics appended to each. -export class GetRelease extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "Get Release", summary: "Get more details for a specific release by its ID, including source control information and associated builds", @@ -51,25 +48,23 @@ export class GetRelease extends Tool { idempotent: true, outputDescription: "JSON object containing release details along with stability metrics such as user and session stability, and whether it meets project targets", - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const releaseResponse = await this.client.projectApi.getReleaseGroup( - params.releaseId, + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const releaseResponse = await client.projectApi.getReleaseGroup( + args.releaseId, ); if (!releaseResponse.body) - throw new ToolError(`No release for ${params.releaseId} found.`); - const release = this.client.addStabilityData(releaseResponse.body, project); + throw new ToolError(`No release for ${args.releaseId} found.`); + const release = client.addStabilityData(releaseResponse.body, project); let builds: (Build & StabilityData)[] = []; if (releaseResponse.body) { - const buildsResponse = await this.client.projectApi.listBuildsInRelease( - params.releaseId, + const buildsResponse = await client.projectApi.listBuildsInRelease( + args.releaseId, ); if (buildsResponse.body) { builds = buildsResponse.body.map((b) => - this.client.addStabilityData(b, project), + client.addStabilityData(b, project), ); } } @@ -84,5 +79,5 @@ export class GetRelease extends Tool { }, ], }; - }; -} + }, +); diff --git a/src/bugsnag/tool/release/list-releases.ts b/src/bugsnag/tool/release/list-releases.ts index 61eba40f..beb7672e 100644 --- a/src/bugsnag/tool/release/list-releases.ts +++ b/src/bugsnag/tool/release/list-releases.ts @@ -1,10 +1,6 @@ -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 type { Release } from "../../client/api/index"; +import { BugsnagClient } from "../../client"; +import type { Release } from "../../client/api"; import { toolInputParameters } from "../../input-schemas"; interface StabilityData { @@ -31,8 +27,8 @@ const inputSchema = z.object({ }); // Lists release groups for a project with stability metrics appended to each result. -export class ListReleases extends Tool { - specification: ToolParams = { +export default BugsnagClient.createTool( + { title: "List Releases", summary: "List releases for a project", purpose: @@ -74,25 +70,21 @@ export class ListReleases extends Tool { idempotent: true, outputDescription: "JSON array of release summary objects with metadata, with a URL to the next page if more results are available", - }; - - handle: ToolCallback = async (args, _extra) => { - const params = inputSchema.parse(args); - const project = await this.client.getInputProject(params.projectId); - const response = await this.client.projectApi.listProjectReleaseGroups( + }, + async ({ client, args }) => { + const project = await client.getInputProject(args.projectId); + const response = await client.projectApi.listProjectReleaseGroups( project.id, - params.releaseStage, + args.releaseStage, false, // Not top-only - params.visibleOnly, - params.perPage, - params.nextUrl, + args.visibleOnly, + args.perPage, + args.nextUrl, ); let releases: (Release & StabilityData)[] = []; if (response.body) { - releases = response.body.map((r) => - this.client.addStabilityData(r, project), - ); + releases = response.body.map((r) => client.addStabilityData(r, project)); } return { @@ -108,5 +100,5 @@ export class ListReleases extends Tool { }, ], }; - }; -} + }, +); diff --git a/src/common/bugsnag.ts b/src/common/bugsnag.ts index 95549708..93fd528f 100644 --- a/src/common/bugsnag.ts +++ b/src/common/bugsnag.ts @@ -1,3 +1,4 @@ // workaround for a known issue with Bugsnag types in node16 modules: https://github.com/bugsnag/bugsnag-js/issues/2052 import * as Bugsnag from "@bugsnag/js"; +// @ts-expect-error export default Bugsnag.default as unknown as typeof Bugsnag.default.default; diff --git a/src/common/resources.ts b/src/common/resources.ts new file mode 100644 index 00000000..7669222b --- /dev/null +++ b/src/common/resources.ts @@ -0,0 +1,49 @@ +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { + ReadResourceResult, + ServerNotification, + ServerRequest, +} from "@modelcontextprotocol/sdk/types.js"; +import type { Client, RegisterResourceFunction } from "./types"; + +/** + * Turn a path template into an object type of the path parameters + * @example + * type T = ExtractPathParams<'/foo/{bar}'>; + * // => { bar: string } + */ +export type ExtractPathParams = + T extends `${string}{${infer Param}}/${infer Rest}` + ? ExtractPathParams extends void + ? { [k in Param]: string } + : { [k in Param | keyof ExtractPathParams]: string } + : T extends `${string}{${infer Param}}` + ? { [k in Param]: string } + : undefined; + +export interface ResourceConfig { + name: Name; + path: Path; +} + +export type ResourceTemplateCallback< + T extends Client, + Path extends string, +> = (args: { + client: T; + uri: URL; + variables: ExtractPathParams; + extra: RequestHandlerExtra; +}) => ReadResourceResult | Promise; + +/** + * A resource that can be registered to the MCP server + */ +export interface Resource< + T extends Client, + Config extends ResourceConfig, +> { + config: Config; + handle: ResourceTemplateCallback; + register(client: T, register: RegisterResourceFunction): void; +} diff --git a/src/common/server.ts b/src/common/server.ts index 92fcfe12..3f20fc16 100644 --- a/src/common/server.ts +++ b/src/common/server.ts @@ -172,7 +172,7 @@ export class SmartBearMcpServer extends McpServer { ); if (client.registerResources) { - client.registerResources((name, path, cb) => { + await client.registerResources((name, path, cb) => { const url = `${client.toolPrefix}://${name}/${path}`; return super.registerResource( name, diff --git a/src/common/tools.ts b/src/common/tools.ts index 95c481f0..1732a88a 100644 --- a/src/common/tools.ts +++ b/src/common/tools.ts @@ -1,14 +1,22 @@ import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ZodRawShape } from "zod"; -import type { Client, ToolParams } from "./types"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { ZodRawShape, ZodType, z } from "zod"; +import type { + Client, + GetInputFunction, + RegisterToolsFunction, + SnakeCase, + ToolParams, + ToolParamsWithInputSchema, +} from "./types"; /** - * Error class for tool-specific errors – these result in a response to the LLM with `isError: true` + * Error class for tool-specific errors – these result in a response to the LLM with `isError: true` * and are not reported to BugSnag */ export class ToolError extends Error { // can be used to set misc properties like response status code, etc. - public metadata?: Map | undefined; + public metadata: Map | undefined; constructor( cause?: string | undefined, @@ -32,3 +40,65 @@ export abstract class Tool { abstract specification: ToolParams; abstract handle: ToolCallback; } + +/** + * Extract the structuredContent type from a CallToolResult + */ +type ExtractStructuredContent = T extends Promise + ? U extends { structuredContent?: infer S } + ? S + : never + : T extends { structuredContent?: infer S } + ? S + : never; + +export type ToolInput = T extends TypesafeTool + ? C extends { inputSchema: infer I } + ? z.infer + : never + : never; + +export type ToolOutput = T extends TypesafeTool + ? C extends { outputSchema: infer O } + ? z.infer + : H extends (...args: any[]) => infer R + ? ExtractStructuredContent + : never + : never; + +export interface ToolArgs { + client: T; + getInput: GetInputFunction; + args: Parameters>[0]; + extra: Parameters>[1]; +} + +/** + * Make the structuredContent inferable + */ +interface Result + extends CallToolResult { + structuredContent?: T; +} + +export type ToolHandler = ( + args: ToolArgs, +) => Promise; + +/** + * Represents a type-safe tool that can be registered to the MCP server via a client. + */ +export interface TypesafeTool< + T extends Client, + Config extends ToolParamsWithInputSchema, + Handler extends ToolHandler, +> { + name: SnakeCase; + config: Config; + handle: Handler; + register( + client: T, + register: RegisterToolsFunction, + getInput: GetInputFunction, + ): void; +} diff --git a/src/common/types.ts b/src/common/types.ts index 097423ec..7f54f006 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -12,13 +12,46 @@ import type { ElicitResult, } from "@modelcontextprotocol/sdk/types.js"; import type { ZodObject, ZodRawShape, ZodType } from "zod"; +import type { + ExtractPathParams, + Resource, + ResourceConfig, + ResourceTemplateCallback, +} from "./resources.ts"; import type { SmartBearMcpServer } from "./server"; +import type { ToolHandler, TypesafeTool } from "./tools.ts"; + +/** + * Replace all occurrences of a substring. + * StringReplace<"hello world", "l", "y"> // "heyyo woryd" + */ +export type StringReplace< + TString extends string, + TToReplace extends string, + TReplacement extends string, +> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}` + ? `${TPrefix}${TReplacement}${StringReplace}` + : TString; + +/** + * SnakeCase<"Hello World !"> // "hello_world_!" + */ +export type SnakeCase = StringReplace< + Lowercase, + " ", + "_" +>; -export interface ToolParams { - title: string; +export type ToolParams< + Title extends string = string, + InputSchema extends ZodType = ZodType, + OutputSchema extends ZodType = ZodType, +> = { + title: Title; summary: string; + /** @deprecated Use `inputSchema` instead to define structured input parameters */ parameters?: Parameters; // either 'parameters' or an 'inputSchema' should be present - inputSchema?: ZodType; + inputSchema?: InputSchema; /** * Specifies the type of object returned by the tool.
* When `outputSchema` is specified, make sure the tool returns `structuredContent` in its callback.
@@ -26,7 +59,7 @@ export interface ToolParams { * * https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema */ - outputSchema?: ZodType; + outputSchema?: OutputSchema; purpose?: string; useCases?: string[]; examples?: Array<{ @@ -40,7 +73,15 @@ export interface ToolParams { destructive?: boolean; idempotent?: boolean; openWorld?: boolean; -} +}; + +export type ToolParamsWithInputSchema< + Title extends string = string, + InputSchema extends ZodType = ZodType, + OutputSchema extends ZodType = ZodType, +> = Omit, "parameters"> & { + inputSchema: InputSchema; +}; export interface PromptParams { name: string; @@ -87,31 +128,135 @@ export type Parameters = Array<{ constraints?: string[]; }>; -export interface Client { +export abstract class Client { /** Human-readable name for the client - usually the product name - used to prefix tool names */ - name: string; + abstract name: string; /** Prefix for tool IDs */ - toolPrefix: string; + abstract toolPrefix: string; /** Prefix for configuration (environment variables and http headers) */ - configPrefix: string; + abstract configPrefix: string; /** * Zod schema defining configuration fields for this client * Field names must use snake case to ensure they are mapped to environment variables and HTTP headers correctly. * e.g., `config.my_property` would refer to the environment variable `TOOL_MY_PROPERTY`, http header `Tool-My-Property` */ - config: ZodObject<{ + abstract config: ZodObject<{ [key: string]: ZodType; }>; + /** * Configure the client with the given server and configuration */ - configure: (server: SmartBearMcpServer, config: any) => Promise; - isConfigured: () => boolean; - registerTools( + abstract configure(server: SmartBearMcpServer, config: any): Promise; + abstract isConfigured(): boolean; + abstract registerTools( register: RegisterToolsFunction, getInput: GetInputFunction, ): Promise; - registerResources?(register: RegisterResourceFunction): void; + registerResources?(register: RegisterResourceFunction): Promise | void; registerPrompts?(register: RegisterPromptFunction): void; cleanupSession?(mcpSessionId: string): Promise; + + /** + * Create a new type-safe tool that can be registered to the MCP server. + * + * @example + * ```typescript + * const myTool = MyClient.createTool( + * { + * title: "My Tool", + * summary: "Does something useful", + * inputSchema: z.object({ id: z.string() }), + * }, + * async ({ client, args }) => { + * const result = await client.doSomething(args.id); + * return { + * content: [{ type: "text", text: JSON.stringify(result) }], + * structuredContent: result, // This type is automatically inferred + * }; + * } + * ); + * + * // Access inferred types: + * type Name = typeof myTool.name; // "my_tool" + * type Input = ToolInput; // { id: string } + * type Output = ToolOutput; // typeof result + * ``` + */ + static createTool< + T extends InstanceType, + const Config extends ToolParamsWithInputSchema, + Handler extends ToolHandler, + >( + this: new ( + ...args: any[] + ) => T, + config: Config, + handle: Handler, + ): TypesafeTool { + return { + name: config.title.toLowerCase().replaceAll(/\s+/g, "_") as SnakeCase< + Config["title"] + >, + config, + handle, + + register( + client: T, + register: RegisterToolsFunction, + getInput: GetInputFunction, + ) { + register(config, async (args, extra) => { + const parsedArgs = config.inputSchema.parse(args); + return handle({ client, getInput, args: parsedArgs, extra }); + }); + }, + }; + } + + /** + * Create a new resource template that can be registered to the MCP server. + * + * @example + * ```typescript + * const myResource = MyClient.createResource( + * { + * name: "user", + * path: "{userId}" + * }, + * async ({ client, uri, variables }) => { + * const userData = await client.getUser(variables.userId); + * return { + * content: [{ type: "text", text: JSON.stringify(userData) }], + * }; + * } + * ); + * ``` + */ + static createResource< + T extends InstanceType, + const Config extends ResourceConfig, + >( + this: new ( + ...args: any[] + ) => T, + config: Config, + handle: ResourceTemplateCallback, + ): Resource { + return { + config, + handle, + + register(client: T, register: RegisterResourceFunction) { + register(config.name, config.path, async (uri, variables, extra) => + handle({ + client, + uri, + variables: variables as ExtractPathParams, + extra, + }), + ); + }, + }; + } } diff --git a/src/tests/unit/bugsnag/client.test.ts b/src/tests/unit/bugsnag/client.test.ts index e2eec5af..101ddd7a 100644 --- a/src/tests/unit/bugsnag/client.test.ts +++ b/src/tests/unit/bugsnag/client.test.ts @@ -152,6 +152,8 @@ describe("BugsnagClient", () => { let clientWithNoApiKey: BugsnagClient; beforeEach(async () => { + await createConfiguredClient(); + vi.clearAllMocks(); // Reset mock implementations to ensure no persistent return values affect tests mockCache.get.mockReset(); @@ -718,11 +720,12 @@ describe("BugsnagClient", () => { }); it("should register common tools", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const registeredTools = registerToolsSpy.mock.calls.map( (call: any) => call[0].title, ); + console.log(registeredTools); expect(registeredTools).toContain("Get Current Project"); expect(registeredTools).toContain("List Projects"); expect(registeredTools).toContain("Get Error"); @@ -753,7 +756,7 @@ describe("BugsnagClient", () => { }); it("should register event resource", async () => { - client.registerResources(registerResourcesSpy); + await client.registerResources(registerResourcesSpy); expect(registerResourcesSpy).toHaveBeenCalledWith( "event", @@ -795,7 +798,10 @@ describe("BugsnagClient", () => { totalCount: 1, }); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Project Errors", )[1]; @@ -828,7 +834,10 @@ describe("BugsnagClient", () => { const mockProjects = [getMockProject("proj-1", "Project 1")]; mockCache.get.mockReturnValue(mockProjects); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Projects", )[1]; @@ -845,7 +854,10 @@ describe("BugsnagClient", () => { it("should handle no projects found", async () => { mockCache.get.mockReturnValue([]); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Projects", )[1]; @@ -862,7 +874,10 @@ describe("BugsnagClient", () => { ]; mockCache.get.mockReturnValue(mockProjects); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Projects", )[1]; @@ -879,7 +894,10 @@ describe("BugsnagClient", () => { const mockProjects = [getMockProject("proj-1", "Project 1", "key-1")]; mockCache.get.mockReturnValue(mockProjects); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Projects", )[1]; @@ -935,7 +953,7 @@ describe("BugsnagClient", () => { body: mockPivots, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Error", )[1]; @@ -978,7 +996,7 @@ describe("BugsnagClient", () => { body: [], }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Error", )[1]; @@ -1004,7 +1022,7 @@ describe("BugsnagClient", () => { .mockReturnValueOnce(mockProject) .mockReturnValueOnce(mockOrg); mockErrorAPI.viewErrorOnProject.mockResolvedValue({ body: null }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Error", )[1]; @@ -1029,7 +1047,7 @@ describe("BugsnagClient", () => { mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Event", )[1]; @@ -1060,7 +1078,7 @@ describe("BugsnagClient", () => { mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Event Details From Dashboard URL", @@ -1078,7 +1096,7 @@ describe("BugsnagClient", () => { }); it("should throw error when link is invalid", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Event Details From Dashboard URL", @@ -1092,7 +1110,7 @@ describe("BugsnagClient", () => { getMockProject("proj-1", "Other Project", "other-project"), ]); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Event Details From Dashboard URL", @@ -1106,7 +1124,7 @@ describe("BugsnagClient", () => { }); it("should throw error when URL is missing required parameters", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Event Details From Dashboard URL", @@ -1149,7 +1167,7 @@ describe("BugsnagClient", () => { totalCount: 1, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Project Errors", )[1]; @@ -1202,7 +1220,7 @@ describe("BugsnagClient", () => { totalCount: 3, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Project Errors", )[1]; @@ -1247,7 +1265,7 @@ describe("BugsnagClient", () => { .mockReturnValueOnce(mockProject) .mockReturnValueOnce(mockEventFields); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Project Errors", )[1]; @@ -1260,7 +1278,10 @@ describe("BugsnagClient", () => { it("should throw error when no project ID available", async () => { mockCache.get.mockReturnValueOnce(null); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Project Errors", )[1]; @@ -1285,7 +1306,7 @@ describe("BugsnagClient", () => { .mockReturnValueOnce(mockProject) .mockReturnValueOnce(mockEventFields); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Project Event Filters", )[1]; @@ -1306,7 +1327,7 @@ describe("BugsnagClient", () => { "test-project-key", ); mockCache.get.mockReturnValueOnce(mockProject); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Current Project", )[1]; @@ -1316,7 +1337,7 @@ describe("BugsnagClient", () => { it("should throw error when no current project is configured", async () => { mockCache.get.mockReturnValueOnce(null); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Current Project", )[1]; @@ -1363,7 +1384,7 @@ describe("BugsnagClient", () => { mockProjectAPI.getProjectReleaseById.mockResolvedValue({ body: basicBuild, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Build", )[1]; @@ -1401,7 +1422,7 @@ describe("BugsnagClient", () => { mockProjectAPI.getProjectReleaseById.mockResolvedValue({ body: basicBuild, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Build", )[1]; @@ -1448,7 +1469,7 @@ describe("BugsnagClient", () => { mockProjectAPI.getProjectReleaseById.mockResolvedValue({ body: basicBuild, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Build", )[1]; @@ -1489,7 +1510,7 @@ describe("BugsnagClient", () => { body: basicBuild, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Build", )[1]; @@ -1523,7 +1544,7 @@ describe("BugsnagClient", () => { .mockReturnValueOnce([mockProjects[0]]); mockProjectAPI.getProjectReleaseById.mockResolvedValue({ body: null }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Build", )[1]; @@ -1536,7 +1557,7 @@ describe("BugsnagClient", () => { it("should throw error when no project ID available", async () => { mockCache.get.mockReturnValue(null); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Build", )[1]; @@ -1579,7 +1600,7 @@ describe("BugsnagClient", () => { body: mockReleases, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Releases", )[1]; @@ -1635,7 +1656,7 @@ describe("BugsnagClient", () => { body: mockReleases, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Releases", )[1]; @@ -1678,7 +1699,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValueOnce(mockProjects[0]); mockProjectAPI.listProjectReleaseGroups.mockResolvedValue({ body: [] }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Releases", )[1]; @@ -1703,7 +1724,7 @@ describe("BugsnagClient", () => { it("should throw error when no project ID available", async () => { mockCache.get.mockReturnValue(null); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Releases", )[1]; @@ -1760,7 +1781,7 @@ describe("BugsnagClient", () => { body: mockBuildsInRelease, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Release", )[1]; @@ -1807,7 +1828,7 @@ describe("BugsnagClient", () => { .mockReturnValueOnce(null); mockProjectAPI.getReleaseGroup.mockResolvedValue({ body: null }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Release", )[1]; @@ -1825,7 +1846,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -1852,7 +1873,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -1876,7 +1897,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -1908,7 +1929,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -1940,7 +1961,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -1972,7 +1993,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2006,7 +2027,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2032,7 +2053,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProjects); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 204 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2058,7 +2079,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2090,7 +2111,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2132,7 +2153,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2154,7 +2175,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValue(mockProject); mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 400 }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2170,7 +2191,7 @@ describe("BugsnagClient", () => { it("should throw error when no project found", async () => { mockCache.get.mockReturnValue(null); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2190,7 +2211,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValueOnce(null).mockReturnValueOnce(mockOrg); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const toolHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Update Error", )[1]; @@ -2220,7 +2241,7 @@ describe("BugsnagClient", () => { nextUrl: null, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const listSpanGroupsHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Span Groups", @@ -2273,7 +2294,7 @@ describe("BugsnagClient", () => { nextUrl: "/next", }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const listSpanGroupsHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Span Groups", @@ -2337,7 +2358,7 @@ describe("BugsnagClient", () => { body: mockDistribution, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const getSpanGroupHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Span Group", @@ -2411,7 +2432,7 @@ describe("BugsnagClient", () => { nextUrl: null, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const listSpansHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Spans", @@ -2470,7 +2491,7 @@ describe("BugsnagClient", () => { nextUrl: null, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const getTraceHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Trace", @@ -2528,7 +2549,7 @@ describe("BugsnagClient", () => { body: mockTraceFields, }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const listTraceFieldsHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Trace Fields", @@ -2571,7 +2592,7 @@ describe("BugsnagClient", () => { return undefined; }); - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const listTraceFieldsHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Trace Fields", @@ -2610,7 +2631,10 @@ describe("BugsnagClient", () => { body: mockTraceFields, }); - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const listTraceFieldsHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "List Trace Fields", @@ -2642,7 +2666,7 @@ describe("BugsnagClient", () => { describe("Get Network Endpoint Groupings tool handler", () => { it("should get network grouping rules with project from cache", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const getNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Network Endpoint Groupings", )?.[1]; @@ -2673,7 +2697,10 @@ describe("BugsnagClient", () => { }); it("should get network grouping rules with explicit project ID", async () => { - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const getNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Network Endpoint Groupings", )?.[1]; @@ -2706,7 +2733,7 @@ describe("BugsnagClient", () => { }); it("should return empty array when no endpoints configured", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const getNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Network Endpoint Groupings", )?.[1]; @@ -2728,7 +2755,10 @@ describe("BugsnagClient", () => { }); it("should throw error when no project ID available", async () => { - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const getNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Get Network Endpoint Groupings", )?.[1]; @@ -2743,7 +2773,7 @@ describe("BugsnagClient", () => { describe("Set Network Endpoint Groupings tool handler", () => { it("should update network grouping rules with project from cache", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const setNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Set Network Endpoint Groupings", )?.[1]; @@ -2777,7 +2807,10 @@ describe("BugsnagClient", () => { }); it("should update network grouping rules with explicit project ID", async () => { - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const setNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Set Network Endpoint Groupings", )?.[1]; @@ -2815,7 +2848,7 @@ describe("BugsnagClient", () => { }); it("should handle 204 status as success", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const setNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Set Network Endpoint Groupings", )?.[1]; @@ -2836,7 +2869,7 @@ describe("BugsnagClient", () => { }); it("should update with empty endpoints array", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const setNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Set Network Endpoint Groupings", )?.[1]; @@ -2861,7 +2894,7 @@ describe("BugsnagClient", () => { }); it("should handle complex endpoint patterns", async () => { - client.registerTools(registerToolsSpy, getInputFunctionSpy); + await client.registerTools(registerToolsSpy, getInputFunctionSpy); const setNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Set Network Endpoint Groupings", )?.[1]; @@ -2890,7 +2923,10 @@ describe("BugsnagClient", () => { }); it("should throw error when no project ID available", async () => { - clientWithNoApiKey.registerTools(registerToolsSpy, getInputFunctionSpy); + await clientWithNoApiKey.registerTools( + registerToolsSpy, + getInputFunctionSpy, + ); const setNetworkGroupingHandler = registerToolsSpy.mock.calls.find( (call: any) => call[0].title === "Set Network Endpoint Groupings", )?.[1]; @@ -2919,7 +2955,7 @@ describe("BugsnagClient", () => { mockCache.get.mockReturnValueOnce(mockProjects); mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent }); - client.registerResources(registerResourcesSpy); + await client.registerResources(registerResourcesSpy); const resourceHandler = registerResourcesSpy.mock.calls[0][2]; const result = await resourceHandler(