From 67206a63f0817f93aaf983dc32180bbd9e185d7e Mon Sep 17 00:00:00 2001 From: sergical Date: Mon, 22 Jun 2026 22:33:17 +0000 Subject: [PATCH 1/4] feat(get-sentry-resource): add dashboard URL routing Add support for Sentry dashboard URLs in get_sentry_resource so that pasting a dashboard URL (e.g. /dashboard/542438/) auto-routes to the existing get_dashboard_details catalog tool without exposing any new top-level tool. Changes: - url-helpers: add 'dashboard' to SentryResourceType, dashboardId field to ParsedSentryUrl, 'dashboard' to knownSegments, and numeric-ID parser branch in identifyResource() - get-sentry-resource: add 'dashboard' to FULLY_SUPPORTED_TYPES, ResolvedResourceParams.dashboardId, explicit-mode switch case, resolveFromParsedUrl case, conditional description/examples (mirrors monitor availability pattern), and handler dispatch with assertCatalogToolAvailable gate - Tests: url-helpers dashboard URL suite (5 cases), get-sentry-resource URL dispatch / explicit resourceType / availability-gate tests - Evals: dashboardUrl fixture + one get-sentry-resource eval case Co-Authored-By: Claude Sonnet 4.5 (Anthropic) Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../mcp-core/src/internal/url-helpers.test.ts | 48 ++++++++++++++ packages/mcp-core/src/internal/url-helpers.ts | 21 +++++- packages/mcp-core/src/skillDefinitions.json | 2 +- packages/mcp-core/src/toolDefinitions.json | 9 +-- .../tools/catalog/get-sentry-resource.test.ts | 41 +++++++++++- .../src/tools/catalog/get-sentry-resource.ts | 64 +++++++++++++++++-- .../src/evals/get-sentry-resource.eval.ts | 11 ++++ .../src/evals/utils/fixtures.ts | 1 + 8 files changed, 184 insertions(+), 13 deletions(-) diff --git a/packages/mcp-core/src/internal/url-helpers.test.ts b/packages/mcp-core/src/internal/url-helpers.test.ts index 5d2ac3057..01dca5c0f 100644 --- a/packages/mcp-core/src/internal/url-helpers.test.ts +++ b/packages/mcp-core/src/internal/url-helpers.test.ts @@ -543,6 +543,54 @@ describe("parseSentryUrl", () => { }); }); + describe("dashboard URLs", () => { + it("parses dashboard URL with numeric ID (subdomain)", () => { + expect( + parseSentryUrl("https://my-org.sentry.io/dashboard/542438/"), + ).toMatchInlineSnapshot(` + { + "dashboardId": "542438", + "organizationSlug": "my-org", + "type": "dashboard", + } + `); + }); + + it("parses dashboard URL with organizations path", () => { + expect( + parseSentryUrl("https://sentry.io/organizations/my-org/dashboard/101/"), + ).toMatchInlineSnapshot(` + { + "dashboardId": "101", + "organizationSlug": "my-org", + "type": "dashboard", + } + `); + }); + + it("parses dashboard URL with path-based org", () => { + expect( + parseSentryUrl("https://sentry.io/my-org/dashboard/101/"), + ).toMatchInlineSnapshot(` + { + "dashboardId": "101", + "organizationSlug": "my-org", + "type": "dashboard", + } + `); + }); + + it("returns unknown for /dashboard/new/ (non-numeric ID)", () => { + const result = parseSentryUrl("https://my-org.sentry.io/dashboard/new/"); + expect(result.type).toBe("unknown"); + }); + + it("returns unknown for /dashboards/ list URL (plural, no ID)", () => { + const result = parseSentryUrl("https://my-org.sentry.io/dashboards/"); + expect(result.type).toBe("unknown"); + }); + }); + describe("performance summary URLs", () => { it("extracts transaction from performance summary URL", () => { expect( diff --git a/packages/mcp-core/src/internal/url-helpers.ts b/packages/mcp-core/src/internal/url-helpers.ts index 1546b3d5d..164f20c1e 100644 --- a/packages/mcp-core/src/internal/url-helpers.ts +++ b/packages/mcp-core/src/internal/url-helpers.ts @@ -2,7 +2,7 @@ * Unified URL parsing utilities for Sentry resources. * * Parses Sentry URLs to identify resource types and extract relevant identifiers. - * Supports issue, trace, profile, event, replay, and monitor URLs across different + * Supports issue, trace, profile, event, replay, monitor, and dashboard URLs across different * Sentry URL formats (subdomain, path-based organization, self-hosted). */ @@ -21,6 +21,7 @@ export type SentryResourceType = | "monitor" | "release" | "snapshot" + | "dashboard" | "unknown"; /** @@ -64,6 +65,8 @@ export interface ParsedSentryUrl { snapshotId?: string; /** Selected snapshot image name (from ?selectedSnapshot= query param) */ selectedSnapshot?: string; + /** Dashboard numeric ID (for dashboard URLs) */ + dashboardId?: string; } /** @@ -79,6 +82,7 @@ export interface ParsedSentryUrl { * - Replay: `/explore/replays/{replayId}/` or `/replays/{replayId}/` * - Monitor: `/crons/{monitorSlug}/` or `/monitors/{monitorSlug}/` * - Release: `/releases/{version}/` + * - Dashboard: `/dashboard/{dashboardId}/` * * Organization slug is extracted from: * 1. Subdomain (e.g., `my-org.sentry.io`) @@ -164,6 +168,7 @@ function extractOrganizationSlug(parsedUrl: URL, pathParts: string[]): string { "monitors", "alerts", "feedback", + "dashboard", "dashboards", "discover", "insights", @@ -398,6 +403,20 @@ function identifyResource( } } + // Dashboard URL: /dashboard/{dashboardId}/ + // Dashboard IDs are always numeric integers. + const dashboardIndex = pathParts.indexOf("dashboard"); + if (dashboardIndex !== -1) { + const dashboardId = pathParts[dashboardIndex + 1]; + if (dashboardId && /^\d+$/.test(dashboardId)) { + return { + type: "dashboard", + organizationSlug, + dashboardId, + }; + } + } + // Snapshot URL: /preprod/snapshots/{snapshotId}/ const preprodIndex = pathParts.indexOf("preprod"); if (preprodIndex !== -1 && pathParts[preprodIndex + 1] === "snapshots") { diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 1d39b4d50..49df79ca6 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -119,7 +119,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 3fb9d69f0..1a22c4026 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -1533,7 +1533,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "inputSchema": { "type": "object", "properties": { @@ -1554,13 +1554,14 @@ "replay", "monitor", "snapshot", - "snapshotImage" + "snapshotImage", + "dashboard" ], - "description": "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, or `snapshotImage` with `:`." + "description": "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, `snapshotImage` with `:`, or `dashboard` with a numeric dashboard ID when inspect dashboard tools are available." }, "resourceId": { "type": "string", - "description": "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, AI conversation ID, replay ID, monitor slug when inspect monitor tools are available, snapshot artifact ID, `:` for snapshot image resources, or `traceId:spanId` for span resources. Required when not using a URL." + "description": "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, AI conversation ID, replay ID, monitor slug when inspect monitor tools are available, dashboard ID or title when inspect dashboard tools are available, snapshot artifact ID, `:` for snapshot image resources, or `traceId:spanId` for span resources. Required when not using a URL." }, "organizationSlug": { "type": "string", 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 f8d4e9254..b4dfdd0c0 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 @@ -50,7 +50,8 @@ function callHandler(params: { | "replay" | "monitor" | "snapshot" - | "snapshotImage"; + | "snapshotImage" + | "dashboard"; resourceId?: string; organizationSlug?: string; }) { @@ -731,6 +732,44 @@ describe("get_sentry_resource", () => { }); }); + // ─── URL mode: dashboard URLs ────────────────────────────────────────────── + describe("URL mode — dashboard URLs", () => { + it("dispatches dashboard URL to get_dashboard_details", async () => { + const result = await callHandler({ + url: "https://sentry-mcp-evals.sentry.io/dashboard/101/", + }); + expect(result).toContain( + "# Dashboard Errors Overview in **sentry-mcp-evals**", + ); + expect(result).toContain("**ID**: 101"); + expect(result).toContain("[Open Dashboard]"); + }); + + it("dispatches explicit dashboard resourceType to get_dashboard_details", async () => { + const result = await callHandler({ + resourceType: "dashboard", + resourceId: "101", + organizationSlug: "sentry-mcp-evals", + }); + expect(result).toContain( + "# Dashboard Errors Overview in **sentry-mcp-evals**", + ); + expect(result).toContain("**ID**: 101"); + }); + + it("rejects dashboard URL when get_dashboard_details is not available", async () => { + await expect( + getSentryResource.handler( + { url: "https://sentry-mcp-evals.sentry.io/dashboard/101/" }, + { + ...baseContext, + availableToolNames: new Set(["get_sentry_resource"]), + }, + ), + ).rejects.toThrow("Dashboard resources require the inspect skill"); + }); + }); + // ─── URL mode: error cases ──────────────────────────────────────────────── describe("URL mode — error cases", () => { it("throws for unsupported URL path (settings)", async () => { diff --git a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts index 83ef29c29..30a4e044b 100644 --- a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts +++ b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts @@ -24,6 +24,7 @@ import { fetchSnapshotSummary, } from "../support/snapshots/handlers"; import getAIConversationDetails from "./get-ai-conversation-details"; +import getDashboardDetails from "./get-dashboard-details"; import getIssueDetails from "./get-issue-details"; import getMonitorDetails from "./get-monitor-details"; import getProfileDetails from "./get-profile-details"; @@ -42,6 +43,7 @@ export const FULLY_SUPPORTED_TYPES = [ "monitor", "snapshot", "snapshotImage", + "dashboard", ] as const; export type FullySupportedType = (typeof FULLY_SUPPORTED_TYPES)[number]; @@ -80,6 +82,8 @@ export interface ResolvedResourceParams { // Snapshot params snapshotId?: string; selectedSnapshot?: string; + // Dashboard params + dashboardId?: string; } export function resolveResourceParams(params: { @@ -203,6 +207,13 @@ export function resolveResourceParams(params: { selectedSnapshot: imageName, }; } + + case "dashboard": + return { + type: "dashboard", + organizationSlug, + dashboardId: resourceId, + }; } } @@ -234,7 +245,7 @@ function resolveFromParsedUrl( } throw new UserInputError( "Could not determine resource type from URL. " + - "Supported URL patterns: issues, events, traces, AI conversations, profiles, replays, monitors, and releases.", + "Supported URL patterns: issues, events, traces, AI conversations, profiles, replays, monitors, dashboards, and releases.", ); } @@ -403,6 +414,16 @@ function resolveFromParsedUrl( snapshotId: parsed.snapshotId, selectedSnapshot: parsed.selectedSnapshot, }; + + case "dashboard": + if (!parsed.dashboardId) { + throw new UserInputError("Could not extract dashboard ID from URL."); + } + return { + type: "dashboard", + organizationSlug, + dashboardId: parsed.dashboardId, + }; } } @@ -515,6 +536,10 @@ export default defineTool({ "get_monitor_details", availableToolNames, ); + const dashboardResourcesAvailable = isToolAvailable( + "get_dashboard_details", + availableToolNames, + ); const fullResolutionInstruction = formatToolCallInstruction({ toolName: "get_snapshot_image", arguments: { @@ -530,12 +555,18 @@ export default defineTool({ "Full-resolution snapshot image bytes are not available in this session", purpose: "for full-resolution image bytes", }); - const supportedResources = monitorResourcesAvailable - ? "issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, preprod snapshots, and snapshot images." - : "issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images."; + const extraResources = [ + ...(monitorResourcesAvailable ? ["monitors"] : []), + ...(dashboardResourcesAvailable ? ["dashboards"] : []), + ]; + const supportedResources = + extraResources.length > 0 + ? `issues, events, traces, spans, AI conversations, breadcrumbs, replays, ${extraResources.join(", ")}, preprod snapshots, and snapshot images.` + : "issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images."; const resourceIds = [ "- span: :", ...(monitorResourcesAvailable ? ["- monitor: "] : []), + ...(dashboardResourcesAvailable ? ["- dashboard: "] : []), "- snapshot: ", "- snapshotImage: :", ]; @@ -561,6 +592,11 @@ export default defineTool({ "get_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')", "get_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')", "get_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')", + ...(dashboardResourcesAvailable + ? [ + "get_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')", + ] + : []), "get_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')", "get_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')", "", @@ -588,10 +624,11 @@ export default defineTool({ "monitor", "snapshot", "snapshotImage", + "dashboard", ]) .optional() .describe( - "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, or `snapshotImage` with `:`.", + "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, `snapshotImage` with `:`, or `dashboard` with a numeric dashboard ID when inspect dashboard tools are available.", ), resourceId: z @@ -599,7 +636,7 @@ export default defineTool({ .trim() .optional() .describe( - "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, AI conversation ID, replay ID, monitor slug when inspect monitor tools are available, snapshot artifact ID, `:` for snapshot image resources, or `traceId:spanId` for span resources. Required when not using a URL.", + "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, AI conversation ID, replay ID, monitor slug when inspect monitor tools are available, dashboard ID or title when inspect dashboard tools are available, snapshot artifact ID, `:` for snapshot image resources, or `traceId:spanId` for span resources. Required when not using a URL.", ), organizationSlug: ParamOrganizationSlug.optional(), @@ -766,6 +803,21 @@ export default defineTool({ context, ); + case "dashboard": + assertCatalogToolAvailable( + context, + "get_dashboard_details", + "Dashboard", + ); + return getDashboardDetails.handler( + { + organizationSlug: resolved.organizationSlug, + dashboardIdOrTitle: resolved.dashboardId!, + regionUrl: context.constraints.regionUrl ?? null, + }, + context, + ); + case "snapshot": case "snapshotImage": { const apiService = apiServiceFromContext(context, { diff --git a/packages/mcp-server-evals/src/evals/get-sentry-resource.eval.ts b/packages/mcp-server-evals/src/evals/get-sentry-resource.eval.ts index 42437788e..48ac823ec 100644 --- a/packages/mcp-server-evals/src/evals/get-sentry-resource.eval.ts +++ b/packages/mcp-server-evals/src/evals/get-sentry-resource.eval.ts @@ -51,6 +51,17 @@ describeEval("get-sentry-resource", { }, ], }, + { + input: `What widgets are on this dashboard? ${FIXTURES.dashboardUrl}`, + expectedTools: [ + { + name: "get_sentry_resource", + arguments: { + url: FIXTURES.dashboardUrl, + }, + }, + ], + }, ]; }, task: NoOpTaskRunner(), diff --git a/packages/mcp-server-evals/src/evals/utils/fixtures.ts b/packages/mcp-server-evals/src/evals/utils/fixtures.ts index 82758ab35..073d692d6 100644 --- a/packages/mcp-server-evals/src/evals/utils/fixtures.ts +++ b/packages/mcp-server-evals/src/evals/utils/fixtures.ts @@ -22,4 +22,5 @@ export const FIXTURES = { traceUrl: "https://sentry-mcp-evals.sentry.io/explore/traces/trace/a4d1aae7216b47ff8117cf4e09ce9d0a/", dsn: "https://d20df0a1ab5031c7f3c7edca9c02814d@o4509106732793856.ingest.us.sentry.io/4509109104082945", + dashboardUrl: "https://sentry-mcp-evals.sentry.io/dashboard/101/", }; From b4a7eaa7c43e60f83da17f9f782233358039a071 Mon Sep 17 00:00:00 2001 From: sergical Date: Tue, 23 Jun 2026 15:44:41 +0000 Subject: [PATCH 2/4] fix(get-sentry-resource): advertise dashboard URLs in all skill contexts identifyResource() routes /dashboard/{id}/ URLs unconditionally, so the description should always mention dashboards regardless of whether get_dashboard_details is in scope. Execution is still gated at runtime by assertCatalogToolAvailable. Previously dashboards were conditional in the description (like monitors), which caused skillDefinitions.json to produce stale copies omitting dashboards for triage/non-inspect skill contexts. Regenerated skillDefinitions.json and toolDefinitions.json. Co-Authored-By: Claude Sonnet 4.5 (Anthropic) Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- packages/mcp-core/src/skillDefinitions.json | 4 +-- .../src/tools/catalog/get-sentry-resource.ts | 28 +++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 49df79ca6..83339ae67 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -204,7 +204,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { @@ -310,7 +310,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { diff --git a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts index 30a4e044b..88931931b 100644 --- a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts +++ b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts @@ -536,10 +536,6 @@ export default defineTool({ "get_monitor_details", availableToolNames, ); - const dashboardResourcesAvailable = isToolAvailable( - "get_dashboard_details", - availableToolNames, - ); const fullResolutionInstruction = formatToolCallInstruction({ toolName: "get_snapshot_image", arguments: { @@ -555,18 +551,18 @@ export default defineTool({ "Full-resolution snapshot image bytes are not available in this session", purpose: "for full-resolution image bytes", }); - const extraResources = [ - ...(monitorResourcesAvailable ? ["monitors"] : []), - ...(dashboardResourcesAvailable ? ["dashboards"] : []), - ]; - const supportedResources = - extraResources.length > 0 - ? `issues, events, traces, spans, AI conversations, breadcrumbs, replays, ${extraResources.join(", ")}, preprod snapshots, and snapshot images.` - : "issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images."; + // Monitors are only listed when available — they require the inspect skill + // and are gated at execution time via assertCatalogToolAvailable. + // Dashboards are always advertised because identifyResource() routes all + // /dashboard/{id}/ URLs unconditionally; execution is still gated at + // runtime by assertCatalogToolAvailable. + const supportedResources = monitorResourcesAvailable + ? "issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images." + : "issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images."; const resourceIds = [ "- span: :", ...(monitorResourcesAvailable ? ["- monitor: "] : []), - ...(dashboardResourcesAvailable ? ["- dashboard: "] : []), + "- dashboard: ", "- snapshot: ", "- snapshotImage: :", ]; @@ -592,11 +588,7 @@ export default defineTool({ "get_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')", "get_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')", "get_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')", - ...(dashboardResourcesAvailable - ? [ - "get_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')", - ] - : []), + "get_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')", "get_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')", "get_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')", "", From b52bf93b6e46dec6259fd8be2547b0345ee26f49 Mon Sep 17 00:00:00 2001 From: sergical Date: Tue, 23 Jun 2026 16:18:33 +0000 Subject: [PATCH 3/4] fix(get-sentry-resource): align dashboard docs with actual behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Holistic review of the dashboard path revealed three inconsistencies between implementation and documentation: 1. ResolvedResourceParams.dashboardId → renamed to dashboardIdOrTitle The field holds a numeric ID (from URL parsing) OR an exact title (from explicit resourceType+resourceId mode). The old name implied numeric-only, which was misleading. 2. resourceType.describe() said 'numeric dashboard ID' The explicit mode passes resourceId straight through to getDashboardDetails.handler({ dashboardIdOrTitle }), which resolves both numeric IDs and exact titles. The description now says 'dashboard ID or exact title', matching the sibling resourceId field. 3. Resource IDs section placeholder '' → '' Consistent with the corrected field name and describe() text. All four generated copies (toolDefinitions.json + skillDefinitions.json ×3) updated via generate-definitions. Added a title-path routing test to confirm get_sentry_resource correctly passes titles through to get_dashboard_details. Co-Authored-By: Claude Sonnet 4.5 (Anthropic) Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- packages/mcp-core/src/skillDefinitions.json | 6 +++--- packages/mcp-core/src/toolDefinitions.json | 4 ++-- .../src/tools/catalog/get-sentry-resource.test.ts | 14 +++++++++++++- .../src/tools/catalog/get-sentry-resource.ts | 12 ++++++------ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 83339ae67..b8fde02bc 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -119,7 +119,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { @@ -204,7 +204,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { @@ -310,7 +310,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 1a22c4026..f9012e3ba 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -1533,7 +1533,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: :\n- monitor: \n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "inputSchema": { "type": "object", "properties": { @@ -1557,7 +1557,7 @@ "snapshotImage", "dashboard" ], - "description": "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, `snapshotImage` with `:`, or `dashboard` with a numeric dashboard ID when inspect dashboard tools are available." + "description": "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, `snapshotImage` with `:`, or `dashboard` with a dashboard ID or exact title when inspect dashboard tools are available." }, "resourceId": { "type": "string", 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 b4dfdd0c0..74eacc138 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 @@ -745,7 +745,7 @@ describe("get_sentry_resource", () => { expect(result).toContain("[Open Dashboard]"); }); - it("dispatches explicit dashboard resourceType to get_dashboard_details", async () => { + it("dispatches explicit dashboard resourceType to get_dashboard_details by ID", async () => { const result = await callHandler({ resourceType: "dashboard", resourceId: "101", @@ -757,6 +757,18 @@ describe("get_sentry_resource", () => { expect(result).toContain("**ID**: 101"); }); + it("dispatches explicit dashboard resourceType to get_dashboard_details by title", async () => { + const result = await callHandler({ + resourceType: "dashboard", + resourceId: "Errors Overview", + organizationSlug: "sentry-mcp-evals", + }); + expect(result).toContain( + "# Dashboard Errors Overview in **sentry-mcp-evals**", + ); + expect(result).toContain("**ID**: 101"); + }); + it("rejects dashboard URL when get_dashboard_details is not available", async () => { await expect( getSentryResource.handler( diff --git a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts index 88931931b..3241f19ec 100644 --- a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts +++ b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts @@ -83,7 +83,7 @@ export interface ResolvedResourceParams { snapshotId?: string; selectedSnapshot?: string; // Dashboard params - dashboardId?: string; + dashboardIdOrTitle?: string; } export function resolveResourceParams(params: { @@ -212,7 +212,7 @@ export function resolveResourceParams(params: { return { type: "dashboard", organizationSlug, - dashboardId: resourceId, + dashboardIdOrTitle: resourceId, }; } } @@ -422,7 +422,7 @@ function resolveFromParsedUrl( return { type: "dashboard", organizationSlug, - dashboardId: parsed.dashboardId, + dashboardIdOrTitle: parsed.dashboardId, }; } } @@ -562,7 +562,7 @@ export default defineTool({ const resourceIds = [ "- span: :", ...(monitorResourcesAvailable ? ["- monitor: "] : []), - "- dashboard: ", + "- dashboard: ", "- snapshot: ", "- snapshotImage: :", ]; @@ -620,7 +620,7 @@ export default defineTool({ ]) .optional() .describe( - "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, `snapshotImage` with `:`, or `dashboard` with a numeric dashboard ID when inspect dashboard tools are available.", + "Resource type. With a URL, can override the auto-detected type for breadcrumbs on an issue/event URL or for `trace` on a span-focused trace URL. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, `snapshotImage` with `:`, or `dashboard` with a dashboard ID or exact title when inspect dashboard tools are available.", ), resourceId: z @@ -804,7 +804,7 @@ export default defineTool({ return getDashboardDetails.handler( { organizationSlug: resolved.organizationSlug, - dashboardIdOrTitle: resolved.dashboardId!, + dashboardIdOrTitle: resolved.dashboardIdOrTitle!, regionUrl: context.constraints.regionUrl ?? null, }, context, From 439e39d1b97c9d736850c2d0c01c24681abdd358 Mon Sep 17 00:00:00 2001 From: sergical Date: Tue, 23 Jun 2026 23:05:15 +0000 Subject: [PATCH 4/4] fix(get-sentry-resource): make dashboard description conditional like monitors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard and monitor resources both delegate to inspect-only catalog tools (get_dashboard_details and get_monitor_details respectively). The description should only advertise capabilities that are usable in the current session. The previous commit made dashboards unconditional to address a warden finding about inconsistent generated copies. The correct fix is to make dashboards consistently conditional — matching the existing monitor pattern — so that: - inspect session: description includes monitors + dashboards - non-inspect session: description omits both This satisfies both the warden concern (all copies are now intentionally and consistently different based on session capabilities) and the Cursor bugbot concern (agents in non-inspect sessions will not see dashboard hints that would fail with an inspect-skill error). URL parsing remains unconditional — /dashboard/{id}/ is always recognised. Non-inspect sessions still get a clear 'requires the inspect skill' error. Added three description-level tests to lock in this conditional behavior for both monitors and dashboards. Co-Authored-By: Claude Sonnet 4.5 (Anthropic) Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- packages/mcp-core/src/skillDefinitions.json | 4 +- .../tools/catalog/get-sentry-resource.test.ts | 39 +++++++++++++++++++ .../src/tools/catalog/get-sentry-resource.ts | 34 +++++++++++----- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index b8fde02bc..f6feeb29c 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -204,7 +204,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { @@ -310,7 +310,7 @@ }, { "name": "get_sentry_resource", - "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- dashboard: \n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID to fetch all spans for that conversation. To discover or list conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", "requiredScopes": ["event:read", "project:read"] }, { 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 74eacc138..363d90537 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 @@ -12,6 +12,7 @@ import { encode as encodePng } from "fast-png"; import { http, HttpResponse } from "msw"; import { afterAll, beforeEach, describe, expect, it } from "vitest"; import getSentryResource from "./get-sentry-resource.js"; +import { resolveDescription } from "../../tools/types.js"; const originalOpenAIApiKey = process.env.OPENAI_API_KEY; const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY; @@ -1514,5 +1515,43 @@ describe("get_sentry_resource", () => { "organizationSlug", ]); }); + + it("omits dashboard hints when get_dashboard_details is unavailable", () => { + const desc = resolveDescription(getSentryResource.description, { + experimentalMode: false, + availableToolNames: new Set([ + "get_sentry_resource", + "get_monitor_details", + ]), + }); + expect(desc).not.toContain("dashboard"); + expect(desc).toContain("monitor"); + }); + + it("includes dashboard hints when get_dashboard_details is available", () => { + const desc = resolveDescription(getSentryResource.description, { + experimentalMode: false, + availableToolNames: new Set([ + "get_sentry_resource", + "get_monitor_details", + "get_dashboard_details", + ]), + }); + expect(desc).toContain("dashboards"); + expect(desc).toContain("dashboard: "); + expect(desc).toContain("/dashboard/542438/"); + }); + + it("omits monitor hints when get_monitor_details is unavailable", () => { + const desc = resolveDescription(getSentryResource.description, { + experimentalMode: false, + availableToolNames: new Set([ + "get_sentry_resource", + "get_dashboard_details", + ]), + }); + expect(desc).not.toContain("monitor"); + expect(desc).toContain("dashboard"); + }); }); }); diff --git a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts index 3241f19ec..5524ddef8 100644 --- a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts +++ b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts @@ -536,6 +536,14 @@ export default defineTool({ "get_monitor_details", availableToolNames, ); + // Monitors and dashboards both delegate to inspect-only catalog tools. + // Only advertise them when their backing tools are available in this session. + // The URL parser still recognises /crons/ and /dashboard/{id}/ unconditionally; + // non-inspect sessions get a clear "requires the inspect skill" error at runtime. + const dashboardResourcesAvailable = isToolAvailable( + "get_dashboard_details", + availableToolNames, + ); const fullResolutionInstruction = formatToolCallInstruction({ toolName: "get_snapshot_image", arguments: { @@ -551,18 +559,20 @@ export default defineTool({ "Full-resolution snapshot image bytes are not available in this session", purpose: "for full-resolution image bytes", }); - // Monitors are only listed when available — they require the inspect skill - // and are gated at execution time via assertCatalogToolAvailable. - // Dashboards are always advertised because identifyResource() routes all - // /dashboard/{id}/ URLs unconditionally; execution is still gated at - // runtime by assertCatalogToolAvailable. - const supportedResources = monitorResourcesAvailable - ? "issues, events, traces, spans, AI conversations, breadcrumbs, replays, monitors, dashboards, preprod snapshots, and snapshot images." - : "issues, events, traces, spans, AI conversations, breadcrumbs, replays, dashboards, preprod snapshots, and snapshot images."; + const extraResources = [ + ...(monitorResourcesAvailable ? ["monitors"] : []), + ...(dashboardResourcesAvailable ? ["dashboards"] : []), + ]; + const supportedResources = + extraResources.length > 0 + ? `issues, events, traces, spans, AI conversations, breadcrumbs, replays, ${extraResources.join(", ")}, preprod snapshots, and snapshot images.` + : "issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images."; const resourceIds = [ "- span: :", ...(monitorResourcesAvailable ? ["- monitor: "] : []), - "- dashboard: ", + ...(dashboardResourcesAvailable + ? ["- dashboard: "] + : []), "- snapshot: ", "- snapshotImage: :", ]; @@ -588,7 +598,11 @@ export default defineTool({ "get_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')", "get_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')", "get_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')", - "get_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')", + ...(dashboardResourcesAvailable + ? [ + "get_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')", + ] + : []), "get_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')", "get_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')", "",