diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 988733871..05b2e441a 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -3200,9 +3200,16 @@ export class SentryApiService { /** * Create an uptime monitor. * - * POST /projects/{org}/{project}/uptime/ - * Source: src/sentry/uptime/endpoints/project_uptime_alert_index.py - * Body fields verified from UptimeMonitorValidator (camelCase via CamelSnakeSerializer). + * POST /organizations/{org}/projects/{project}/detectors/ + * Source: src/sentry/uptime/endpoints/validators.py (UptimeDomainCheckFailureValidator) + * + * Uses the detectors endpoint, the same one the Sentry UI uses, so Sentry + * records the acting user as the monitor's creator. The legacy + * POST /projects/{org}/{project}/uptime/ endpoint does not. + * + * The detector validator always enables new uptime detectors, so a + * `disabled` status is applied with a follow-up update. The monitor is then + * returned in the uptime monitor shape the other uptime tools use. */ async createUptimeMonitor( { @@ -3244,39 +3251,63 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const payload: Record = { - name, + const dataSource: Record = { url, intervalSeconds, timeoutMs, }; - if (method !== undefined) payload.method = method; - if (headers !== undefined) payload.headers = headers; - if (body !== undefined) payload.body = body; - if (assertion !== undefined) payload.assertion = assertion; - if (status !== undefined) payload.status = status; - if (owner !== undefined) payload.owner = owner; - if (environment !== undefined) payload.environment = environment; - if (traceSampling !== undefined) payload.traceSampling = traceSampling; + if (method !== undefined) dataSource.method = method; + if (headers !== undefined) dataSource.headers = headers; + if (body !== undefined) dataSource.body = body; + if (assertion !== undefined) dataSource.assertion = assertion; + if (traceSampling !== undefined) dataSource.traceSampling = traceSampling; if (responseCaptureEnabled !== undefined) { - payload.responseCaptureEnabled = responseCaptureEnabled; + dataSource.responseCaptureEnabled = responseCaptureEnabled; } + + const config: Record = {}; + if (environment !== undefined) config.environment = environment; if (recoveryThreshold !== undefined) { - payload.recoveryThreshold = recoveryThreshold; + config.recoveryThreshold = recoveryThreshold; } if (downtimeThreshold !== undefined) { - payload.downtimeThreshold = downtimeThreshold; + config.downtimeThreshold = downtimeThreshold; } + const payload: Record = { + type: "uptime_domain_failure", + name, + dataSources: [dataSource], + config, + }; + if (owner !== undefined) payload.owner = owner; + const responseBody = await this.requestJSON( - apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/`, + apiPath`/organizations/${organizationSlug}/projects/${projectSlug}/detectors/`, { method: "POST", body: JSON.stringify(payload), }, opts, ); - return UptimeMonitorSchema.parse(responseBody); + const detector = DetectorSchema.parse(responseBody); + + if (status === "disabled") { + return await this.updateUptimeMonitor( + { + organizationSlug, + projectSlug, + uptimeMonitorId: detector.id, + status, + }, + opts, + ); + } + + return await this.getUptimeMonitorDetails( + { organizationSlug, projectSlug, uptimeMonitorId: detector.id }, + opts, + ); } /** diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts index 9c59ab45e..c46ce8c9f 100644 --- a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts @@ -18,39 +18,55 @@ const context = { }; describe("create_uptime_monitor", () => { - it("creates an uptime monitor with camelCase payload", async () => { + const detectorsUrl = + "https://sentry.io/api/0/organizations/sentry-mcp-evals/projects/cloudflare-mcp/detectors/"; + const monitorUrl = + "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001002/"; + const createdDetector = { + id: "4509100000001002", + projectId: "4509062593708032", + name: "Checkout Health", + type: "uptime_domain_failure", + enabled: true, + config: { mode: 1, environment: "production" }, + conditionGroup: null, + dataSources: null, + workflowIds: [], + owner: null, + createdBy: "1", + dateCreated: "2026-11-02T00:00:00.000000Z", + dateUpdated: "2026-11-02T00:00:00.000000Z", + }; + const savedMonitor = { + id: "4509100000001002", + projectSlug: "cloudflare-mcp", + environment: "production", + name: "Checkout Health", + status: "active", + uptimeStatus: 1, + mode: 1, + owner: null, + recoveryThreshold: 1, + downtimeThreshold: 3, + url: "https://example.com/checkout", + method: "GET", + body: null, + headers: [], + intervalSeconds: 300, + timeoutMs: 8000, + traceSampling: false, + responseCaptureEnabled: true, + assertion: null, + }; + + it("creates the monitor through the detectors endpoint", async () => { let requestBody: Record | null = null; mswServer.use( - http.post( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/", - async ({ request }) => { - requestBody = (await request.json()) as Record; - return HttpResponse.json( - { - id: "4509100000001002", - projectSlug: "cloudflare-mcp", - environment: "production", - name: "Checkout Health", - status: "active", - uptimeStatus: 1, - mode: 1, - owner: null, - recoveryThreshold: 1, - downtimeThreshold: 3, - url: "https://example.com/checkout", - method: "GET", - body: null, - headers: [], - intervalSeconds: 300, - timeoutMs: 8000, - traceSampling: false, - responseCaptureEnabled: true, - assertion: null, - }, - { status: 201 }, - ); - }, - ), + http.post(detectorsUrl, async ({ request }) => { + requestBody = (await request.json()) as Record; + return HttpResponse.json(createdDetector, { status: 201 }); + }), + http.get(monitorUrl, () => HttpResponse.json(savedMonitor)), ); const result = await createUptimeMonitor.handler( @@ -64,21 +80,27 @@ describe("create_uptime_monitor", () => { timeoutMs: 8000, method: "GET", environment: "production", + downtimeThreshold: 3, }, context, ); - expect(requestBody).toMatchObject({ + expect(requestBody).toEqual({ + type: "uptime_domain_failure", name: "Checkout Health", - url: "https://example.com/checkout", - intervalSeconds: 300, - timeoutMs: 8000, - method: "GET", - environment: "production", + dataSources: [ + { + url: "https://example.com/checkout", + intervalSeconds: 300, + timeoutMs: 8000, + method: "GET", + }, + ], + config: { + environment: "production", + downtimeThreshold: 3, + }, }); - expect(requestBody).not.toHaveProperty("interval_seconds"); - expect(requestBody).not.toHaveProperty("timeout_ms"); - expect(requestBody).not.toHaveProperty("assertion"); assertStructuredOnlyResult(result); const structuredContent = getStructuredContent(result); @@ -108,6 +130,42 @@ describe("create_uptime_monitor", () => { `); }); + it("disables the monitor after creation when status is disabled", async () => { + let createBody: Record | null = null; + let updateBody: Record | null = null; + mswServer.use( + http.post(detectorsUrl, async ({ request }) => { + createBody = (await request.json()) as Record; + return HttpResponse.json(createdDetector, { status: 201 }); + }), + http.put(monitorUrl, async ({ request }) => { + updateBody = (await request.json()) as Record; + return HttpResponse.json({ ...savedMonitor, status: "disabled" }); + }), + ); + + const result = await createUptimeMonitor.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + name: "Checkout Health", + url: "https://example.com/checkout", + intervalSeconds: 300, + timeoutMs: 8000, + status: "disabled", + }, + context, + ); + + expect(createBody).not.toHaveProperty("status"); + expect(createBody).not.toHaveProperty("enabled"); + expect(updateBody).toEqual({ status: "disabled" }); + expect(getStructuredContent(result)).toMatchObject({ + monitor: { id: "4509100000001002", status: "disabled" }, + }); + }); + it("rejects empty owner in the input schema", () => { const result = createUptimeMonitor.inputSchema.owner.safeParse(" "); expect(result.success).toBe(false); diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index ceff40cbe..e99916d88 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -975,30 +975,38 @@ export const restHandlers = buildHandlers([ }, { method: "post", - path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/", + path: "/api/0/organizations/sentry-mcp-evals/projects/cloudflare-mcp/detectors/", fetch: async ({ request }) => { const body = (await request.json()) as Record; return HttpResponse.json( { - ...uptimeMonitorFixture, - ...body, id: "4509100000001002", - projectSlug: "cloudflare-mcp", - status: body.status ?? "active", - uptimeStatus: 1, - method: body.method ?? "GET", - headers: body.headers ?? [], - body: body.body ?? null, - assertion: body.assertion ?? null, - recoveryThreshold: body.recoveryThreshold ?? 1, - downtimeThreshold: body.downtimeThreshold ?? 3, - traceSampling: body.traceSampling ?? false, - responseCaptureEnabled: body.responseCaptureEnabled ?? true, + projectId: "4509062593708032", + name: body.name, + type: body.type, + enabled: true, + config: { mode: 1, ...(body.config as Record) }, + conditionGroup: null, + dataSources: null, + workflowIds: [], + owner: body.owner ?? null, + createdBy: "1", + dateCreated: "2026-11-02T00:00:00.000000Z", + dateUpdated: "2026-11-02T00:00:00.000000Z", }, { status: 201 }, ); }, }, + { + method: "get", + path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001002/", + fetch: () => + HttpResponse.json({ + ...uptimeMonitorFixture, + id: "4509100000001002", + }), + }, { method: "put", path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/",