Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/mcp-core/src/internal/url-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 20 additions & 1 deletion packages/mcp-core/src/internal/url-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/

Expand All @@ -21,6 +21,7 @@ export type SentryResourceType =
| "monitor"
| "release"
| "snapshot"
| "dashboard"
| "unknown";

/**
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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`)
Expand Down Expand Up @@ -164,6 +168,7 @@ function extractOrganizationSlug(parsedUrl: URL, pathParts: string[]): string {
"monitors",
"alerts",
"feedback",
"dashboard",
"dashboards",
"discover",
"insights",
Expand Down Expand Up @@ -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") {
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,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, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_ai_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=<image_file_name>: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: <traceId>:<spanId>\n- monitor: <monitorSlug>\n- snapshot: <snapshotId>\n- snapshotImage: <snapshotId>:<image_file_name>\n\n<examples>\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='<traceId>:<spanId>')\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</examples>",
"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, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_ai_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=<image_file_name>: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: <traceId>:<spanId>\n- monitor: <monitorSlug>\n- dashboard: <dashboardId or title>\n- snapshot: <snapshotId>\n- snapshotImage: <snapshotId>:<image_file_name>\n\n<examples>\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='<traceId>:<spanId>')\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</examples>",
"requiredScopes": ["event:read", "project:read"]
},
{
Expand Down
9 changes: 5 additions & 4 deletions packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2015,7 +2015,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, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_ai_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=<image_file_name>: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: <traceId>:<spanId>\n- monitor: <monitorSlug>\n- snapshot: <snapshotId>\n- snapshotImage: <snapshotId>:<image_file_name>\n\n<examples>\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='<traceId>:<spanId>')\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</examples>",
"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, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_ai_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=<image_file_name>: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- span: <traceId>:<spanId>\n- monitor: <monitorSlug>\n- dashboard: <dashboardId or title>\n- snapshot: <snapshotId>\n- snapshotImage: <snapshotId>:<image_file_name>\n\n<examples>\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='<traceId>:<spanId>')\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</examples>",
"inputSchema": {
"type": "object",
"properties": {
Expand All @@ -2036,13 +2036,14 @@
"replay",
"monitor",
"snapshot",
"snapshotImage"
"snapshotImage",
"dashboard"
Comment thread
sentry-warden[bot] marked this conversation as resolved.
],
"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 `<snapshotId>:<image_file_name>`."
"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 `<snapshotId>:<image_file_name>`, or `dashboard` with a dashboard ID or exact title 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, `<snapshotId>:<image_file_name>` 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, `<snapshotId>:<image_file_name>` for snapshot image resources, or `traceId:spanId` for span resources. Required when not using a URL."
},
"organizationSlug": {
"type": "string",
Expand Down
92 changes: 91 additions & 1 deletion packages/mcp-core/src/tools/catalog/get-sentry-resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,7 +52,8 @@ function callHandler(params: {
| "replay"
| "monitor"
| "snapshot"
| "snapshotImage";
| "snapshotImage"
| "dashboard";
resourceId?: string;
organizationSlug?: string;
}) {
Expand Down Expand Up @@ -739,6 +741,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 () => {
Expand Down Expand Up @@ -1471,5 +1523,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: <dashboardId or title>");
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");
});
});
});
Loading
Loading