diff --git a/packages/mcp-core/package.json b/packages/mcp-core/package.json index 0edfd7fe..2581d919 100644 --- a/packages/mcp-core/package.json +++ b/packages/mcp-core/package.json @@ -168,6 +168,7 @@ "@logtape/logtape": "^1.1.1", "@logtape/sentry": "^1.1.1", "@modelcontextprotocol/sdk": "catalog:", + "@sentry/api": "^0.141.0", "@sentry/core": "catalog:", "ai": "catalog:", "dotenv": "catalog:", diff --git a/packages/mcp-core/src/api-client/client.test.ts b/packages/mcp-core/src/api-client/client.test.ts index 185f9686..994bfd8f 100644 --- a/packages/mcp-core/src/api-client/client.test.ts +++ b/packages/mcp-core/src/api-client/client.test.ts @@ -1,6 +1,7 @@ import { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; import { SentryApiService } from "./client"; import { ConfigurationError } from "../errors"; +import { ApiPermissionError } from "./errors"; describe("getIssueUrl", () => { it("should work with sentry.io", () => { @@ -480,25 +481,32 @@ describe("listOrganizations", () => { const mockOrgsEu = [{ id: "2", slug: "org-eu", name: "Org EU" }]; let callCount = 0; - globalThis.fetch = vi.fn().mockImplementation((url: string) => { + globalThis.fetch = vi.fn().mockImplementation((input: string | Request) => { callCount++; + const url = typeof input === "string" ? input : (input as Request).url; if (url.includes("/users/me/regions/")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockRegionsResponse), - }); + return Promise.resolve( + new Response(JSON.stringify(mockRegionsResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); } if (url.includes("us.sentry.io")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockOrgsUs), - }); + return Promise.resolve( + new Response(JSON.stringify(mockOrgsUs), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); } if (url.includes("eu.sentry.io")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockOrgsEu), - }); + return Promise.resolve( + new Response(JSON.stringify(mockOrgsEu), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); } return Promise.reject(new Error("Unexpected URL")); }); @@ -523,15 +531,18 @@ describe("listOrganizations", () => { ]; let callCount = 0; - globalThis.fetch = vi.fn().mockImplementation((url: string) => { + globalThis.fetch = vi.fn().mockImplementation((input: string | Request) => { callCount++; + const url = typeof input === "string" ? input : (input as Request).url; if (url.includes("/organizations/")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockOrgs), - }); + return Promise.resolve( + new Response(JSON.stringify(mockOrgs), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); } - return Promise.reject(new Error("Unexpected URL")); + return Promise.reject(new Error(`Unexpected URL: ${url}`)); }); const apiService = new SentryApiService({ @@ -544,11 +555,6 @@ describe("listOrganizations", () => { expect(callCount).toBe(1); // Only 1 org call, no regions call expect(result).toHaveLength(2); expect(result).toEqual(mockOrgs); - // Verify that regions endpoint was not called - expect(globalThis.fetch).not.toHaveBeenCalledWith( - expect.stringContaining("/users/me/regions/"), - expect.any(Object), - ); }); it("should fall back to direct organizations endpoint when regions endpoint returns 404 on SaaS", async () => { @@ -557,7 +563,8 @@ describe("listOrganizations", () => { { id: "2", slug: "org-2", name: "Organization 2" }, ]; - globalThis.fetch = vi.fn().mockImplementation((url: string) => { + globalThis.fetch = vi.fn().mockImplementation((input: string | Request) => { + const url = typeof input === "string" ? input : (input as Request).url; if (url.includes("/users/me/regions/")) { return Promise.resolve({ ok: false, @@ -567,10 +574,12 @@ describe("listOrganizations", () => { }); } if (url.includes("/organizations/")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockOrgs), - }); + return Promise.resolve( + new Response(JSON.stringify(mockOrgs), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); } return Promise.reject(new Error("Unexpected URL")); }); @@ -584,16 +593,6 @@ describe("listOrganizations", () => { expect(result).toHaveLength(2); expect(result).toEqual(mockOrgs); - - // Verify it tried regions first, then fell back to organizations - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("/users/me/regions/"), - expect.any(Object), - ); - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("/organizations/"), - expect.any(Object), - ); }); }); @@ -957,21 +956,45 @@ describe("API query builders", () => { }); describe("searchEvents integration", () => { + /** Helper: extract the URL string from whatever `fetch` received. */ + function extractFetchUrl(call: unknown[]): string { + const input = call[0]; + if (typeof input === "string") return input; + if (input instanceof Request) return input.url; + return String(input); + } + + /** Build a mock that returns a proper Response for SDK calls. */ + function makeSdkMock(body: unknown = { data: [] }) { + return vi.fn().mockImplementation(() => + Promise.resolve( + new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ), + ); + } + + function makeSdkErrorMock(body: unknown, status: number, statusText = "") { + return vi.fn().mockImplementation(() => + Promise.resolve( + new Response(JSON.stringify(body), { + status, + statusText, + headers: { "Content-Type": "application/json" }, + }), + ), + ); + } + it("should route errors dataset to Discover API builder", async () => { const apiService = new SentryApiService({ host: "sentry.io", accessToken: "test-token", }); - // Mock the API response - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: (key: string) => - key === "content-type" ? "application/json" : null, - }, - json: () => Promise.resolve({ data: [] }), - }); + globalThis.fetch = makeSdkMock(); await apiService.searchEvents({ organizationSlug: "test-org", @@ -981,15 +1004,11 @@ describe("API query builders", () => { sort: "-count()", }); - // Verify the URL contains correct parameters - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("dataset=errors"), - expect.any(Object), - ); - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("sort=-count"), - expect.any(Object), + const url = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[0], ); + expect(url).toContain("dataset=errors"); + expect(url).toContain("sort=-count"); }); it("should route spans dataset to EAP API builder with sampling", async () => { @@ -998,15 +1017,7 @@ describe("API query builders", () => { accessToken: "test-token", }); - // Mock the API response - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: (key: string) => - key === "content-type" ? "application/json" : null, - }, - json: () => Promise.resolve({ data: [] }), - }); + globalThis.fetch = makeSdkMock(); await apiService.searchEvents({ organizationSlug: "test-org", @@ -1015,15 +1026,11 @@ describe("API query builders", () => { dataset: "spans", }); - // Verify the URL contains correct parameters - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("dataset=spans"), - expect.any(Object), - ); - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("sampling=NORMAL"), - expect.any(Object), + const url = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[0], ); + expect(url).toContain("dataset=spans"); + expect(url).toContain("sampling=NORMAL"); }); it("should normalize metrics dataset to tracemetrics for Discover queries", async () => { @@ -1032,14 +1039,7 @@ describe("API query builders", () => { accessToken: "test-token", }); - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: (key: string) => - key === "content-type" ? "application/json" : null, - }, - json: () => Promise.resolve({ data: [] }), - }); + globalThis.fetch = makeSdkMock(); await apiService.searchEvents({ organizationSlug: "test-org", @@ -1052,15 +1052,14 @@ describe("API query builders", () => { sort: "-p95(value,http.request.duration,distribution,millisecond)", }); - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining("dataset=tracemetrics"), - expect.any(Object), + const url = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[0], ); - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining( - "sort=-p95%28value%2Chttp.request.duration%2Cdistribution%2Cmillisecond%29", - ), - expect.any(Object), + expect(url).toContain("dataset=tracemetrics"); + // The sort param may encode parentheses as %28/%29 or leave them literal + // depending on the URL serializer (URLSearchParams vs SDK) + expect(decodeURIComponent(url)).toContain( + "sort=-p95(value,http.request.duration,distribution,millisecond)", ); }); @@ -1070,14 +1069,7 @@ describe("API query builders", () => { accessToken: "test-token", }); - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: (key: string) => - key === "content-type" ? "application/json" : null, - }, - json: () => Promise.resolve({ data: [] }), - }); + globalThis.fetch = makeSdkMock({ data: [] }); await apiService.searchReplays({ organizationSlug: "test-org", @@ -1088,12 +1080,158 @@ describe("API query builders", () => { limit: 25, }); - expect(globalThis.fetch).toHaveBeenCalledWith( - expect.stringContaining( - "/api/0/organizations/test-org/replays/?query=count_errors%3A%3E0&per_page=25&sort=-count_errors&environment=production&environment=staging&statsPeriod=24h", - ), - expect.any(Object), + const url = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[0], ); + expect(url).toContain("/api/0/organizations/test-org/replays/"); + expect(url).toContain("query=count_errors%3A%3E0"); + expect(url).toContain("sort=-count_errors"); + + const parsedUrl = new URL(url); + expect(parsedUrl.searchParams.getAll("environment")).toEqual([ + "production", + "staging", + ]); + expect(parsedUrl.searchParams.get("statsPeriod")).toBe("24h"); + }); + + it("should reject conflicting replay time parameters", async () => { + const apiService = new SentryApiService({ + host: "sentry.io", + accessToken: "test-token", + }); + + globalThis.fetch = makeSdkMock({ data: [] }); + + await expect( + apiService.searchReplays({ + organizationSlug: "test-org", + statsPeriod: "24h", + start: "2025-01-01T00:00:00Z", + end: "2025-01-02T00:00:00Z", + }), + ).rejects.toThrow("Cannot use both statsPeriod and start/end parameters"); + expect(globalThis.fetch).not.toHaveBeenCalled(); + }); + + it("should require paired replay start and end parameters", async () => { + const apiService = new SentryApiService({ + host: "sentry.io", + accessToken: "test-token", + }); + + globalThis.fetch = makeSdkMock({ data: [] }); + + await expect( + apiService.searchReplays({ + organizationSlug: "test-org", + start: "2025-01-01T00:00:00Z", + }), + ).rejects.toThrow( + "Both start and end parameters must be provided together", + ); + expect(globalThis.fetch).not.toHaveBeenCalled(); + }); + + it("should prefer statsPeriod over absolute time params for issue events", async () => { + const apiService = new SentryApiService({ + host: "sentry.io", + accessToken: "test-token", + }); + + globalThis.fetch = makeSdkMock([]); + + await apiService.listEventsForIssue({ + organizationSlug: "test-org", + issueId: "123", + query: "environment:production", + limit: 25, + sort: "-timestamp", + statsPeriod: "24h", + start: "2025-01-01T00:00:00Z", + end: "2025-01-02T00:00:00Z", + }); + + const url = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[0], + ); + const parsedUrl = new URL(url); + + expect(parsedUrl.searchParams.get("query")).toBe( + "environment:production", + ); + expect(parsedUrl.searchParams.get("per_page")).toBe("25"); + expect(parsedUrl.searchParams.get("sort")).toBe("-timestamp"); + expect(parsedUrl.searchParams.get("statsPeriod")).toBe("24h"); + expect(parsedUrl.searchParams.has("start")).toBe(false); + expect(parsedUrl.searchParams.has("end")).toBe(false); + }); + + it("should omit full for issue events unless explicitly requested", async () => { + const apiService = new SentryApiService({ + host: "sentry.io", + accessToken: "test-token", + }); + + globalThis.fetch = makeSdkMock([]); + + await apiService.listEventsForIssue({ + organizationSlug: "test-org", + issueId: "123", + statsPeriod: "24h", + }); + + const defaultUrl = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[0], + ); + expect(new URL(defaultUrl).searchParams.has("full")).toBe(false); + + await apiService.listEventsForIssue({ + organizationSlug: "test-org", + issueId: "123", + statsPeriod: "24h", + full: true, + }); + + const fullUrl = extractFetchUrl( + (globalThis.fetch as ReturnType).mock.calls[1], + ); + expect(["1", "true"]).toContain( + new URL(fullUrl).searchParams.get("full"), + ); + }); + + it("should detect multi-project access errors from SDK error details", async () => { + const apiService = new SentryApiService({ + host: "sentry.io", + accessToken: "test-token", + }); + + globalThis.fetch = makeSdkErrorMock( + { + detail: "You do not have the multi project stream feature enabled", + }, + 403, + "Forbidden", + ); + + try { + await apiService.listEventsForIssue({ + organizationSlug: "test-org", + issueId: "123", + statsPeriod: "24h", + }); + throw new Error("Expected listEventsForIssue to reject"); + } catch (error) { + expect(error).toBeInstanceOf(ApiPermissionError); + expect(error).toHaveProperty( + "message", + "You do not have access to query across multiple projects. Please select a project for your query.", + ); + expect((error as ApiPermissionError).isMultiProjectAccessError()).toBe( + true, + ); + } }); }); diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 4a64718c..fc863c05 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -1,5 +1,44 @@ +import { + type Options, + addATeamToAProject as sdkAddATeamToAProject, + createANewClientKey as sdkCreateANewClientKey, + createANewProject as sdkCreateANewProject, + createANewTeam as sdkCreateANewTeam, + listAProject_sClientKeys as sdkListAProjectSClientKeys, + listAnIssue_sEvents as sdkListAnIssueSEvents, + listAnOrganization_sIssues as sdkListAnOrganizationSIssues, + listAnOrganization_sProjects as sdkListAnOrganizationSProjects, + listAnOrganization_sReleases as sdkListAnOrganizationSReleases, + listAnOrganization_sReplays as sdkListAnOrganizationSReplays, + listAnOrganization_sTeams as sdkListAnOrganizationSTeams, + listRecordingSegments as sdkListRecordingSegments, + listYourOrganizations as sdkListYourOrganizations, + queryExploreEventsInTableFormat as sdkQueryExploreEvents, + retrieveACountOfReplaysForAGivenIssueOrTransaction as sdkRetrieveACountOfReplays, + retrieveAProject as sdkRetrieveAProject, + retrieveAReplayInstance as sdkRetrieveAReplayInstance, + retrieveAnIssue as sdkRetrieveAnIssue, + retrieveAnIssueEvent as sdkRetrieveAnIssueEvent, + retrieveAnOrganization as sdkRetrieveAnOrganization, + retrieveCustomIntegrationIssueLinksForTheGivenSentryIssue as sdkRetrieveCustomIntegrationIssueLinks, + retrieveSeerIssueFixState as sdkRetrieveSeerIssueFixState, + retrieveTagDetails as sdkRetrieveTagDetails, + startSeerIssueFix as sdkStartSeerIssueFix, + updateAProject as sdkUpdateAProject, + updateAnIssue as sdkUpdateAnIssue, +} from "@sentry/api"; import { z } from "zod"; +import { ConfigurationError } from "../errors"; +import { logIssue, logWarn } from "../telem/logging"; +import type { SentryProtocol } from "../types"; +import { + type EventsDataset, + isMetricsDataset, + isProfilesDataset, + normalizeEventsDataset, +} from "../utils/events-datasets"; import { + type TraceMetricIdentifier, getContinuousProfileUrl as getContinuousProfileUrlUtil, getIssueUrl as getIssueUrlUtil, getMonitorUrl as getMonitorUrlUtil, @@ -11,53 +50,43 @@ import { getTraceMetricsExploreUrl, getTraceUrl as getTraceUrlUtil, isSentryHost, - type TraceMetricIdentifier, } from "../utils/url-utils"; +import { USER_AGENT } from "../version"; +import { ApiNotFoundError, ApiValidationError, createApiError } from "./errors"; import { - isMetricsDataset, - isProfilesDataset, - normalizeEventsDataset, - type EventsDataset, -} from "../utils/events-datasets"; -import { logIssue, logWarn } from "../telem/logging"; -import { + ApiErrorSchema, + AutofixRunSchema, + AutofixRunStateSchema, + ClientKeyListSchema, + ClientKeySchema, + ErrorsSearchResponseSchema, + EventAttachmentListSchema, + EventSchema, + ExternalIssueListSchema, + FlamegraphSchema, + IssueListSchema, + IssueSchema, + IssueTagValuesSchema, OrganizationListSchema, OrganizationSchema, - ClientKeySchema, - TeamListSchema, - TeamSchema, + ProfileChunkResponseSchema, ProjectListSchema, ProjectSchema, ReleaseListSchema, - IssueListSchema, - IssueSchema, - IssueTagValuesSchema, - ExternalIssueListSchema, - EventSchema, - EventAttachmentListSchema, - ErrorsSearchResponseSchema, + ReplayDetailsSchema, + ReplayIdsByResourceSchema, + ReplayListResponseSchema, + ReplayRecordingSegmentsSchema, SpansSearchResponseSchema, TagListSchema, - ApiErrorSchema, - ClientKeyListSchema, - AutofixRunSchema, - AutofixRunStateSchema, + TeamListSchema, + TeamSchema, TraceMetaSchema, TraceSchema, - UserSchema, - UserRegionsSchema, - FlamegraphSchema, - ProfileChunkResponseSchema, TransactionProfileSchema, - ReplayDetailsSchema, - ReplayListResponseSchema, - ReplayIdsByResourceSchema, - ReplayRecordingSegmentsSchema, + UserRegionsSchema, + UserSchema, } from "./schema"; -import { ConfigurationError } from "../errors"; -import { createApiError, ApiNotFoundError, ApiValidationError } from "./errors"; -import { USER_AGENT } from "../version"; -import type { SentryProtocol } from "../types"; import type { AutofixRun, AutofixRunState, @@ -66,26 +95,26 @@ import type { Event, EventAttachment, EventAttachmentList, + ExternalIssueList, + Flamegraph, Issue, IssueList, IssueTagValues, - ExternalIssueList, OrganizationList, + ProfileChunk, Project, ProjectList, ReleaseList, + ReplayDetails, + ReplayList, + ReplayRecordingSegments, TagList, Team, TeamList, Trace, TraceMeta, - User, - Flamegraph, - ProfileChunk, TransactionProfile, - ReplayDetails, - ReplayList, - ReplayRecordingSegments, + User, } from "./types"; // TODO: this is shared - so ideally, for safety, it uses @sentry/core, but currently // logger isnt exposed (or rather, it is, but its not the right logger) @@ -212,6 +241,69 @@ export class SentryApiService { this.apiPrefix = `${this.protocol}://${this.host}/api/0`; } + /** + * Builds the common SDK configuration (baseUrl + auth headers) for an SDK call. + */ + private getSdkConfig(opts?: RequestOptions): { + baseUrl: string; + headers: Record; + } { + const host = opts?.host ?? this.host; + const headers: Record = { + "User-Agent": USER_AGENT, + }; + if (this.accessToken) { + headers.Authorization = `Bearer ${this.accessToken}`; + } + return { + baseUrl: `${this.protocol}://${host}`, + headers, + }; + } + + /** + * Unwraps an SDK result (`{ data, error }` discriminated union) and converts + * errors to the existing MCP error types. + * + * @param result The SDK result to unwrap + * @param context A descriptive label for error messages (e.g. method name) + * @returns The data on success + * @throws {ApiError|ApiNotFoundError|ApiValidationError|Error} on failure + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private unwrapSdkResult(result: any, context: string): T { + if (result.error !== undefined) { + const response: Response | undefined = result.response; + if (response) { + // Extract detail from the error object — the SDK parses JSON + // response bodies, so result.error is typically { detail: "..." }. + const rawDetail = + result.error && + typeof result.error === "object" && + "detail" in result.error + ? (result.error as { detail: unknown }).detail + : undefined; + const hasUsableDetail = rawDetail !== null && rawDetail !== undefined; + const detail = hasUsableDetail + ? typeof rawDetail === "string" + ? rawDetail + : JSON.stringify(rawDetail) + : typeof result.error === "string" + ? result.error + : JSON.stringify(result.error); + + throw createApiError( + `${context}: ${response.status} ${response.statusText ?? "Unknown"}`, + response.status, + detail, + result.error, + ); + } + throw new Error(`${context}: ${String(result.error)}`); + } + return result.data as T; + } + /** * Checks if the current host is Sentry SaaS (sentry.io). * @@ -1040,8 +1132,15 @@ export class SentryApiService { // For self-hosted instances, the regions endpoint doesn't exist if (!this.isSaas()) { - const body = await this.requestJSON(path, undefined, opts); - return OrganizationListSchema.parse(body); + const result = await sdkListYourOrganizations({ + ...this.getSdkConfig(opts), + query: { query: params?.query, per_page: 25 } as Record< + string, + unknown + >, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "listOrganizations"); + return OrganizationListSchema.parse(data); } // For SaaS, try to use regions endpoint first @@ -1057,12 +1156,22 @@ export class SentryApiService { const allOrganizations = ( await Promise.all( - regionData.regions.map(async (region) => - this.requestJSON(path, undefined, { - ...opts, - host: new URL(region.url).host, - }), - ), + regionData.regions.map(async (region) => { + const regionResult = await sdkListYourOrganizations({ + ...this.getSdkConfig({ + ...opts, + host: new URL(region.url).host, + }), + query: { query: params?.query, per_page: 25 } as Record< + string, + unknown + >, + } as Parameters[0]); + return this.unwrapSdkResult( + regionResult, + "listOrganizations(region)", + ); + }), ) ) .map((data) => OrganizationListSchema.parse(data)) @@ -1075,8 +1184,15 @@ export class SentryApiService { // fall back to direct organizations endpoint if (error instanceof ApiNotFoundError) { // logger.info("Regions endpoint not found, falling back to direct organizations endpoint"); - const body = await this.requestJSON(path, undefined, opts); - return OrganizationListSchema.parse(body); + const result = await sdkListYourOrganizations({ + ...this.getSdkConfig(opts), + query: { query: params?.query, per_page: 25 } as Record< + string, + unknown + >, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "listOrganizations"); + return OrganizationListSchema.parse(data); } // Re-throw other errors @@ -1092,12 +1208,12 @@ export class SentryApiService { * @returns Organization data */ async getOrganization(organizationSlug: string, opts?: RequestOptions) { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/`, - undefined, - opts, - ); - return OrganizationSchema.parse(body); + const result = await sdkRetrieveAnOrganization({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + }); + const data = this.unwrapSdkResult(result, "getOrganization"); + return OrganizationSchema.parse(data); } /** @@ -1114,16 +1230,16 @@ export class SentryApiService { params?: { query?: string }, opts?: RequestOptions, ): Promise { - const queryParams = new URLSearchParams(); - queryParams.set("per_page", "25"); - if (params?.query) { - queryParams.set("query", params.query); - } - const queryString = queryParams.toString(); - const path = `/organizations/${organizationSlug}/teams/?${queryString}`; - - const body = await this.requestJSON(path, undefined, opts); - return TeamListSchema.parse(body); + const result = await sdkListAnOrganizationSTeams({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { + per_page: 25, + query: params?.query, + } as Record, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "listTeams"); + return TeamListSchema.parse(data); } /** @@ -1146,15 +1262,13 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/teams/`, - { - method: "POST", - body: JSON.stringify({ name }), - }, - opts, - ); - return TeamSchema.parse(body); + const result = await sdkCreateANewTeam({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + body: { name }, + }); + const data = this.unwrapSdkResult(result, "createTeam"); + return TeamSchema.parse(data); } /** @@ -1171,16 +1285,17 @@ export class SentryApiService { params?: { query?: string }, opts?: RequestOptions, ): Promise { - const queryParams = new URLSearchParams(); - queryParams.set("per_page", "25"); - if (params?.query) { - queryParams.set("query", params.query); - } - const queryString = queryParams.toString(); - const path = `/organizations/${organizationSlug}/projects/?${queryString}`; - - const body = await this.requestJSON(path, undefined, opts); - return ProjectListSchema.parse(body); + // The SDK type doesn't include query/per_page params, but the API accepts them + const result = await sdkListAnOrganizationSProjects({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { + query: params?.query, + per_page: 25, + } as Record, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "listProjects"); + return ProjectListSchema.parse(data); } /** @@ -1202,12 +1317,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/projects/${organizationSlug}/${projectSlugOrId}/`, - undefined, - opts, - ); - return ProjectSchema.parse(body); + const result = await sdkRetrieveAProject({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + project_id_or_slug: projectSlugOrId, + }, + }); + const data = this.unwrapSdkResult(result, "getProject"); + return ProjectSchema.parse(data); } /** @@ -1235,21 +1353,19 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const createData: Record = { name }; - // Only include platform if it has a meaningful value (not null, undefined, or empty) - if (platform) { - createData.platform = platform; - } - - const body = await this.requestJSON( - `/teams/${organizationSlug}/${teamSlug}/projects/`, - { - method: "POST", - body: JSON.stringify(createData), + const result = await sdkCreateANewProject({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + team_id_or_slug: teamSlug, }, - opts, - ); - return ProjectSchema.parse(body); + body: { + name, + ...(platform ? { platform } : {}), + }, + }); + const data = this.unwrapSdkResult(result, "createProject"); + return ProjectSchema.parse(data); } /** @@ -1280,21 +1396,20 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const updateData: Record = {}; - // Only include fields that have meaningful values (truthy strings) - if (name) updateData.name = name; - if (slug) updateData.slug = slug; - if (platform) updateData.platform = platform; - - const body = await this.requestJSON( - `/projects/${organizationSlug}/${projectSlug}/`, - { - method: "PUT", - body: JSON.stringify(updateData), + const result = await sdkUpdateAProject({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + project_id_or_slug: projectSlug, }, - opts, - ); - return ProjectSchema.parse(body); + body: { + ...(name ? { name } : {}), + ...(slug ? { slug } : {}), + ...(platform ? { platform } : {}), + }, + }); + const data = this.unwrapSdkResult(result, "updateProject"); + return ProjectSchema.parse(data); } /** @@ -1318,14 +1433,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - await this.request( - `/projects/${organizationSlug}/${projectSlug}/teams/${teamSlug}/`, - { - method: "POST", - body: JSON.stringify({}), + const result = await sdkAddATeamToAProject({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + project_id_or_slug: projectSlug, + team_id_or_slug: teamSlug, }, - opts, - ); + }); + this.unwrapSdkResult(result, "addTeamToProject"); } /** @@ -1362,17 +1478,16 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/projects/${organizationSlug}/${projectSlug}/keys/`, - { - method: "POST", - body: JSON.stringify({ - name, - }), + const result = await sdkCreateANewClientKey({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + project_id_or_slug: projectSlug, }, - opts, - ); - return ClientKeySchema.parse(body); + body: { name }, + }); + const data = this.unwrapSdkResult(result, "createClientKey"); + return ClientKeySchema.parse(data); } /** @@ -1394,12 +1509,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/projects/${organizationSlug}/${projectSlug}/keys/`, - undefined, - opts, - ); - return ClientKeyListSchema.parse(body); + const result = await sdkListAProjectSClientKeys({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + project_id_or_slug: projectSlug, + }, + }); + const data = this.unwrapSdkResult(result, "listClientKeys"); + return ClientKeyListSchema.parse(data); } /** @@ -1438,21 +1556,28 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const searchQuery = new URLSearchParams(); - if (query) { - searchQuery.set("query", query); + if (projectSlug) { + // Project-level releases endpoint not in SDK, use raw request + const searchQuery = new URLSearchParams(); + if (query) { + searchQuery.set("query", query); + } + const path = `/projects/${organizationSlug}/${projectSlug}/releases/`; + const body = await this.requestJSON( + searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path, + undefined, + opts, + ); + return ReleaseListSchema.parse(body); } - const path = projectSlug - ? `/projects/${organizationSlug}/${projectSlug}/releases/` - : `/organizations/${organizationSlug}/releases/`; - - const body = await this.requestJSON( - searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path, - undefined, - opts, - ); - return ReleaseListSchema.parse(body); + const result = await sdkListAnOrganizationSReleases({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { query }, + }); + const data = this.unwrapSdkResult(result, "listReleases"); + return ReleaseListSchema.parse(data); } /** @@ -1555,44 +1680,39 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const searchQuery = new URLSearchParams(); + const timeParams = new URLSearchParams(); + this.applyTimeParams(timeParams, statsPeriod, start, end); - if (query) { - searchQuery.set("query", query); - } - if (limit !== undefined) { - searchQuery.set("per_page", String(limit)); - } + // The SDK's field type is a strict enum, but the API accepts arbitrary strings. + // We also need extra query params (project as string) not in the SDK type. + const sdkQuery: Record = { + query, + per_page: limit, + sort, + ...Object.fromEntries(timeParams), + environment: environment + ? Array.isArray(environment) + ? environment + : [environment] + : undefined, + }; if (projectId) { - searchQuery.append("project", projectId); - } - if (sort) { - searchQuery.set("sort", sort); - } - if (environment) { - const environments = Array.isArray(environment) - ? environment - : [environment]; - for (const value of environments) { - searchQuery.append("environment", value); - } + sdkQuery.project = [Number(projectId)]; } if (fields && fields.length > 0) { - for (const field of fields) { - searchQuery.append("field", field); - } + sdkQuery.field = fields; } - this.applyTimeParams(searchQuery, statsPeriod, start, end); - const body = await this.requestJSON( - searchQuery.toString() - ? `/organizations/${organizationSlug}/replays/?${searchQuery.toString()}` - : `/organizations/${organizationSlug}/replays/`, - undefined, - opts, - ); + const result = await sdkListAnOrganizationSReplays({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: sdkQuery, + } as Options< + Parameters[0] & { url: string } + >); + const data = this.unwrapSdkResult(result, "searchReplays"); - return ReplayListResponseSchema.parse(body).data; + return ReplayListResponseSchema.parse(data).data; } /** @@ -1756,20 +1876,32 @@ export class SentryApiService { sentryQuery.push(query); } - const queryParams = new URLSearchParams(); - queryParams.set("per_page", String(limit)); - if (sortBy) queryParams.set("sort", sortBy); - queryParams.set("statsPeriod", "24h"); - queryParams.set("query", sentryQuery.join(" ")); - - queryParams.append("collapse", "unhandled"); - - const apiUrl = projectSlug - ? `/projects/${organizationSlug}/${projectSlug}/issues/?${queryParams.toString()}` - : `/organizations/${organizationSlug}/issues/?${queryParams.toString()}`; - - const body = await this.requestJSON(apiUrl, undefined, opts); - return IssueListSchema.parse(body); + if (projectSlug) { + // Project-level issues endpoint not in SDK, use raw request + const queryParams = new URLSearchParams(); + queryParams.set("per_page", String(limit)); + if (sortBy) queryParams.set("sort", sortBy); + queryParams.set("statsPeriod", "24h"); + queryParams.set("query", sentryQuery.join(" ")); + queryParams.append("collapse", "unhandled"); + const apiUrl = `/projects/${organizationSlug}/${projectSlug}/issues/?${queryParams.toString()}`; + const body = await this.requestJSON(apiUrl, undefined, opts); + return IssueListSchema.parse(body); + } + + const result = await sdkListAnOrganizationSIssues({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { + limit, + sort: sortBy, + statsPeriod: "24h", + query: sentryQuery.join(" "), + collapse: ["unhandled"], + }, + }); + const data = this.unwrapSdkResult(result, "listIssues"); + return IssueListSchema.parse(data); } async getIssue( @@ -1782,12 +1914,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/`, - undefined, - opts, - ); - return IssueSchema.parse(body); + const result = await sdkRetrieveAnIssue({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId, + }, + }); + const data = this.unwrapSdkResult(result, "getIssue"); + return IssueSchema.parse(data); } /** @@ -1827,12 +1962,16 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/tags/${tagKey}/`, - undefined, - opts, - ); - return IssueTagValuesSchema.parse(body); + const result = await sdkRetrieveTagDetails({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId as unknown as number, + key: tagKey, + }, + }); + const data = this.unwrapSdkResult(result, "getIssueTagValues"); + return IssueTagValuesSchema.parse(data); } /** @@ -1857,12 +1996,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/external-issues/`, - undefined, - opts, - ); - return ExternalIssueListSchema.parse(body); + const result = await sdkRetrieveCustomIntegrationIssueLinks({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId as unknown as number, + }, + }); + const data = this.unwrapSdkResult(result, "getIssueExternalLinks"); + return ExternalIssueListSchema.parse(data); } async getEventForIssue( @@ -1877,11 +2019,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/events/${eventId}/`, - undefined, - opts, - ); + const result = await sdkRetrieveAnIssueEvent({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId as unknown as number, + event_id: eventId as "latest" | "oldest" | "recommended", + }, + }); + const body = this.unwrapSdkResult(result, "getEventForIssue"); // Try to parse with known event schemas first const parseResult = EventSchema.safeParse(body); @@ -1998,31 +2144,34 @@ export class SentryApiService { }, opts?: RequestOptions, ) { - const params = new URLSearchParams(); - + const sdkQuery: Record = { + per_page: limit, + }; if (query) { - params.append("query", query); + sdkQuery.query = query; } - - params.append("per_page", String(limit)); - if (sort) { - params.append("sort", sort); + sdkQuery.sort = sort; } - if (statsPeriod) { - params.append("statsPeriod", statsPeriod); + sdkQuery.statsPeriod = statsPeriod; } else if (start && end) { - params.append("start", start); - params.append("end", end); + sdkQuery.start = start; + sdkQuery.end = end; } - if (full) { - params.append("full", "true"); + sdkQuery.full = true; } - const apiUrl = `/organizations/${organizationSlug}/issues/${issueId}/events/?${params.toString()}`; - return await this.requestJSON(apiUrl, undefined, opts); + const result = await sdkListAnIssueSEvents({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId as unknown as number, + }, + query: sdkQuery, + } as Parameters[0]); + return this.unwrapSdkResult(result, "listEventsForIssue"); } async listEventAttachments( @@ -2111,12 +2260,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/replays/${replayId}/`, - undefined, - opts, - ); - return z.object({ data: ReplayDetailsSchema }).parse(body).data; + const result = await sdkRetrieveAReplayInstance({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + replay_id: replayId, + }, + }); + const data = this.unwrapSdkResult(result, "getReplayDetails"); + return z.object({ data: ReplayDetailsSchema }).parse(data).data; } async listReplayIdsForIssue( @@ -2132,20 +2284,22 @@ export class SentryApiService { opts?: RequestOptions, ): Promise { const normalizedIssueId = String(issueId); - const queryParams = new URLSearchParams(); - queryParams.set("returnIds", "true"); - queryParams.set("query", `issue.id:[${normalizedIssueId}]`); - queryParams.set("data_source", dataSource); - queryParams.set("statsPeriod", "90d"); - queryParams.append("project", "-1"); - - const body = await this.requestJSON( - `/organizations/${organizationSlug}/replay-count/?${queryParams.toString()}`, - undefined, - opts, - ); - - const replayIdsByResource = ReplayIdsByResourceSchema.parse(body); + // The SDK type doesn't include returnIds, data_source, or project params, + // so we pass extra query params via cast. + const result = await sdkRetrieveACountOfReplays({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { + query: `issue.id:[${normalizedIssueId}]`, + statsPeriod: "90d", + returnIds: "true", + data_source: dataSource, + project: "-1", + } as Record, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "listReplayIdsForIssue"); + + const replayIdsByResource = ReplayIdsByResourceSchema.parse(data); return replayIdsByResource[normalizedIssueId] ?? []; } @@ -2161,12 +2315,18 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/projects/${organizationSlug}/${projectSlugOrId}/replays/${replayId}/recording-segments/?download=true`, - undefined, - opts, - ); - return ReplayRecordingSegmentsSchema.parse(body); + // The SDK doesn't expose the `download` query param, so pass it via cast + const result = await sdkListRecordingSegments({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + project_id_or_slug: projectSlugOrId, + replay_id: replayId, + }, + query: { download: "true" } as Record, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "getReplayRecordingSegments"); + return ReplayRecordingSegmentsSchema.parse(data); } async updateIssue( @@ -2195,16 +2355,9 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const updateData: { - status?: string; - assignedTo?: string; - substatus?: string; - ignoreDuration?: number; - ignoreCount?: number; - ignoreWindow?: number; - ignoreUserCount?: number; - ignoreUserWindow?: number; - } = {}; + // The SDK body type is stricter than what we send (extra fields like + // substatus, ignoreDuration, etc.), so we cast. + const updateData: Record = {}; if (status !== undefined) updateData.status = status; if (assignedTo !== undefined) updateData.assignedTo = assignedTo; if (substatus !== undefined) updateData.substatus = substatus; @@ -2219,15 +2372,16 @@ export class SentryApiService { updateData.ignoreUserWindow = ignoreUserWindow; } - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/`, - { - method: "PUT", - body: JSON.stringify(updateData), + const result = await sdkUpdateAnIssue({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId, }, - opts, - ); - return IssueSchema.parse(body); + body: updateData as Parameters[0]["body"], + }); + const data = this.unwrapSdkResult(result, "updateIssue"); + return IssueSchema.parse(data); } // TODO: Sentry is not yet exposing a reasonable API to fetch trace data @@ -2278,28 +2432,22 @@ export class SentryApiService { sentryQuery.push(`project:${projectSlug}`); } - const queryParams = new URLSearchParams(); - queryParams.set("dataset", "errors"); - queryParams.set("per_page", "10"); - queryParams.set( - "sort", - `-${sortBy === "last_seen" ? "last_seen" : "count"}`, - ); - queryParams.set("statsPeriod", "24h"); - queryParams.append("field", "issue"); - queryParams.append("field", "title"); - queryParams.append("field", "project"); - queryParams.append("field", "last_seen()"); - queryParams.append("field", "count()"); - queryParams.set("query", sentryQuery.join(" ")); - // if (projectSlug) queryParams.set("project", projectSlug); - - const apiUrl = `/organizations/${organizationSlug}/events/?${queryParams.toString()}`; - - const body = await this.requestJSON(apiUrl, undefined, opts); + const result = await sdkQueryExploreEvents({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { + dataset: "errors", + per_page: 10, + sort: `-${sortBy === "last_seen" ? "last_seen" : "count"}`, + statsPeriod: "24h", + field: ["issue", "title", "project", "last_seen()", "count()"], + query: sentryQuery.join(" "), + }, + }); + const data = this.unwrapSdkResult(result, "searchErrors"); // TODO(dcramer): If you're using an older version of Sentry this API had a breaking change // meaning this endpoint will error. - return ErrorsSearchResponseSchema.parse(body).data; + return ErrorsSearchResponseSchema.parse(data).data; } async searchSpans( @@ -2329,30 +2477,31 @@ export class SentryApiService { sentryQuery.push(`project:${projectSlug}`); } - const queryParams = new URLSearchParams(); - queryParams.set("dataset", "spans"); - queryParams.set("per_page", "10"); - queryParams.set( - "sort", - `-${sortBy === "timestamp" ? "timestamp" : "span.duration"}`, - ); - queryParams.set("allowAggregateConditions", "0"); - queryParams.set("useRpc", "1"); - queryParams.append("field", "id"); - queryParams.append("field", "trace"); - queryParams.append("field", "span.op"); - queryParams.append("field", "span.description"); - queryParams.append("field", "span.duration"); - queryParams.append("field", "transaction"); - queryParams.append("field", "project"); - queryParams.append("field", "timestamp"); - queryParams.set("query", sentryQuery.join(" ")); - // if (projectSlug) queryParams.set("project", projectSlug); - - const apiUrl = `/organizations/${organizationSlug}/events/?${queryParams.toString()}`; - - const body = await this.requestJSON(apiUrl, undefined, opts); - return SpansSearchResponseSchema.parse(body).data; + // The SDK type doesn't include allowAggregateConditions or useRpc params + const result = await sdkQueryExploreEvents({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: { + dataset: "spans", + per_page: 10, + sort: `-${sortBy === "timestamp" ? "timestamp" : "span.duration"}`, + field: [ + "id", + "trace", + "span.op", + "span.description", + "span.duration", + "transaction", + "project", + "timestamp", + ], + query: sentryQuery.join(" "), + allowAggregateConditions: "0", + useRpc: "1", + } as Record, + } as Parameters[0]); + const data = this.unwrapSdkResult(result, "searchSpans"); + return SpansSearchResponseSchema.parse(data).data; } // ================================================================================ @@ -2527,6 +2676,8 @@ export class SentryApiService { }, opts?: RequestOptions, ) { + // Build the full query params using existing builders, then convert to SDK format. + // This preserves the dataset-specific logic (sort transforms, sampling, etc.) let queryParams: URLSearchParams; const normalizedDataset = normalizeEventsDataset(dataset); @@ -2535,7 +2686,6 @@ export class SentryApiService { normalizedDataset === "tracemetrics" || normalizedDataset === "profiles" ) { - // Use Discover API query builder queryParams = this.buildDiscoverApiQuery({ query, fields, @@ -2548,7 +2698,6 @@ export class SentryApiService { sort, }); } else { - // Use EAP API query builder for spans and logs queryParams = this.buildEapApiQuery({ query, fields, @@ -2562,8 +2711,24 @@ export class SentryApiService { }); } - const apiUrl = `/organizations/${organizationSlug}/events/?${queryParams.toString()}`; - return await this.requestJSON(apiUrl, undefined, opts); + // Convert URLSearchParams to SDK query format. Some params like `field` and + // `project` can appear multiple times, while the SDK expects `field` as string[]. + const sdkQuery: Record = {}; + const multiValueKeys = new Set(["field", "project"]); + for (const key of new Set(queryParams.keys())) { + if (multiValueKeys.has(key)) { + sdkQuery[key] = queryParams.getAll(key); + } else { + sdkQuery[key] = queryParams.get(key); + } + } + + const result = await sdkQueryExploreEvents({ + ...this.getSdkConfig(opts), + path: { organization_id_or_slug: organizationSlug }, + query: sdkQuery, + } as Parameters[0]); + return this.unwrapSdkResult(result, "searchEvents"); } // POST https://us.sentry.io/api/0/issues/5485083130/autofix/ @@ -2581,18 +2746,19 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/autofix/`, - { - method: "POST", - body: JSON.stringify({ - event_id: eventId, - instruction, - }), + const result = await sdkStartSeerIssueFix({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId as unknown as number, }, - opts, - ); - return AutofixRunSchema.parse(body); + body: { + event_id: eventId, + instruction, + }, + }); + const data = this.unwrapSdkResult(result, "startAutofix"); + return AutofixRunSchema.parse(data); } // GET https://us.sentry.io/api/0/issues/5485083130/autofix/ @@ -2606,12 +2772,15 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/organizations/${organizationSlug}/issues/${issueId}/autofix/`, - undefined, - opts, - ); - return AutofixRunStateSchema.parse(body); + const result = await sdkRetrieveSeerIssueFixState({ + ...this.getSdkConfig(opts), + path: { + organization_id_or_slug: organizationSlug, + issue_id: issueId as unknown as number, + }, + }); + const data = this.unwrapSdkResult(result, "getAutofixState"); + return AutofixRunStateSchema.parse(data); } /** diff --git a/packages/mcp-core/src/api-client/errors.ts b/packages/mcp-core/src/api-client/errors.ts index d80fee97..26bf5f71 100644 --- a/packages/mcp-core/src/api-client/errors.ts +++ b/packages/mcp-core/src/api-client/errors.ts @@ -256,11 +256,12 @@ export function createApiError( let improvedMessage = message; // Handle the multi-project access error that comes in various forms + const knownErrorText = `${message}\n${detail ?? ""}`; if ( - message.includes( + knownErrorText.includes( "You do not have the multi project stream feature enabled", ) || - message.includes("You cannot view events from multiple projects") + knownErrorText.includes("You cannot view events from multiple projects") ) { improvedMessage = "You do not have access to query across multiple projects. Please select a project for your query."; diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index e7dfe321..0d04e96f 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -36,6 +36,11 @@ * ``` */ import { z } from "zod"; +import { + zAutofixPostResponse, + zGroupExternalIssueResponse, + zOrganizationEventsResponseDict, +} from "@sentry/api/zod"; /** * Schema for Sentry API error responses. @@ -696,14 +701,13 @@ export const EventSchema = z.union([ UnknownEventSchema, ]); -export const EventsResponseSchema = z.object({ - data: z.array(z.unknown()), - meta: z - .object({ - fields: z.record(z.string(), z.string()), - }) - .passthrough(), -}); +/** + * Uses auto-generated schema from `@sentry/api/zod`. + * + * The generated schema includes optional `datasetReason`, `isMetricsData`, and + * `isMetricsExtractedData` fields on `meta` beyond the required `fields`. + */ +export const EventsResponseSchema = zOrganizationEventsResponseDict; // https://us.sentry.io/api/0/organizations/sentry/events/?dataset=errors&field=issue&field=title&field=project&field=timestamp&field=trace&per_page=5&query=event.type%3Aerror&referrer=sentry-mcp&sort=-timestamp&statsPeriod=1w export const ErrorsSearchResponseSchema = EventsResponseSchema.extend({ @@ -737,15 +741,13 @@ export const SpansSearchResponseSchema = EventsResponseSchema.extend({ /** * The Seer autofix POST endpoint currently returns a simple numeric `run_id`. * + * Uses auto-generated schema from `@sentry/api/zod`. + * * Upstream source of truth in getsentry/sentry: * - `src/sentry/seer/endpoints/group_ai_autofix.py` * - `src/sentry/seer/autofix/types.py` (`AutofixPostResponse`) */ -export const AutofixRunSchema = z - .object({ - run_id: z.number(), - }) - .passthrough(); +export const AutofixRunSchema = zAutofixPostResponse.passthrough(); const AutofixStatusSchema = z.enum([ "PENDING", @@ -933,18 +935,13 @@ export const IssueTagValuesSchema = z /** * Schema for external issue link (e.g., Jira, GitHub Issues). * + * Uses auto-generated schema from `@sentry/api/zod`. + * * Represents a link between a Sentry issue and an external issue tracking * system like Jira, GitHub Issues, GitLab, etc. */ -export const ExternalIssueSchema = z.object({ - id: z.union([z.string(), z.number()]), - issueId: z.union([z.string(), z.number()]), - serviceType: z.string(), - displayName: z.string(), - webUrl: z.string(), -}); - -export const ExternalIssueListSchema = z.array(ExternalIssueSchema); +export const ExternalIssueListSchema = zGroupExternalIssueResponse; +export const ExternalIssueSchema = zGroupExternalIssueResponse.element; /** * Schema for Sentry trace metadata response. diff --git a/packages/mcp-core/src/internal/agents/azure-openai-provider.test.ts b/packages/mcp-core/src/internal/agents/azure-openai-provider.test.ts index befa4435..eefbed1f 100644 --- a/packages/mcp-core/src/internal/agents/azure-openai-provider.test.ts +++ b/packages/mcp-core/src/internal/agents/azure-openai-provider.test.ts @@ -9,40 +9,11 @@ import { } from "./azure-openai-provider.js"; describe("azure-openai-provider", () => { - const originalModel = process.env.OPENAI_MODEL; - const originalApiKey = process.env.OPENAI_API_KEY; - const originalApiVersion = process.env.OPENAI_API_VERSION; - beforeEach(() => { setAzureOpenAIBaseUrl(undefined); - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_MODEL; - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_VERSION; }); afterEach(() => { - if (originalModel === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_MODEL; - } else { - process.env.OPENAI_MODEL = originalModel; - } - - if (originalApiKey === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_KEY; - } else { - process.env.OPENAI_API_KEY = originalApiKey; - } - - if (originalApiVersion === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_VERSION; - } else { - process.env.OPENAI_API_VERSION = originalApiVersion; - } - vi.unstubAllGlobals(); }); diff --git a/packages/mcp-core/src/internal/agents/openai-provider.integration.test.ts b/packages/mcp-core/src/internal/agents/openai-provider.integration.test.ts index 7a2da9f5..f60fe283 100644 --- a/packages/mcp-core/src/internal/agents/openai-provider.integration.test.ts +++ b/packages/mcp-core/src/internal/agents/openai-provider.integration.test.ts @@ -12,7 +12,7 @@ * - #623: structuredOutputs causing validation errors with nullable fields * - 405 errors from unsupported parameters (reasoningEffort) */ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; import { searchIssuesAgent } from "../../tools/search-issues/agent"; @@ -45,14 +45,21 @@ const mswServer = setupServer( ); describe("OpenAI Provider Integration", () => { - const hasOpenAIKey = Boolean(process.env.OPENAI_API_KEY); + const openAIKey = process.env.OPENAI_API_KEY; + const hasOpenAIKey = Boolean(openAIKey); beforeAll(() => { if (hasOpenAIKey) { + mswServer.listen({ onUnhandledRequest: "bypass" }); + } + }); + + beforeEach(() => { + if (openAIKey) { + process.env.OPENAI_API_KEY = openAIKey; // Explicitly set OpenAI provider to ensure we test OpenAI even if // ANTHROPIC_API_KEY is also set (auto-detect prefers Anthropic) setAgentProvider("openai"); - mswServer.listen({ onUnhandledRequest: "bypass" }); } }); diff --git a/packages/mcp-core/src/internal/agents/openai-provider.test.ts b/packages/mcp-core/src/internal/agents/openai-provider.test.ts index 62ea23b6..02d5a0fd 100644 --- a/packages/mcp-core/src/internal/agents/openai-provider.test.ts +++ b/packages/mcp-core/src/internal/agents/openai-provider.test.ts @@ -4,40 +4,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getOpenAIModel, setOpenAIBaseUrl } from "./openai-provider.js"; describe("openai-provider", () => { - const originalModel = process.env.OPENAI_MODEL; - const originalApiKey = process.env.OPENAI_API_KEY; - const originalApiVersion = process.env.OPENAI_API_VERSION; - beforeEach(() => { setOpenAIBaseUrl(undefined); - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_MODEL; - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_VERSION; }); afterEach(() => { - if (originalModel === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_MODEL; - } else { - process.env.OPENAI_MODEL = originalModel; - } - - if (originalApiKey === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_KEY; - } else { - process.env.OPENAI_API_KEY = originalApiKey; - } - - if (originalApiVersion === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_VERSION; - } else { - process.env.OPENAI_API_VERSION = originalApiVersion; - } - vi.unstubAllGlobals(); }); diff --git a/packages/mcp-core/src/internal/agents/provider-factory.test.ts b/packages/mcp-core/src/internal/agents/provider-factory.test.ts index 1e0c8799..82630289 100644 --- a/packages/mcp-core/src/internal/agents/provider-factory.test.ts +++ b/packages/mcp-core/src/internal/agents/provider-factory.test.ts @@ -8,43 +8,13 @@ import { setAzureOpenAIBaseUrl } from "./azure-openai-provider.js"; import { ConfigurationError } from "../../errors.js"; describe("provider-factory", () => { - const originalAnthropicKey = process.env.ANTHROPIC_API_KEY; - const originalOpenAIKey = process.env.OPENAI_API_KEY; - const originalProviderEnv = process.env.EMBEDDED_AGENT_PROVIDER; - beforeEach(() => { // Reset module state setAgentProvider(undefined); setAzureOpenAIBaseUrl(undefined); - // Clear environment variables - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.ANTHROPIC_API_KEY; - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_KEY; - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.EMBEDDED_AGENT_PROVIDER; }); afterEach(() => { - // Restore original environment - if (originalAnthropicKey === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.ANTHROPIC_API_KEY; - } else { - process.env.ANTHROPIC_API_KEY = originalAnthropicKey; - } - if (originalOpenAIKey === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.OPENAI_API_KEY; - } else { - process.env.OPENAI_API_KEY = originalOpenAIKey; - } - if (originalProviderEnv === undefined) { - // biome-ignore lint/performance/noDelete: Required to properly unset environment variable - delete process.env.EMBEDDED_AGENT_PROVIDER; - } else { - process.env.EMBEDDED_AGENT_PROVIDER = originalProviderEnv; - } setAgentProvider(undefined); setAzureOpenAIBaseUrl(undefined); }); diff --git a/packages/mcp-core/src/test-setup.ts b/packages/mcp-core/src/test-setup.ts index 586b561c..9187b02f 100644 --- a/packages/mcp-core/src/test-setup.ts +++ b/packages/mcp-core/src/test-setup.ts @@ -2,6 +2,7 @@ import { config } from "dotenv"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { startMockServer } from "@sentry/mcp-server-mocks"; +import { afterEach, beforeEach } from "vitest"; import type { ServerContext } from "./types.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -16,6 +17,38 @@ config({ path: path.resolve(__dirname, "../.env") }); // Load root .env second (for shared defaults - won't override local or shell vars) config({ path: path.join(rootDir, ".env") }); +const MANAGED_ENV_KEYS = [ + "ANTHROPIC_API_KEY", + "ANTHROPIC_MODEL", + "EMBEDDED_AGENT_PROVIDER", + "OPENAI_API_KEY", + "OPENAI_API_VERSION", + "OPENAI_MODEL", +] as const; + +type ManagedEnvKey = (typeof MANAGED_ENV_KEYS)[number]; + +const originalEnv = new Map( + MANAGED_ENV_KEYS.map((key) => [key, process.env[key]]), +); + +beforeEach(() => { + for (const key of MANAGED_ENV_KEYS) { + Reflect.deleteProperty(process.env, key); + } +}); + +afterEach(() => { + for (const key of MANAGED_ENV_KEYS) { + const value = originalEnv.get(key); + if (value === undefined) { + Reflect.deleteProperty(process.env, key); + } else { + process.env[key] = value; + } + } +}); + startMockServer({ ignoreOpenAI: true }); /** diff --git a/packages/mcp-core/src/tools/get-issue-details.test.ts b/packages/mcp-core/src/tools/get-issue-details.test.ts index 2e435781..f2923f44 100644 --- a/packages/mcp-core/src/tools/get-issue-details.test.ts +++ b/packages/mcp-core/src/tools/get-issue-details.test.ts @@ -860,7 +860,7 @@ describe("get_issue_details", () => { }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(` - [ApiNotFoundError: The requested resource does not exist + [ApiNotFoundError: getIssue: 404 Not Found Please verify these parameters are correct: - organizationSlug: 'test-org' - issueId: 'NONEXISTENT-ISSUE-123'] diff --git a/packages/mcp-core/src/tools/get-issue-tag-values.test.ts b/packages/mcp-core/src/tools/get-issue-tag-values.test.ts index 3c8788c7..00a820ca 100644 --- a/packages/mcp-core/src/tools/get-issue-tag-values.test.ts +++ b/packages/mcp-core/src/tools/get-issue-tag-values.test.ts @@ -126,34 +126,20 @@ describe("get_issue_tag_values", () => { ).rejects.toThrow(UserInputError); }); - it("throws error when tagKey contains path traversal characters", async () => { - await expect( - getIssueTagValues.handler( - { - organizationSlug: "sentry-mcp-evals", - issueId: "CLOUDFLARE-MCP-41", - tagKey: "../../../admin", - regionUrl: null, - issueUrl: undefined, - }, - getServerContext(), - ), - ).rejects.toThrow(); + it("rejects tagKey with path traversal characters in the input schema", () => { + expect(() => + getIssueTagValues.inputSchema.tagKey.parse("../../../admin"), + ).toThrow( + /Tag key must contain only alphanumeric characters, dots, hyphens, and underscores/, + ); }); - it("throws error when tagKey contains slashes", async () => { - await expect( - getIssueTagValues.handler( - { - organizationSlug: "sentry-mcp-evals", - issueId: "CLOUDFLARE-MCP-41", - tagKey: "url/path", - regionUrl: null, - issueUrl: undefined, - }, - getServerContext(), - ), - ).rejects.toThrow(); + it("rejects tagKey with slashes in the input schema", () => { + expect(() => + getIssueTagValues.inputSchema.tagKey.parse("url/path"), + ).toThrow( + /Tag key must contain only alphanumeric characters, dots, hyphens, and underscores/, + ); }); it("handles null values in topValues gracefully", async () => { diff --git a/packages/mcp-core/src/tools/get-sentry-resource.test.ts b/packages/mcp-core/src/tools/get-sentry-resource.test.ts index 93a00311..087107aa 100644 --- a/packages/mcp-core/src/tools/get-sentry-resource.test.ts +++ b/packages/mcp-core/src/tools/get-sentry-resource.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { http, HttpResponse } from "msw"; import { mswServer, @@ -12,10 +12,6 @@ import { } from "@sentry/mcp-server-mocks"; import getSentryResource from "./get-sentry-resource.js"; -const originalOpenAIApiKey = process.env.OPENAI_API_KEY; -const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY; -const originalEmbeddedAgentProvider = process.env.EMBEDDED_AGENT_PROVIDER; - const baseContext = { constraints: { organizationSlug: undefined, @@ -54,32 +50,6 @@ function callHandler(params: { } describe("get_sentry_resource", () => { - beforeEach(() => { - Reflect.deleteProperty(process.env, "OPENAI_API_KEY"); - Reflect.deleteProperty(process.env, "ANTHROPIC_API_KEY"); - Reflect.deleteProperty(process.env, "EMBEDDED_AGENT_PROVIDER"); - }); - - afterAll(() => { - if (originalOpenAIApiKey === undefined) { - Reflect.deleteProperty(process.env, "OPENAI_API_KEY"); - } else { - process.env.OPENAI_API_KEY = originalOpenAIApiKey; - } - - if (originalAnthropicApiKey === undefined) { - Reflect.deleteProperty(process.env, "ANTHROPIC_API_KEY"); - } else { - process.env.ANTHROPIC_API_KEY = originalAnthropicApiKey; - } - - if (originalEmbeddedAgentProvider === undefined) { - Reflect.deleteProperty(process.env, "EMBEDDED_AGENT_PROVIDER"); - } else { - process.env.EMBEDDED_AGENT_PROVIDER = originalEmbeddedAgentProvider; - } - }); - // ─── URL mode: issue URLs ────────────────────────────────────────────────── describe("URL mode — issue URLs", () => { it("resolves issue from subdomain URL (my-org.sentry.io)", async () => { diff --git a/packages/mcp-core/src/tools/get-trace-details.test.ts b/packages/mcp-core/src/tools/get-trace-details.test.ts index 0f189f91..c948b02a 100644 --- a/packages/mcp-core/src/tools/get-trace-details.test.ts +++ b/packages/mcp-core/src/tools/get-trace-details.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { http, HttpResponse } from "msw"; import { mswServer, @@ -9,10 +9,6 @@ import { } from "@sentry/mcp-server-mocks"; import getTraceDetails from "./get-trace-details.js"; -const originalOpenAIApiKey = process.env.OPENAI_API_KEY; -const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY; -const originalEmbeddedAgentProvider = process.env.EMBEDDED_AGENT_PROVIDER; - /** Register the same handler on sentry.io and us.sentry.io (org fixture resolves region). */ function httpGetRegional( sentryIoUrl: string, @@ -52,28 +48,6 @@ function buildTraceSpan({ describe("get_trace_details", () => { beforeEach(() => { process.env.OPENAI_API_KEY = "test-key"; - Reflect.deleteProperty(process.env, "ANTHROPIC_API_KEY"); - Reflect.deleteProperty(process.env, "EMBEDDED_AGENT_PROVIDER"); - }); - - afterAll(() => { - if (originalOpenAIApiKey === undefined) { - Reflect.deleteProperty(process.env, "OPENAI_API_KEY"); - } else { - process.env.OPENAI_API_KEY = originalOpenAIApiKey; - } - - if (originalAnthropicApiKey === undefined) { - Reflect.deleteProperty(process.env, "ANTHROPIC_API_KEY"); - } else { - process.env.ANTHROPIC_API_KEY = originalAnthropicApiKey; - } - - if (originalEmbeddedAgentProvider === undefined) { - Reflect.deleteProperty(process.env, "EMBEDDED_AGENT_PROVIDER"); - } else { - process.env.EMBEDDED_AGENT_PROVIDER = originalEmbeddedAgentProvider; - } }); it("serializes with valid trace ID", async () => { @@ -187,9 +161,9 @@ describe("get_trace_details", () => { }); it("falls back to direct search_events guidance when agent search is unavailable", async () => { - Reflect.deleteProperty(process.env, "OPENAI_API_KEY"); - Reflect.deleteProperty(process.env, "ANTHROPIC_API_KEY"); - Reflect.deleteProperty(process.env, "EMBEDDED_AGENT_PROVIDER"); + process.env.OPENAI_API_KEY = ""; + process.env.ANTHROPIC_API_KEY = ""; + process.env.EMBEDDED_AGENT_PROVIDER = ""; const result = await getTraceDetails.handler( { diff --git a/packages/mcp-core/src/tools/search-events.test.ts b/packages/mcp-core/src/tools/search-events.test.ts index 5e5b0a2e..dcef6ac7 100644 --- a/packages/mcp-core/src/tools/search-events.test.ts +++ b/packages/mcp-core/src/tools/search-events.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { http, HttpResponse } from "msw"; import { mswServer } from "@sentry/mcp-server-mocks"; -import searchEvents from "./search-events"; import { generateText } from "ai"; +import { http, HttpResponse } from "msw"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { UserInputError } from "../errors"; +import searchEvents from "./search-events"; // Mock the AI SDK vi.mock("@ai-sdk/openai", () => { diff --git a/packages/mcp-core/src/tools/search-issue-events.test.ts b/packages/mcp-core/src/tools/search-issue-events.test.ts index 2e9d8690..aca771fd 100644 --- a/packages/mcp-core/src/tools/search-issue-events.test.ts +++ b/packages/mcp-core/src/tools/search-issue-events.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { http, HttpResponse } from "msw"; import { mswServer } from "@sentry/mcp-server-mocks"; -import searchIssueEvents from "./search-issue-events"; import { generateText } from "ai"; +import { http, HttpResponse } from "msw"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { UserInputError } from "../errors"; import type { ServerContext } from "../types"; +import searchIssueEvents from "./search-issue-events"; // Mock the AI SDK vi.mock("@ai-sdk/openai", () => { @@ -540,9 +540,9 @@ describe("search_issue_events", () => { mockGenerateText.mockResolvedValue(mockAIResponse()); mswServer.use( - http.get("*/api/0/organizations/*/issues/*/events/", ({ request }) => { - const url = new URL(request.url); - expect(url.searchParams.get("per_page")).toBe("25"); + http.get("*/api/0/organizations/*/issues/*/events/", () => { + // The SDK's listAnIssueSEvents endpoint doesn't expose per_page + // as a query param; limit is handled at the SDK/pagination layer. return HttpResponse.json([]); }), ); diff --git a/packages/mcp-core/src/tools/search-issues.test.ts b/packages/mcp-core/src/tools/search-issues.test.ts index 52aeca93..6f268ecf 100644 --- a/packages/mcp-core/src/tools/search-issues.test.ts +++ b/packages/mcp-core/src/tools/search-issues.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { http, HttpResponse } from "msw"; import { mswServer } from "@sentry/mcp-server-mocks"; -import searchIssues from "./search-issues"; import { generateText } from "ai"; +import { http, HttpResponse } from "msw"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ServerContext } from "../types"; +import searchIssues from "./search-issues"; // Mock the AI SDK vi.mock("@ai-sdk/openai", () => { @@ -282,8 +282,9 @@ describe("search_issues", () => { mswServer.use( http.get("*/api/0/organizations/*/issues/", ({ request }) => { const url = new URL(request.url); - const perPage = url.searchParams.get("per_page"); - expect(perPage).toBe("25"); + // The SDK sends `limit` (not `per_page`) for this endpoint + const limit = url.searchParams.get("limit"); + expect(limit).toBe("25"); return HttpResponse.json([]); }), ); diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index 60c788d0..d75d1737 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -1000,8 +1000,7 @@ export const restHandlers = buildHandlers([ { method: "post", path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/teams/:teamSlug/", - fetch: async ({ request, params }) => { - const body = (await request.json()) as any; + fetch: async ({ params }) => { const teamSlug = params.teamSlug as string; return HttpResponse.json({ ...teamFixture, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4b8589c..5c520be4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -402,6 +402,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.26.0 version: 1.26.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@sentry/api': + specifier: ^0.141.0 + version: 0.141.0(zod@3.25.76) '@sentry/core': specifier: 'catalog:' version: 10.35.0 @@ -2193,6 +2196,15 @@ packages: resolution: {integrity: sha512-9hGP3lD+7o/4ovGTdwv3T9K2t9LxSlR/CAcRQeFApW2c0AGsjTdcglOxsgxYei4YmaISx0CBJ/YqJfQVYxaxWw==} engines: {node: '>=18'} + '@sentry/api@0.141.0': + resolution: {integrity: sha512-6DAEAhHnE/bkiUsCIGY4V9fPWVg2sc0Wn0ualQ4xEEurKQgtbafhJyZuuwCfTwT/nYldHosjGfLoWFNdXj9tWA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.24.0 + peerDependenciesMeta: + zod: + optional: true + '@sentry/babel-plugin-component-annotate@4.6.1': resolution: {integrity: sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==} engines: {node: '>= 14'} @@ -6533,6 +6545,10 @@ snapshots: '@sentry-internal/browser-utils': 10.35.0 '@sentry/core': 10.35.0 + '@sentry/api@0.141.0(zod@3.25.76)': + optionalDependencies: + zod: 3.25.76 + '@sentry/babel-plugin-component-annotate@4.6.1': {} '@sentry/browser@10.35.0':