Skip to content
Merged
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
106 changes: 70 additions & 36 deletions packages/mcp-core/src/internal/url-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
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 @@ -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=<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- monitor: <monitorSlug>\n- snapshot: <snapshotId>\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='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, 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=<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- monitor: <monitorSlug>\n- dashboard: <dashboardId or title>\n- snapshot: <snapshotId>\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='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 @@ -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=<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- monitor: <monitorSlug>\n- snapshot: <snapshotId>\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='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, 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=<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- monitor: <monitorSlug>\n- dashboard: <dashboardId or title>\n- snapshot: <snapshotId>\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='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 @@ -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",
Expand All @@ -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": {
Expand Down
Loading
Loading