diff --git a/packages/mcp-core/src/internal/url-helpers.test.ts b/packages/mcp-core/src/internal/url-helpers.test.ts index 5d2ac3057..72c655652 100644 --- a/packages/mcp-core/src/internal/url-helpers.test.ts +++ b/packages/mcp-core/src/internal/url-helpers.test.ts @@ -4,9 +4,8 @@ import { parseSentryUrl, isProfileUrl } from "./url-helpers"; describe("parseSentryUrl", () => { describe("issue URLs", () => { it("parses subdomain-based issue URL", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/issues/PROJECT-123"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/issues/PROJECT-123")) + .toMatchInlineSnapshot(` { "issueId": "PROJECT-123", "organizationSlug": "my-org", @@ -16,9 +15,8 @@ describe("parseSentryUrl", () => { }); it("parses path-based organization issue URL", () => { - expect( - parseSentryUrl("https://sentry.io/my-org/issues/123"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://sentry.io/my-org/issues/123")) + .toMatchInlineSnapshot(` { "issueId": "123", "organizationSlug": "my-org", @@ -40,9 +38,8 @@ describe("parseSentryUrl", () => { }); it("parses self-hosted Sentry issue URL", () => { - expect( - parseSentryUrl("https://sentry.mycompany.com/issues/789"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://sentry.mycompany.com/issues/789")) + .toMatchInlineSnapshot(` { "issueId": "789", "organizationSlug": "sentry", @@ -52,9 +49,8 @@ describe("parseSentryUrl", () => { }); it("parses numeric issue ID", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/issues/12345"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/issues/12345")) + .toMatchInlineSnapshot(` { "issueId": "12345", "organizationSlug": "my-org", @@ -337,9 +333,8 @@ describe("parseSentryUrl", () => { }); it("parses replay URL without trailing slash", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/replays/abc123"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/replays/abc123")) + .toMatchInlineSnapshot(` { "organizationSlug": "my-org", "replayId": "abc123", @@ -358,9 +353,8 @@ describe("parseSentryUrl", () => { describe("monitor/cron URLs", () => { it("parses crons URL with monitor slug", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/crons/my-cron-job/"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/crons/my-cron-job/")) + .toMatchInlineSnapshot(` { "monitorSlug": "my-cron-job", "organizationSlug": "my-org", @@ -416,9 +410,8 @@ describe("parseSentryUrl", () => { describe("release URLs", () => { it("parses release URL with version", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/releases/v1.2.3/"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/releases/v1.2.3/")) + .toMatchInlineSnapshot(` { "organizationSlug": "my-org", "releaseVersion": "v1.2.3", @@ -523,9 +516,8 @@ describe("parseSentryUrl", () => { }); it("parses snapshot URL without trailing slash", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/preprod/snapshots/99999"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/preprod/snapshots/99999")) + .toMatchInlineSnapshot(` { "organizationSlug": "my-org", "selectedSnapshot": undefined, @@ -543,6 +535,52 @@ 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( @@ -569,9 +607,8 @@ describe("parseSentryUrl", () => { describe("unknown URLs", () => { it("returns unknown for unrecognized path", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/settings/account"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/settings/account")) + .toMatchInlineSnapshot(` { "organizationSlug": "my-org", "type": "unknown", @@ -580,9 +617,8 @@ describe("parseSentryUrl", () => { }); it("returns unknown for dashboard URL", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/dashboards/overview"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/dashboards/overview")) + .toMatchInlineSnapshot(` { "organizationSlug": "my-org", "type": "unknown", @@ -591,9 +627,8 @@ describe("parseSentryUrl", () => { }); it("returns unknown for discover URL", () => { - expect( - parseSentryUrl("https://my-org.sentry.io/discover/results/"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://my-org.sentry.io/discover/results/")) + .toMatchInlineSnapshot(` { "organizationSlug": "my-org", "type": "unknown", @@ -604,9 +639,8 @@ describe("parseSentryUrl", () => { describe("region URL handling", () => { it("handles us.sentry.io region URL with org in path", () => { - expect( - parseSentryUrl("https://us.sentry.io/my-org/issues/123"), - ).toMatchInlineSnapshot(` + expect(parseSentryUrl("https://us.sentry.io/my-org/issues/123")) + .toMatchInlineSnapshot(` { "issueId": "123", "organizationSlug": "my-org", 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 ad3b73447..b6c491fc1 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -159,7 +159,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, agent conversations, replays, monitors, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAgent Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_agent_conversations. 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- monitor: \n- snapshot: \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='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, agent conversations, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAgent Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_agent_conversations. 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- monitor: \n- dashboard: \n- snapshot: \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='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 e686c09ba..7b621db13 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -6168,7 +6168,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, agent conversations, replays, monitors, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAgent Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_agent_conversations. 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- monitor: \n- snapshot: \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='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, agent conversations, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAgent Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_agent_conversations. 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- monitor: \n- dashboard: \n- snapshot: \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='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": { @@ -6178,7 +6178,7 @@ "format": "uri" }, "resourceType": { - "description": "Resource type. With a URL, can override a span-focused trace URL with `trace`. Use `monitor` with a monitor slug only when inspect monitor tools are available or `snapshot` with a snapshot artifact ID.", + "description": "Resource type. With a URL, can override a span-focused trace URL with `trace`. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, or `dashboard` with a dashboard ID or exact title when inspect dashboard tools are available.", "type": "string", "enum": [ "issue", @@ -6187,11 +6187,12 @@ "ai_conversation", "replay", "monitor", - "snapshot" + "snapshot", + "dashboard" ] }, "resourceId": { - "description": "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, agent conversation ID, replay ID, monitor slug when inspect monitor tools are available, or snapshot artifact ID. Required when not using a URL.", + "description": "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, agent conversation ID, replay ID, monitor slug when inspect monitor tools are available, dashboard ID or title when inspect dashboard tools are available, or snapshot artifact ID. Required when not using a URL.", "type": "string" }, "organizationSlug": { 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 2225340ab..771ff207a 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 @@ -10,6 +10,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; @@ -46,7 +47,8 @@ function callHandler(params: { | "trace" | "replay" | "monitor" - | "snapshot"; + | "snapshot" + | "dashboard"; resourceId?: string; organizationSlug?: string; }) { @@ -590,6 +592,56 @@ 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 by ID", 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("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( + { 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 () => { @@ -1006,5 +1058,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 d7fdd2008..287430cd2 100644 --- a/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts +++ b/packages/mcp-core/src/tools/catalog/get-sentry-resource.ts @@ -18,6 +18,7 @@ import { setTargetTagsAndAttributes } from "../../telem/scope"; import type { ServerContext } from "../../types"; import { isNumericId } from "../../utils/slug-validation"; import getAIConversationDetails from "./get-agent-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"; @@ -36,6 +37,7 @@ export const FULLY_SUPPORTED_TYPES = [ "replay", "monitor", "snapshot", + "dashboard", ] as const; export type FullySupportedType = (typeof FULLY_SUPPORTED_TYPES)[number]; @@ -75,6 +77,8 @@ export interface ResolvedResourceParams { // Snapshot params snapshotId?: string; selectedSnapshot?: string; + // Dashboard params + dashboardIdOrTitle?: string; } export function resolveResourceParams(params: { @@ -170,6 +174,13 @@ export function resolveResourceParams(params: { organizationSlug, snapshotId: resourceId, }; + + case "dashboard": + return { + type: "dashboard", + organizationSlug, + dashboardIdOrTitle: resourceId, + }; } } @@ -201,7 +212,7 @@ function resolveFromParsedUrl( } throw new UserInputError( "Could not determine resource type from URL. " + - "Supported URL patterns: issues, events, traces, agent conversations, profiles, replays, monitors, and releases.", + "Supported URL patterns: issues, events, traces, agent conversations, profiles, replays, monitors, dashboards, and releases.", ); } @@ -352,6 +363,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, + dashboardIdOrTitle: parsed.dashboardId, + }; } } @@ -428,6 +449,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: { @@ -443,11 +472,19 @@ 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, agent conversations, replays, monitors, preprod snapshots, and snapshot images." - : "issues, events, traces, spans, agent conversations, replays, preprod snapshots, and snapshot images."; + const extraResources = [ + ...(monitorResourcesAvailable ? ["monitors"] : []), + ...(dashboardResourcesAvailable ? ["dashboards"] : []), + ]; + const supportedResources = + extraResources.length > 0 + ? `issues, events, traces, spans, agent conversations, replays, ${extraResources.join(", ")}, preprod snapshots, and snapshot images.` + : "issues, events, traces, spans, agent conversations, replays, preprod snapshots, and snapshot images."; const resourceIds = [ ...(monitorResourcesAvailable ? ["- monitor: "] : []), + ...(dashboardResourcesAvailable + ? ["- dashboard: "] + : []), "- snapshot: ", ]; @@ -471,6 +508,11 @@ export default defineTool({ "get_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')", "get_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')", "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')", "", @@ -495,10 +537,11 @@ export default defineTool({ "replay", "monitor", "snapshot", + "dashboard", ]) .optional() .describe( - "Resource type. With a URL, can override a span-focused trace URL with `trace`. Use `monitor` with a monitor slug only when inspect monitor tools are available or `snapshot` with a snapshot artifact ID.", + "Resource type. With a URL, can override a span-focused trace URL with `trace`. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, or `dashboard` with a dashboard ID or exact title when inspect dashboard tools are available.", ), resourceId: z @@ -506,7 +549,7 @@ export default defineTool({ .trim() .optional() .describe( - "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, agent conversation ID, replay ID, monitor slug when inspect monitor tools are available, or snapshot artifact ID. Required when not using a URL.", + "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, agent conversation ID, replay ID, monitor slug when inspect monitor tools are available, dashboard ID or title when inspect dashboard tools are available, or snapshot artifact ID. Required when not using a URL.", ), organizationSlug: ParamOrganizationSlug.optional(), @@ -654,6 +697,21 @@ export default defineTool({ context, ); + case "dashboard": + assertCatalogToolAvailable( + context, + "get_dashboard_details", + "Dashboard", + ); + return getDashboardDetails.handler( + { + organizationSlug: resolved.organizationSlug, + dashboardIdOrTitle: resolved.dashboardIdOrTitle!, + regionUrl: context.constraints.regionUrl ?? null, + }, + context, + ); + case "snapshot": if (resolved.selectedSnapshot) { return getSnapshotImage.handler( 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 a7a258e3e..efe0e08ef 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 @@ -49,6 +49,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/", };