diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json
index 9c410cac2..5f458cab8 100644
--- a/packages/mcp-core/src/skillDefinitions.json
+++ b/packages/mcp-core/src/skillDefinitions.json
@@ -129,7 +129,7 @@
},
{
"name": "get_monitor_details",
- "description": "Get details and recent check-ins for a Sentry cron monitor (scheduled job).\n\nUse this tool when you need to:\n- Inspect a monitor's schedule, status, owner, project, and environments\n- Review recent check-ins for missed, failed, timeout, or OK runs\n- Check monitor stats over a recent time range\n\n\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import')\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import', projectSlugOrId='backend', environment='production', period='7d')\n",
+ "description": "Get details and recent check-ins for a Sentry cron monitor (scheduled job).\n\nUse this tool when you need to:\n- Inspect a monitor's schedule, status, owner, project, and environments\n- Review recent check-ins for missed, failed, timeout, or OK runs\n- Check monitor stats over a recent time range\n\nTo find issues from a monitor, call `search_issues` with query `monitor.slug:`.\n\n\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import')\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import', projectSlugOrId='backend', environment='production', period='7d')\n",
"requiredScopes": ["project:read"]
},
{
diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json
index e2598f4ca..f8af26a7e 100644
--- a/packages/mcp-core/src/toolDefinitions.json
+++ b/packages/mcp-core/src/toolDefinitions.json
@@ -5482,7 +5482,7 @@
{
"name": "get_monitor_details",
"title": "Get Cron Monitor Details",
- "description": "Get details and recent check-ins for a Sentry cron monitor (scheduled job).\n\nUse this tool when you need to:\n- Inspect a monitor's schedule, status, owner, project, and environments\n- Review recent check-ins for missed, failed, timeout, or OK runs\n- Check monitor stats over a recent time range\n\n\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import')\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import', projectSlugOrId='backend', environment='production', period='7d')\n",
+ "description": "Get details and recent check-ins for a Sentry cron monitor (scheduled job).\n\nUse this tool when you need to:\n- Inspect a monitor's schedule, status, owner, project, and environments\n- Review recent check-ins for missed, failed, timeout, or OK runs\n- Check monitor stats over a recent time range\n\nTo find issues from a monitor, call `search_issues` with query `monitor.slug:`.\n\n\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import')\nget_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import', projectSlugOrId='backend', environment='production', period='7d')\n",
"inputSchema": {
"type": "object",
"properties": {
@@ -5598,6 +5598,211 @@
},
"required": ["organizationSlug", "monitorSlug"]
},
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "monitor": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "name": {
+ "type": ["string", "null"]
+ },
+ "project": {
+ "type": ["string", "null"]
+ },
+ "status": {
+ "type": ["string", "null"]
+ },
+ "type": {
+ "type": ["string", "null"]
+ },
+ "owner": {
+ "type": ["string", "null"]
+ },
+ "ownerEmail": {
+ "description": "Set when a user owns the monitor.",
+ "type": ["string", "null"]
+ },
+ "lastCheckIn": {
+ "type": ["string", "null"]
+ },
+ "nextCheckIn": {
+ "type": ["string", "null"]
+ },
+ "url": {
+ "type": "string"
+ },
+ "config": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "scheduleType": {
+ "type": ["string", "null"]
+ },
+ "schedule": {
+ "anyOf": [
+ {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": ["number", "string"]
+ }
+ }
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Crontab string, or [value, unit] for interval schedules."
+ },
+ "timezone": {
+ "type": ["string", "null"]
+ },
+ "checkinMarginMinutes": {
+ "type": ["number", "null"]
+ },
+ "maxRuntimeMinutes": {
+ "type": ["number", "null"]
+ }
+ },
+ "required": [
+ "scheduleType",
+ "schedule",
+ "timezone",
+ "checkinMarginMinutes",
+ "maxRuntimeMinutes"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "slug",
+ "name",
+ "project",
+ "status",
+ "type",
+ "owner",
+ "ownerEmail",
+ "lastCheckIn",
+ "nextCheckIn",
+ "url",
+ "config"
+ ],
+ "additionalProperties": false
+ },
+ "environments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": ["string", "null"]
+ },
+ "status": {
+ "type": ["string", "null"]
+ },
+ "lastCheckIn": {
+ "type": ["string", "null"]
+ },
+ "nextCheckIn": {
+ "type": ["string", "null"]
+ }
+ },
+ "required": ["name", "status", "lastCheckIn", "nextCheckIn"],
+ "additionalProperties": false
+ }
+ },
+ "checkIns": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "date": {
+ "type": ["string", "null"]
+ },
+ "status": {
+ "type": ["string", "null"]
+ },
+ "durationMs": {
+ "type": ["number", "null"]
+ },
+ "environment": {
+ "type": ["string", "null"]
+ }
+ },
+ "required": ["date", "status", "durationMs", "environment"],
+ "additionalProperties": false
+ }
+ },
+ "stats": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "timestamp": {
+ "type": "string"
+ },
+ "ok": {
+ "type": ["number", "null"]
+ },
+ "error": {
+ "type": ["number", "null"]
+ },
+ "missed": {
+ "type": ["number", "null"]
+ },
+ "timeout": {
+ "type": ["number", "null"]
+ },
+ "unknown": {
+ "type": ["number", "null"]
+ },
+ "avgDurationMs": {
+ "type": ["number", "null"]
+ }
+ },
+ "required": [
+ "timestamp",
+ "ok",
+ "error",
+ "missed",
+ "timeout",
+ "unknown",
+ "avgDurationMs"
+ ],
+ "additionalProperties": false
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "At most 10 most recent stat buckets, oldest first. Null when includeStats is false."
+ }
+ },
+ "required": ["monitor", "environments", "checkIns", "stats"],
+ "additionalProperties": false
+ },
"requiredScopes": ["project:read"],
"skills": ["inspect"],
"surface": "direct"
diff --git a/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts b/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts
index 3ae59031f..01ba89335 100644
--- a/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts
+++ b/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts
@@ -1,7 +1,13 @@
import { mswServer } from "@sentry/mcp-server-mocks";
import { http, HttpResponse } from "msw";
import { describe, expect, it } from "vitest";
-import getMonitorDetails from "./get-monitor-details.js";
+import {
+ assertStructuredOnlyResult,
+ getStructuredContent,
+} from "../../test-utils/structured-content";
+import getMonitorDetails, {
+ getMonitorDetailsOutputSchema,
+} from "./get-monitor-details.js";
import { prepareToolParams } from "../catalog-runtime/availability";
const context = {
@@ -12,6 +18,28 @@ const context = {
userId: "1",
};
+const baseParams = {
+ organizationSlug: "sentry-mcp-evals",
+ regionUrl: null,
+ projectSlugOrId: null,
+ monitorSlug: "nightly-import",
+ environment: null,
+ period: "24h",
+ start: null,
+ end: null,
+ checkInLimit: 10,
+ includeStats: true,
+ rollupSeconds: null,
+};
+
+function getOutput(result: unknown) {
+ assertStructuredOnlyResult(result);
+ const content = getStructuredContent(result);
+ const parsed = getMonitorDetailsOutputSchema.parse(content);
+ expect(parsed).toEqual(content);
+ return parsed;
+}
+
describe("get_monitor_details", () => {
it("serializes monitor details", async () => {
const result = await getMonitorDetails.handler(
@@ -31,52 +59,151 @@ describe("get_monitor_details", () => {
context,
);
- expect(result).toMatchInlineSnapshot(`
- "# Monitor Nightly Import in **sentry-mcp-evals**
-
- **Slug**: nightly-import
- **ID**: 4509100000000001
- **Project**: cloudflare-mcp
- **Status**: ok
- **Owner**: the-goats
- **Last Check-In**: 2025-04-14T02:00:13.000Z
- **Next Check-In**: 2025-04-15T02:00:00.000Z
- **URL**: [Open Monitor](https://sentry-mcp-evals.sentry.io/crons/cloudflare-mcp/nightly-import/)
-
- ## Schedule
-
- - **schedule**: ["crontab","0 2 * * *"]
- - **schedule_type**: crontab
- - **timezone**: UTC
- - **checkin_margin**: 5
- - **max_runtime**: 30
-
- ## Environments
-
- - production
- - Status: ok
- - Last check-in: 2025-04-14T02:00:13.000Z
- - Next check-in: 2025-04-15T02:00:00.000Z
- - staging
- - Status: missed_checkin
- - Last check-in: 2025-04-13T02:00:18.000Z
- - Next check-in: 2025-04-14T02:00:00.000Z
-
- ## Recent Check-Ins
+ expect(getOutput(result)).toMatchInlineSnapshot(`
+ {
+ "checkIns": [
+ {
+ "date": "2025-04-14T02:00:13.000Z",
+ "durationMs": 13200,
+ "environment": "production",
+ "status": "ok",
+ },
+ {
+ "date": "2025-04-14T02:05:00.000Z",
+ "durationMs": null,
+ "environment": "staging",
+ "status": "missed",
+ },
+ ],
+ "environments": [
+ {
+ "lastCheckIn": "2025-04-14T02:00:13.000Z",
+ "name": "production",
+ "nextCheckIn": "2025-04-15T02:00:00.000Z",
+ "status": "ok",
+ },
+ {
+ "lastCheckIn": "2025-04-13T02:00:18.000Z",
+ "name": "staging",
+ "nextCheckIn": "2025-04-14T02:00:00.000Z",
+ "status": "missed_checkin",
+ },
+ ],
+ "monitor": {
+ "config": {
+ "checkinMarginMinutes": 5,
+ "maxRuntimeMinutes": 30,
+ "schedule": "0 2 * * *",
+ "scheduleType": "crontab",
+ "timezone": "UTC",
+ },
+ "id": "4509100000000001",
+ "lastCheckIn": "2025-04-14T02:00:13.000Z",
+ "name": "Nightly Import",
+ "nextCheckIn": "2025-04-15T02:00:00.000Z",
+ "owner": "the-goats",
+ "ownerEmail": null,
+ "project": "cloudflare-mcp",
+ "slug": "nightly-import",
+ "status": "ok",
+ "type": null,
+ "url": "https://sentry-mcp-evals.sentry.io/crons/cloudflare-mcp/nightly-import/",
+ },
+ "stats": [
+ {
+ "avgDurationMs": 13200,
+ "error": 0,
+ "missed": 0,
+ "ok": 1,
+ "timeout": 0,
+ "timestamp": "2025-04-14T02:00:00.000Z",
+ "unknown": 0,
+ },
+ {
+ "avgDurationMs": 0,
+ "error": 0,
+ "missed": 1,
+ "ok": 0,
+ "timeout": 0,
+ "timestamp": "2025-04-15T02:00:00.000Z",
+ "unknown": 0,
+ },
+ ],
+ }
+ `);
+ });
- - 2025-04-14T02:00:13.000Z: ok, 13.2s, production
- - 2025-04-14T02:05:00.000Z: missed, staging
+ it("returns a null owner when Sentry has no usable identity", async () => {
+ mswServer.use(
+ http.get(
+ "https://sentry.io/api/0/organizations/sentry-mcp-evals/monitors/nightly-import/",
+ () =>
+ HttpResponse.json({
+ id: "4509100000000001",
+ slug: "nightly-import",
+ name: "Nightly Import",
+ status: "ok",
+ project: { id: "4509109104082945", slug: "cloudflare-mcp" },
+ environments: [],
+ owner: { type: "user" },
+ config: { schedule_type: "crontab", schedule: "0 2 * * *" },
+ }),
+ ),
+ );
- ## Stats
+ const output = getOutput(
+ await getMonitorDetails.handler(baseParams, context),
+ );
- - 2025-04-14T02:00:00.000Z: ok=1, error=0, missed=0, timeout=0, unknown=0, duration=13.2
- - 2025-04-15T02:00:00.000Z: ok=0, error=0, missed=1, timeout=0, unknown=0, duration=0
+ expect(output.monitor.owner).toBeNull();
+ expect(output.monitor.ownerEmail).toBeNull();
+ });
- ## Response Notes
+ it("does not leak backend-only monitor fields", async () => {
+ mswServer.use(
+ http.get(
+ "https://sentry.io/api/0/organizations/sentry-mcp-evals/monitors/nightly-import/",
+ () =>
+ HttpResponse.json({
+ id: "4509100000000001",
+ slug: "nightly-import",
+ name: "Nightly Import",
+ status: "ok",
+ isUpserting: false,
+ project: { id: "4509109104082945", slug: "cloudflare-mcp" },
+ environments: [],
+ owner: {
+ type: "user",
+ id: "1",
+ name: "Jane Developer",
+ email: "jane@example.com",
+ },
+ alertRule: { targets: [{ targetIdentifier: "secret-target" }] },
+ config: {
+ schedule_type: "crontab",
+ schedule: "0 2 * * *",
+ failure_issue_threshold: 3,
+ alert_rule_id: 987654,
+ },
+ }),
+ ),
+ );
- - Search issues from this monitor with \`search_issues\` query \`monitor.slug:nightly-import\`.
- "
- `);
+ const result = await getMonitorDetails.handler(baseParams, context);
+
+ const output = getOutput(result);
+ // The owner's email is user data Sentry stored, so it comes through.
+ expect(output.monitor.ownerEmail).toBe("jane@example.com");
+ const text = JSON.stringify(output);
+ for (const leaked of [
+ "secret-target",
+ "alertRule",
+ "failure_issue_threshold",
+ "987654",
+ "isUpserting",
+ ]) {
+ expect(text).not.toContain(leaked);
+ }
});
it("rejects monitors outside the active project constraint", async () => {
@@ -123,7 +250,7 @@ describe("get_monitor_details", () => {
},
config: {
schedule_type: "crontab",
- schedule: ["crontab", "0 2 * * *"],
+ schedule: "0 2 * * *",
},
environments: [],
};
@@ -174,9 +301,7 @@ describe("get_monitor_details", () => {
},
);
- expect(result).toContain(
- "# Monitor Nightly Import in **sentry-mcp-evals**",
- );
+ expect(getOutput(result).monitor.name).toBe("Nightly Import");
expect(paths).toEqual([
"/api/0/projects/sentry-mcp-evals/cloudflare-mcp/monitors/nightly-import/",
"/api/0/projects/sentry-mcp-evals/cloudflare-mcp/monitors/nightly-import/checkins/",
@@ -199,7 +324,7 @@ describe("get_monitor_details", () => {
},
config: {
schedule_type: "crontab",
- schedule: ["crontab", "0 2 * * *"],
+ schedule: "0 2 * * *",
},
environments: [],
};
@@ -240,7 +365,7 @@ describe("get_monitor_details", () => {
const result = await getMonitorDetails.handler(params, context);
- expect(result).toContain("# Monitor Nightly Import in **MyOrg**");
+ expect(getOutput(result).monitor.name).toBe("Nightly Import");
expect(paths).toEqual([
"/api/0/projects/MyOrg/MyProject/monitors/nightly-import/",
"/api/0/projects/MyOrg/MyProject/monitors/nightly-import/checkins/",
@@ -263,7 +388,7 @@ describe("get_monitor_details", () => {
},
config: {
schedule_type: "crontab",
- schedule: ["crontab", "0 2 * * *"],
+ schedule: "0 2 * * *",
},
environments: [],
};
@@ -379,7 +504,7 @@ describe("get_monitor_details", () => {
},
config: {
schedule_type: "crontab",
- schedule: ["crontab", "0 2 * * *"],
+ schedule: "0 2 * * *",
},
environments: [],
};
@@ -446,7 +571,7 @@ describe("get_monitor_details", () => {
},
config: {
schedule_type: "crontab",
- schedule: ["crontab", "0 2 * * *"],
+ schedule: "0 2 * * *",
},
environments: [],
};
@@ -486,10 +611,11 @@ describe("get_monitor_details", () => {
context,
);
- expect(result).toContain(
- "[Open Monitor](https://sentry-mcp-evals.sentry.io/crons/cloudflare-mcp/nightly%2Fimport%201/)",
+ const output = getOutput(result);
+ expect(output.monitor.url).toBe(
+ "https://sentry-mcp-evals.sentry.io/crons/cloudflare-mcp/nightly%2Fimport%201/",
);
- expect(result).not.toContain("## Stats");
+ expect(output.stats).toBeNull();
});
describe("tool definition", () => {
diff --git a/packages/mcp-core/src/tools/catalog/get-monitor-details.ts b/packages/mcp-core/src/tools/catalog/get-monitor-details.ts
index 54168df80..0d53fed47 100644
--- a/packages/mcp-core/src/tools/catalog/get-monitor-details.ts
+++ b/packages/mcp-core/src/tools/catalog/get-monitor-details.ts
@@ -7,6 +7,7 @@ import type {
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,
ParamPeriod,
@@ -16,14 +17,82 @@ import { setTargetTagsAndAttributes } from "../../telem/scope";
import type { ServerContext } from "../../types";
import { isNumericId, validateSlugOrId } from "../../utils/slug-validation";
import {
- compactLines,
formatActor,
formatDate,
formatId,
- formatUnknown,
+ readString,
} from "./support/api-formatting";
import { assertProjectRefWithinConstraint } from "./support/project-constraints";
+const STATS_BUCKET_LIMIT = 10;
+const nullableString = z.string().nullable();
+const nullableNumber = z.number().nullable();
+
+const monitorConfigSchema = z.object({
+ scheduleType: nullableString,
+ schedule: z
+ .union([z.string(), z.array(z.union([z.number(), z.string()]))])
+ .nullable()
+ .describe("Crontab string, or [value, unit] for interval schedules."),
+ timezone: nullableString,
+ checkinMarginMinutes: nullableNumber,
+ maxRuntimeMinutes: nullableNumber,
+});
+
+const checkInSchema = z.object({
+ date: nullableString,
+ status: nullableString,
+ durationMs: nullableNumber,
+ environment: nullableString,
+});
+
+const statBucketSchema = z.object({
+ timestamp: z.string(),
+ ok: nullableNumber,
+ error: nullableNumber,
+ missed: nullableNumber,
+ timeout: nullableNumber,
+ unknown: nullableNumber,
+ avgDurationMs: nullableNumber,
+});
+
+export const getMonitorDetailsOutputSchema = z.object({
+ monitor: z.object({
+ id: z.string(),
+ slug: z.string(),
+ name: nullableString,
+ project: nullableString,
+ status: nullableString,
+ type: nullableString,
+ owner: nullableString,
+ ownerEmail: nullableString.describe("Set when a user owns the monitor."),
+ lastCheckIn: nullableString,
+ nextCheckIn: nullableString,
+ url: z.string(),
+ config: monitorConfigSchema.nullable(),
+ }),
+ environments: z.array(
+ z.object({
+ name: nullableString,
+ status: nullableString,
+ lastCheckIn: nullableString,
+ nextCheckIn: nullableString,
+ }),
+ ),
+ checkIns: z.array(checkInSchema),
+ stats: z
+ .array(statBucketSchema)
+ .nullable()
+ .describe(
+ `At most ${STATS_BUCKET_LIMIT} most recent stat buckets, oldest first. Null when includeStats is false.`,
+ ),
+});
+
+function formatOwner(owner: Monitor["owner"]): string | null {
+ const name = owner ? formatActor(owner) : null;
+ return name === "unknown" ? null : name;
+}
+
function formatProject(monitor: Monitor): string | null {
if (!monitor.project) {
return null;
@@ -32,43 +101,64 @@ function formatProject(monitor: Monitor): string | null {
return monitor.project.slug ?? monitor.project.name ?? null;
}
-function formatConfig(monitor: Monitor): string[] {
+function readNumber(
+ value: Record,
+ key: string,
+): number | null {
+ const field = value[key];
+ return typeof field === "number" ? field : null;
+}
+
+function readSchedule(
+ value: unknown,
+): z.infer["schedule"] {
+ if (typeof value === "string") {
+ return value;
+ }
+ if (Array.isArray(value)) {
+ return value.filter(
+ (item): item is string | number =>
+ typeof item === "string" || typeof item === "number",
+ );
+ }
+ return null;
+}
+
+function toConfig(
+ monitor: Monitor,
+): z.infer | null {
const config = monitor.config;
if (!config) {
- return [];
+ return null;
}
-
- return [
- "schedule",
- "schedule_type",
- "timezone",
- "checkin_margin",
- "max_runtime",
- ]
- .filter((key) => config[key] !== undefined)
- .map((key) => `- **${key}**: ${formatUnknown(config[key])}`);
+ return {
+ scheduleType: readString(config, "schedule_type"),
+ schedule: readSchedule(config.schedule),
+ timezone: readString(config, "timezone"),
+ checkinMarginMinutes: readNumber(config, "checkin_margin"),
+ maxRuntimeMinutes: readNumber(config, "max_runtime"),
+ };
}
-function formatCheckIn(checkIn: MonitorCheckIn): string {
- const date =
- formatDate(checkIn.dateCreated) ??
- formatDate(checkIn.dateUpdated) ??
- "unknown time";
- const duration =
- checkIn.duration === undefined || checkIn.duration === null
- ? ""
- : `, ${checkIn.duration}s`;
- const environment = checkIn.environment ? `, ${checkIn.environment}` : "";
- return `- ${date}: ${checkIn.status ?? "unknown"}${duration}${environment}`;
+function toCheckIn(checkIn: MonitorCheckIn): z.infer {
+ return {
+ date: formatDate(checkIn.dateCreated) ?? formatDate(checkIn.dateUpdated),
+ status: checkIn.status ?? null,
+ durationMs: checkIn.duration ?? null,
+ environment: checkIn.environment ?? null,
+ };
}
-function formatStat(stat: MonitorStat): string {
- const timestamp =
- formatDate(new Date(stat.ts * 1000).toISOString()) ?? String(stat.ts);
- const parts = Object.entries(stat)
- .filter(([key]) => key !== "ts")
- .map(([key, value]) => `${key}=${formatUnknown(value)}`);
- return `- ${timestamp}: ${parts.join(", ") || "no check-ins"}`;
+function toStat(stat: MonitorStat): z.infer {
+ return {
+ timestamp: new Date(stat.ts * 1000).toISOString(),
+ ok: readNumber(stat, "ok"),
+ error: readNumber(stat, "error"),
+ missed: readNumber(stat, "missed"),
+ timeout: readNumber(stat, "timeout"),
+ unknown: readNumber(stat, "unknown"),
+ avgDurationMs: readNumber(stat, "duration"),
+ };
}
export default defineTool({
@@ -84,6 +174,8 @@ export default defineTool({
"- Review recent check-ins for missed, failed, timeout, or OK runs",
"- Check monitor stats over a recent time range",
"",
+ "To find issues from a monitor, call `search_issues` with query `monitor.slug:`.",
+ "",
"",
"get_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import')",
"get_monitor_details(organizationSlug='my-organization', monitorSlug='nightly-import', projectSlugOrId='backend', environment='production', period='7d')",
@@ -152,6 +244,7 @@ export default defineTool({
.nullable()
.default(null),
},
+ outputSchema: getMonitorDetailsOutputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
@@ -252,69 +345,34 @@ export default defineTool({
: Promise.resolve([]),
]);
- const owner = monitor.owner ? formatActor(monitor.owner) : null;
- const output = compactLines([
- `# Monitor ${monitor.name ?? monitor.slug} in **${organizationSlug}**`,
- "",
- `**Slug**: ${monitor.slug}`,
- `**ID**: ${formatId(monitor.id)}`,
- project ? `**Project**: ${project}` : null,
- monitor.status ? `**Status**: ${monitor.status}` : null,
- monitor.type ? `**Type**: ${monitor.type}` : null,
- owner ? `**Owner**: ${owner}` : null,
- formatDate(monitor.lastCheckIn)
- ? `**Last Check-In**: ${formatDate(monitor.lastCheckIn)}`
- : null,
- formatDate(monitor.nextCheckIn)
- ? `**Next Check-In**: ${formatDate(monitor.nextCheckIn)}`
+ return structuredResult({
+ monitor: {
+ id: formatId(monitor.id),
+ slug: monitor.slug,
+ name: monitor.name ?? null,
+ project,
+ status: monitor.status ?? null,
+ type: monitor.type ?? null,
+ owner: formatOwner(monitor.owner),
+ ownerEmail:
+ monitor.owner && typeof monitor.owner === "object"
+ ? monitor.owner.email || null
+ : null,
+ lastCheckIn: formatDate(monitor.lastCheckIn),
+ nextCheckIn: formatDate(monitor.nextCheckIn),
+ url: monitorUrl,
+ config: toConfig(monitor),
+ },
+ environments: (monitor.environments ?? []).map((environment) => ({
+ name: environment.name ?? null,
+ status: environment.status ?? null,
+ lastCheckIn: formatDate(environment.lastCheckIn),
+ nextCheckIn: formatDate(environment.nextCheckIn),
+ })),
+ checkIns: checkIns.slice(0, params.checkInLimit).map(toCheckIn),
+ stats: params.includeStats
+ ? stats.slice(-STATS_BUCKET_LIMIT).map(toStat)
: null,
- `**URL**: [Open Monitor](${monitorUrl})`,
- ]);
-
- const config = formatConfig(monitor);
- if (config.length > 0) {
- output.push("", "## Schedule", "", ...config);
- }
-
- if (monitor.environments && monitor.environments.length > 0) {
- output.push("", "## Environments", "");
- for (const environment of monitor.environments) {
- output.push(
- compactLines([
- `- ${environment.name ?? "unknown"}`,
- environment.status ? ` - Status: ${environment.status}` : null,
- formatDate(environment.lastCheckIn)
- ? ` - Last check-in: ${formatDate(environment.lastCheckIn)}`
- : null,
- formatDate(environment.nextCheckIn)
- ? ` - Next check-in: ${formatDate(environment.nextCheckIn)}`
- : null,
- ]).join("\n"),
- );
- }
- }
-
- output.push("", "## Recent Check-Ins", "");
- output.push(
- checkIns.length === 0
- ? "No check-ins found in this time range."
- : checkIns.slice(0, params.checkInLimit).map(formatCheckIn).join("\n"),
- );
-
- if (params.includeStats) {
- output.push("", "## Stats", "");
- output.push(
- stats.length === 0
- ? "No stats found in this time range."
- : stats.slice(-10).map(formatStat).join("\n"),
- );
- }
-
- output.push("", "## Response Notes", "");
- output.push(
- `- Search issues from this monitor with \`search_issues\` query \`monitor.slug:${monitor.slug}\`.`,
- );
-
- return `${output.join("\n")}\n`;
+ });
},
});
diff --git a/packages/mcp-core/src/tools/catalog/get-sentry-resource.test.ts b/packages/mcp-core/src/tools/catalog/get-sentry-resource.test.ts
index 8778be9bb..f5523eb0f 100644
--- a/packages/mcp-core/src/tools/catalog/get-sentry-resource.test.ts
+++ b/packages/mcp-core/src/tools/catalog/get-sentry-resource.test.ts
@@ -403,10 +403,10 @@ describe("get_sentry_resource", () => {
const result = await callHandler({
url: "https://my-org.sentry.io/crons/daily-backup/",
});
- expect(result).toContain("# Monitor daily-backup in **my-org**");
- expect(result).toContain(
- "[Open Monitor](https://my-org.sentry.io/crons/backend/daily-backup/)",
- );
+ expect(getStructuredContent(result).monitor).toMatchObject({
+ slug: "daily-backup",
+ url: "https://my-org.sentry.io/crons/backend/daily-backup/",
+ });
});
it("dispatches monitor URL with project/slug path to get_monitor_details", async () => {
@@ -421,10 +421,10 @@ describe("get_sentry_resource", () => {
const result = await callHandler({
url: "https://my-org.sentry.io/crons/my-project/my-monitor/",
});
- expect(result).toContain("# Monitor my-monitor in **my-org**");
- expect(result).toContain(
- "[Open Monitor](https://my-org.sentry.io/crons/my-project/my-monitor/)",
- );
+ expect(getStructuredContent(result).monitor).toMatchObject({
+ slug: "my-monitor",
+ url: "https://my-org.sentry.io/crons/my-project/my-monitor/",
+ });
});
it("dispatches monitor URL with numeric project ID under an active project constraint", async () => {
@@ -450,10 +450,10 @@ describe("get_sentry_resource", () => {
},
);
- expect(result).toContain("# Monitor my-monitor in **my-org**");
- expect(result).toContain(
- "[Open Monitor](https://my-org.sentry.io/crons/backend/my-monitor/)",
- );
+ expect(getStructuredContent(result).monitor).toMatchObject({
+ slug: "my-monitor",
+ url: "https://my-org.sentry.io/crons/backend/my-monitor/",
+ });
});
it("rejects numeric monitor project IDs outside the active project constraint before monitor reads", async () => {
@@ -530,10 +530,10 @@ describe("get_sentry_resource", () => {
},
);
- expect(result).toContain("# Monitor my-monitor in **my-org**");
- expect(result).toContain(
- "[Open Monitor](https://my-org.sentry.io/crons/backend/my-monitor/)",
- );
+ expect(getStructuredContent(result).monitor).toMatchObject({
+ slug: "my-monitor",
+ url: "https://my-org.sentry.io/crons/backend/my-monitor/",
+ });
});
it("returns guidance for release URL", async () => {
@@ -594,9 +594,9 @@ describe("get_sentry_resource", () => {
sentryProtocol: "http",
},
);
- expect(result).toContain(
- "[Open Monitor](http://sentry.internal:9000/organizations/my-org/crons/backend/daily-backup/)",
- );
+ expect(getStructuredContent(result).monitor).toMatchObject({
+ url: "http://sentry.internal:9000/organizations/my-org/crons/backend/daily-backup/",
+ });
});
it("uses configured host and protocol for self-hosted release URL", async () => {
diff --git a/packages/mcp-server-mocks/src/fixtures/monitor-checkins.json b/packages/mcp-server-mocks/src/fixtures/monitor-checkins.json
index ef63f1bb4..9ef82baba 100644
--- a/packages/mcp-server-mocks/src/fixtures/monitor-checkins.json
+++ b/packages/mcp-server-mocks/src/fixtures/monitor-checkins.json
@@ -3,7 +3,7 @@
"id": "11111111-1111-4111-8111-111111111111",
"environment": "production",
"status": "ok",
- "duration": 13.2,
+ "duration": 13200,
"dateCreated": "2025-04-14T02:00:13.000Z",
"dateAdded": "2025-04-14T02:00:13.000Z",
"dateUpdated": "2025-04-14T02:00:26.000Z",
@@ -11,7 +11,7 @@
"dateClock": "2025-04-14T02:00:00.000Z",
"expectedTime": "2025-04-14T02:00:00.000Z",
"monitorConfig": {
- "schedule": ["crontab", "0 2 * * *"],
+ "schedule": "0 2 * * *",
"schedule_type": "crontab"
}
},
@@ -27,7 +27,7 @@
"dateClock": "2025-04-14T02:00:00.000Z",
"expectedTime": "2025-04-14T02:00:00.000Z",
"monitorConfig": {
- "schedule": ["crontab", "0 2 * * *"],
+ "schedule": "0 2 * * *",
"schedule_type": "crontab"
}
}
diff --git a/packages/mcp-server-mocks/src/fixtures/monitor-stats.json b/packages/mcp-server-mocks/src/fixtures/monitor-stats.json
index b87fe867e..f89e4b4b1 100644
--- a/packages/mcp-server-mocks/src/fixtures/monitor-stats.json
+++ b/packages/mcp-server-mocks/src/fixtures/monitor-stats.json
@@ -6,7 +6,7 @@
"missed": 0,
"timeout": 0,
"unknown": 0,
- "duration": 13.2
+ "duration": 13200
},
{
"ts": 1744682400,
diff --git a/packages/mcp-server-mocks/src/fixtures/monitor.json b/packages/mcp-server-mocks/src/fixtures/monitor.json
index 4adc39e2e..9c2cd99dd 100644
--- a/packages/mcp-server-mocks/src/fixtures/monitor.json
+++ b/packages/mcp-server-mocks/src/fixtures/monitor.json
@@ -19,7 +19,7 @@
"lastCheckIn": "2025-04-14T02:00:13.000Z",
"nextCheckIn": "2025-04-15T02:00:00.000Z",
"config": {
- "schedule": ["crontab", "0 2 * * *"],
+ "schedule": "0 2 * * *",
"schedule_type": "crontab",
"timezone": "UTC",
"checkin_margin": 5,