diff --git a/package.json b/package.json index 67e804572..9cbacd85b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "author": "Sentry", "description": "Sentry MCP Server", "homepage": "https://github.com/getsentry/sentry-mcp", - "keywords": ["sentry"], + "keywords": [ + "sentry" + ], "bugs": { "url": "https://github.com/getsentry/sentry-mcp/issues" }, diff --git a/packages/mcp-cloudflare/package.json b/packages/mcp-cloudflare/package.json index 97a33fbde..6995d7fc8 100644 --- a/packages/mcp-cloudflare/package.json +++ b/packages/mcp-cloudflare/package.json @@ -4,7 +4,9 @@ "private": true, "type": "module", "license": "FSL-1.1-ALv2", - "files": ["./dist/*"], + "files": [ + "./dist/*" + ], "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts b/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts index af97f1c6d..0f0432458 100644 --- a/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts +++ b/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts @@ -313,7 +313,6 @@ async function handleAuthenticatedMcpRequest( const organizationSlug = groups?.org || null; const projectSlug = groups?.project || null; - // Check for experimental mode query parameter const isExperimentalMode = url.searchParams.get("experimental") === "1"; @@ -451,7 +450,7 @@ async function handleAuthenticatedMcpRequest( () => buildServer({ context: serverContext, - experimentalMode: isExperimentalMode, + experimentalMode: isExperimentalMode, sdkVersion: "v2", }), { diff --git a/packages/mcp-core/package.json b/packages/mcp-core/package.json index ec7ae7080..5812170be 100644 --- a/packages/mcp-core/package.json +++ b/packages/mcp-core/package.json @@ -11,7 +11,9 @@ "author": "Sentry", "description": "Sentry MCP Core - Shared code for MCP transports", "homepage": "https://github.com/getsentry/sentry-mcp", - "keywords": ["sentry"], + "keywords": [ + "sentry" + ], "bugs": { "url": "https://github.com/getsentry/sentry-mcp/issues" }, @@ -19,7 +21,9 @@ "type": "git", "url": "git@github.com:getsentry/sentry-mcp.git" }, - "files": ["./dist/*"], + "files": [ + "./dist/*" + ], "exports": { "./api-client": { "types": "./dist/api-client/index.d.ts", diff --git a/packages/mcp-core/src/api-client/client.test.ts b/packages/mcp-core/src/api-client/client.test.ts index cea8ae942..264792d58 100644 --- a/packages/mcp-core/src/api-client/client.test.ts +++ b/packages/mcp-core/src/api-client/client.test.ts @@ -818,36 +818,19 @@ describe("listOrganizations", () => { globalThis.fetch = originalFetch; }); - it("should fetch from regions endpoint for SaaS", async () => { - const mockRegionsResponse = { - regions: [ - { name: "US", url: "https://us.sentry.io" }, - { name: "EU", url: "https://eu.sentry.io" }, - ], - }; - - const mockOrgsUs = [{ id: "1", slug: "org-us", name: "Org US" }]; - const mockOrgsEu = [{ id: "2", slug: "org-eu", name: "Org EU" }]; + it("should fetch from the organizations endpoint on the root host for SaaS", async () => { + const mockOrgs = [ + { id: "1", slug: "org-us", name: "Org US" }, + { id: "2", slug: "org-eu", name: "Org EU" }, + ]; let callCount = 0; globalThis.fetch = vi.fn().mockImplementation((url: string) => { callCount++; - if (url.includes("/users/me/regions/")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockRegionsResponse), - }); - } - if (url.includes("us.sentry.io")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockOrgsUs), - }); - } - if (url.includes("eu.sentry.io")) { + if (url.includes("/organizations/")) { return Promise.resolve({ ok: true, - json: () => Promise.resolve(mockOrgsEu), + json: () => Promise.resolve(mockOrgs), }); } return Promise.reject(new Error("Unexpected URL")); @@ -860,10 +843,15 @@ describe("listOrganizations", () => { const result = await apiService.listOrganizations(); - expect(callCount).toBe(3); // 1 regions call + 2 org calls + expect(callCount).toBe(1); // Single call, no region fanout expect(result).toHaveLength(2); expect(result).toContainEqual(expect.objectContaining({ slug: "org-us" })); expect(result).toContainEqual(expect.objectContaining({ slug: "org-eu" })); + // Region fanout is no longer used + expect(globalThis.fetch).not.toHaveBeenCalledWith( + expect.stringContaining("/users/me/regions/"), + expect.any(Object), + ); }); it("should fetch directly from organizations endpoint for self-hosted", async () => { @@ -900,51 +888,6 @@ describe("listOrganizations", () => { expect.any(Object), ); }); - - it("should fall back to direct organizations endpoint when regions endpoint returns 404 on SaaS", async () => { - const mockOrgs = [ - { id: "1", slug: "org-1", name: "Organization 1" }, - { id: "2", slug: "org-2", name: "Organization 2" }, - ]; - - globalThis.fetch = vi.fn().mockImplementation((url: string) => { - if (url.includes("/users/me/regions/")) { - return Promise.resolve({ - ok: false, - status: 404, - statusText: "Not Found", - text: () => Promise.resolve(JSON.stringify({ detail: "Not found" })), - }); - } - if (url.includes("/organizations/")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(mockOrgs), - }); - } - return Promise.reject(new Error("Unexpected URL")); - }); - - const apiService = new SentryApiService({ - host: "sentry.io", - accessToken: "test-token", - }); - - const result = await apiService.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), - ); - }); }); describe("host configuration", () => { @@ -1136,7 +1079,7 @@ describe("Content-Type validation", () => { ); }); - it("should handle HTML response from regions endpoint", async () => { + it("should handle HTML response from organizations endpoint", async () => { const htmlContent = ` Login Required diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index dd5a7df2a..c11f3d4c3 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -66,7 +66,6 @@ import { TraceMetaSchema, TraceSchema, UserSchema, - UserRegionsSchema, IssueAlertRuleListSchema, MetricAlertRuleListSchema, MetricAlertRuleSchema, @@ -1498,13 +1497,11 @@ export class SentryApiService { /** * Lists all organizations accessible to the authenticated user. * - * Automatically handles multi-region queries by fetching from all - * available regions and combining results. + * Queries the `/organizations/` endpoint on the root host. * * @param params Query parameters * @param params.query Search query to filter organizations by name/slug * @param params.limit Maximum number of organizations to return (defaults to 25) - * @param opts Request options * @returns Array of organizations across all accessible regions * * @example @@ -1516,10 +1513,10 @@ export class SentryApiService { * }); * ``` */ - async listOrganizations( - params?: { query?: string; limit?: number }, - opts?: RequestOptions, - ): Promise { + async listOrganizations(params?: { + query?: string; + limit?: number; + }): Promise { const limit = params?.limit ?? 25; // Build query parameters @@ -1531,50 +1528,14 @@ export class SentryApiService { const queryString = queryParams.toString(); const path = `/organizations/?${queryString}`; - // 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); + let host = undefined; + // For SaaS, always use the main sentry.io host, not regional hosts + if (this.isSaas()) { + host = "sentry.io"; } - // For SaaS, try to use regions endpoint first - try { - // TODO: Sentry is currently not returning all orgs without hitting region endpoints - // The regions endpoint only exists on the main API server, not on regional endpoints - const regionsBody = await this.requestJSON( - "/users/me/regions/", - undefined, - {}, // Don't pass opts to ensure we use the main host - ); - const regionData = UserRegionsSchema.parse(regionsBody); - - const allOrganizations = ( - await Promise.all( - regionData.regions.map(async (region) => - this.requestJSON(path, undefined, { - ...opts, - host: new URL(region.url).host, - }), - ), - ) - ) - .map((data) => OrganizationListSchema.parse(data)) - .reduce((acc, curr) => acc.concat(curr), []); - - // Apply the limit after combining results from all regions - return allOrganizations.slice(0, limit); - } catch (error) { - // If regions endpoint fails (e.g., older self-hosted versions identifying as sentry.io), - // 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); - } - - // Re-throw other errors - throw error; - } + const body = await this.requestJSON(path, undefined, { host }); + return OrganizationListSchema.parse(body); } /** diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index 8a370f18a..8e54c64c7 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -57,15 +57,6 @@ export const UserSchema = z }) .passthrough(); -export const UserRegionsSchema = z.object({ - regions: z.array( - z.object({ - name: z.string(), - url: z.string().url(), - }), - ), -}); - /** * Schema for Sentry organization API responses. * diff --git a/packages/mcp-core/src/tools/catalog-runtime/availability.ts b/packages/mcp-core/src/tools/catalog-runtime/availability.ts index 930f27da1..9d04931cb 100644 --- a/packages/mcp-core/src/tools/catalog-runtime/availability.ts +++ b/packages/mcp-core/src/tools/catalog-runtime/availability.ts @@ -90,7 +90,6 @@ function isAllowedBySkills({ tool: ToolConfig; context: ServerContext; }): boolean { - const grantedSkills: Set | undefined = context.grantedSkills ? new Set(context.grantedSkills) : undefined; @@ -202,7 +201,6 @@ export function getToolsForMcpRegistration({ useDefaultSurfacePolicy, }); - return availableTools.filter(({ isTopLevel }) => isTopLevel); } diff --git a/packages/mcp-core/src/tools/catalog/find-organizations.test.ts b/packages/mcp-core/src/tools/catalog/find-organizations.test.ts index d8f23f82d..8c5e84dd3 100644 --- a/packages/mcp-core/src/tools/catalog/find-organizations.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-organizations.test.ts @@ -11,12 +11,7 @@ import findOrganizations from "./find-organizations.js"; function mockOrganizations(organizations: unknown[]) { mswServer.use( - http.get("https://sentry.io/api/0/users/me/regions/", () => - HttpResponse.json({ - regions: [{ name: "us", url: "https://us.sentry.io" }], - }), - ), - http.get("https://us.sentry.io/api/0/organizations/", ({ request }) => { + http.get("https://sentry.io/api/0/organizations/", ({ request }) => { expect(new URL(request.url).searchParams.get("per_page")).toBe("26"); return HttpResponse.json(organizations); }), diff --git a/packages/mcp-core/src/tools/catalog/find-organizations.ts b/packages/mcp-core/src/tools/catalog/find-organizations.ts index d65e6d765..de916db03 100644 --- a/packages/mcp-core/src/tools/catalog/find-organizations.ts +++ b/packages/mcp-core/src/tools/catalog/find-organizations.ts @@ -47,8 +47,8 @@ export default defineTool({ }, outputSchema: findOrganizationsOutputSchema, async handler(params, context: ServerContext) { - // User data endpoints (like /users/me/regions/) should never use regionUrl - // as they must always query the main API server, not region-specific servers + // Organizations are listed from the root host, which returns orgs across + // all regions, so no regionUrl is passed here. const apiService = apiServiceFromContext(context); const organizations = await apiService.listOrganizations({ query: params.query ?? undefined, diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 32b75b698..bfc59b353 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -304,7 +304,6 @@ async function main() { (process.env.NODE_ENV !== "production" ? "development" : "production"), }); - // Log experimental mode status if (cli.experimental) { console.warn( diff --git a/packages/mcp-test-client/src/mcp-url.ts b/packages/mcp-test-client/src/mcp-url.ts index 14802fd07..856921a43 100644 --- a/packages/mcp-test-client/src/mcp-url.ts +++ b/packages/mcp-test-client/src/mcp-url.ts @@ -40,7 +40,6 @@ export function applyProtectedResourceFlags( useExperimental?: boolean; }, ): URL { - if (options.useExperimental) { protectedResourceUrl.searchParams.set("experimental", "1"); }