From f6b9c0bc589ec17cc008278947867dbcc8587a42 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 04:47:54 +0000 Subject: [PATCH 1/4] feat(uptime): add MVP tools for uptime monitors Add catalog tools for listing, inspecting, creating, updating, and deleting Sentry HTTP uptime monitors, backed by verified upstream API schemas and endpoints. Closes #1136 Co-Authored-By: David Cramer Co-Authored-By: David Cramer --- packages/mcp-core/src/api-client/client.ts | 479 ++++- packages/mcp-core/src/api-client/schema.ts | 59 + packages/mcp-core/src/api-client/types.ts | 8 + packages/mcp-core/src/skillDefinitions.json | 33 +- packages/mcp-core/src/skills.ts | 4 +- packages/mcp-core/src/toolDefinitions.json | 1545 ++++++++++++++--- .../catalog/create-uptime-monitor.test.ts | 119 ++ .../tools/catalog/create-uptime-monitor.ts | 162 ++ .../catalog/delete-uptime-monitor.test.ts | 43 + .../tools/catalog/delete-uptime-monitor.ts | 79 + .../catalog/find-uptime-monitors.test.ts | 100 ++ .../src/tools/catalog/find-uptime-monitors.ts | 125 ++ .../get-uptime-monitor-details.test.ts | 58 + .../catalog/get-uptime-monitor-details.ts | 241 +++ packages/mcp-core/src/tools/catalog/index.ts | 10 + .../tools/catalog/support/uptime-monitors.ts | 126 ++ .../catalog/update-uptime-monitor.test.ts | 76 + .../tools/catalog/update-uptime-monitor.ts | 145 ++ packages/mcp-core/src/utils/url-utils.test.ts | 34 +- packages/mcp-core/src/utils/url-utils.ts | 24 +- .../src/fixtures/uptime-checks.json | 34 + .../src/fixtures/uptime-monitor.json | 25 + packages/mcp-server-mocks/src/index.ts | 203 ++- 23 files changed, 3358 insertions(+), 374 deletions(-) create mode 100644 packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts create mode 100644 packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts create mode 100644 packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts create mode 100644 packages/mcp-core/src/tools/catalog/delete-uptime-monitor.ts create mode 100644 packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts create mode 100644 packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts create mode 100644 packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts create mode 100644 packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts create mode 100644 packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts create mode 100644 packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts create mode 100644 packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts create mode 100644 packages/mcp-server-mocks/src/fixtures/uptime-checks.json create mode 100644 packages/mcp-server-mocks/src/fixtures/uptime-monitor.json diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 51c6e0c27..065b6b625 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -1,9 +1,20 @@ import { z } from "zod"; import { DEFAULT_SEARCH_ISSUES_PERIOD } from "../constants"; +import { ConfigurationError } from "../errors"; +import { logIssue, logWarn } from "../telem/logging"; +import type { SentryProtocol } from "../types"; import { - getContinuousProfileUrl as getContinuousProfileUrlUtil, + type EventsDataset, + isMetricsDataset, + isProfilesDataset, + normalizeEventsDataset, +} from "../utils/events-datasets"; +import { isNumericId } from "../utils/slug-validation"; +import { + type DashboardUrlOptions, getAIConversationsUrl as getAIConversationsUrlUtil, getAIConversationUrl as getAIConversationUrlUtil, + getContinuousProfileUrl as getContinuousProfileUrlUtil, getDashboardUrl as getDashboardUrlUtil, getIssueUrl as getIssueUrlUtil, getMonitorUrl as getMonitorUrlUtil, @@ -11,92 +22,92 @@ import { getProfileUrl as getProfileUrlUtil, getProfilingExplorerUrl, getReleaseUrl as getReleaseUrlUtil, - getReplayUrl as getReplayUrlUtil, getReplaysSearchUrl as getReplaysSearchUrlUtil, + getReplayUrl as getReplayUrlUtil, getTraceMetricsExploreUrl, getTraceUrl as getTraceUrlUtil, + getUptimeMonitorUrl as getUptimeMonitorUrlUtil, isSentryHost, - type DashboardUrlOptions, type TraceMetricIdentifier, } from "../utils/url-utils"; -import { isNumericId } from "../utils/slug-validation"; +import { USER_AGENT } from "../version"; import { apiPath } from "./api-path"; +import { ApiNotFoundError, ApiValidationError, createApiError } from "./errors"; import { - isMetricsDataset, - isProfilesDataset, - normalizeEventsDataset, - type EventsDataset, -} from "../utils/events-datasets"; -import { logIssue, logWarn } from "../telem/logging"; -import { - OrganizationListSchema, - OrganizationSchema, + AIConversationDetailsResponseSchema, + AIConversationSummaryListSchema, + ApiErrorSchema, + AutofixRunSchema, + AutofixRunStateSchema, + ClientKeyListSchema, ClientKeySchema, - TeamListSchema, - TeamSchema, - ProjectListSchema, - ProjectRepositoryMappingSchema, - ProjectSchema, CommitListSchema, + DashboardListSchema, + DashboardSchema, DeployListSchema, - MonitorCheckInListSchema, - MonitorListSchema, - MonitorSchema, - MonitorStatsSchema, - RepositoryListSchema, - ReleaseDetailsSchema, - ReleaseListSchema, + ErrorsSearchResponseSchema, + EventAttachmentListSchema, + EventSchema, + ExternalIssueListSchema, + FlamegraphSchema, IssueActivityListResponseSchema, + IssueAlertRuleListSchema, IssueCommentListSchema, IssueCommentSchema, IssueListSchema, IssueSchema, IssueTagValuesSchema, - ExternalIssueListSchema, - EventSchema, - EventAttachmentListSchema, - ErrorsSearchResponseSchema, - SpansSearchResponseSchema, - TagListSchema, - ApiErrorSchema, - ClientKeyListSchema, - AutofixRunSchema, - AutofixRunStateSchema, - DashboardListSchema, - DashboardSchema, - TraceMetaSchema, - TraceSchema, - UserSchema, - UserRegionsSchema, - IssueAlertRuleListSchema, MetricAlertRuleListSchema, MetricAlertRuleSchema, - FlamegraphSchema, + MonitorCheckInListSchema, + MonitorListSchema, + MonitorSchema, + MonitorStatsSchema, + OrganizationListSchema, + OrganizationSchema, ProfileChunkResponseSchema, - TransactionProfileSchema, + ProjectListSchema, + ProjectRepositoryMappingSchema, + ProjectSchema, + ReleaseDetailsSchema, + ReleaseListSchema, ReplayDetailsSchema, - ReplayListResponseSchema, ReplayIdsByResourceSchema, + ReplayListResponseSchema, ReplayRecordingSegmentsSchema, + RepositoryListSchema, + SpansSearchResponseSchema, StacktraceLinkSchema, - AIConversationSummaryListSchema, - AIConversationDetailsResponseSchema, + TagListSchema, + TeamListSchema, + TeamSchema, + TraceMetaSchema, + TraceSchema, + TransactionProfileSchema, + UptimeCheckListSchema, + UptimeMonitorListSchema, + UptimeMonitorSchema, + UserRegionsSchema, UserReportListSchema, + 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 { + AIConversationDetails, + AIConversationSpanList, + AIConversationSummary, AutofixRun, AutofixRunState, ClientKey, ClientKeyList, + CommitList, Dashboard, DashboardListItem, + DeployList, Event, EventAttachment, EventAttachmentList, + ExternalIssueList, + Flamegraph, Issue, IssueActivityList, IssueAlertRule, @@ -105,38 +116,35 @@ import type { IssueCommentList, IssueList, IssueTagValues, - ExternalIssueList, - CommitList, - DeployList, + MetricAlertRule, + MetricAlertRuleList, Monitor, MonitorCheckInList, MonitorList, MonitorStats, - MetricAlertRule, - MetricAlertRuleList, OrganizationList, + ProfileChunk, Project, ProjectList, ReleaseDetails, ReleaseList, + ReplayDetails, + ReplayList, + ReplayRecordingSegments, + StacktraceLink, TagList, Team, TeamList, Trace, TraceMeta, - User, - Flamegraph, - ProfileChunk, TransactionProfile, - ReplayDetails, - ReplayList, - ReplayRecordingSegments, - StacktraceLink, - AIConversationSummary, - AIConversationDetails, - AIConversationSpanList, + UptimeCheckList, + UptimeMonitor, + UptimeMonitorList, + User, UserReportList, } 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) // import { logger } from "@sentry/node"; @@ -243,6 +251,7 @@ function getNextCursor(linkHeader: string | null): string | null { type RequestOptions = { host?: string; + allowStatuses?: number[]; }; export type TraceItemType = "spans" | "logs" | "tracemetrics"; @@ -1050,6 +1059,18 @@ export class SentryApiService { ); } + getUptimeMonitorUrl( + organizationSlug: string, + uptimeMonitorId: string | number, + ): string { + return getUptimeMonitorUrlUtil( + this.host, + organizationSlug, + uptimeMonitorId, + this.protocol, + ); + } + getReleaseUrl(organizationSlug: string, releaseVersion: string): string { return getReleaseUrlUtil( this.host, @@ -2753,6 +2774,332 @@ export class SentryApiService { return MonitorStatsSchema.parse(body); } + /** + * List uptime monitors for an organization. + * + * GET /organizations/{org}/uptime/ + * Source: src/sentry/uptime/endpoints/organiation_uptime_alert_index.py + */ + async listUptimeMonitors( + { + organizationSlug, + projectSlug, + environment, + owner, + query, + limit, + }: { + organizationSlug: string; + projectSlug?: string; + environment?: string; + owner?: string; + query?: string; + limit?: number; + }, + opts?: RequestOptions, + ): Promise { + const searchQuery = new URLSearchParams(); + if (projectSlug) { + // OrganizationEndpoint filter params accept project IDs via `project`. + const project = await this.getProject( + { + organizationSlug, + projectSlugOrId: projectSlug, + }, + opts, + ); + searchQuery.append("project", String(project.id)); + } + if (environment) { + searchQuery.append("environment", environment); + } + if (owner) { + searchQuery.append("owner", owner); + } + if (query) { + searchQuery.set("query", query); + } + if (limit !== undefined) { + searchQuery.set("per_page", String(limit)); + } + + const path = apiPath`/organizations/${organizationSlug}/uptime/`; + const body = await this.requestJSON( + searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path, + undefined, + opts, + ); + return UptimeMonitorListSchema.parse(body); + } + + /** + * Get a single uptime monitor. + * + * GET /projects/{org}/{project}/uptime/{id}/ + * Source: src/sentry/uptime/endpoints/project_uptime_alert_details.py + */ + async getUptimeMonitorDetails( + { + organizationSlug, + projectSlug, + uptimeMonitorId, + }: { + organizationSlug: string; + projectSlug: string; + uptimeMonitorId: string; + }, + opts?: RequestOptions, + ): Promise { + const body = await this.requestJSON( + apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeMonitorId}/`, + undefined, + opts, + ); + return UptimeMonitorSchema.parse(body); + } + + /** + * List recent uptime checks for a monitor. + * + * GET /projects/{org}/{project}/uptime/{id}/checks/ + * Source: src/sentry/uptime/endpoints/project_uptime_alert_checks_index.py + */ + async listUptimeMonitorChecks( + { + organizationSlug, + projectSlug, + uptimeMonitorId, + statsPeriod, + start, + end, + limit, + }: { + organizationSlug: string; + projectSlug: string; + uptimeMonitorId: string; + statsPeriod?: string; + start?: string; + end?: string; + limit?: number; + }, + opts?: RequestOptions, + ): Promise { + const searchQuery = new URLSearchParams(); + if (limit !== undefined) { + searchQuery.set("per_page", String(limit)); + } + const normalizedStatsPeriod = normalizeStatsPeriod(statsPeriod); + const effectiveStatsPeriod = + start || end ? normalizedStatsPeriod : (normalizedStatsPeriod ?? "24h"); + if (effectiveStatsPeriod) { + parseStatsPeriod(effectiveStatsPeriod); + } + this.applyTimeParams(searchQuery, effectiveStatsPeriod, start, end); + + const path = + apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeMonitorId}/checks/`; + const body = await this.requestJSON( + searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path, + undefined, + opts, + ); + return UptimeCheckListSchema.parse(body); + } + + /** + * 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). + */ + async createUptimeMonitor( + { + organizationSlug, + projectSlug, + name, + url, + intervalSeconds, + timeoutMs, + method, + headers, + body, + assertion, + status, + owner, + environment, + traceSampling, + responseCaptureEnabled, + recoveryThreshold, + downtimeThreshold, + }: { + organizationSlug: string; + projectSlug: string; + name: string; + url: string; + intervalSeconds: number; + timeoutMs: number; + method?: string; + headers?: Array<[string, string]>; + body?: string | null; + assertion?: unknown | null; + status?: "active" | "disabled"; + owner?: string | null; + environment?: string | null; + traceSampling?: boolean; + responseCaptureEnabled?: boolean; + recoveryThreshold?: number; + downtimeThreshold?: number; + }, + opts?: RequestOptions, + ): Promise { + const payload: Record = { + name, + 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 (responseCaptureEnabled !== undefined) { + payload.responseCaptureEnabled = responseCaptureEnabled; + } + if (recoveryThreshold !== undefined) { + payload.recoveryThreshold = recoveryThreshold; + } + if (downtimeThreshold !== undefined) { + payload.downtimeThreshold = downtimeThreshold; + } + + const responseBody = await this.requestJSON( + apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/`, + { + method: "POST", + body: JSON.stringify(payload), + }, + opts, + ); + return UptimeMonitorSchema.parse(responseBody); + } + + /** + * Update an uptime monitor. + * + * PUT /projects/{org}/{project}/uptime/{id}/ + * Source: src/sentry/uptime/endpoints/project_uptime_alert_details.py + */ + async updateUptimeMonitor( + { + organizationSlug, + projectSlug, + uptimeMonitorId, + name, + url, + intervalSeconds, + timeoutMs, + method, + headers, + body, + assertion, + status, + owner, + environment, + traceSampling, + responseCaptureEnabled, + recoveryThreshold, + downtimeThreshold, + }: { + organizationSlug: string; + projectSlug: string; + uptimeMonitorId: string; + name?: string; + url?: string; + intervalSeconds?: number; + timeoutMs?: number; + method?: string; + headers?: Array<[string, string]>; + body?: string | null; + assertion?: unknown | null; + status?: "active" | "disabled"; + owner?: string | null; + environment?: string | null; + traceSampling?: boolean; + responseCaptureEnabled?: boolean; + recoveryThreshold?: number; + downtimeThreshold?: number; + }, + opts?: RequestOptions, + ): Promise { + const payload: Record = {}; + if (name !== undefined) payload.name = name; + if (url !== undefined) payload.url = url; + if (intervalSeconds !== undefined) + payload.intervalSeconds = intervalSeconds; + if (timeoutMs !== undefined) payload.timeoutMs = 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 (responseCaptureEnabled !== undefined) { + payload.responseCaptureEnabled = responseCaptureEnabled; + } + if (recoveryThreshold !== undefined) { + payload.recoveryThreshold = recoveryThreshold; + } + if (downtimeThreshold !== undefined) { + payload.downtimeThreshold = downtimeThreshold; + } + + const responseBody = await this.requestJSON( + apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeMonitorId}/`, + { + method: "PUT", + body: JSON.stringify(payload), + }, + opts, + ); + return UptimeMonitorSchema.parse(responseBody); + } + + /** + * Delete an uptime monitor. + * + * DELETE /projects/{org}/{project}/uptime/{id}/ + * Source: src/sentry/uptime/endpoints/project_uptime_alert_details.py + * Returns 202 with empty body. + */ + async deleteUptimeMonitor( + { + organizationSlug, + projectSlug, + uptimeMonitorId, + }: { + organizationSlug: string; + projectSlug: string; + uptimeMonitorId: string; + }, + opts?: RequestOptions, + ): Promise { + // Treat 404 as success so repeated deletes are idempotent. + await this.request( + apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeMonitorId}/`, + { + method: "DELETE", + }, + { ...opts, allowStatuses: [404] }, + ); + } + /** * Lists available tags for search queries. * diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index cdd5f0cb6..0f8dff974 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -639,6 +639,65 @@ export const MonitorStatSchema = z export const MonitorStatsSchema = z.array(MonitorStatSchema); +/** + * Uptime monitor (detector) schemas. + * + * Verified against getsentry/sentry: + * - src/sentry/uptime/endpoints/serializers.py (UptimeDetectorSerializer) + * - src/sentry/uptime/endpoints/validators.py (UptimeMonitorValidator) + * - src/sentry/uptime/models.py (IntervalSeconds, SupportedHTTPMethods) + * Response fields are camelCase from CamelSnakeSerializer. + */ +export const UptimeMonitorSchema = z + .object({ + id: ApiResourceIdSchema, + projectSlug: z.string(), + environment: z.string().nullable().optional(), + name: z.string(), + status: z.string(), + uptimeStatus: z.union([z.number(), z.string()]).optional(), + mode: z.number().optional(), + owner: ApiActorSchema.nullable().optional(), + recoveryThreshold: z.number().optional(), + downtimeThreshold: z.number().optional(), + url: z.string(), + method: z.string().optional(), + body: z.string().nullable().optional(), + headers: z + .array(z.tuple([z.string(), z.string()])) + .or(z.array(z.array(z.string()))) + .optional(), + intervalSeconds: z.number(), + timeoutMs: z.number(), + traceSampling: z.boolean().optional(), + responseCaptureEnabled: z.boolean().optional(), + assertion: z.unknown().nullable().optional(), + }) + .passthrough(); + +export const UptimeMonitorListSchema = z.array(UptimeMonitorSchema); + +export const UptimeCheckSchema = z + .object({ + uptimeCheckId: z.string().optional(), + timestamp: z.string().optional(), + scheduledCheckTime: z.string().optional(), + checkStatus: z.string().optional(), + checkStatusReason: z.string().nullable().optional(), + assertionFailureData: z.unknown().nullable().optional(), + httpStatusCode: z.number().nullable().optional(), + durationMs: z.number().optional(), + traceId: z.string().optional(), + traceItemId: z.string().optional(), + incidentStatus: z.number().optional(), + environment: z.string().optional(), + region: z.string().optional(), + regionName: z.string().optional(), + }) + .passthrough(); + +export const UptimeCheckListSchema = z.array(UptimeCheckSchema); + export const ReleaseDetailsSchema = ReleaseSchema.extend({ adoptionStages: z.unknown().optional(), authors: z.array(ApiActorSchema).optional(), diff --git a/packages/mcp-core/src/api-client/types.ts b/packages/mcp-core/src/api-client/types.ts index 391f594ec..7d609f82a 100644 --- a/packages/mcp-core/src/api-client/types.ts +++ b/packages/mcp-core/src/api-client/types.ts @@ -86,6 +86,10 @@ import type { MonitorSchema, MonitorStatsSchema, MonitorStatSchema, + UptimeCheckListSchema, + UptimeCheckSchema, + UptimeMonitorListSchema, + UptimeMonitorSchema, MetricAlertRuleListSchema, MetricAlertRuleSchema, OrganizationListSchema, @@ -141,6 +145,8 @@ export type IssueComment = z.infer; export type Monitor = z.infer; export type MonitorCheckIn = z.infer; export type MonitorStat = z.infer; +export type UptimeMonitor = z.infer; +export type UptimeCheck = z.infer; // Individual event types export type ErrorEvent = z.infer; @@ -187,6 +193,8 @@ export type IssueCommentList = z.infer; export type MonitorList = z.infer; export type MonitorCheckInList = z.infer; export type MonitorStats = z.infer; +export type UptimeMonitorList = z.infer; +export type UptimeCheckList = z.infer; export type EventAttachmentList = z.infer; export type TagList = z.infer; export type ClientKeyList = z.infer; diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 8f4eed553..996318cff 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -2,10 +2,10 @@ { "id": "inspect", "name": "Inspect Issues & Events", - "description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, monitors, profiles, documentation, and project metadata", + "description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, cron monitors, uptime monitors, profiles, documentation, and project metadata", "defaultEnabled": true, "order": 1, - "toolCount": 35, + "toolCount": 37, "tools": [ { "name": "find_alert_rules", @@ -42,6 +42,11 @@ "description": "Find teams in an organization in Sentry.\n\nUse this tool when you need to:\n- View teams in a Sentry organization\n- Find a team's slug and numeric ID to aid other tool requests\n- Search for specific teams by name or slug\n\nReturns up to 25 results. When hasMore is true, use the query parameter to narrow down results.", "requiredScopes": ["team:read"] }, + { + "name": "find_uptime_monitors", + "description": "Find Sentry uptime monitors.\n\nUse this tool when you need to:\n- List HTTP uptime monitors in an organization\n- Find a monitor by name or URL before getting details\n- Check monitor status, owner, project, interval, or target URL\n- When `hasMore` is true, narrow results with project, environment, owner, or query filters\n\nThis is separate from cron monitors (`find_monitors`).\n\n\nfind_uptime_monitors(organizationSlug='my-organization')\nfind_uptime_monitors(organizationSlug='my-organization', projectSlug='backend', query='api')\n", + "requiredScopes": ["org:read", "project:read"] + }, { "name": "get_ai_conversation_details", "description": "Fetch the chronological transcript and debugging details for one AI conversation.\n\nReturns a timeline of user messages, assistant messages, and tool calls, with trace/span IDs for deeper debugging. To discover or list conversations, use search_ai_conversations.", @@ -152,6 +157,11 @@ "description": "Get detailed information about a specific Sentry trace by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific trace ID (e.g., 'a4d1aae7216b47ff8117cf4e09ce9d0a')\n- Ask to 'show me trace [TRACE-ID]', 'explain trace [TRACE-ID]'\n- Want high-level overview and link to view trace details in Sentry\n- Need trace statistics and span breakdown\n- Want an overview first, then a guided pivot into additional spans or events\n\nDO NOT USE for:\n- General searching for traces (use search_events with trace queries)\n- Complete span enumeration or branch-by-branch reconstruction (use search_events scoped to the trace)\n\nTRIGGER PATTERNS:\n- 'Show me trace abc123' → use get_trace_details\n- 'Explain trace a4d1aae7216b47ff8117cf4e09ce9d0a' → use get_trace_details\n- 'What is trace [trace-id]' → use get_trace_details\n\n\n### Get trace overview\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a')\n```\n\n### Focus a single span\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a', spanId='aa8e7f3384ef4ff5')\n```\n\n\n\n- Trace IDs are 32-character hexadecimal strings\n- This returns a condensed trace overview, not a full span dump\n- Provide `spanId` to focus on a single span within the trace\n- If the response says it shows a subset of spans, use search_events to inspect the rest of the trace\n", "requiredScopes": ["event:read"] }, + { + "name": "get_uptime_monitor_details", + "description": "Get details for a Sentry uptime monitor, including recent checks.\n\nUse this tool when you need to:\n- Inspect an uptime monitor's URL, interval, thresholds, and status\n- Review recent HTTP check results (success/failure, status code, duration)\n- Debug why an uptime monitor is failing\n\nThis is separate from cron monitors (`get_monitor_details`).\n\n\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)\n", + "requiredScopes": ["project:read"] + }, { "name": "search_ai_conversations", "description": "Search Sentry AI Conversations and return one summary row per conversation.\n\nUse this tool to find or list AI Conversations. Results are conversation summaries, not raw span rows.\nUse get_ai_conversation_details with a conversationId to fetch the transcript. Use get_sentry_resource for Sentry conversation URLs.\n\n\nsearch_ai_conversations(organizationSlug='my-org', query='failed conversations', period='7d')\nsearch_ai_conversations(organizationSlug='my-org', query='checkout', project='backend')\n", @@ -383,10 +393,10 @@ { "id": "project-management", "name": "Manage Projects & Teams", - "description": "Create and modify projects, teams, and DSNs", + "description": "Create and modify projects, teams, DSNs, and uptime monitors", "defaultEnabled": false, "order": 5, - "toolCount": 12, + "toolCount": 15, "tools": [ { "name": "add_team_to_project", @@ -408,6 +418,16 @@ "description": "Create a new team in Sentry.\n\nUSE THIS TOOL WHEN USERS WANT TO:\n- 'Create a new team'\n- 'Set up a team called [X]'\n- 'I need a team for my project'\n\nBe careful when using this tool!\n\n\n### Create a new team\n```\ncreate_team(organizationSlug='my-organization', name='the-goats')\n```\n\n\n\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n", "requiredScopes": ["team:write"] }, + { + "name": "create_uptime_monitor", + "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced assertions can be passed as JSON via `assertion` when needed.\n", + "requiredScopes": ["project:write"] + }, + { + "name": "delete_uptime_monitor", + "description": "Delete a Sentry HTTP uptime monitor.\n\nUse this tool when you need to permanently remove an uptime monitor.\n\nBe careful when using this tool! Deletion cannot be undone.\n\n\ndelete_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\n", + "requiredScopes": ["project:write"] + }, { "name": "find_dsns", "description": "List all Sentry DSNs for a specific project.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n", @@ -443,6 +463,11 @@ "description": "Update project metadata in Sentry, such as name, slug, and platform.\n\nBe careful when using this tool!\n\nUse this tool when you need to:\n- Update a project's name or slug to fix onboarding mistakes\n- Change the platform assigned to a project\n\n\n### Update a project's name and slug\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')\n```\n\n### Update platform\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')\n```\n\n\n\n\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of /.\n- Team access changes are handled by separate project-management tools.\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n- When updating the slug, the project will be accessible at the new slug after the update\n- Do not update the slug from a project-scoped session; reconnect with an organization-scoped or unconstrained session first.\n", "requiredScopes": ["project:write"] }, + { + "name": "update_uptime_monitor", + "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\n", + "requiredScopes": ["project:write"] + }, { "name": "whoami", "description": "Identify the authenticated user in Sentry.\n\nUse this tool when you need to:\n- Get the user's name and email address.", diff --git a/packages/mcp-core/src/skills.ts b/packages/mcp-core/src/skills.ts index 259ace4ec..2b0a41dc0 100644 --- a/packages/mcp-core/src/skills.ts +++ b/packages/mcp-core/src/skills.ts @@ -29,7 +29,7 @@ export const SKILLS: Record = { id: "inspect", name: "Inspect Issues & Events", description: - "Read-only access to core Sentry data: issues, events, traces, replays, releases, monitors, profiles, documentation, and project metadata", + "Read-only access to core Sentry data: issues, events, traces, replays, releases, cron monitors, uptime monitors, profiles, documentation, and project metadata", defaultEnabled: true, order: 1, }, @@ -60,7 +60,7 @@ export const SKILLS: Record = { "project-management": { id: "project-management", name: "Manage Projects & Teams", - description: "Create and modify projects, teams, and DSNs", + description: "Create and modify projects, teams, DSNs, and uptime monitors", defaultEnabled: false, order: 5, }, diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 2770a2d38..6ebcc3d65 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -339,6 +339,420 @@ "skills": ["project-management"], "surface": "catalog" }, + { + "name": "create_uptime_monitor", + "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced assertions can be passed as JSON via `assertion` when needed.\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Monitor display name." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to check." + }, + "intervalSeconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Seconds between checks. Allowed values: 60, 300, 600, 1200, 1800, 3600." + }, + "timeoutMs": { + "default": 5000, + "type": "integer", + "minimum": 1000, + "maximum": 60000, + "description": "Request timeout in milliseconds (1000-60000)." + }, + "method": { + "default": null, + "anyOf": [ + { + "type": "string", + "enum": [ + "GET", + "POST", + "HEAD", + "PUT", + "DELETE", + "PATCH", + "OPTIONS" + ], + "description": "HTTP method used for the uptime check request." + }, + { + "type": "null" + } + ] + }, + "headers": { + "default": null, + "anyOf": [ + { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Authorization\",\"Bearer ...\"]]." + }, + { + "type": "null" + } + ] + }, + "body": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Optional request body for methods that support a body." + }, + { + "type": "null" + } + ] + }, + "assertion": { + "default": null, + "anyOf": [ + { + "description": "Optional assertion JSON payload accepted by Sentry uptime monitors." + }, + { + "type": "null" + } + ] + }, + "status": { + "default": null, + "anyOf": [ + { + "type": "string", + "enum": ["active", "disabled"], + "description": "Monitor status. `disabled` stops checks and does not count against quota." + }, + { + "type": "null" + } + ] + }, + "owner": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Optional owner actor, e.g. `user:123` or `team:456`." + }, + { + "type": "null" + } + ] + }, + "environment": { + "default": null, + "anyOf": [ + { + "type": "string", + "maxLength": 64, + "description": "Optional environment name for created uptime issues." + }, + { + "type": "null" + } + ] + }, + "traceSampling": { + "default": null, + "anyOf": [ + { + "type": "boolean", + "description": "Whether check requests may be considered for tracing." + }, + { + "type": "null" + } + ] + }, + "responseCaptureEnabled": { + "default": null, + "anyOf": [ + { + "type": "boolean", + "description": "Capture response body/headers on failures." + }, + { + "type": "null" + } + ] + }, + "recoveryThreshold": { + "default": null, + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Consecutive successful checks required to recover." + }, + { + "type": "null" + } + ] + }, + "downtimeThreshold": { + "default": null, + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Consecutive failed checks required to mark down." + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "organizationSlug", + "projectSlug", + "name", + "url", + "intervalSeconds" + ] + }, + "outputSchema": { + "type": "object", + "properties": { + "monitor": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "projectSlug": { + "type": "string" + }, + "status": { + "type": "string" + }, + "uptimeStatus": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "url": { + "type": "string" + }, + "method": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "intervalSeconds": { + "type": "number" + }, + "timeoutMs": { + "type": "number" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "recoveryThreshold": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "downtimeThreshold": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "traceSampling": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "responseCaptureEnabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "webUrl": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "name", + "projectSlug", + "status", + "uptimeStatus", + "url", + "method", + "intervalSeconds", + "timeoutMs", + "environment", + "owner", + "recoveryThreshold", + "downtimeThreshold", + "traceSampling", + "responseCaptureEnabled", + "webUrl" + ], + "additionalProperties": false + } + }, + "required": ["monitor"], + "additionalProperties": false + }, + "requiredScopes": ["project:write"], + "skills": ["project-management"], + "surface": "catalog" + }, + { + "name": "delete_uptime_monitor", + "description": "Delete a Sentry HTTP uptime monitor.\n\nUse this tool when you need to permanently remove an uptime monitor.\n\nBe careful when using this tool! Deletion cannot be undone.\n\n\ndelete_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + "uptimeMonitorId": { + "type": "string", + "minLength": 1, + "description": "Uptime monitor ID (detector id)." + } + }, + "required": ["organizationSlug", "projectSlug", "uptimeMonitorId"] + }, + "outputSchema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true + }, + "uptimeMonitorId": { + "type": "string" + }, + "projectSlug": { + "type": "string" + } + }, + "required": ["success", "uptimeMonitorId", "projectSlug"], + "additionalProperties": false + }, + "requiredScopes": ["project:write"], + "skills": ["project-management"], + "surface": "catalog" + }, { "name": "execute_sentry_tool", "description": "Execute an available Sentry MCP tool discovered through search_sentry_tools.\n\nUse this tool when you need to:\n- Call a Sentry operation returned by search_sentry_tools\n- Execute a tool by name using arguments that match its returned schema\n\n\nexecute_sentry_tool(name='find_projects', arguments={ organizationSlug: 'my-org' })\nexecute_sentry_tool(name='whoami', arguments={})\n\n\n\n- Use search_sentry_tools first if you are not sure which name or arguments to pass.\n- Arguments are validated against the target tool's schema before execution.\n- Active organization, project, and region constraints are injected automatically.\n", @@ -1336,7 +1750,305 @@ ] } }, - "required": ["slug", "webUrl", "regionUrl"], + "required": ["slug", "webUrl", "regionUrl"], + "additionalProperties": false + } + }, + "hasMore": { + "type": "boolean" + } + }, + "required": ["organizations", "hasMore"], + "additionalProperties": false + }, + "requiredScopes": ["org:read"], + "skills": ["inspect", "seer", "docs", "triage", "project-management"], + "surface": "direct" + }, + { + "name": "find_projects", + "description": "Find projects in Sentry.\n\nUse this tool when you need to:\n- View projects in a Sentry organization\n- Find a project's slug to aid other tool requests\n- Search for specific projects by name or slug\n\nReturns up to 25 results. When hasMore is true, use the query parameter to narrow down results.", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "query": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items." + }, + { + "type": "null" + } + ] + } + }, + "required": ["organizationSlug"] + }, + "outputSchema": { + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "slug": { + "type": "string" + } + }, + "required": ["slug"], + "additionalProperties": false + } + }, + "hasMore": { + "type": "boolean" + } + }, + "required": ["projects", "hasMore"], + "additionalProperties": false + }, + "requiredScopes": ["project:read"], + "skills": ["inspect", "seer", "docs", "triage", "project-management"], + "surface": "direct" + }, + { + "name": "find_releases", + "description": "Find releases in Sentry.\n\nUse this tool when you need to:\n- Find recent releases in a Sentry organization\n- Find the most recent version released of a specific project\n- Determine when a release was deployed to an environment\n\nReturns up to 25 results. When hasMore is true, use the projectSlug or query parameter to narrow down results.\n\n\n### Find the most recent releases in the 'my-organization' organization\n\n```\nfind_releases(organizationSlug='my-organization')\n```\n\n### Find releases matching '2ce6a27' in the 'my-organization' organization\n\n```\nfind_releases(organizationSlug='my-organization', query='2ce6a27')\n```\n\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug, or exact lowercase `all` when a tool supports all-projects scope. Other casing is treated as a project slug." + }, + { + "type": "null" + } + ] + }, + "query": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Search for versions which contain the provided string." + }, + { + "type": "null" + } + ] + } + }, + "required": ["organizationSlug"] + }, + "outputSchema": { + "type": "object", + "properties": { + "releases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "dateCreated": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "dateReleased": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "firstEvent": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "lastEvent": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "newIssues": { + "type": "number" + }, + "projects": { + "type": "array", + "items": { + "type": "string" + } + }, + "lastCommit": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "author": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "dateCreated": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "required": ["id", "message", "author", "dateCreated"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "lastDeploy": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "dateStarted": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "dateFinished": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "environment", + "dateStarted", + "dateFinished" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "version", + "dateCreated", + "dateReleased", + "firstEvent", + "lastEvent", + "newIssues", + "projects", + "lastCommit", + "lastDeploy" + ], "additionalProperties": false } }, @@ -1344,16 +2056,16 @@ "type": "boolean" } }, - "required": ["organizations", "hasMore"], + "required": ["releases", "hasMore"], "additionalProperties": false }, - "requiredScopes": ["org:read"], - "skills": ["inspect", "seer", "docs", "triage", "project-management"], - "surface": "direct" + "requiredScopes": ["project:read"], + "skills": ["inspect"], + "surface": "catalog" }, { - "name": "find_projects", - "description": "Find projects in Sentry.\n\nUse this tool when you need to:\n- View projects in a Sentry organization\n- Find a project's slug to aid other tool requests\n- Search for specific projects by name or slug\n\nReturns up to 25 results. When hasMore is true, use the query parameter to narrow down results.", + "name": "find_teams", + "description": "Find teams in an organization in Sentry.\n\nUse this tool when you need to:\n- View teams in a Sentry organization\n- Find a team's slug and numeric ID to aid other tool requests\n- Search for specific teams by name or slug\n\nReturns up to 25 results. When hasMore is true, use the query parameter to narrow down results.", "inputSchema": { "type": "object", "properties": { @@ -1391,16 +2103,19 @@ "outputSchema": { "type": "object", "properties": { - "projects": { + "teams": { "type": "array", "items": { "type": "object", "properties": { "slug": { "type": "string" + }, + "id": { + "type": "string" } }, - "required": ["slug"], + "required": ["slug", "id"], "additionalProperties": false } }, @@ -1408,16 +2123,16 @@ "type": "boolean" } }, - "required": ["projects", "hasMore"], + "required": ["teams", "hasMore"], "additionalProperties": false }, - "requiredScopes": ["project:read"], - "skills": ["inspect", "seer", "docs", "triage", "project-management"], - "surface": "direct" + "requiredScopes": ["team:read"], + "skills": ["inspect", "triage", "project-management"], + "surface": "catalog" }, { - "name": "find_releases", - "description": "Find releases in Sentry.\n\nUse this tool when you need to:\n- Find recent releases in a Sentry organization\n- Find the most recent version released of a specific project\n- Determine when a release was deployed to an environment\n\nReturns up to 25 results. When hasMore is true, use the projectSlug or query parameter to narrow down results.\n\n\n### Find the most recent releases in the 'my-organization' organization\n\n```\nfind_releases(organizationSlug='my-organization')\n```\n\n### Find releases matching '2ce6a27' in the 'my-organization' organization\n\n```\nfind_releases(organizationSlug='my-organization', query='2ce6a27')\n```\n\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n", + "name": "find_uptime_monitors", + "description": "Find Sentry uptime monitors.\n\nUse this tool when you need to:\n- List HTTP uptime monitors in an organization\n- Find a monitor by name or URL before getting details\n- Check monitor status, owner, project, interval, or target URL\n- When `hasMore` is true, narrow results with project, environment, owner, or query filters\n\nThis is separate from cron monitors (`find_monitors`).\n\n\nfind_uptime_monitors(organizationSlug='my-organization')\nfind_uptime_monitors(organizationSlug='my-organization', projectSlug='backend', query='api')\n", "inputSchema": { "type": "object", "properties": { @@ -1449,17 +2164,48 @@ } ] }, + "environment": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Optional environment name to limit monitors." + }, + { + "type": "null" + } + ] + }, + "owner": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Optional owner filter, such as `user:123`, `team:456`, `myteams`, or `unassigned`." + }, + { + "type": "null" + } + ] + }, "query": { "default": null, "anyOf": [ { "type": "string", - "description": "Search for versions which contain the provided string." + "description": "Optional search query for monitor name or URL." }, { "type": "null" } ] + }, + "limit": { + "default": 10, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 99, + "description": "Maximum number of monitors to return." } }, "required": ["organizationSlug"] @@ -1467,173 +2213,137 @@ "outputSchema": { "type": "object", "properties": { - "releases": { + "monitors": { "type": "array", "items": { "type": "object", "properties": { - "version": { + "id": { "type": "string" }, - "dateCreated": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + "name": { + "type": "string" }, - "dateReleased": { + "projectSlug": { + "type": "string" + }, + "status": { + "type": "string" + }, + "uptimeStatus": { "anyOf": [ { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + "type": "number" + }, + { + "type": "string" }, { "type": "null" } ] }, - "firstEvent": { + "url": { + "type": "string" + }, + "method": { "anyOf": [ { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + "type": "string" }, { "type": "null" } ] }, - "lastEvent": { + "intervalSeconds": { + "type": "number" + }, + "timeoutMs": { + "type": "number" + }, + "environment": { "anyOf": [ { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + "type": "string" }, { "type": "null" } ] }, - "newIssues": { - "type": "number" + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "projects": { - "type": "array", - "items": { - "type": "string" - } + "recoveryThreshold": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] }, - "lastCommit": { + "downtimeThreshold": { "anyOf": [ { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "author": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "dateCreated": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" - } - }, - "required": ["id", "message", "author", "dateCreated"], - "additionalProperties": false + "type": "number" }, { "type": "null" } ] }, - "lastDeploy": { + "traceSampling": { "anyOf": [ { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "environment": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "dateStarted": { - "anyOf": [ - { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" - }, - { - "type": "null" - } - ] - }, - "dateFinished": { - "anyOf": [ - { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "environment", - "dateStarted", - "dateFinished" - ], - "additionalProperties": false + "type": "boolean" }, { "type": "null" } ] + }, + "responseCaptureEnabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "webUrl": { + "type": "string", + "format": "uri" } }, "required": [ - "version", - "dateCreated", - "dateReleased", - "firstEvent", - "lastEvent", - "newIssues", - "projects", - "lastCommit", - "lastDeploy" + "id", + "name", + "projectSlug", + "status", + "uptimeStatus", + "url", + "method", + "intervalSeconds", + "timeoutMs", + "environment", + "owner", + "recoveryThreshold", + "downtimeThreshold", + "traceSampling", + "responseCaptureEnabled", + "webUrl" ], "additionalProperties": false } @@ -1642,80 +2352,13 @@ "type": "boolean" } }, - "required": ["releases", "hasMore"], + "required": ["monitors", "hasMore"], "additionalProperties": false }, - "requiredScopes": ["project:read"], + "requiredScopes": ["org:read", "project:read"], "skills": ["inspect"], "surface": "catalog" }, - { - "name": "find_teams", - "description": "Find teams in an organization in Sentry.\n\nUse this tool when you need to:\n- View teams in a Sentry organization\n- Find a team's slug and numeric ID to aid other tool requests\n- Search for specific teams by name or slug\n\nReturns up to 25 results. When hasMore is true, use the query parameter to narrow down results.", - "inputSchema": { - "type": "object", - "properties": { - "organizationSlug": { - "type": "string", - "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." - }, - "regionUrl": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." - }, - { - "type": "null" - } - ] - }, - "query": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items." - }, - { - "type": "null" - } - ] - } - }, - "required": ["organizationSlug"] - }, - "outputSchema": { - "type": "object", - "properties": { - "teams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "slug": { - "type": "string" - }, - "id": { - "type": "string" - } - }, - "required": ["slug", "id"], - "additionalProperties": false - } - }, - "hasMore": { - "type": "boolean" - } - }, - "required": ["teams", "hasMore"], - "additionalProperties": false - }, - "requiredScopes": ["team:read"], - "skills": ["inspect", "triage", "project-management"], - "surface": "catalog" - }, { "name": "get_ai_conversation_details", "description": "Fetch the chronological transcript and debugging details for one AI conversation.\n\nReturns a timeline of user messages, assistant messages, and tool calls, with trace/span IDs for deeper debugging. To discover or list conversations, use search_ai_conversations.", @@ -3345,6 +3988,92 @@ "skills": ["inspect"], "surface": "catalog" }, + { + "name": "get_uptime_monitor_details", + "description": "Get details for a Sentry uptime monitor, including recent checks.\n\nUse this tool when you need to:\n- Inspect an uptime monitor's URL, interval, thresholds, and status\n- Review recent HTTP check results (success/failure, status code, duration)\n- Debug why an uptime monitor is failing\n\nThis is separate from cron monitors (`get_monitor_details`).\n\n\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + "uptimeMonitorId": { + "type": "string", + "minLength": 1, + "description": "Uptime monitor ID (detector id)." + }, + "period": { + "default": null, + "anyOf": [ + { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time range for recent checks. Defaults to `24h` when `start` and `end` are omitted." + }, + { + "type": "null" + } + ] + }, + "start": { + "default": null, + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Absolute start time. Must be provided with `end`; do not combine with `period`." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Absolute end time. Must be provided with `start`; do not combine with `period`." + }, + { + "type": "null" + } + ] + }, + "checkLimit": { + "default": 10, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 50, + "description": "Maximum number of recent checks to include." + } + }, + "required": ["organizationSlug", "projectSlug", "uptimeMonitorId"] + }, + "requiredScopes": ["project:read"], + "skills": ["inspect"], + "surface": "catalog" + }, { "name": "remove_team_from_project", "description": "Revoke a team's access to an existing Sentry project.\n\nUse this tool when you need to:\n- Remove a team from a project\n- Revoke team access without changing project metadata\n- Check project team assignments before removing access\n\nBe careful when using this tool because it revokes project access.\n\n\nremove_team_from_project(organizationSlug='my-organization', projectSlug='my-project', teamSlug='my-team')\n\n\n\n- The team must already be assigned to the project.\n- This tool will not remove the last team assigned to a project.\n", @@ -4355,15 +5084,117 @@ "minLength": 1, "description": "Optional reason for taking this action. When provided, it will be posted as a comment on the issue's activity feed." } - } + } + }, + "requiredScopes": ["event:write"], + "skills": ["triage"], + "surface": "direct" + }, + { + "name": "update_project", + "description": "Update project metadata in Sentry, such as name, slug, and platform.\n\nBe careful when using this tool!\n\nUse this tool when you need to:\n- Update a project's name or slug to fix onboarding mistakes\n- Change the platform assigned to a project\n\n\n### Update a project's name and slug\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')\n```\n\n### Update platform\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')\n```\n\n\n\n\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of /.\n- Team access changes are handled by separate project-management tools.\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n- When updating the slug, the project will be accessible at the new slug after the update\n- Do not update the slug from a project-scoped session; reconnect with an organization-scoped or unconstrained session first.\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + "name": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The new name for the project" + }, + { + "type": "null" + } + ] + }, + "slug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The new slug for the project (must be unique)" + }, + { + "type": "null" + } + ] + }, + "platform": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The platform for the project. e.g., python, javascript, react, etc." + }, + { + "type": "null" + } + ] + } + }, + "required": ["organizationSlug", "projectSlug"] + }, + "outputSchema": { + "type": "object", + "properties": { + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "platform": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "slug", "name", "platform"], + "additionalProperties": false + } + }, + "required": ["project"], + "additionalProperties": false }, - "requiredScopes": ["event:write"], - "skills": ["triage"], - "surface": "direct" + "requiredScopes": ["project:write"], + "skills": ["project-management"], + "surface": "catalog" }, { - "name": "update_project", - "description": "Update project metadata in Sentry, such as name, slug, and platform.\n\nBe careful when using this tool!\n\nUse this tool when you need to:\n- Update a project's name or slug to fix onboarding mistakes\n- Change the platform assigned to a project\n\n\n### Update a project's name and slug\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')\n```\n\n### Update platform\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')\n```\n\n\n\n\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of /.\n- Team access changes are handled by separate project-management tools.\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n- When updating the slug, the project will be accessible at the new slug after the update\n- Do not update the slug from a project-scoped session; reconnect with an organization-scoped or unconstrained session first.\n", + "name": "update_uptime_monitor", + "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\n", "inputSchema": { "type": "object", "properties": { @@ -4387,36 +5218,206 @@ "type": "string", "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." }, + "uptimeMonitorId": { + "type": "string", + "minLength": 1, + "description": "Uptime monitor ID (detector id)." + }, "name": { "default": null, "anyOf": [ { "type": "string", - "description": "The new name for the project" + "minLength": 1, + "maxLength": 128 }, { "type": "null" } ] }, - "slug": { + "url": { "default": null, "anyOf": [ { "type": "string", - "description": "The new slug for the project (must be unique)" + "format": "uri" }, { "type": "null" } ] }, - "platform": { + "intervalSeconds": { + "default": null, + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Seconds between checks. Allowed values: 60, 300, 600, 1200, 1800, 3600." + }, + { + "type": "null" + } + ] + }, + "timeoutMs": { + "default": null, + "anyOf": [ + { + "type": "integer", + "minimum": 1000, + "maximum": 60000, + "description": "Request timeout in milliseconds (1000-60000)." + }, + { + "type": "null" + } + ] + }, + "method": { "default": null, "anyOf": [ { "type": "string", - "description": "The platform for the project. e.g., python, javascript, react, etc." + "enum": [ + "GET", + "POST", + "HEAD", + "PUT", + "DELETE", + "PATCH", + "OPTIONS" + ], + "description": "HTTP method used for the uptime check request." + }, + { + "type": "null" + } + ] + }, + "headers": { + "default": null, + "anyOf": [ + { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Authorization\",\"Bearer ...\"]]." + }, + { + "type": "null" + } + ] + }, + "body": { + "default": null, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "assertion": { + "default": null, + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "status": { + "default": null, + "anyOf": [ + { + "type": "string", + "enum": ["active", "disabled"], + "description": "Monitor status. `disabled` stops checks and does not count against quota." + }, + { + "type": "null" + } + ] + }, + "owner": { + "default": null, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "environment": { + "default": null, + "anyOf": [ + { + "type": "string", + "maxLength": 64 + }, + { + "type": "null" + } + ] + }, + "traceSampling": { + "default": null, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "responseCaptureEnabled": { + "default": null, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "recoveryThreshold": { + "default": null, + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "downtimeThreshold": { + "default": null, + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, { "type": "null" @@ -4424,24 +5425,69 @@ ] } }, - "required": ["organizationSlug", "projectSlug"] + "required": ["organizationSlug", "projectSlug", "uptimeMonitorId"] }, "outputSchema": { "type": "object", "properties": { - "project": { + "monitor": { "type": "object", "properties": { "id": { "type": "string" }, - "slug": { + "name": { "type": "string" }, - "name": { + "projectSlug": { "type": "string" }, - "platform": { + "status": { + "type": "string" + }, + "uptimeStatus": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "url": { + "type": "string" + }, + "method": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "intervalSeconds": { + "type": "number" + }, + "timeoutMs": { + "type": "number" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "owner": { "anyOf": [ { "type": "string" @@ -4450,13 +5496,74 @@ "type": "null" } ] + }, + "recoveryThreshold": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "downtimeThreshold": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "traceSampling": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "responseCaptureEnabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "webUrl": { + "type": "string", + "format": "uri" } }, - "required": ["id", "slug", "name", "platform"], + "required": [ + "id", + "name", + "projectSlug", + "status", + "uptimeStatus", + "url", + "method", + "intervalSeconds", + "timeoutMs", + "environment", + "owner", + "recoveryThreshold", + "downtimeThreshold", + "traceSampling", + "responseCaptureEnabled", + "webUrl" + ], "additionalProperties": false } }, - "required": ["project"], + "required": ["monitor"], "additionalProperties": false }, "requiredScopes": ["project:write"], 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 new file mode 100644 index 000000000..354d73b7d --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts @@ -0,0 +1,119 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; +import { describe, expect, it } from "vitest"; +import { + assertStructuredOnlyResult, + getStructuredContent, +} from "../../test-utils/structured-content.js"; +import createUptimeMonitor, { + createUptimeMonitorOutputSchema, +} from "./create-uptime-monitor.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +describe("create_uptime_monitor", () => { + it("creates an uptime monitor with camelCase payload", 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 }, + ); + }, + ), + ); + + 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, + method: "GET", + headers: null, + body: null, + assertion: null, + status: null, + owner: null, + environment: "production", + traceSampling: null, + responseCaptureEnabled: null, + recoveryThreshold: null, + downtimeThreshold: null, + }, + context, + ); + + expect(requestBody).toMatchObject({ + name: "Checkout Health", + url: "https://example.com/checkout", + intervalSeconds: 300, + timeoutMs: 8000, + method: "GET", + environment: "production", + }); + expect(requestBody).not.toHaveProperty("interval_seconds"); + expect(requestBody).not.toHaveProperty("timeout_ms"); + + assertStructuredOnlyResult(result); + const structuredContent = getStructuredContent(result); + expect(createUptimeMonitorOutputSchema.parse(structuredContent)).toEqual( + structuredContent, + ); + expect(structuredContent).toMatchInlineSnapshot(` + { + "monitor": { + "downtimeThreshold": 3, + "environment": "production", + "id": "4509100000001002", + "intervalSeconds": 300, + "method": "GET", + "name": "Checkout Health", + "owner": null, + "projectSlug": "cloudflare-mcp", + "recoveryThreshold": 1, + "responseCaptureEnabled": true, + "status": "active", + "timeoutMs": 8000, + "traceSampling": false, + "uptimeStatus": 1, + "url": "https://example.com/checkout", + "webUrl": "https://sentry-mcp-evals.sentry.io/monitors/4509100000001002/", + }, + } + `); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts new file mode 100644 index 000000000..1e34e5960 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts @@ -0,0 +1,162 @@ +import { setTag } from "@sentry/core"; +import { z } from "zod"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { structuredResult } from "../../internal/tool-helpers/results"; +import { + ParamOrganizationSlug, + ParamProjectSlug, + ParamRegionUrl, +} from "../../schema"; +import type { ServerContext } from "../../types"; +import { assertProjectRefWithinConstraint } from "./support/project-constraints"; +import { + ParamUptimeHeaders, + ParamUptimeHttpMethod, + ParamUptimeIntervalSeconds, + ParamUptimeMonitorStatus, + ParamUptimeTimeoutMs, + toUptimeMonitorSummary, + uptimeMonitorSummarySchema, +} from "./support/uptime-monitors"; + +export const createUptimeMonitorOutputSchema = z.object({ + monitor: uptimeMonitorSummarySchema, +}); + +export default defineTool({ + name: "create_uptime_monitor", + skills: ["project-management"], + requiredScopes: ["project:write"], + description: [ + "Create a Sentry HTTP uptime monitor.", + "", + "Use this tool when you need to:", + "- Start monitoring a URL for availability", + "- Create a new uptime check with interval and timeout", + "", + "Be careful when using this tool!", + "", + "Required fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.", + "", + "", + "create_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)", + "create_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')", + "", + "", + "", + "- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.", + "- timeoutMs must be between 1000 and 60000.", + "- owner format is `user:ID` or `team:ID`.", + "- Advanced assertions can be passed as JSON via `assertion` when needed.", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + projectSlug: ParamProjectSlug, + name: z.string().trim().min(1).max(128).describe("Monitor display name."), + url: z.string().url().describe("URL to check."), + intervalSeconds: ParamUptimeIntervalSeconds, + timeoutMs: ParamUptimeTimeoutMs.default(5000), + method: ParamUptimeHttpMethod.nullable().default(null), + headers: ParamUptimeHeaders.nullable().default(null), + body: z + .string() + .describe("Optional request body for methods that support a body.") + .nullable() + .default(null), + assertion: z + .unknown() + .describe( + "Optional assertion JSON payload accepted by Sentry uptime monitors.", + ) + .nullable() + .default(null), + status: ParamUptimeMonitorStatus.nullable().default(null), + owner: z + .string() + .trim() + .describe("Optional owner actor, e.g. `user:123` or `team:456`.") + .nullable() + .default(null), + environment: z + .string() + .trim() + .max(64) + .describe("Optional environment name for created uptime issues.") + .nullable() + .default(null), + traceSampling: z + .boolean() + .describe("Whether check requests may be considered for tracing.") + .nullable() + .default(null), + responseCaptureEnabled: z + .boolean() + .describe("Capture response body/headers on failures.") + .nullable() + .default(null), + recoveryThreshold: z + .number() + .int() + .min(1) + .describe("Consecutive successful checks required to recover.") + .nullable() + .default(null), + downtimeThreshold: z + .number() + .int() + .min(1) + .describe("Consecutive failed checks required to mark down.") + .nullable() + .default(null), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + outputSchema: createUptimeMonitorOutputSchema, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + const organizationSlug = params.organizationSlug; + setTag("organization.slug", organizationSlug); + setTag("project.slug", params.projectSlug); + + assertProjectRefWithinConstraint({ + resourceLabel: "Uptime monitor", + scopedProjectSlug: context.constraints.projectSlug, + project: { slug: params.projectSlug }, + }); + + const monitor = await apiService.createUptimeMonitor({ + organizationSlug, + projectSlug: params.projectSlug, + name: params.name, + url: params.url, + intervalSeconds: params.intervalSeconds, + timeoutMs: params.timeoutMs, + method: params.method ?? undefined, + headers: params.headers ?? undefined, + body: params.body ?? undefined, + assertion: params.assertion ?? undefined, + status: params.status ?? undefined, + owner: params.owner ?? undefined, + environment: params.environment ?? undefined, + traceSampling: params.traceSampling ?? undefined, + responseCaptureEnabled: params.responseCaptureEnabled ?? undefined, + recoveryThreshold: params.recoveryThreshold ?? undefined, + downtimeThreshold: params.downtimeThreshold ?? undefined, + }); + + return structuredResult({ + monitor: toUptimeMonitorSummary( + monitor, + apiService.getUptimeMonitorUrl(organizationSlug, monitor.id), + ), + }); + }, +}); diff --git a/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts new file mode 100644 index 000000000..9d27d7453 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { + assertStructuredOnlyResult, + getStructuredContent, +} from "../../test-utils/structured-content.js"; +import deleteUptimeMonitor, { + deleteUptimeMonitorOutputSchema, +} from "./delete-uptime-monitor.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +describe("delete_uptime_monitor", () => { + it("deletes an uptime monitor", async () => { + const result = await deleteUptimeMonitor.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + }, + context, + ); + + assertStructuredOnlyResult(result); + const structuredContent = getStructuredContent(result); + expect(deleteUptimeMonitorOutputSchema.parse(structuredContent)).toEqual( + structuredContent, + ); + expect(structuredContent).toMatchInlineSnapshot(` + { + "projectSlug": "cloudflare-mcp", + "success": true, + "uptimeMonitorId": "4509100000001001", + } + `); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.ts new file mode 100644 index 000000000..3e11dd870 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.ts @@ -0,0 +1,79 @@ +import { setTag } from "@sentry/core"; +import { z } from "zod"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { structuredResult } from "../../internal/tool-helpers/results"; +import { + ParamOrganizationSlug, + ParamProjectSlug, + ParamRegionUrl, +} from "../../schema"; +import type { ServerContext } from "../../types"; +import { assertProjectRefWithinConstraint } from "./support/project-constraints"; + +export const deleteUptimeMonitorOutputSchema = z.object({ + success: z.literal(true), + uptimeMonitorId: z.string(), + projectSlug: z.string(), +}); + +export default defineTool({ + name: "delete_uptime_monitor", + skills: ["project-management"], + requiredScopes: ["project:write"], + description: [ + "Delete a Sentry HTTP uptime monitor.", + "", + "Use this tool when you need to permanently remove an uptime monitor.", + "", + "Be careful when using this tool! Deletion cannot be undone.", + "", + "", + "delete_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + projectSlug: ParamProjectSlug, + uptimeMonitorId: z + .string() + .trim() + .min(1) + .describe("Uptime monitor ID (detector id)."), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + outputSchema: deleteUptimeMonitorOutputSchema, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + const organizationSlug = params.organizationSlug; + setTag("organization.slug", organizationSlug); + setTag("project.slug", params.projectSlug); + setTag("uptime.monitor_id", params.uptimeMonitorId); + + assertProjectRefWithinConstraint({ + resourceLabel: "Uptime monitor", + scopedProjectSlug: context.constraints.projectSlug, + project: { slug: params.projectSlug }, + }); + + await apiService.deleteUptimeMonitor({ + organizationSlug, + projectSlug: params.projectSlug, + uptimeMonitorId: params.uptimeMonitorId, + }); + + return structuredResult({ + success: true as const, + uptimeMonitorId: params.uptimeMonitorId, + projectSlug: params.projectSlug, + }); + }, +}); diff --git a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts new file mode 100644 index 000000000..9764d9a08 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts @@ -0,0 +1,100 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; +import { describe, expect, it } from "vitest"; +import { + assertStructuredOnlyResult, + getStructuredContent, +} from "../../test-utils/structured-content.js"; +import findUptimeMonitors, { + findUptimeMonitorsOutputSchema, +} from "./find-uptime-monitors.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +describe("find_uptime_monitors", () => { + it("serializes uptime monitors", async () => { + const result = await findUptimeMonitors.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: null, + environment: null, + owner: null, + query: null, + limit: 10, + }, + context, + ); + + assertStructuredOnlyResult(result); + const structuredContent = getStructuredContent(result); + expect(findUptimeMonitorsOutputSchema.parse(structuredContent)).toEqual( + structuredContent, + ); + expect(structuredContent).toMatchInlineSnapshot(` + { + "hasMore": false, + "monitors": [ + { + "downtimeThreshold": 3, + "environment": "production", + "id": "4509100000001001", + "intervalSeconds": 60, + "method": "GET", + "name": "API Health", + "owner": "the-goats", + "projectSlug": "cloudflare-mcp", + "recoveryThreshold": 1, + "responseCaptureEnabled": true, + "status": "active", + "timeoutMs": 5000, + "traceSampling": false, + "uptimeStatus": 1, + "url": "https://example.com/health", + "webUrl": "https://sentry-mcp-evals.sentry.io/monitors/4509100000001001/", + }, + ], + } + `); + }); + + it("resolves projectSlug to project id query param", async () => { + let requestUrl: string | null = null; + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/uptime/", + ({ request }) => { + requestUrl = request.url; + return HttpResponse.json([]); + }, + ), + ); + + await findUptimeMonitors.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + environment: "production", + owner: "team:123", + query: "health", + limit: 25, + }, + context, + ); + + expect(requestUrl).not.toBeNull(); + const params = new URL(requestUrl ?? "").searchParams; + expect(params.get("project")).toBe("4509109104082945"); + expect(params.get("environment")).toBe("production"); + expect(params.get("owner")).toBe("team:123"); + expect(params.get("query")).toBe("health"); + expect(params.get("per_page")).toBe("26"); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts new file mode 100644 index 000000000..c3bc3ae3c --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts @@ -0,0 +1,125 @@ +import { setTag } from "@sentry/core"; +import { z } from "zod"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { structuredResult } from "../../internal/tool-helpers/results"; +import { + ParamOrganizationSlug, + ParamProjectSlugOrAll, + ParamRegionUrl, +} from "../../schema"; +import type { ServerContext } from "../../types"; +import { assertProjectRefWithinConstraint } from "./support/project-constraints"; +import { + toUptimeMonitorSummary, + uptimeMonitorSummarySchema, +} from "./support/uptime-monitors"; + +export const findUptimeMonitorsOutputSchema = z.object({ + monitors: z.array(uptimeMonitorSummarySchema), + hasMore: z.boolean(), +}); + +export default defineTool({ + name: "find_uptime_monitors", + skills: ["inspect"], + requiredScopes: ["org:read", "project:read"], + description: [ + "Find Sentry uptime monitors.", + "", + "Use this tool when you need to:", + "- List HTTP uptime monitors in an organization", + "- Find a monitor by name or URL before getting details", + "- Check monitor status, owner, project, interval, or target URL", + "- When `hasMore` is true, narrow results with project, environment, owner, or query filters", + "", + "This is separate from cron monitors (`find_monitors`).", + "", + "", + "find_uptime_monitors(organizationSlug='my-organization')", + "find_uptime_monitors(organizationSlug='my-organization', projectSlug='backend', query='api')", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + projectSlug: ParamProjectSlugOrAll.nullable().default(null), + environment: z + .string() + .trim() + .describe("Optional environment name to limit monitors.") + .nullable() + .default(null), + owner: z + .string() + .trim() + .describe( + "Optional owner filter, such as `user:123`, `team:456`, `myteams`, or `unassigned`.", + ) + .nullable() + .default(null), + query: z + .string() + .trim() + .describe("Optional search query for monitor name or URL.") + .nullable() + .default(null), + limit: z + .number() + .int() + .positive() + .max(99) + .describe("Maximum number of monitors to return.") + .default(10), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + outputSchema: findUptimeMonitorsOutputSchema, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + const organizationSlug = params.organizationSlug; + setTag("organization.slug", organizationSlug); + + const requestedProjectSlug = + params.projectSlug && params.projectSlug !== "all" + ? params.projectSlug + : undefined; + if (requestedProjectSlug) { + assertProjectRefWithinConstraint({ + resourceLabel: "Uptime monitor list", + scopedProjectSlug: context.constraints.projectSlug, + project: { slug: requestedProjectSlug }, + }); + } + const projectSlug = context.constraints.projectSlug ?? requestedProjectSlug; + if (projectSlug) { + setTag("project.slug", projectSlug); + } + + const monitors = await apiService.listUptimeMonitors({ + organizationSlug, + projectSlug, + environment: params.environment ?? undefined, + owner: params.owner ?? undefined, + query: params.query ?? undefined, + limit: params.limit + 1, + }); + + return structuredResult({ + monitors: monitors + .slice(0, params.limit) + .map((monitor) => + toUptimeMonitorSummary( + monitor, + apiService.getUptimeMonitorUrl(organizationSlug, monitor.id), + ), + ), + hasMore: monitors.length > params.limit, + }); + }, +}); diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts new file mode 100644 index 000000000..fd669b07a --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import getUptimeMonitorDetails from "./get-uptime-monitor-details.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +describe("get_uptime_monitor_details", () => { + it("serializes uptime monitor details", async () => { + const result = await getUptimeMonitorDetails.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + period: "24h", + start: null, + end: null, + checkLimit: 10, + }, + context, + ); + + expect(result).toMatchInlineSnapshot(` + "# Uptime Monitor API Health in **sentry-mcp-evals** + + **ID**: 4509100000001001 + **Project**: cloudflare-mcp + **Status**: active + **Uptime Status**: ok + **URL**: https://example.com/health + **Method**: GET + **Interval**: 60s + **Timeout**: 5000ms + **Environment**: production + **Owner**: the-goats + **Recovery Threshold**: 1 + **Downtime Threshold**: 3 + **Trace Sampling**: false + **Response Capture**: true + **Web URL**: [Open Monitor](https://sentry-mcp-evals.sentry.io/monitors/4509100000001001/) + + ## Recent Checks + + - 2025-04-14T02:00:13.000Z: success, HTTP 200, 142ms, US East, production + - 2025-04-14T01:59:13.000Z: failure_incident (timeout), 5000ms, US East, production + + ## Response Notes + + - Search related issues with \`search_issues\` query \`uptime_rule:4509100000001001\`. + " + `); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts new file mode 100644 index 000000000..600803c1d --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts @@ -0,0 +1,241 @@ +import { setTag } from "@sentry/core"; +import { z } from "zod"; +import { UserInputError } from "../../errors"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { + ParamOrganizationSlug, + ParamPeriod, + ParamProjectSlug, + ParamRegionUrl, +} from "../../schema"; +import type { ServerContext } from "../../types"; +import { + compactLines, + formatDate, + formatUnknown, +} from "./support/api-formatting"; +import { assertProjectRefWithinConstraint } from "./support/project-constraints"; +import { + formatUptimeStatus, + getUptimeOwnerName, +} from "./support/uptime-monitors"; + +function formatCheck(check: { + timestamp?: string; + scheduledCheckTime?: string; + checkStatus?: string; + checkStatusReason?: string | null; + httpStatusCode?: number | null; + durationMs?: number; + regionName?: string; + region?: string; + environment?: string; +}): string { + const time = + formatDate(check.timestamp) ?? + formatDate(check.scheduledCheckTime) ?? + "unknown time"; + const status = check.checkStatus ?? "unknown"; + const reason = check.checkStatusReason ? ` (${check.checkStatusReason})` : ""; + const http = + check.httpStatusCode === undefined || check.httpStatusCode === null + ? "" + : `, HTTP ${check.httpStatusCode}`; + const duration = + check.durationMs === undefined ? "" : `, ${check.durationMs}ms`; + const region = check.regionName || check.region; + const regionPart = region ? `, ${region}` : ""; + const environment = check.environment ? `, ${check.environment}` : ""; + return `- ${time}: ${status}${reason}${http}${duration}${regionPart}${environment}`; +} + +export default defineTool({ + name: "get_uptime_monitor_details", + skills: ["inspect"], + requiredScopes: ["project:read"], + description: [ + "Get details for a Sentry uptime monitor, including recent checks.", + "", + "Use this tool when you need to:", + "- Inspect an uptime monitor's URL, interval, thresholds, and status", + "- Review recent HTTP check results (success/failure, status code, duration)", + "- Debug why an uptime monitor is failing", + "", + "This is separate from cron monitors (`get_monitor_details`).", + "", + "", + "get_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')", + "get_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + projectSlug: ParamProjectSlug, + uptimeMonitorId: z + .string() + .trim() + .min(1) + .describe("Uptime monitor ID (detector id)."), + period: ParamPeriod.describe( + "Relative time range for recent checks. Defaults to `24h` when `start` and `end` are omitted.", + ) + .nullable() + .default(null), + start: z + .string() + .datetime() + .describe( + "Absolute start time. Must be provided with `end`; do not combine with `period`.", + ) + .nullable() + .default(null), + end: z + .string() + .datetime() + .describe( + "Absolute end time. Must be provided with `start`; do not combine with `period`.", + ) + .nullable() + .default(null), + checkLimit: z + .number() + .int() + .positive() + .max(50) + .describe("Maximum number of recent checks to include.") + .default(10), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + const organizationSlug = params.organizationSlug; + setTag("organization.slug", organizationSlug); + setTag("project.slug", params.projectSlug); + setTag("uptime.monitor_id", params.uptimeMonitorId); + + assertProjectRefWithinConstraint({ + resourceLabel: "Uptime monitor", + scopedProjectSlug: context.constraints.projectSlug, + project: { slug: params.projectSlug }, + }); + + const start = params.start ?? undefined; + const end = params.end ?? undefined; + if ((start && !end) || (!start && end)) { + throw new UserInputError("`start` and `end` must be provided together."); + } + const hasAbsoluteTimeRange = start !== undefined || end !== undefined; + const requestedPeriod = params.period?.trim() || undefined; + if (hasAbsoluteTimeRange && requestedPeriod) { + throw new UserInputError( + "`period` cannot be combined with `start` and `end`.", + ); + } + const statsPeriod = hasAbsoluteTimeRange + ? undefined + : (requestedPeriod ?? "24h"); + + const monitor = await apiService.getUptimeMonitorDetails({ + organizationSlug, + projectSlug: params.projectSlug, + uptimeMonitorId: params.uptimeMonitorId, + }); + assertProjectRefWithinConstraint({ + resourceLabel: "Uptime monitor", + scopedProjectSlug: context.constraints.projectSlug, + project: { slug: monitor.projectSlug }, + }); + + const checks = await apiService.listUptimeMonitorChecks({ + organizationSlug, + projectSlug: params.projectSlug, + uptimeMonitorId: params.uptimeMonitorId, + statsPeriod, + start, + end, + limit: params.checkLimit, + }); + + const webUrl = apiService.getUptimeMonitorUrl(organizationSlug, monitor.id); + const owner = getUptimeOwnerName(monitor); + const uptimeStatus = formatUptimeStatus(monitor.uptimeStatus); + + const output = compactLines([ + `# Uptime Monitor ${monitor.name} in **${organizationSlug}**`, + "", + `**ID**: ${monitor.id}`, + `**Project**: ${monitor.projectSlug}`, + `**Status**: ${monitor.status}`, + uptimeStatus ? `**Uptime Status**: ${uptimeStatus}` : null, + `**URL**: ${monitor.url}`, + monitor.method ? `**Method**: ${monitor.method}` : null, + `**Interval**: ${monitor.intervalSeconds}s`, + `**Timeout**: ${monitor.timeoutMs}ms`, + monitor.environment ? `**Environment**: ${monitor.environment}` : null, + owner ? `**Owner**: ${owner}` : null, + monitor.recoveryThreshold !== undefined + ? `**Recovery Threshold**: ${monitor.recoveryThreshold}` + : null, + monitor.downtimeThreshold !== undefined + ? `**Downtime Threshold**: ${monitor.downtimeThreshold}` + : null, + monitor.traceSampling !== undefined + ? `**Trace Sampling**: ${monitor.traceSampling}` + : null, + monitor.responseCaptureEnabled !== undefined + ? `**Response Capture**: ${monitor.responseCaptureEnabled}` + : null, + `**Web URL**: [Open Monitor](${webUrl})`, + ]); + + if (monitor.headers && monitor.headers.length > 0) { + output.push("", "## Headers", ""); + for (const header of monitor.headers) { + const [name, value] = header; + output.push(`- ${name}: ${value}`); + } + } + + if (monitor.body) { + output.push("", "## Body", "", "```", monitor.body, "```"); + } + + if (monitor.assertion !== undefined && monitor.assertion !== null) { + output.push( + "", + "## Assertion", + "", + "```json", + JSON.stringify(monitor.assertion, null, 2), + "```", + ); + } + + output.push("", "## Recent Checks", ""); + output.push( + checks.length === 0 + ? "No checks found in this time range." + : checks.slice(0, params.checkLimit).map(formatCheck).join("\n"), + ); + + output.push("", "## Response Notes", ""); + output.push( + `- Search related issues with \`search_issues\` query \`uptime_rule:${monitor.id}\`.`, + ); + if (monitor.assertion !== undefined && monitor.assertion !== null) { + output.push( + `- Assertion payload is included as returned by Sentry: ${formatUnknown(monitor.assertion)}.`, + ); + } + + return `${output.join("\n")}\n`; + }, +}); diff --git a/packages/mcp-core/src/tools/catalog/index.ts b/packages/mcp-core/src/tools/catalog/index.ts index dff8de82a..ab77d4818 100644 --- a/packages/mcp-core/src/tools/catalog/index.ts +++ b/packages/mcp-core/src/tools/catalog/index.ts @@ -8,6 +8,11 @@ import findDashboards from "./find-dashboards"; import getDashboardDetails from "./get-dashboard-details"; import findMonitors from "./find-monitors"; import getMonitorDetails from "./get-monitor-details"; +import findUptimeMonitors from "./find-uptime-monitors"; +import getUptimeMonitorDetails from "./get-uptime-monitor-details"; +import createUptimeMonitor from "./create-uptime-monitor"; +import updateUptimeMonitor from "./update-uptime-monitor"; +import deleteUptimeMonitor from "./delete-uptime-monitor"; import findAlertRules from "./find-alert-rules"; import getAlertRule from "./get-alert-rule"; import getIssueDetails from "./get-issue-details"; @@ -66,6 +71,11 @@ const catalogTools = { get_dashboard_details: getDashboardDetails, find_monitors: findMonitors, get_monitor_details: getMonitorDetails, + find_uptime_monitors: findUptimeMonitors, + get_uptime_monitor_details: getUptimeMonitorDetails, + create_uptime_monitor: createUptimeMonitor, + update_uptime_monitor: updateUptimeMonitor, + delete_uptime_monitor: deleteUptimeMonitor, find_alert_rules: findAlertRules, get_alert_rule: getAlertRule, get_issue_details: getIssueDetails, diff --git a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts new file mode 100644 index 000000000..516cfae9e --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts @@ -0,0 +1,126 @@ +import { z } from "zod"; +import type { UptimeMonitor } from "../../../api-client/types"; +import { formatActor } from "./api-formatting"; + +/** Verified against UptimeSubscription.IntervalSeconds in getsentry/sentry. */ +export const UPTIME_INTERVAL_SECONDS = [ + 60, 300, 600, 1200, 1800, 3600, +] as const; + +/** Verified against UptimeSubscription.SupportedHTTPMethods in getsentry/sentry. */ +export const UPTIME_HTTP_METHODS = [ + "GET", + "POST", + "HEAD", + "PUT", + "DELETE", + "PATCH", + "OPTIONS", +] as const; + +export const UPTIME_MONITOR_STATUSES = ["active", "disabled"] as const; + +export const ParamUptimeIntervalSeconds = z + .number() + .int() + .refine( + (value): value is (typeof UPTIME_INTERVAL_SECONDS)[number] => + (UPTIME_INTERVAL_SECONDS as readonly number[]).includes(value), + { + message: `intervalSeconds must be one of: ${UPTIME_INTERVAL_SECONDS.join(", ")}`, + }, + ) + .describe( + "Seconds between checks. Allowed values: 60, 300, 600, 1200, 1800, 3600.", + ); + +export const ParamUptimeHttpMethod = z + .enum(UPTIME_HTTP_METHODS) + .describe("HTTP method used for the uptime check request."); + +export const ParamUptimeMonitorStatus = z + .enum(UPTIME_MONITOR_STATUSES) + .describe( + "Monitor status. `disabled` stops checks and does not count against quota.", + ); + +export const ParamUptimeTimeoutMs = z + .number() + .int() + .min(1000) + .max(60_000) + .describe("Request timeout in milliseconds (1000-60000)."); + +export const ParamUptimeHeaders = z + .array(z.tuple([z.string(), z.string()])) + .describe( + 'Optional HTTP headers as an array of [name, value] pairs, e.g. [["Authorization","Bearer ..."]].', + ); + +export const uptimeMonitorSummarySchema = z.object({ + id: z.string(), + name: z.string(), + projectSlug: z.string(), + status: z.string(), + uptimeStatus: z.union([z.number(), z.string(), z.null()]), + url: z.string(), + method: z.string().nullable(), + intervalSeconds: z.number(), + timeoutMs: z.number(), + environment: z.string().nullable(), + owner: z.string().nullable(), + recoveryThreshold: z.number().nullable(), + downtimeThreshold: z.number().nullable(), + traceSampling: z.boolean().nullable(), + responseCaptureEnabled: z.boolean().nullable(), + webUrl: z.string().url(), +}); + +export type UptimeMonitorSummary = z.infer; + +export function getUptimeOwnerName( + monitor: Pick, +): string | null { + if (!monitor.owner) { + return null; + } + const formatted = formatActor(monitor.owner); + return formatted === "unknown" ? null : formatted; +} + +export function toUptimeMonitorSummary( + monitor: UptimeMonitor, + webUrl: string, +): UptimeMonitorSummary { + return { + id: String(monitor.id), + name: monitor.name, + projectSlug: monitor.projectSlug, + status: monitor.status, + uptimeStatus: monitor.uptimeStatus ?? null, + url: monitor.url, + method: monitor.method ?? null, + intervalSeconds: monitor.intervalSeconds, + timeoutMs: monitor.timeoutMs, + environment: monitor.environment ?? null, + owner: getUptimeOwnerName(monitor), + recoveryThreshold: monitor.recoveryThreshold ?? null, + downtimeThreshold: monitor.downtimeThreshold ?? null, + traceSampling: monitor.traceSampling ?? null, + responseCaptureEnabled: monitor.responseCaptureEnabled ?? null, + webUrl, + }; +} + +export function formatUptimeStatus(value: unknown): string | null { + if (value === 1 || value === "1" || value === "ok") { + return "ok"; + } + if (value === 2 || value === "2" || value === "failed") { + return "failed"; + } + if (value === undefined || value === null) { + return null; + } + return String(value); +} diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts new file mode 100644 index 000000000..1b07c9fc0 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { + assertStructuredOnlyResult, + getStructuredContent, +} from "../../test-utils/structured-content.js"; +import updateUptimeMonitor, { + updateUptimeMonitorOutputSchema, +} from "./update-uptime-monitor.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +describe("update_uptime_monitor", () => { + it("updates monitor status", async () => { + const result = await updateUptimeMonitor.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + name: null, + url: null, + intervalSeconds: null, + timeoutMs: null, + method: null, + headers: null, + body: null, + assertion: null, + status: "disabled", + owner: null, + environment: null, + traceSampling: null, + responseCaptureEnabled: null, + recoveryThreshold: null, + downtimeThreshold: null, + }, + context, + ); + + assertStructuredOnlyResult(result); + const structuredContent = getStructuredContent(result); + expect(updateUptimeMonitorOutputSchema.parse(structuredContent)).toEqual( + structuredContent, + ); + expect( + (structuredContent as { monitor: { status: string } }).monitor.status, + ).toBe("disabled"); + expect(structuredContent).toMatchInlineSnapshot(` + { + "monitor": { + "downtimeThreshold": 3, + "environment": "production", + "id": "4509100000001001", + "intervalSeconds": 60, + "method": "GET", + "name": "API Health", + "owner": "the-goats", + "projectSlug": "cloudflare-mcp", + "recoveryThreshold": 1, + "responseCaptureEnabled": true, + "status": "disabled", + "timeoutMs": 5000, + "traceSampling": false, + "uptimeStatus": 1, + "url": "https://example.com/health", + "webUrl": "https://sentry-mcp-evals.sentry.io/monitors/4509100000001001/", + }, + } + `); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts new file mode 100644 index 000000000..44a917b2d --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts @@ -0,0 +1,145 @@ +import { setTag } from "@sentry/core"; +import { z } from "zod"; +import { UserInputError } from "../../errors"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { structuredResult } from "../../internal/tool-helpers/results"; +import { + ParamOrganizationSlug, + ParamProjectSlug, + ParamRegionUrl, +} from "../../schema"; +import type { ServerContext } from "../../types"; +import { assertProjectRefWithinConstraint } from "./support/project-constraints"; +import { + ParamUptimeHeaders, + ParamUptimeHttpMethod, + ParamUptimeIntervalSeconds, + ParamUptimeMonitorStatus, + ParamUptimeTimeoutMs, + toUptimeMonitorSummary, + uptimeMonitorSummarySchema, +} from "./support/uptime-monitors"; + +export const updateUptimeMonitorOutputSchema = z.object({ + monitor: uptimeMonitorSummarySchema, +}); + +export default defineTool({ + name: "update_uptime_monitor", + skills: ["project-management"], + requiredScopes: ["project:write"], + description: [ + "Update a Sentry HTTP uptime monitor.", + "", + "Use this tool when you need to:", + "- Change URL, interval, timeout, method, headers, or body", + "- Enable or disable a monitor (`status`)", + "- Update owner, environment, or failure thresholds", + "", + "Be careful when using this tool!", + "", + "", + "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')", + "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + projectSlug: ParamProjectSlug, + uptimeMonitorId: z + .string() + .trim() + .min(1) + .describe("Uptime monitor ID (detector id)."), + name: z.string().trim().min(1).max(128).nullable().default(null), + url: z.string().url().nullable().default(null), + intervalSeconds: ParamUptimeIntervalSeconds.nullable().default(null), + timeoutMs: ParamUptimeTimeoutMs.nullable().default(null), + method: ParamUptimeHttpMethod.nullable().default(null), + headers: ParamUptimeHeaders.nullable().default(null), + body: z.string().nullable().default(null), + assertion: z.unknown().nullable().default(null), + status: ParamUptimeMonitorStatus.nullable().default(null), + owner: z.string().trim().nullable().default(null), + environment: z.string().trim().max(64).nullable().default(null), + traceSampling: z.boolean().nullable().default(null), + responseCaptureEnabled: z.boolean().nullable().default(null), + recoveryThreshold: z.number().int().min(1).nullable().default(null), + downtimeThreshold: z.number().int().min(1).nullable().default(null), + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: true, + }, + outputSchema: updateUptimeMonitorOutputSchema, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + const organizationSlug = params.organizationSlug; + setTag("organization.slug", organizationSlug); + setTag("project.slug", params.projectSlug); + setTag("uptime.monitor_id", params.uptimeMonitorId); + + assertProjectRefWithinConstraint({ + resourceLabel: "Uptime monitor", + scopedProjectSlug: context.constraints.projectSlug, + project: { slug: params.projectSlug }, + }); + + const hasUpdate = + params.name !== null || + params.url !== null || + params.intervalSeconds !== null || + params.timeoutMs !== null || + params.method !== null || + params.headers !== null || + params.body !== null || + params.assertion !== null || + params.status !== null || + params.owner !== null || + params.environment !== null || + params.traceSampling !== null || + params.responseCaptureEnabled !== null || + params.recoveryThreshold !== null || + params.downtimeThreshold !== null; + + if (!hasUpdate) { + throw new UserInputError( + "Provide at least one field to update on the uptime monitor.", + ); + } + + const monitor = await apiService.updateUptimeMonitor({ + organizationSlug, + projectSlug: params.projectSlug, + uptimeMonitorId: params.uptimeMonitorId, + name: params.name ?? undefined, + url: params.url ?? undefined, + intervalSeconds: params.intervalSeconds ?? undefined, + timeoutMs: params.timeoutMs ?? undefined, + method: params.method ?? undefined, + headers: params.headers ?? undefined, + body: params.body ?? undefined, + assertion: params.assertion ?? undefined, + status: params.status ?? undefined, + owner: params.owner ?? undefined, + environment: params.environment ?? undefined, + traceSampling: params.traceSampling ?? undefined, + responseCaptureEnabled: params.responseCaptureEnabled ?? undefined, + recoveryThreshold: params.recoveryThreshold ?? undefined, + downtimeThreshold: params.downtimeThreshold ?? undefined, + }); + + return structuredResult({ + monitor: toUptimeMonitorSummary( + monitor, + apiService.getUptimeMonitorUrl(organizationSlug, monitor.id), + ), + }); + }, +}); diff --git a/packages/mcp-core/src/utils/url-utils.test.ts b/packages/mcp-core/src/utils/url-utils.test.ts index 770dd862a..cb70d5b9c 100644 --- a/packages/mcp-core/src/utils/url-utils.test.ts +++ b/packages/mcp-core/src/utils/url-utils.test.ts @@ -1,20 +1,21 @@ import { describe, expect, it } from "vitest"; import { - validateSentryHostThrows, - validateAndParseSentryUrlThrows, - validateOpenAiBaseUrlThrows, - getIssueUrl, + extractConversationIdFromSearchQuery, + getAIConversationUrl, + getEventsExplorerUrl, getIssuesSearchUrl, + getIssueUrl, + getMonitorUrl, getPreprodSnapshotUrl, + getReleaseUrl, getReplaysSearchUrl, getReplayUrl, - getReleaseUrl, - getMonitorUrl, - getTraceUrl, - getEventsExplorerUrl, - getAIConversationUrl, - extractConversationIdFromSearchQuery, getTraceMetricsExploreUrl, + getTraceUrl, + getUptimeMonitorUrl, + validateAndParseSentryUrlThrows, + validateOpenAiBaseUrlThrows, + validateSentryHostThrows, } from "./url-utils"; describe("url-utils", () => { @@ -572,4 +573,17 @@ describe("url-utils", () => { ]); }); }); + + describe("getUptimeMonitorUrl", () => { + it("builds saas and self-hosted monitor urls", () => { + expect(getUptimeMonitorUrl("sentry.io", "my-org", "12345")).toBe( + "https://my-org.sentry.io/monitors/12345/", + ); + expect( + getUptimeMonitorUrl("sentry.internal:9000", "my-org", "12345", "http"), + ).toBe( + "http://sentry.internal:9000/organizations/my-org/monitors/12345/", + ); + }); + }); }); diff --git a/packages/mcp-core/src/utils/url-utils.ts b/packages/mcp-core/src/utils/url-utils.ts index 83993a05f..dc24cbf6c 100644 --- a/packages/mcp-core/src/utils/url-utils.ts +++ b/packages/mcp-core/src/utils/url-utils.ts @@ -1,9 +1,9 @@ +import type { SentryProtocol } from "../types"; import { + type EventsDataset, isMetricsDataset, isProfilesDataset, - type EventsDataset, } from "./events-datasets"; -import type { SentryProtocol } from "../types"; /** * Determines if a Sentry instance is SaaS or self-hosted based on the host. @@ -436,6 +436,26 @@ export function getMonitorUrl( * @param releaseVersion Release version identifier * @returns The complete release URL */ +/** + * Generates a Sentry uptime monitor URL. + * + * Uses the monitors UI path (`/monitors/{id}/`), which is the current home for + * uptime detectors in Sentry. + */ +export function getUptimeMonitorUrl( + host: string, + organizationSlug: string, + uptimeMonitorId: string | number, + protocol: SentryProtocol = "https", +): string { + const encodedOrg = encodeURIComponent(organizationSlug); + const encodedId = encodeURIComponent(String(uptimeMonitorId)); + if (isSentryHost(host)) { + return `${protocol}://${encodedOrg}.${host}/monitors/${encodedId}/`; + } + return `${protocol}://${host}/organizations/${encodedOrg}/monitors/${encodedId}/`; +} + export function getReleaseUrl( host: string, organizationSlug: string, diff --git a/packages/mcp-server-mocks/src/fixtures/uptime-checks.json b/packages/mcp-server-mocks/src/fixtures/uptime-checks.json new file mode 100644 index 000000000..b97f4be4a --- /dev/null +++ b/packages/mcp-server-mocks/src/fixtures/uptime-checks.json @@ -0,0 +1,34 @@ +[ + { + "uptimeCheckId": "chk_001", + "timestamp": "2025-04-14T02:00:13.000Z", + "scheduledCheckTime": "2025-04-14T02:00:00.000Z", + "checkStatus": "success", + "checkStatusReason": null, + "assertionFailureData": null, + "httpStatusCode": 200, + "durationMs": 142, + "traceId": "trace-uptime-1", + "traceItemId": "item-uptime-1", + "incidentStatus": 0, + "environment": "production", + "region": "us-east-1", + "regionName": "US East" + }, + { + "uptimeCheckId": "chk_002", + "timestamp": "2025-04-14T01:59:13.000Z", + "scheduledCheckTime": "2025-04-14T01:59:00.000Z", + "checkStatus": "failure_incident", + "checkStatusReason": "timeout", + "assertionFailureData": null, + "httpStatusCode": null, + "durationMs": 5000, + "traceId": "trace-uptime-2", + "traceItemId": "item-uptime-2", + "incidentStatus": 1, + "environment": "production", + "region": "us-east-1", + "regionName": "US East" + } +] diff --git a/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json b/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json new file mode 100644 index 000000000..552af8303 --- /dev/null +++ b/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json @@ -0,0 +1,25 @@ +{ + "id": "4509100000001001", + "projectSlug": "cloudflare-mcp", + "environment": "production", + "name": "API Health", + "status": "active", + "uptimeStatus": 1, + "mode": 1, + "owner": { + "type": "team", + "id": "4509109078196224", + "name": "the-goats" + }, + "recoveryThreshold": 1, + "downtimeThreshold": 3, + "url": "https://example.com/health", + "method": "GET", + "body": null, + "headers": [], + "intervalSeconds": 60, + "timeoutMs": 5000, + "traceSampling": false, + "responseCaptureEnabled": true, + "assertion": null +} diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index 1794b834c..d80589a42 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -1,4 +1,4 @@ -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; /** * MSW-based Mock Server for Sentry MCP Development and Testing. * @@ -37,57 +37,55 @@ import dashboardDetailsFixture from "./fixtures/dashboard-details.json" with { import dashboardListFixture from "./fixtures/dashboard-list.json" with { type: "json", }; +import eventsFixture from "./fixtures/event.json" with { type: "json" }; import eventAttachmentsFixture from "./fixtures/event-attachments.json" with { type: "json", }; -import eventsFixture from "./fixtures/event.json" with { type: "json" }; -import eventsErrorsEmptyFixture from "./fixtures/events-errors-empty.json" with { +import eventsErrorsFixture from "./fixtures/events-errors.json" with { type: "json", }; -import eventsErrorsFixture from "./fixtures/events-errors.json" with { +import eventsErrorsEmptyFixture from "./fixtures/events-errors-empty.json" with { type: "json", }; -import eventsTraceMetricsAggregateFixture from "./fixtures/events-tracemetrics-aggregate.json" with { +import eventsSpansFixture from "./fixtures/events-spans.json" with { type: "json", }; -import eventsTraceMetricsEmptyFixture from "./fixtures/events-tracemetrics-empty.json" with { +import eventsSpansEmptyFixture from "./fixtures/events-spans-empty.json" with { type: "json", }; import eventsTraceMetricsFixture from "./fixtures/events-tracemetrics.json" with { type: "json", }; -import eventsSpansEmptyFixture from "./fixtures/events-spans-empty.json" with { +import eventsTraceMetricsAggregateFixture from "./fixtures/events-tracemetrics-aggregate.json" with { type: "json", }; -import eventsSpansFixture from "./fixtures/events-spans.json" with { +import eventsTraceMetricsEmptyFixture from "./fixtures/events-tracemetrics-empty.json" with { type: "json", }; import flamegraphFixture from "./fixtures/flamegraph.json" with { type: "json", }; -import transactionProfileV1Fixture from "./fixtures/transaction-profile-v1.json" with { +import issueFixture from "./fixtures/issue.json" with { type: "json" }; +import issueActivityFixture from "./fixtures/issue-activity.json" with { type: "json", }; -import transactionProfileV1MissingFunctionFixture from "./fixtures/transaction-profile-v1-missing-function.json" with { +import issueCommentsFixture from "./fixtures/issue-comments.json" with { type: "json", }; -import profileChunkFixture from "./fixtures/profile-chunk.json" with { +import issueNullCulpritFixture from "./fixtures/issue-null-culprit.json" with { type: "json", }; import issueTagValuesFixture from "./fixtures/issue-tag-values.json" with { type: "json", }; -import issueActivityFixture from "./fixtures/issue-activity.json" with { - type: "json", -}; -import issueCommentsFixture from "./fixtures/issue-comments.json" with { +import issueUserReportsFixture from "./fixtures/issue-user-reports.json" with { type: "json", }; -import issueUserReportsFixture from "./fixtures/issue-user-reports.json" with { +import monitorFixture from "./fixtures/monitor.json" with { type: "json" }; +import monitorCheckInsFixture from "./fixtures/monitor-checkins.json" with { type: "json", }; -import issueFixture from "./fixtures/issue.json" with { type: "json" }; -import issueNullCulpritFixture from "./fixtures/issue-null-culprit.json" with { +import monitorStatsFixture from "./fixtures/monitor-stats.json" with { type: "json", }; import organizationFixture from "./fixtures/organization.json" with { @@ -96,6 +94,9 @@ import organizationFixture from "./fixtures/organization.json" with { import performanceEventFixture from "./fixtures/performance-event.json" with { type: "json", }; +import profileChunkFixture from "./fixtures/profile-chunk.json" with { + type: "json", +}; import projectFixture from "./fixtures/project.json" with { type: "json" }; import releaseFixture from "./fixtures/release.json" with { type: "json" }; import releaseCommitsFixture from "./fixtures/release-commits.json" with { @@ -104,27 +105,21 @@ import releaseCommitsFixture from "./fixtures/release-commits.json" with { import releaseDeploysFixture from "./fixtures/release-deploys.json" with { type: "json", }; -import monitorFixture from "./fixtures/monitor.json" with { type: "json" }; -import monitorCheckInsFixture from "./fixtures/monitor-checkins.json" with { +import replayDetailsFixture from "./fixtures/replay-details.json" with { type: "json", }; -import monitorStatsFixture from "./fixtures/monitor-stats.json" with { +import replayRecordingSegmentsFixture from "./fixtures/replay-recording-segments.json" with { type: "json", }; import tagsFixture from "./fixtures/tags.json" with { type: "json" }; import teamFixture from "./fixtures/team.json" with { type: "json" }; +import traceFixture from "./fixtures/trace.json" with { type: "json" }; import traceEventFixture from "./fixtures/trace-event.json" with { type: "json", }; import traceItemsAttributesLogsNumberFixture from "./fixtures/trace-items-attributes-logs-number.json" with { type: "json", }; -import replayDetailsFixture from "./fixtures/replay-details.json" with { - type: "json", -}; -import replayRecordingSegmentsFixture from "./fixtures/replay-recording-segments.json" with { - type: "json", -}; import traceItemsAttributesLogsStringFixture from "./fixtures/trace-items-attributes-logs-string.json" with { type: "json", }; @@ -140,14 +135,25 @@ import traceItemsAttributesTraceMetricsNumberFixture from "./fixtures/trace-item import traceItemsAttributesTraceMetricsStringFixture from "./fixtures/trace-items-attributes-tracemetrics-string.json" with { type: "json", }; +import traceMetaFixture from "./fixtures/trace-meta.json" with { type: "json" }; import traceMetaWithNullsFixture from "./fixtures/trace-meta-with-nulls.json" with { type: "json", }; -import traceMetaFixture from "./fixtures/trace-meta.json" with { type: "json" }; import traceMixedFixture from "./fixtures/trace-mixed.json" with { type: "json", }; -import traceFixture from "./fixtures/trace.json" with { type: "json" }; +import transactionProfileV1Fixture from "./fixtures/transaction-profile-v1.json" with { + type: "json", +}; +import transactionProfileV1MissingFunctionFixture from "./fixtures/transaction-profile-v1-missing-function.json" with { + type: "json", +}; +import uptimeChecksFixture from "./fixtures/uptime-checks.json" with { + type: "json", +}; +import uptimeMonitorFixture from "./fixtures/uptime-monitor.json" with { + type: "json", +}; import userFixture from "./fixtures/user.json" with { type: "json" }; import { issueFixture2 } from "./payloads"; @@ -991,6 +997,63 @@ export const restHandlers = buildHandlers([ path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/releases/8ce89484-0fec-4913-a2cd-e8e2d41dee36/commits/", fetch: () => HttpResponse.json(releaseCommitsFixture), }, + { + method: "get", + path: "/api/0/organizations/sentry-mcp-evals/uptime/", + fetch: () => HttpResponse.json([uptimeMonitorFixture]), + }, + { + method: "get", + path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/", + fetch: () => HttpResponse.json(uptimeMonitorFixture), + }, + { + method: "get", + path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/checks/", + fetch: () => HttpResponse.json(uptimeChecksFixture), + }, + { + method: "post", + path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/", + 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, + }, + { status: 201 }, + ); + }, + }, + { + method: "put", + path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/", + fetch: async ({ request }) => { + const body = (await request.json()) as Record; + return HttpResponse.json({ + ...uptimeMonitorFixture, + ...body, + }); + }, + }, + { + method: "delete", + path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/", + fetch: () => new HttpResponse(null, { status: 202 }), + }, { method: "get", path: "/api/0/organizations/sentry-mcp-evals/monitors/", @@ -1858,59 +1921,57 @@ export const mswServer = setupServer( ...docsHandlers, ); +// Export fixture factories +export { + createCspEvent, + createCspIssue, + createDefaultEvent, + createFeedbackIssue, + createGenericEvent, + createPerformanceEvent, + createPerformanceIssue, + createRegressedIssue, + createUnknownEvent, + createUnsupportedIssue, +} from "./fixtures"; +// Export utilities for creating mock servers +export { setupMockServer, startMockServer } from "./utils"; // Export fixtures for use in tests export { - autofixStateFixture, autofixStateExplorerFixture, - eventsFixture as eventFixture, - replayDetailsFixture, - replayRecordingSegmentsFixture, - traceMetaFixture, - traceMetaWithNullsFixture, - performanceEventFixture, - traceFixture, - traceMixedFixture, - traceEventFixture, - flamegraphFixture, - transactionProfileV1Fixture, - transactionProfileV1MissingFunctionFixture, - organizationFixture, - releaseFixture, + autofixStateFixture, clientKeyFixture, dashboardDetailsFixture, dashboardListFixture, - userFixture, - eventsErrorsFixture, + eventAttachmentsFixture, eventsErrorsEmptyFixture, - eventsSpansFixture, + eventsErrorsFixture, + eventsFixture as eventFixture, + eventsFixture, eventsSpansEmptyFixture, + eventsSpansFixture, + flamegraphFixture, issueFixture, issueNullCulpritFixture, - eventsFixture, + organizationFixture, + performanceEventFixture, + profileChunkFixture, projectFixture, - teamFixture, + releaseFixture, + replayDetailsFixture, + replayRecordingSegmentsFixture, tagsFixture, - traceItemsAttributesSpansStringFixture, - traceItemsAttributesSpansNumberFixture, - traceItemsAttributesLogsStringFixture, + teamFixture, + traceEventFixture, + traceFixture, traceItemsAttributesLogsNumberFixture, - eventAttachmentsFixture, - profileChunkFixture, + traceItemsAttributesLogsStringFixture, + traceItemsAttributesSpansNumberFixture, + traceItemsAttributesSpansStringFixture, + traceMetaFixture, + traceMetaWithNullsFixture, + traceMixedFixture, + transactionProfileV1Fixture, + transactionProfileV1MissingFunctionFixture, + userFixture, }; - -// Export fixture factories -export { - createDefaultEvent, - createGenericEvent, - createUnknownEvent, - createPerformanceEvent, - createPerformanceIssue, - createRegressedIssue, - createUnsupportedIssue, - createCspIssue, - createCspEvent, - createFeedbackIssue, -} from "./fixtures"; - -// Export utilities for creating mock servers -export { setupMockServer, startMockServer } from "./utils"; From e64778d09c0874ae5afffc3be6332166ea7ea3ba Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 05:21:42 +0000 Subject: [PATCH 2/4] fix(uptime): address review feedback on monitor tools Redact sensitive headers and omit request bodies from details output, normalize regional SaaS web URLs, make delete idempotent on 404, reject empty owners, and support clearing nullable update fields. Co-Authored-By: David Cramer Co-Authored-By: David Cramer --- packages/mcp-core/src/skillDefinitions.json | 2 +- packages/mcp-core/src/toolDefinitions.json | 27 ++++++-- .../catalog/create-uptime-monitor.test.ts | 5 ++ .../tools/catalog/create-uptime-monitor.ts | 8 +-- .../catalog/delete-uptime-monitor.test.ts | 32 +++++++++ .../get-uptime-monitor-details.test.ts | 58 +++++++++++++++- .../catalog/get-uptime-monitor-details.ts | 39 ++++++----- .../tools/catalog/support/uptime-monitors.ts | 64 +++++++++++++++++- .../catalog/update-uptime-monitor.test.ts | 66 +++++++++++++++++++ .../tools/catalog/update-uptime-monitor.ts | 56 +++++++++++++--- packages/mcp-core/src/utils/url-utils.test.ts | 9 +++ packages/mcp-core/src/utils/url-utils.ts | 11 ++-- .../src/fixtures/uptime-monitor.json | 13 +++- 13 files changed, 341 insertions(+), 49 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 996318cff..25e201c28 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -465,7 +465,7 @@ }, { "name": "update_uptime_monitor", - "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\n", + "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\nTo clear nullable fields: pass empty string for `owner`, `environment`, or `body`; pass `{}` for `assertion`.\nOmit a field (or pass null) to leave it unchanged.\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner='')\n", "requiredScopes": ["project:write"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 6ebcc3d65..46ec9db8f 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -426,7 +426,7 @@ } ] }, - "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Authorization\",\"Bearer ...\"]]." + "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." }, { "type": "null" @@ -474,7 +474,9 @@ "anyOf": [ { "type": "string", - "description": "Optional owner actor, e.g. `user:123` or `team:456`." + "minLength": 1, + "pattern": "^(user|team):.+$", + "description": "Owner actor in `user:ID` or `team:ID` format." }, { "type": "null" @@ -5194,7 +5196,7 @@ }, { "name": "update_uptime_monitor", - "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\n", + "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\nTo clear nullable fields: pass empty string for `owner`, `environment`, or `body`; pass `{}` for `assertion`.\nOmit a field (or pass null) to leave it unchanged.\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner='')\n", "inputSchema": { "type": "object", "properties": { @@ -5313,7 +5315,7 @@ } ] }, - "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Authorization\",\"Bearer ...\"]]." + "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." }, { "type": "null" @@ -5322,6 +5324,7 @@ }, "body": { "default": null, + "description": "Request body. Pass an empty string to clear the body.", "anyOf": [ { "type": "string" @@ -5333,6 +5336,7 @@ }, "assertion": { "default": null, + "description": "Assertion JSON. Pass an empty object `{}` to clear assertions.", "anyOf": [ {}, { @@ -5355,9 +5359,21 @@ }, "owner": { "default": null, + "description": "Owner actor in `user:ID` or `team:ID` format. Pass an empty string to clear the owner.", "anyOf": [ { - "type": "string" + "anyOf": [ + { + "type": "string", + "minLength": 1, + "pattern": "^(user|team):.+$", + "description": "Owner actor in `user:ID` or `team:ID` format." + }, + { + "type": "string", + "const": "" + } + ] }, { "type": "null" @@ -5366,6 +5382,7 @@ }, "environment": { "default": null, + "description": "Environment name. Pass an empty string to clear the environment.", "anyOf": [ { "type": "string", 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 354d73b7d..746b32f93 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 @@ -116,4 +116,9 @@ describe("create_uptime_monitor", () => { } `); }); + + it("rejects empty owner in the input schema", () => { + const result = createUptimeMonitor.inputSchema.owner.safeParse(" "); + expect(result.success).toBe(false); + }); }); diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts index 1e34e5960..71725da54 100644 --- a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts @@ -15,6 +15,7 @@ import { ParamUptimeHttpMethod, ParamUptimeIntervalSeconds, ParamUptimeMonitorStatus, + ParamUptimeOwner, ParamUptimeTimeoutMs, toUptimeMonitorSummary, uptimeMonitorSummarySchema, @@ -74,12 +75,7 @@ export default defineTool({ .nullable() .default(null), status: ParamUptimeMonitorStatus.nullable().default(null), - owner: z - .string() - .trim() - .describe("Optional owner actor, e.g. `user:123` or `team:456`.") - .nullable() - .default(null), + owner: ParamUptimeOwner.nullable().default(null), environment: z .string() .trim() diff --git a/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts index 9d27d7453..7a22e0583 100644 --- a/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts +++ b/packages/mcp-core/src/tools/catalog/delete-uptime-monitor.test.ts @@ -1,3 +1,5 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; import { describe, expect, it } from "vitest"; import { assertStructuredOnlyResult, @@ -40,4 +42,34 @@ describe("delete_uptime_monitor", () => { } `); }); + + it("treats a second delete 404 as success", async () => { + mswServer.use( + http.delete( + "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/", + () => new HttpResponse(null, { status: 404 }), + ), + ); + + const result = await deleteUptimeMonitor.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + }, + context, + ); + + assertStructuredOnlyResult(result); + expect(getStructuredContent(result)).toEqual({ + success: true, + uptimeMonitorId: "4509100000001001", + projectSlug: "cloudflare-mcp", + }); + }); + + it("claims idempotency", () => { + expect(deleteUptimeMonitor.annotations.idempotentHint).toBe(true); + }); }); diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts index fd669b07a..6ff0f51bd 100644 --- a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts @@ -1,3 +1,5 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; import { describe, expect, it } from "vitest"; import getUptimeMonitorDetails from "./get-uptime-monitor-details.js"; @@ -10,7 +12,7 @@ const context = { }; describe("get_uptime_monitor_details", () => { - it("serializes uptime monitor details", async () => { + it("serializes uptime monitor details without leaking secrets", async () => { const result = await getUptimeMonitorDetails.handler( { organizationSlug: "sentry-mcp-evals", @@ -44,6 +46,13 @@ describe("get_uptime_monitor_details", () => { **Response Capture**: true **Web URL**: [Open Monitor](https://sentry-mcp-evals.sentry.io/monitors/4509100000001001/) + ## Headers + + - Accept: application/json + - Authorization: [REDACTED] + + _Sensitive header values are redacted. Request body is omitted from this view._ + ## Recent Checks - 2025-04-14T02:00:13.000Z: success, HTTP 200, 142ms, US East, production @@ -52,7 +61,54 @@ describe("get_uptime_monitor_details", () => { ## Response Notes - Search related issues with \`search_issues\` query \`uptime_rule:4509100000001001\`. + - Request body is never included in this response. Sensitive header values are redacted. " `); + expect(String(result)).not.toContain("secret-token"); + expect(String(result)).not.toContain("should-not-appear"); + }); + + it("skips malformed short header arrays", async () => { + mswServer.use( + http.get( + "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/", + () => + HttpResponse.json({ + id: "4509100000001001", + projectSlug: "cloudflare-mcp", + environment: null, + name: "API Health", + status: "active", + uptimeStatus: 1, + url: "https://example.com/health", + method: "GET", + body: null, + headers: [["Authorization"], ["X-Custom", "ok"]], + intervalSeconds: 60, + timeoutMs: 5000, + }), + ), + http.get( + "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/checks/", + () => HttpResponse.json([]), + ), + ); + + const result = await getUptimeMonitorDetails.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + period: "24h", + start: null, + end: null, + checkLimit: 10, + }, + context, + ); + + expect(String(result)).toContain("- X-Custom: ok"); + expect(String(result)).not.toContain("undefined"); }); }); diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts index 600803c1d..998e41260 100644 --- a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts @@ -10,13 +10,10 @@ import { ParamRegionUrl, } from "../../schema"; import type { ServerContext } from "../../types"; -import { - compactLines, - formatDate, - formatUnknown, -} from "./support/api-formatting"; +import { compactLines, formatDate } from "./support/api-formatting"; import { assertProjectRefWithinConstraint } from "./support/project-constraints"; import { + formatUptimeHeadersForOutput, formatUptimeStatus, getUptimeOwnerName, } from "./support/uptime-monitors"; @@ -196,16 +193,20 @@ export default defineTool({ `**Web URL**: [Open Monitor](${webUrl})`, ]); - if (monitor.headers && monitor.headers.length > 0) { - output.push("", "## Headers", ""); - for (const header of monitor.headers) { - const [name, value] = header; - output.push(`- ${name}: ${value}`); - } - } - - if (monitor.body) { - output.push("", "## Body", "", "```", monitor.body, "```"); + const headerLines = formatUptimeHeadersForOutput(monitor.headers); + if (headerLines.length > 0) { + output.push("", "## Headers", "", ...headerLines); + output.push( + "", + "_Sensitive header values are redacted. Request body is omitted from this view._", + ); + } else if (monitor.body) { + output.push( + "", + "## Request Payload", + "", + "Request body is configured but omitted from this view to avoid leaking secrets.", + ); } if (monitor.assertion !== undefined && monitor.assertion !== null) { @@ -230,11 +231,9 @@ export default defineTool({ output.push( `- Search related issues with \`search_issues\` query \`uptime_rule:${monitor.id}\`.`, ); - if (monitor.assertion !== undefined && monitor.assertion !== null) { - output.push( - `- Assertion payload is included as returned by Sentry: ${formatUnknown(monitor.assertion)}.`, - ); - } + output.push( + "- Request body is never included in this response. Sensitive header values are redacted.", + ); return `${output.join("\n")}\n`; }, diff --git a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts index 516cfae9e..e62e4482a 100644 --- a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts +++ b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts @@ -54,9 +54,71 @@ export const ParamUptimeTimeoutMs = z export const ParamUptimeHeaders = z .array(z.tuple([z.string(), z.string()])) .describe( - 'Optional HTTP headers as an array of [name, value] pairs, e.g. [["Authorization","Bearer ..."]].', + 'Optional HTTP headers as an array of [name, value] pairs, e.g. [["Accept","application/json"]].', ); +/** Owner actor for create/update. Empty/whitespace rejected. */ +export const ParamUptimeOwner = z + .string() + .trim() + .min(1) + .regex( + /^(user|team):.+$/, + "owner must use `user:ID` or `team:ID` format (e.g. `user:123` or `team:456`).", + ) + .describe("Owner actor in `user:ID` or `team:ID` format."); + +const SENSITIVE_HEADER_NAMES = new Set([ + "authorization", + "proxy-authorization", + "cookie", + "set-cookie", + "x-api-key", + "x-auth-token", + "x-access-token", + "x-csrf-token", + "x-sentry-auth", +]); + +export function isSensitiveHeaderName(name: string): boolean { + const normalized = name.trim().toLowerCase(); + if (SENSITIVE_HEADER_NAMES.has(normalized)) { + return true; + } + return ( + normalized.includes("authorization") || + normalized.includes("api-key") || + normalized.includes("access-token") || + normalized.includes("secret") || + normalized.includes("password") + ); +} + +/** + * Format headers for tool output without leaking secrets. + * Malformed short arrays are skipped. Sensitive values are redacted. + */ +export function formatUptimeHeadersForOutput(headers: unknown): string[] { + if (!Array.isArray(headers) || headers.length === 0) { + return []; + } + + const lines: string[] = []; + for (const header of headers) { + if (!Array.isArray(header) || header.length < 2) { + continue; + } + const name = header[0]; + const value = header[1]; + if (typeof name !== "string" || typeof value !== "string") { + continue; + } + const displayValue = isSensitiveHeaderName(name) ? "[REDACTED]" : value; + lines.push(`- ${name}: ${displayValue}`); + } + return lines; +} + export const uptimeMonitorSummarySchema = z.object({ id: z.string(), name: z.string(), diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts index 1b07c9fc0..b660da8fa 100644 --- a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts @@ -1,3 +1,5 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; import { describe, expect, it } from "vitest"; import { assertStructuredOnlyResult, @@ -73,4 +75,68 @@ describe("update_uptime_monitor", () => { } `); }); + + it("clears owner when empty string is provided", async () => { + let requestBody: Record | null = null; + mswServer.use( + http.put( + "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/uptime/4509100000001001/", + async ({ request }) => { + requestBody = (await request.json()) as Record; + return HttpResponse.json({ + id: "4509100000001001", + projectSlug: "cloudflare-mcp", + environment: "production", + name: "API Health", + status: "active", + uptimeStatus: 1, + owner: null, + recoveryThreshold: 1, + downtimeThreshold: 3, + url: "https://example.com/health", + method: "GET", + body: null, + headers: [], + intervalSeconds: 60, + timeoutMs: 5000, + traceSampling: false, + responseCaptureEnabled: true, + assertion: null, + }); + }, + ), + ); + + const result = await updateUptimeMonitor.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + name: null, + url: null, + intervalSeconds: null, + timeoutMs: null, + method: null, + headers: null, + body: null, + assertion: null, + status: null, + owner: "", + environment: null, + traceSampling: null, + responseCaptureEnabled: null, + recoveryThreshold: null, + downtimeThreshold: null, + }, + context, + ); + + expect(requestBody).toEqual({ owner: null }); + assertStructuredOnlyResult(result); + expect( + (getStructuredContent(result) as { monitor: { owner: string | null } }) + .monitor.owner, + ).toBeNull(); + }); }); diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts index 44a917b2d..337f73138 100644 --- a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts @@ -16,6 +16,7 @@ import { ParamUptimeHttpMethod, ParamUptimeIntervalSeconds, ParamUptimeMonitorStatus, + ParamUptimeOwner, ParamUptimeTimeoutMs, toUptimeMonitorSummary, uptimeMonitorSummarySchema, @@ -39,9 +40,13 @@ export default defineTool({ "", "Be careful when using this tool!", "", + "To clear nullable fields: pass empty string for `owner`, `environment`, or `body`; pass `{}` for `assertion`.", + "Omit a field (or pass null) to leave it unchanged.", + "", "", "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')", "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)", + "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner='')", "", ].join("\n"), inputSchema: { @@ -59,11 +64,35 @@ export default defineTool({ timeoutMs: ParamUptimeTimeoutMs.nullable().default(null), method: ParamUptimeHttpMethod.nullable().default(null), headers: ParamUptimeHeaders.nullable().default(null), - body: z.string().nullable().default(null), - assertion: z.unknown().nullable().default(null), + body: z + .string() + .nullable() + .default(null) + .describe("Request body. Pass an empty string to clear the body."), + assertion: z + .unknown() + .nullable() + .default(null) + .describe( + "Assertion JSON. Pass an empty object `{}` to clear assertions.", + ), status: ParamUptimeMonitorStatus.nullable().default(null), - owner: z.string().trim().nullable().default(null), - environment: z.string().trim().max(64).nullable().default(null), + owner: z + .union([ParamUptimeOwner, z.literal("")]) + .nullable() + .default(null) + .describe( + "Owner actor in `user:ID` or `team:ID` format. Pass an empty string to clear the owner.", + ), + environment: z + .string() + .trim() + .max(64) + .nullable() + .default(null) + .describe( + "Environment name. Pass an empty string to clear the environment.", + ), traceSampling: z.boolean().nullable().default(null), responseCaptureEnabled: z.boolean().nullable().default(null), recoveryThreshold: z.number().int().min(1).nullable().default(null), @@ -91,6 +120,17 @@ export default defineTool({ project: { slug: params.projectSlug }, }); + // null/default = omit. Empty string clears owner/environment/body. + // Empty object clears assertion. + const clearOwner = params.owner === ""; + const clearEnvironment = params.environment === ""; + const clearBody = params.body === ""; + const clearAssertion = + params.assertion !== null && + typeof params.assertion === "object" && + !Array.isArray(params.assertion) && + Object.keys(params.assertion as Record).length === 0; + const hasUpdate = params.name !== null || params.url !== null || @@ -124,11 +164,11 @@ export default defineTool({ timeoutMs: params.timeoutMs ?? undefined, method: params.method ?? undefined, headers: params.headers ?? undefined, - body: params.body ?? undefined, - assertion: params.assertion ?? undefined, + body: clearBody ? null : (params.body ?? undefined), + assertion: clearAssertion ? null : (params.assertion ?? undefined), status: params.status ?? undefined, - owner: params.owner ?? undefined, - environment: params.environment ?? undefined, + owner: clearOwner ? null : (params.owner ?? undefined), + environment: clearEnvironment ? null : (params.environment ?? undefined), traceSampling: params.traceSampling ?? undefined, responseCaptureEnabled: params.responseCaptureEnabled ?? undefined, recoveryThreshold: params.recoveryThreshold ?? undefined, diff --git a/packages/mcp-core/src/utils/url-utils.test.ts b/packages/mcp-core/src/utils/url-utils.test.ts index cb70d5b9c..dfbef62eb 100644 --- a/packages/mcp-core/src/utils/url-utils.test.ts +++ b/packages/mcp-core/src/utils/url-utils.test.ts @@ -585,5 +585,14 @@ describe("url-utils", () => { "http://sentry.internal:9000/organizations/my-org/monitors/12345/", ); }); + + it("normalizes regional saas hosts to sentry.io web urls", () => { + expect(getUptimeMonitorUrl("us.sentry.io", "my-org", "12345")).toBe( + "https://my-org.sentry.io/monitors/12345/", + ); + expect(getUptimeMonitorUrl("de.sentry.io", "my-org", "12345")).toBe( + "https://my-org.sentry.io/monitors/12345/", + ); + }); }); }); diff --git a/packages/mcp-core/src/utils/url-utils.ts b/packages/mcp-core/src/utils/url-utils.ts index dc24cbf6c..c464963a5 100644 --- a/packages/mcp-core/src/utils/url-utils.ts +++ b/packages/mcp-core/src/utils/url-utils.ts @@ -448,12 +448,13 @@ export function getUptimeMonitorUrl( uptimeMonitorId: string | number, protocol: SentryProtocol = "https", ): string { - const encodedOrg = encodeURIComponent(organizationSlug); const encodedId = encodeURIComponent(String(uptimeMonitorId)); - if (isSentryHost(host)) { - return `${protocol}://${encodedOrg}.${host}/monitors/${encodedId}/`; - } - return `${protocol}://${host}/organizations/${encodedOrg}/monitors/${encodedId}/`; + return getSentryWebBaseUrl( + host, + organizationSlug, + `/monitors/${encodedId}/`, + protocol, + ); } export function getReleaseUrl( diff --git a/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json b/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json index 552af8303..a85d3b3fb 100644 --- a/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json +++ b/packages/mcp-server-mocks/src/fixtures/uptime-monitor.json @@ -15,8 +15,17 @@ "downtimeThreshold": 3, "url": "https://example.com/health", "method": "GET", - "body": null, - "headers": [], + "body": "{\"token\":\"should-not-appear\"}", + "headers": [ + [ + "Accept", + "application/json" + ], + [ + "Authorization", + "Bearer secret-token" + ] + ], "intervalSeconds": 60, "timeoutMs": 5000, "traceSampling": false, From 812ccde6b7095c9064e965f643bfe5b86306743b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:51:52 +0000 Subject: [PATCH 3/4] fix(uptime): tighten agent-facing input/output schemas Slim list results, normalize uptimeStatus, use optional fields instead of null placeholders, drop opaque assertion inputs, and make update clear semantics omit-vs-null. Co-Authored-By: David Cramer --- packages/mcp-core/src/skillDefinitions.json | 6 +- packages/mcp-core/src/toolDefinitions.json | 784 ++++-------------- .../catalog/create-uptime-monitor.test.ts | 13 +- .../tools/catalog/create-uptime-monitor.ts | 57 +- .../catalog/find-uptime-monitors.test.ts | 11 +- .../src/tools/catalog/find-uptime-monitors.ts | 29 +- .../get-uptime-monitor-details.test.ts | 11 +- .../catalog/get-uptime-monitor-details.ts | 76 +- .../tools/catalog/support/uptime-monitors.ts | 106 ++- .../catalog/update-uptime-monitor.test.ts | 54 +- .../tools/catalog/update-uptime-monitor.ts | 129 ++- 11 files changed, 389 insertions(+), 887 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 25e201c28..36f645c60 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -159,7 +159,7 @@ }, { "name": "get_uptime_monitor_details", - "description": "Get details for a Sentry uptime monitor, including recent checks.\n\nUse this tool when you need to:\n- Inspect an uptime monitor's URL, interval, thresholds, and status\n- Review recent HTTP check results (success/failure, status code, duration)\n- Debug why an uptime monitor is failing\n\nThis is separate from cron monitors (`get_monitor_details`).\n\n\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)\n", + "description": "Get details for a Sentry uptime monitor, including recent checks.\n\nUse this tool when you need to:\n- Inspect an uptime monitor's URL, interval, thresholds, and status\n- Review recent HTTP check results (success/failure, status code, duration)\n- Debug why an uptime monitor is failing\n\nThis is separate from cron monitors (`get_monitor_details`).\n\nRequest bodies are never returned. Sensitive header values are redacted.\n\n\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)\n", "requiredScopes": ["project:read"] }, { @@ -420,7 +420,7 @@ }, { "name": "create_uptime_monitor", - "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced assertions can be passed as JSON via `assertion` when needed.\n", + "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.\n", "requiredScopes": ["project:write"] }, { @@ -465,7 +465,7 @@ }, { "name": "update_uptime_monitor", - "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\nTo clear nullable fields: pass empty string for `owner`, `environment`, or `body`; pass `{}` for `assertion`.\nOmit a field (or pass null) to leave it unchanged.\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner='')\n", + "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\nOmit a field to leave it unchanged. Pass explicit `null` to clear `owner`, `environment`, or `body`.\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner=null)\n", "requiredScopes": ["project:write"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 46ec9db8f..f93ff9d0e 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -341,7 +341,7 @@ }, { "name": "create_uptime_monitor", - "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced assertions can be passed as JSON via `assertion` when needed.\n", + "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.\n", "inputSchema": { "type": "object", "properties": { @@ -390,163 +390,65 @@ "description": "Request timeout in milliseconds (1000-60000)." }, "method": { - "default": null, - "anyOf": [ - { - "type": "string", - "enum": [ - "GET", - "POST", - "HEAD", - "PUT", - "DELETE", - "PATCH", - "OPTIONS" - ], - "description": "HTTP method used for the uptime check request." - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["GET", "POST", "HEAD", "PUT", "DELETE", "PATCH", "OPTIONS"], + "description": "HTTP method used for the uptime check request." }, "headers": { - "default": null, - "anyOf": [ - { - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "type": "string" - } - ] + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" }, - "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." - }, - { - "type": "null" - } - ] + { + "type": "string" + } + ] + }, + "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." }, "body": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "Optional request body for methods that support a body." - }, - { - "type": "null" - } - ] - }, - "assertion": { - "default": null, - "anyOf": [ - { - "description": "Optional assertion JSON payload accepted by Sentry uptime monitors." - }, - { - "type": "null" - } - ] + "type": "string", + "description": "Optional request body for methods that support a body." }, "status": { - "default": null, - "anyOf": [ - { - "type": "string", - "enum": ["active", "disabled"], - "description": "Monitor status. `disabled` stops checks and does not count against quota." - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["active", "disabled"], + "description": "Monitor status. `disabled` stops checks and does not count against quota." }, "owner": { - "default": null, - "anyOf": [ - { - "type": "string", - "minLength": 1, - "pattern": "^(user|team):.+$", - "description": "Owner actor in `user:ID` or `team:ID` format." - }, - { - "type": "null" - } - ] + "type": "string", + "minLength": 1, + "pattern": "^(user|team):.+$", + "description": "Owner actor in `user:ID` or `team:ID` format." }, "environment": { - "default": null, - "anyOf": [ - { - "type": "string", - "maxLength": 64, - "description": "Optional environment name for created uptime issues." - }, - { - "type": "null" - } - ] + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Optional environment name for created uptime issues." }, "traceSampling": { - "default": null, - "anyOf": [ - { - "type": "boolean", - "description": "Whether check requests may be considered for tracing." - }, - { - "type": "null" - } - ] + "type": "boolean", + "description": "Whether check requests may be considered for tracing." }, "responseCaptureEnabled": { - "default": null, - "anyOf": [ - { - "type": "boolean", - "description": "Capture response body/headers on failures." - }, - { - "type": "null" - } - ] + "type": "boolean", + "description": "Capture response body/headers on failures." }, "recoveryThreshold": { - "default": null, - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991, - "description": "Consecutive successful checks required to recover." - }, - { - "type": "null" - } - ] + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Consecutive successful checks required to recover." }, "downtimeThreshold": { - "default": null, - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991, - "description": "Consecutive failed checks required to mark down." - }, - { - "type": "null" - } - ] + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Consecutive failed checks required to mark down." } }, "required": [ @@ -576,100 +478,42 @@ "type": "string" }, "uptimeStatus": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["ok", "failed", "unknown"] }, "url": { "type": "string" }, "method": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" }, "intervalSeconds": { "type": "number" }, - "timeoutMs": { - "type": "number" - }, "environment": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" }, "owner": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" + }, + "webUrl": { + "type": "string", + "format": "uri" + }, + "timeoutMs": { + "type": "number" }, "recoveryThreshold": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] + "type": "number" }, "downtimeThreshold": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] + "type": "number" }, "traceSampling": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] + "type": "boolean" }, "responseCaptureEnabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "webUrl": { - "type": "string", - "format": "uri" + "type": "boolean" } }, "required": [ @@ -679,16 +523,9 @@ "status", "uptimeStatus", "url", - "method", "intervalSeconds", - "timeoutMs", - "environment", - "owner", - "recoveryThreshold", - "downtimeThreshold", - "traceSampling", - "responseCaptureEnabled", - "webUrl" + "webUrl", + "timeoutMs" ], "additionalProperties": false } @@ -2155,52 +1992,23 @@ ] }, "projectSlug": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "The project's slug, or exact lowercase `all` when a tool supports all-projects scope. Other casing is treated as a project slug." - }, - { - "type": "null" - } - ] + "type": "string", + "description": "The project's slug, or exact lowercase `all` when a tool supports all-projects scope. Other casing is treated as a project slug." }, "environment": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "Optional environment name to limit monitors." - }, - { - "type": "null" - } - ] + "type": "string", + "minLength": 1, + "description": "Optional environment name to limit monitors." }, "owner": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "Optional owner filter, such as `user:123`, `team:456`, `myteams`, or `unassigned`." - }, - { - "type": "null" - } - ] + "type": "string", + "minLength": 1, + "description": "Optional owner filter, such as `user:123`, `team:456`, `myteams`, or `unassigned`." }, "query": { - "default": null, - "anyOf": [ - { - "type": "string", - "description": "Optional search query for monitor name or URL." - }, - { - "type": "null" - } - ] + "type": "string", + "minLength": 1, + "description": "Optional search query for monitor name or URL." }, "limit": { "default": 10, @@ -2233,96 +2041,23 @@ "type": "string" }, "uptimeStatus": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["ok", "failed", "unknown"] }, "url": { "type": "string" }, "method": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" }, "intervalSeconds": { "type": "number" }, - "timeoutMs": { - "type": "number" - }, "environment": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" }, "owner": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "recoveryThreshold": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "downtimeThreshold": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "traceSampling": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "responseCaptureEnabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] + "type": "string" }, "webUrl": { "type": "string", @@ -2336,15 +2071,7 @@ "status", "uptimeStatus", "url", - "method", "intervalSeconds", - "timeoutMs", - "environment", - "owner", - "recoveryThreshold", - "downtimeThreshold", - "traceSampling", - "responseCaptureEnabled", "webUrl" ], "additionalProperties": false @@ -3992,7 +3719,7 @@ }, { "name": "get_uptime_monitor_details", - "description": "Get details for a Sentry uptime monitor, including recent checks.\n\nUse this tool when you need to:\n- Inspect an uptime monitor's URL, interval, thresholds, and status\n- Review recent HTTP check results (success/failure, status code, duration)\n- Debug why an uptime monitor is failing\n\nThis is separate from cron monitors (`get_monitor_details`).\n\n\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)\n", + "description": "Get details for a Sentry uptime monitor, including recent checks.\n\nUse this tool when you need to:\n- Inspect an uptime monitor's URL, interval, thresholds, and status\n- Review recent HTTP check results (success/failure, status code, duration)\n- Debug why an uptime monitor is failing\n\nThis is separate from cron monitors (`get_monitor_details`).\n\nRequest bodies are never returned. Sensitive header values are redacted.\n\n\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')\nget_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)\n", "inputSchema": { "type": "object", "properties": { @@ -4022,45 +3749,21 @@ "description": "Uptime monitor ID (detector id)." }, "period": { - "default": null, - "anyOf": [ - { - "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time range for recent checks. Defaults to `24h` when `start` and `end` are omitted." - }, - { - "type": "null" - } - ] + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time range for recent checks. Defaults to `24h` when `start` and `end` are omitted." }, "start": { - "default": null, - "anyOf": [ - { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", - "description": "Absolute start time. Must be provided with `end`; do not combine with `period`." - }, - { - "type": "null" - } - ] + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Absolute start time. Must be provided with `end`; do not combine with `period`." }, "end": { - "default": null, - "anyOf": [ - { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", - "description": "Absolute end time. Must be provided with `start`; do not combine with `period`." - }, - { - "type": "null" - } - ] + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Absolute end time. Must be provided with `start`; do not combine with `period`." }, "checkLimit": { "default": 10, @@ -5196,7 +4899,7 @@ }, { "name": "update_uptime_monitor", - "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\nTo clear nullable fields: pass empty string for `owner`, `environment`, or `body`; pass `{}` for `assertion`.\nOmit a field (or pass null) to leave it unchanged.\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner='')\n", + "description": "Update a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Change URL, interval, timeout, method, headers, or body\n- Enable or disable a monitor (`status`)\n- Update owner, environment, or failure thresholds\n\nBe careful when using this tool!\n\nOmit a field to leave it unchanged. Pass explicit `null` to clear `owner`, `environment`, or `body`.\n\n\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)\nupdate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner=null)\n", "inputSchema": { "type": "object", "properties": { @@ -5226,105 +4929,48 @@ "description": "Uptime monitor ID (detector id)." }, "name": { - "default": null, - "anyOf": [ - { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - { - "type": "null" - } - ] + "type": "string", + "minLength": 1, + "maxLength": 128 }, "url": { - "default": null, - "anyOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] + "type": "string", + "format": "uri" }, "intervalSeconds": { - "default": null, - "anyOf": [ - { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991, - "description": "Seconds between checks. Allowed values: 60, 300, 600, 1200, 1800, 3600." - }, - { - "type": "null" - } - ] + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Seconds between checks. Allowed values: 60, 300, 600, 1200, 1800, 3600." }, "timeoutMs": { - "default": null, - "anyOf": [ - { - "type": "integer", - "minimum": 1000, - "maximum": 60000, - "description": "Request timeout in milliseconds (1000-60000)." - }, - { - "type": "null" - } - ] + "type": "integer", + "minimum": 1000, + "maximum": 60000, + "description": "Request timeout in milliseconds (1000-60000)." }, "method": { - "default": null, - "anyOf": [ - { - "type": "string", - "enum": [ - "GET", - "POST", - "HEAD", - "PUT", - "DELETE", - "PATCH", - "OPTIONS" - ], - "description": "HTTP method used for the uptime check request." - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["GET", "POST", "HEAD", "PUT", "DELETE", "PATCH", "OPTIONS"], + "description": "HTTP method used for the uptime check request." }, "headers": { - "default": null, - "anyOf": [ - { - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "type": "string" - } - ] + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" }, - "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." - }, - { - "type": "null" - } - ] + { + "type": "string" + } + ] + }, + "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." }, "body": { - "default": null, - "description": "Request body. Pass an empty string to clear the body.", + "description": "Request body. Pass `null` to clear the body.", "anyOf": [ { "type": "string" @@ -5334,46 +4980,19 @@ } ] }, - "assertion": { - "default": null, - "description": "Assertion JSON. Pass an empty object `{}` to clear assertions.", - "anyOf": [ - {}, - { - "type": "null" - } - ] - }, "status": { - "default": null, - "anyOf": [ - { - "type": "string", - "enum": ["active", "disabled"], - "description": "Monitor status. `disabled` stops checks and does not count against quota." - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["active", "disabled"], + "description": "Monitor status. `disabled` stops checks and does not count against quota." }, "owner": { - "default": null, - "description": "Owner actor in `user:ID` or `team:ID` format. Pass an empty string to clear the owner.", + "description": "Owner actor in `user:ID` or `team:ID` format. Pass `null` to clear the owner.", "anyOf": [ { - "anyOf": [ - { - "type": "string", - "minLength": 1, - "pattern": "^(user|team):.+$", - "description": "Owner actor in `user:ID` or `team:ID` format." - }, - { - "type": "string", - "const": "" - } - ] + "type": "string", + "minLength": 1, + "pattern": "^(user|team):.+$", + "description": "Owner actor in `user:ID` or `team:ID` format." }, { "type": "null" @@ -5381,11 +5000,11 @@ ] }, "environment": { - "default": null, - "description": "Environment name. Pass an empty string to clear the environment.", + "description": "Environment name. Pass `null` to clear the environment.", "anyOf": [ { "type": "string", + "minLength": 1, "maxLength": 64 }, { @@ -5394,52 +5013,20 @@ ] }, "traceSampling": { - "default": null, - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] + "type": "boolean" }, "responseCaptureEnabled": { - "default": null, - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] + "type": "boolean" }, "recoveryThreshold": { - "default": null, - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 }, "downtimeThreshold": { - "default": null, - "anyOf": [ - { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - { - "type": "null" - } - ] + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 } }, "required": ["organizationSlug", "projectSlug", "uptimeMonitorId"] @@ -5463,100 +5050,42 @@ "type": "string" }, "uptimeStatus": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string", + "enum": ["ok", "failed", "unknown"] }, "url": { "type": "string" }, "method": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" }, "intervalSeconds": { "type": "number" }, - "timeoutMs": { - "type": "number" - }, "environment": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" }, "owner": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "type": "string" + }, + "webUrl": { + "type": "string", + "format": "uri" + }, + "timeoutMs": { + "type": "number" }, "recoveryThreshold": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] + "type": "number" }, "downtimeThreshold": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] + "type": "number" }, "traceSampling": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] + "type": "boolean" }, "responseCaptureEnabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "webUrl": { - "type": "string", - "format": "uri" + "type": "boolean" } }, "required": [ @@ -5566,16 +5095,9 @@ "status", "uptimeStatus", "url", - "method", "intervalSeconds", - "timeoutMs", - "environment", - "owner", - "recoveryThreshold", - "downtimeThreshold", - "traceSampling", - "responseCaptureEnabled", - "webUrl" + "webUrl", + "timeoutMs" ], "additionalProperties": false } 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 746b32f93..9c59ab45e 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 @@ -63,16 +63,7 @@ describe("create_uptime_monitor", () => { intervalSeconds: 300, timeoutMs: 8000, method: "GET", - headers: null, - body: null, - assertion: null, - status: null, - owner: null, environment: "production", - traceSampling: null, - responseCaptureEnabled: null, - recoveryThreshold: null, - downtimeThreshold: null, }, context, ); @@ -87,6 +78,7 @@ describe("create_uptime_monitor", () => { }); expect(requestBody).not.toHaveProperty("interval_seconds"); expect(requestBody).not.toHaveProperty("timeout_ms"); + expect(requestBody).not.toHaveProperty("assertion"); assertStructuredOnlyResult(result); const structuredContent = getStructuredContent(result); @@ -102,14 +94,13 @@ describe("create_uptime_monitor", () => { "intervalSeconds": 300, "method": "GET", "name": "Checkout Health", - "owner": null, "projectSlug": "cloudflare-mcp", "recoveryThreshold": 1, "responseCaptureEnabled": true, "status": "active", "timeoutMs": 8000, "traceSampling": false, - "uptimeStatus": 1, + "uptimeStatus": "ok", "url": "https://example.com/checkout", "webUrl": "https://sentry-mcp-evals.sentry.io/monitors/4509100000001002/", }, diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts index 71725da54..b82090f08 100644 --- a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts @@ -49,7 +49,7 @@ export default defineTool({ "- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.", "- timeoutMs must be between 1000 and 60000.", "- owner format is `user:ID` or `team:ID`.", - "- Advanced assertions can be passed as JSON via `assertion` when needed.", + "- Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.", "", ].join("\n"), inputSchema: { @@ -60,53 +60,41 @@ export default defineTool({ url: z.string().url().describe("URL to check."), intervalSeconds: ParamUptimeIntervalSeconds, timeoutMs: ParamUptimeTimeoutMs.default(5000), - method: ParamUptimeHttpMethod.nullable().default(null), - headers: ParamUptimeHeaders.nullable().default(null), + method: ParamUptimeHttpMethod.optional(), + headers: ParamUptimeHeaders.optional(), body: z .string() .describe("Optional request body for methods that support a body.") - .nullable() - .default(null), - assertion: z - .unknown() - .describe( - "Optional assertion JSON payload accepted by Sentry uptime monitors.", - ) - .nullable() - .default(null), - status: ParamUptimeMonitorStatus.nullable().default(null), - owner: ParamUptimeOwner.nullable().default(null), + .optional(), + status: ParamUptimeMonitorStatus.optional(), + owner: ParamUptimeOwner.optional(), environment: z .string() .trim() + .min(1) .max(64) .describe("Optional environment name for created uptime issues.") - .nullable() - .default(null), + .optional(), traceSampling: z .boolean() .describe("Whether check requests may be considered for tracing.") - .nullable() - .default(null), + .optional(), responseCaptureEnabled: z .boolean() .describe("Capture response body/headers on failures.") - .nullable() - .default(null), + .optional(), recoveryThreshold: z .number() .int() .min(1) .describe("Consecutive successful checks required to recover.") - .nullable() - .default(null), + .optional(), downtimeThreshold: z .number() .int() .min(1) .describe("Consecutive failed checks required to mark down.") - .nullable() - .default(null), + .optional(), }, annotations: { readOnlyHint: false, @@ -135,17 +123,16 @@ export default defineTool({ url: params.url, intervalSeconds: params.intervalSeconds, timeoutMs: params.timeoutMs, - method: params.method ?? undefined, - headers: params.headers ?? undefined, - body: params.body ?? undefined, - assertion: params.assertion ?? undefined, - status: params.status ?? undefined, - owner: params.owner ?? undefined, - environment: params.environment ?? undefined, - traceSampling: params.traceSampling ?? undefined, - responseCaptureEnabled: params.responseCaptureEnabled ?? undefined, - recoveryThreshold: params.recoveryThreshold ?? undefined, - downtimeThreshold: params.downtimeThreshold ?? undefined, + method: params.method, + headers: params.headers, + body: params.body, + status: params.status, + owner: params.owner, + environment: params.environment, + traceSampling: params.traceSampling, + responseCaptureEnabled: params.responseCaptureEnabled, + recoveryThreshold: params.recoveryThreshold, + downtimeThreshold: params.downtimeThreshold, }); return structuredResult({ diff --git a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts index 9764d9a08..bb3da292d 100644 --- a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts @@ -23,10 +23,6 @@ describe("find_uptime_monitors", () => { { organizationSlug: "sentry-mcp-evals", regionUrl: null, - projectSlug: null, - environment: null, - owner: null, - query: null, limit: 10, }, context, @@ -42,7 +38,6 @@ describe("find_uptime_monitors", () => { "hasMore": false, "monitors": [ { - "downtimeThreshold": 3, "environment": "production", "id": "4509100000001001", "intervalSeconds": 60, @@ -50,12 +45,8 @@ describe("find_uptime_monitors", () => { "name": "API Health", "owner": "the-goats", "projectSlug": "cloudflare-mcp", - "recoveryThreshold": 1, - "responseCaptureEnabled": true, "status": "active", - "timeoutMs": 5000, - "traceSampling": false, - "uptimeStatus": 1, + "uptimeStatus": "ok", "url": "https://example.com/health", "webUrl": "https://sentry-mcp-evals.sentry.io/monitors/4509100000001001/", }, diff --git a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts index c3bc3ae3c..2fb820026 100644 --- a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts +++ b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.ts @@ -11,12 +11,12 @@ import { import type { ServerContext } from "../../types"; import { assertProjectRefWithinConstraint } from "./support/project-constraints"; import { - toUptimeMonitorSummary, - uptimeMonitorSummarySchema, + toUptimeMonitorListItem, + uptimeMonitorListItemSchema, } from "./support/uptime-monitors"; export const findUptimeMonitorsOutputSchema = z.object({ - monitors: z.array(uptimeMonitorSummarySchema), + monitors: z.array(uptimeMonitorListItemSchema), hasMore: z.boolean(), }); @@ -43,27 +43,28 @@ export default defineTool({ inputSchema: { organizationSlug: ParamOrganizationSlug, regionUrl: ParamRegionUrl.nullable().default(null), - projectSlug: ParamProjectSlugOrAll.nullable().default(null), + // `all` means no project filter (same convention as find_monitors/find_releases). + projectSlug: ParamProjectSlugOrAll.optional(), environment: z .string() .trim() + .min(1) .describe("Optional environment name to limit monitors.") - .nullable() - .default(null), + .optional(), owner: z .string() .trim() + .min(1) .describe( "Optional owner filter, such as `user:123`, `team:456`, `myteams`, or `unassigned`.", ) - .nullable() - .default(null), + .optional(), query: z .string() .trim() + .min(1) .describe("Optional search query for monitor name or URL.") - .nullable() - .default(null), + .optional(), limit: z .number() .int() @@ -104,9 +105,9 @@ export default defineTool({ const monitors = await apiService.listUptimeMonitors({ organizationSlug, projectSlug, - environment: params.environment ?? undefined, - owner: params.owner ?? undefined, - query: params.query ?? undefined, + environment: params.environment, + owner: params.owner, + query: params.query, limit: params.limit + 1, }); @@ -114,7 +115,7 @@ export default defineTool({ monitors: monitors .slice(0, params.limit) .map((monitor) => - toUptimeMonitorSummary( + toUptimeMonitorListItem( monitor, apiService.getUptimeMonitorUrl(organizationSlug, monitor.id), ), diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts index 6ff0f51bd..6cf032a52 100644 --- a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts @@ -20,8 +20,6 @@ describe("get_uptime_monitor_details", () => { projectSlug: "cloudflare-mcp", uptimeMonitorId: "4509100000001001", period: "24h", - start: null, - end: null, checkLimit: 10, }, context, @@ -51,17 +49,14 @@ describe("get_uptime_monitor_details", () => { - Accept: application/json - Authorization: [REDACTED] - _Sensitive header values are redacted. Request body is omitted from this view._ - ## Recent Checks - - 2025-04-14T02:00:13.000Z: success, HTTP 200, 142ms, US East, production - - 2025-04-14T01:59:13.000Z: failure_incident (timeout), 5000ms, US East, production + - 2025-04-14T02:00:13.000Z: success, HTTP 200, 142ms, US East + - 2025-04-14T01:59:13.000Z: failure_incident (timeout), 5000ms, US East ## Response Notes - Search related issues with \`search_issues\` query \`uptime_rule:4509100000001001\`. - - Request body is never included in this response. Sensitive header values are redacted. " `); expect(String(result)).not.toContain("secret-token"); @@ -101,8 +96,6 @@ describe("get_uptime_monitor_details", () => { projectSlug: "cloudflare-mcp", uptimeMonitorId: "4509100000001001", period: "24h", - start: null, - end: null, checkLimit: 10, }, context, diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts index 998e41260..6cd9480c1 100644 --- a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts @@ -18,17 +18,20 @@ import { getUptimeOwnerName, } from "./support/uptime-monitors"; -function formatCheck(check: { - timestamp?: string; - scheduledCheckTime?: string; - checkStatus?: string; - checkStatusReason?: string | null; - httpStatusCode?: number | null; - durationMs?: number; - regionName?: string; - region?: string; - environment?: string; -}): string { +function formatCheck( + check: { + timestamp?: string; + scheduledCheckTime?: string; + checkStatus?: string; + checkStatusReason?: string | null; + httpStatusCode?: number | null; + durationMs?: number; + regionName?: string; + region?: string; + environment?: string; + }, + monitorEnvironment: string | null | undefined, +): string { const time = formatDate(check.timestamp) ?? formatDate(check.scheduledCheckTime) ?? @@ -43,7 +46,13 @@ function formatCheck(check: { check.durationMs === undefined ? "" : `, ${check.durationMs}ms`; const region = check.regionName || check.region; const regionPart = region ? `, ${region}` : ""; - const environment = check.environment ? `, ${check.environment}` : ""; + // Only print environment when it differs from the monitor-level value. + const checkEnvironment = check.environment?.trim(); + const monitorEnv = monitorEnvironment?.trim() || null; + const environment = + checkEnvironment && checkEnvironment !== monitorEnv + ? `, ${checkEnvironment}` + : ""; return `- ${time}: ${status}${reason}${http}${duration}${regionPart}${environment}`; } @@ -61,6 +70,8 @@ export default defineTool({ "", "This is separate from cron monitors (`get_monitor_details`).", "", + "Request bodies are never returned. Sensitive header values are redacted.", + "", "", "get_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345')", "get_uptime_monitor_details(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', period='7d', checkLimit=20)", @@ -77,25 +88,21 @@ export default defineTool({ .describe("Uptime monitor ID (detector id)."), period: ParamPeriod.describe( "Relative time range for recent checks. Defaults to `24h` when `start` and `end` are omitted.", - ) - .nullable() - .default(null), + ).optional(), start: z .string() .datetime() .describe( "Absolute start time. Must be provided with `end`; do not combine with `period`.", ) - .nullable() - .default(null), + .optional(), end: z .string() .datetime() .describe( "Absolute end time. Must be provided with `start`; do not combine with `period`.", ) - .nullable() - .default(null), + .optional(), checkLimit: z .number() .int() @@ -124,8 +131,8 @@ export default defineTool({ project: { slug: params.projectSlug }, }); - const start = params.start ?? undefined; - const end = params.end ?? undefined; + const start = params.start; + const end = params.end; if ((start && !end) || (!start && end)) { throw new UserInputError("`start` and `end` must be provided together."); } @@ -171,23 +178,26 @@ export default defineTool({ `**ID**: ${monitor.id}`, `**Project**: ${monitor.projectSlug}`, `**Status**: ${monitor.status}`, - uptimeStatus ? `**Uptime Status**: ${uptimeStatus}` : null, + `**Uptime Status**: ${uptimeStatus}`, `**URL**: ${monitor.url}`, monitor.method ? `**Method**: ${monitor.method}` : null, `**Interval**: ${monitor.intervalSeconds}s`, `**Timeout**: ${monitor.timeoutMs}ms`, monitor.environment ? `**Environment**: ${monitor.environment}` : null, owner ? `**Owner**: ${owner}` : null, - monitor.recoveryThreshold !== undefined + monitor.recoveryThreshold !== undefined && + monitor.recoveryThreshold !== null ? `**Recovery Threshold**: ${monitor.recoveryThreshold}` : null, - monitor.downtimeThreshold !== undefined + monitor.downtimeThreshold !== undefined && + monitor.downtimeThreshold !== null ? `**Downtime Threshold**: ${monitor.downtimeThreshold}` : null, - monitor.traceSampling !== undefined + monitor.traceSampling !== undefined && monitor.traceSampling !== null ? `**Trace Sampling**: ${monitor.traceSampling}` : null, - monitor.responseCaptureEnabled !== undefined + monitor.responseCaptureEnabled !== undefined && + monitor.responseCaptureEnabled !== null ? `**Response Capture**: ${monitor.responseCaptureEnabled}` : null, `**Web URL**: [Open Monitor](${webUrl})`, @@ -196,16 +206,12 @@ export default defineTool({ const headerLines = formatUptimeHeadersForOutput(monitor.headers); if (headerLines.length > 0) { output.push("", "## Headers", "", ...headerLines); - output.push( - "", - "_Sensitive header values are redacted. Request body is omitted from this view._", - ); } else if (monitor.body) { output.push( "", "## Request Payload", "", - "Request body is configured but omitted from this view to avoid leaking secrets.", + "Request body is configured but omitted from this view.", ); } @@ -224,16 +230,16 @@ export default defineTool({ output.push( checks.length === 0 ? "No checks found in this time range." - : checks.slice(0, params.checkLimit).map(formatCheck).join("\n"), + : checks + .slice(0, params.checkLimit) + .map((check) => formatCheck(check, monitor.environment)) + .join("\n"), ); output.push("", "## Response Notes", ""); output.push( `- Search related issues with \`search_issues\` query \`uptime_rule:${monitor.id}\`.`, ); - output.push( - "- Request body is never included in this response. Sensitive header values are redacted.", - ); return `${output.join("\n")}\n`; }, diff --git a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts index e62e4482a..5cb81c79a 100644 --- a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts +++ b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts @@ -119,25 +119,36 @@ export function formatUptimeHeadersForOutput(headers: unknown): string[] { return lines; } -export const uptimeMonitorSummarySchema = z.object({ +export const uptimeStatusSchema = z.enum(["ok", "failed", "unknown"]); + +/** Compact list item for discovery. Omits config detail agents rarely need in lists. */ +export const uptimeMonitorListItemSchema = z.object({ id: z.string(), name: z.string(), projectSlug: z.string(), status: z.string(), - uptimeStatus: z.union([z.number(), z.string(), z.null()]), + uptimeStatus: uptimeStatusSchema, url: z.string(), - method: z.string().nullable(), + method: z.string().optional(), intervalSeconds: z.number(), - timeoutMs: z.number(), - environment: z.string().nullable(), - owner: z.string().nullable(), - recoveryThreshold: z.number().nullable(), - downtimeThreshold: z.number().nullable(), - traceSampling: z.boolean().nullable(), - responseCaptureEnabled: z.boolean().nullable(), + environment: z.string().optional(), + owner: z.string().optional(), webUrl: z.string().url(), }); +/** + * Mutation/detail summary: list fields plus config knobs useful after create/update. + * Optional fields are omitted when absent instead of returned as null. + */ +export const uptimeMonitorSummarySchema = uptimeMonitorListItemSchema.extend({ + timeoutMs: z.number(), + recoveryThreshold: z.number().optional(), + downtimeThreshold: z.number().optional(), + traceSampling: z.boolean().optional(), + responseCaptureEnabled: z.boolean().optional(), +}); + +export type UptimeMonitorListItem = z.infer; export type UptimeMonitorSummary = z.infer; export function getUptimeOwnerName( @@ -150,39 +161,70 @@ export function getUptimeOwnerName( return formatted === "unknown" ? null : formatted; } -export function toUptimeMonitorSummary( +export function formatUptimeStatus( + value: unknown, +): "ok" | "failed" | "unknown" { + if (value === 1 || value === "1" || value === "ok") { + return "ok"; + } + if (value === 2 || value === "2" || value === "failed") { + return "failed"; + } + return "unknown"; +} + +function optionalString(value: string | null | undefined): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +export function toUptimeMonitorListItem( monitor: UptimeMonitor, webUrl: string, -): UptimeMonitorSummary { +): UptimeMonitorListItem { return { id: String(monitor.id), name: monitor.name, projectSlug: monitor.projectSlug, status: monitor.status, - uptimeStatus: monitor.uptimeStatus ?? null, + uptimeStatus: formatUptimeStatus(monitor.uptimeStatus), url: monitor.url, - method: monitor.method ?? null, + ...(monitor.method ? { method: monitor.method } : {}), intervalSeconds: monitor.intervalSeconds, - timeoutMs: monitor.timeoutMs, - environment: monitor.environment ?? null, - owner: getUptimeOwnerName(monitor), - recoveryThreshold: monitor.recoveryThreshold ?? null, - downtimeThreshold: monitor.downtimeThreshold ?? null, - traceSampling: monitor.traceSampling ?? null, - responseCaptureEnabled: monitor.responseCaptureEnabled ?? null, + ...(optionalString(monitor.environment) + ? { environment: optionalString(monitor.environment) } + : {}), + ...(getUptimeOwnerName(monitor) + ? { owner: getUptimeOwnerName(monitor)! } + : {}), webUrl, }; } -export function formatUptimeStatus(value: unknown): string | null { - if (value === 1 || value === "1" || value === "ok") { - return "ok"; - } - if (value === 2 || value === "2" || value === "failed") { - return "failed"; - } - if (value === undefined || value === null) { - return null; - } - return String(value); +export function toUptimeMonitorSummary( + monitor: UptimeMonitor, + webUrl: string, +): UptimeMonitorSummary { + return { + ...toUptimeMonitorListItem(monitor, webUrl), + timeoutMs: monitor.timeoutMs, + ...(monitor.recoveryThreshold !== undefined && + monitor.recoveryThreshold !== null + ? { recoveryThreshold: monitor.recoveryThreshold } + : {}), + ...(monitor.downtimeThreshold !== undefined && + monitor.downtimeThreshold !== null + ? { downtimeThreshold: monitor.downtimeThreshold } + : {}), + ...(monitor.traceSampling !== undefined && monitor.traceSampling !== null + ? { traceSampling: monitor.traceSampling } + : {}), + ...(monitor.responseCaptureEnabled !== undefined && + monitor.responseCaptureEnabled !== null + ? { responseCaptureEnabled: monitor.responseCaptureEnabled } + : {}), + }; } diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts index b660da8fa..21e02cbef 100644 --- a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.test.ts @@ -25,21 +25,7 @@ describe("update_uptime_monitor", () => { regionUrl: null, projectSlug: "cloudflare-mcp", uptimeMonitorId: "4509100000001001", - name: null, - url: null, - intervalSeconds: null, - timeoutMs: null, - method: null, - headers: null, - body: null, - assertion: null, status: "disabled", - owner: null, - environment: null, - traceSampling: null, - responseCaptureEnabled: null, - recoveryThreshold: null, - downtimeThreshold: null, }, context, ); @@ -68,7 +54,7 @@ describe("update_uptime_monitor", () => { "status": "disabled", "timeoutMs": 5000, "traceSampling": false, - "uptimeStatus": 1, + "uptimeStatus": "ok", "url": "https://example.com/health", "webUrl": "https://sentry-mcp-evals.sentry.io/monitors/4509100000001001/", }, @@ -76,7 +62,7 @@ describe("update_uptime_monitor", () => { `); }); - it("clears owner when empty string is provided", async () => { + it("clears owner when null is provided", async () => { let requestBody: Record | null = null; mswServer.use( http.put( @@ -113,21 +99,7 @@ describe("update_uptime_monitor", () => { regionUrl: null, projectSlug: "cloudflare-mcp", uptimeMonitorId: "4509100000001001", - name: null, - url: null, - intervalSeconds: null, - timeoutMs: null, - method: null, - headers: null, - body: null, - assertion: null, - status: null, - owner: "", - environment: null, - traceSampling: null, - responseCaptureEnabled: null, - recoveryThreshold: null, - downtimeThreshold: null, + owner: null, }, context, ); @@ -135,8 +107,22 @@ describe("update_uptime_monitor", () => { expect(requestBody).toEqual({ owner: null }); assertStructuredOnlyResult(result); expect( - (getStructuredContent(result) as { monitor: { owner: string | null } }) - .monitor.owner, - ).toBeNull(); + (getStructuredContent(result) as { monitor: { owner?: string } }).monitor + .owner, + ).toBeUndefined(); + }); + + it("rejects empty updates", async () => { + await expect( + updateUptimeMonitor.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + projectSlug: "cloudflare-mcp", + uptimeMonitorId: "4509100000001001", + }, + context, + ), + ).rejects.toThrow("Provide at least one field to update"); }); }); diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts index 337f73138..c1fa40627 100644 --- a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts @@ -26,6 +26,12 @@ export const updateUptimeMonitorOutputSchema = z.object({ monitor: uptimeMonitorSummarySchema, }); +function hasDefinedUpdate( + value: unknown, +): value is Exclude { + return value !== undefined; +} + export default defineTool({ name: "update_uptime_monitor", skills: ["project-management"], @@ -40,13 +46,12 @@ export default defineTool({ "", "Be careful when using this tool!", "", - "To clear nullable fields: pass empty string for `owner`, `environment`, or `body`; pass `{}` for `assertion`.", - "Omit a field (or pass null) to leave it unchanged.", + "Omit a field to leave it unchanged. Pass explicit `null` to clear `owner`, `environment`, or `body`.", "", "", "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', status='disabled')", "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', intervalSeconds=300, timeoutMs=8000)", - "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner='')", + "update_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', uptimeMonitorId='12345', owner=null)", "", ].join("\n"), inputSchema: { @@ -58,45 +63,35 @@ export default defineTool({ .trim() .min(1) .describe("Uptime monitor ID (detector id)."), - name: z.string().trim().min(1).max(128).nullable().default(null), - url: z.string().url().nullable().default(null), - intervalSeconds: ParamUptimeIntervalSeconds.nullable().default(null), - timeoutMs: ParamUptimeTimeoutMs.nullable().default(null), - method: ParamUptimeHttpMethod.nullable().default(null), - headers: ParamUptimeHeaders.nullable().default(null), + name: z.string().trim().min(1).max(128).optional(), + url: z.string().url().optional(), + intervalSeconds: ParamUptimeIntervalSeconds.optional(), + timeoutMs: ParamUptimeTimeoutMs.optional(), + method: ParamUptimeHttpMethod.optional(), + headers: ParamUptimeHeaders.optional(), body: z .string() .nullable() - .default(null) - .describe("Request body. Pass an empty string to clear the body."), - assertion: z - .unknown() - .nullable() - .default(null) - .describe( - "Assertion JSON. Pass an empty object `{}` to clear assertions.", - ), - status: ParamUptimeMonitorStatus.nullable().default(null), - owner: z - .union([ParamUptimeOwner, z.literal("")]) - .nullable() - .default(null) + .optional() + .describe("Request body. Pass `null` to clear the body."), + status: ParamUptimeMonitorStatus.optional(), + owner: ParamUptimeOwner.nullable() + .optional() .describe( - "Owner actor in `user:ID` or `team:ID` format. Pass an empty string to clear the owner.", + "Owner actor in `user:ID` or `team:ID` format. Pass `null` to clear the owner.", ), environment: z .string() .trim() + .min(1) .max(64) .nullable() - .default(null) - .describe( - "Environment name. Pass an empty string to clear the environment.", - ), - traceSampling: z.boolean().nullable().default(null), - responseCaptureEnabled: z.boolean().nullable().default(null), - recoveryThreshold: z.number().int().min(1).nullable().default(null), - downtimeThreshold: z.number().int().min(1).nullable().default(null), + .optional() + .describe("Environment name. Pass `null` to clear the environment."), + traceSampling: z.boolean().optional(), + responseCaptureEnabled: z.boolean().optional(), + recoveryThreshold: z.number().int().min(1).optional(), + downtimeThreshold: z.number().int().min(1).optional(), }, annotations: { readOnlyHint: false, @@ -120,33 +115,22 @@ export default defineTool({ project: { slug: params.projectSlug }, }); - // null/default = omit. Empty string clears owner/environment/body. - // Empty object clears assertion. - const clearOwner = params.owner === ""; - const clearEnvironment = params.environment === ""; - const clearBody = params.body === ""; - const clearAssertion = - params.assertion !== null && - typeof params.assertion === "object" && - !Array.isArray(params.assertion) && - Object.keys(params.assertion as Record).length === 0; - + // omitted = unchanged; explicit null clears owner/environment/body. const hasUpdate = - params.name !== null || - params.url !== null || - params.intervalSeconds !== null || - params.timeoutMs !== null || - params.method !== null || - params.headers !== null || - params.body !== null || - params.assertion !== null || - params.status !== null || - params.owner !== null || - params.environment !== null || - params.traceSampling !== null || - params.responseCaptureEnabled !== null || - params.recoveryThreshold !== null || - params.downtimeThreshold !== null; + hasDefinedUpdate(params.name) || + hasDefinedUpdate(params.url) || + hasDefinedUpdate(params.intervalSeconds) || + hasDefinedUpdate(params.timeoutMs) || + hasDefinedUpdate(params.method) || + hasDefinedUpdate(params.headers) || + hasDefinedUpdate(params.body) || + hasDefinedUpdate(params.status) || + hasDefinedUpdate(params.owner) || + hasDefinedUpdate(params.environment) || + hasDefinedUpdate(params.traceSampling) || + hasDefinedUpdate(params.responseCaptureEnabled) || + hasDefinedUpdate(params.recoveryThreshold) || + hasDefinedUpdate(params.downtimeThreshold); if (!hasUpdate) { throw new UserInputError( @@ -158,21 +142,20 @@ export default defineTool({ organizationSlug, projectSlug: params.projectSlug, uptimeMonitorId: params.uptimeMonitorId, - name: params.name ?? undefined, - url: params.url ?? undefined, - intervalSeconds: params.intervalSeconds ?? undefined, - timeoutMs: params.timeoutMs ?? undefined, - method: params.method ?? undefined, - headers: params.headers ?? undefined, - body: clearBody ? null : (params.body ?? undefined), - assertion: clearAssertion ? null : (params.assertion ?? undefined), - status: params.status ?? undefined, - owner: clearOwner ? null : (params.owner ?? undefined), - environment: clearEnvironment ? null : (params.environment ?? undefined), - traceSampling: params.traceSampling ?? undefined, - responseCaptureEnabled: params.responseCaptureEnabled ?? undefined, - recoveryThreshold: params.recoveryThreshold ?? undefined, - downtimeThreshold: params.downtimeThreshold ?? undefined, + name: params.name, + url: params.url, + intervalSeconds: params.intervalSeconds, + timeoutMs: params.timeoutMs, + method: params.method, + headers: params.headers, + body: params.body, + status: params.status, + owner: params.owner, + environment: params.environment, + traceSampling: params.traceSampling, + responseCaptureEnabled: params.responseCaptureEnabled, + recoveryThreshold: params.recoveryThreshold, + downtimeThreshold: params.downtimeThreshold, }); return structuredResult({ From 1b16f0804eafed6117ccfb89779c6ff207317a2c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:16:50 +0000 Subject: [PATCH 4/4] fix(uptime): fold param hints into describes and always note omitted body Move create-tool param guidance out of the block onto existing parameter descriptions, and show the request-body omission note even when headers are also present. Co-Authored-By: David Cramer --- packages/mcp-core/src/skillDefinitions.json | 2 +- packages/mcp-core/src/toolDefinitions.json | 2 +- .../mcp-core/src/tools/catalog/create-uptime-monitor.ts | 8 +------- .../src/tools/catalog/get-uptime-monitor-details.test.ts | 7 +++++++ .../src/tools/catalog/get-uptime-monitor-details.ts | 3 ++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 36f645c60..aba3ed890 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -420,7 +420,7 @@ }, { "name": "create_uptime_monitor", - "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.\n", + "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\nAdvanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n", "requiredScopes": ["project:write"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index f93ff9d0e..13ce0224e 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -341,7 +341,7 @@ }, { "name": "create_uptime_monitor", - "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n\n\n\n- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.\n- timeoutMs must be between 1000 and 60000.\n- owner format is `user:ID` or `team:ID`.\n- Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.\n", + "description": "Create a Sentry HTTP uptime monitor.\n\nUse this tool when you need to:\n- Start monitoring a URL for availability\n- Create a new uptime check with interval and timeout\n\nBe careful when using this tool!\n\nRequired fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.\nAdvanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.\n\n\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)\ncreate_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')\n", "inputSchema": { "type": "object", "properties": { diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts index b82090f08..633706eb9 100644 --- a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts @@ -39,18 +39,12 @@ export default defineTool({ "Be careful when using this tool!", "", "Required fields match Sentry's uptime API: name, url, intervalSeconds, timeoutMs.", + "Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.", "", "", "create_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='API Health', url='https://api.example.com/health', intervalSeconds=60, timeoutMs=5000)", "create_uptime_monitor(organizationSlug='my-organization', projectSlug='backend', name='Checkout', url='https://example.com/checkout', intervalSeconds=300, timeoutMs=10000, method='GET', environment='production')", "", - "", - "", - "- intervalSeconds must be one of 60, 300, 600, 1200, 1800, 3600.", - "- timeoutMs must be between 1000 and 60000.", - "- owner format is `user:ID` or `team:ID`.", - "- Advanced response assertions are not supported in this MVP; configure them in the Sentry UI if needed.", - "", ].join("\n"), inputSchema: { organizationSlug: ParamOrganizationSlug, diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts index 6cf032a52..41daf40ba 100644 --- a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.test.ts @@ -49,6 +49,10 @@ describe("get_uptime_monitor_details", () => { - Accept: application/json - Authorization: [REDACTED] + ## Request Payload + + Request body is configured but omitted from this view. + ## Recent Checks - 2025-04-14T02:00:13.000Z: success, HTTP 200, 142ms, US East @@ -61,6 +65,9 @@ describe("get_uptime_monitor_details", () => { `); expect(String(result)).not.toContain("secret-token"); expect(String(result)).not.toContain("should-not-appear"); + expect(String(result)).toContain( + "Request body is configured but omitted from this view.", + ); }); it("skips malformed short header arrays", async () => { diff --git a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts index 6cd9480c1..5896331ac 100644 --- a/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-uptime-monitor-details.ts @@ -206,7 +206,8 @@ export default defineTool({ const headerLines = formatUptimeHeadersForOutput(monitor.headers); if (headerLines.length > 0) { output.push("", "## Headers", "", ...headerLines); - } else if (monitor.body) { + } + if (monitor.body) { output.push( "", "## Request Payload",