Skip to content

Commit 110b504

Browse files
sentry-junior[bot]codexdcramer
authored
feat(tools): Add issue breadcrumbs catalog tool (#1170)
Add `get_issue_breadcrumbs` as the dedicated catalog-only path for retrieving breadcrumbs from an issue’s latest event. This removes the breadcrumbs resource-type override from `get_sentry_resource`, so callers discover and execute the focused tool through the catalog instead. **Catalog-only exposure** The new tool supports issue IDs and issue URLs, preserves project constraints and enhanced not-found handling, and is intentionally omitted from the direct MCP surface. **Reference migration** Issue detail guidance, generated definitions, resolver coverage, tool tests, and eval expectations now use `get_issue_breadcrumbs` rather than the former `get_sentry_resource(..., resourceType='breadcrumbs')` workaround. <!-- junior-request-attribution:start --> Requested by **David Cramer** via Junior. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1784683235.733369) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: David Cramer <david@sentry.io>
1 parent 3a446cd commit 110b504

12 files changed

Lines changed: 272 additions & 342 deletions

‎packages/mcp-core/src/internal/formatting.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ describe("formatIssueOutput", () => {
182182
experimentalMode: true,
183183
availableToolNames: new Set([
184184
"get_sentry_resource",
185+
"get_issue_breadcrumbs",
185186
"search_issue_events",
186187
]),
187188
directToolNames: new Set(["get_sentry_resource", "search_issue_events"]),

‎packages/mcp-core/src/internal/formatting.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2262,7 +2262,18 @@ export function formatIssueOutput({
22622262
output += `- Related log search: ${logSearchInstruction}\n`;
22632263
}
22642264
if (experimentalMode) {
2265-
output += `- Breadcrumb trail leading up to this error: \`get_sentry_resource(url='${apiService.getIssueUrl(organizationSlug, issue.shortId)}', resourceType='breadcrumbs')\`\n`;
2265+
const breadcrumbsInstruction = formatToolCallInstruction({
2266+
toolName: "get_issue_breadcrumbs",
2267+
arguments: {
2268+
issueUrl: apiService.getIssueUrl(organizationSlug, issue.shortId),
2269+
},
2270+
experimentalMode,
2271+
availableToolNames,
2272+
directToolNames,
2273+
fallbackInstruction:
2274+
"Issue breadcrumbs are not available in this session",
2275+
});
2276+
output += `- Breadcrumb trail leading up to this error: ${breadcrumbsInstruction}\n`;
22662277
}
22672278
return output;
22682279
}

‎packages/mcp-core/src/server.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ describe("buildServer", () => {
649649
expect(toolNames).not.toContain("whoami");
650650
expect(toolNames).toContain("get_sentry_resource");
651651
expect(toolNames).not.toContain("get_issue_details");
652+
expect(toolNames).not.toContain("get_issue_breadcrumbs");
652653
expect(toolNames).not.toContain("get_trace_details");
653654
expect(toolNames).not.toContain("get_snapshot");
654655
expect(toolNames).not.toContain("get_snapshot_image");

‎packages/mcp-core/src/skillDefinitions.json‎

Lines changed: 15 additions & 5 deletions
Large diffs are not rendered by default.

‎packages/mcp-core/src/toolDefinitions.json‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,43 @@
14371437
"skills": ["inspect", "triage"],
14381438
"surface": "catalog"
14391439
},
1440+
{
1441+
"name": "get_issue_breadcrumbs",
1442+
"description": "Get the breadcrumb trail from the latest event for a Sentry issue.\n\nUse this tool when you need to:\n- See the user and application actions leading up to an error\n- Inspect navigation, console, HTTP, and other breadcrumb events\n- Reconstruct the immediate context before an issue occurred\n\n<examples>\nget_issue_breadcrumbs(organizationSlug='my-org', issueId='PROJECT-123')\nget_issue_breadcrumbs(issueUrl='https://my-org.sentry.io/issues/PROJECT-123/')\n</examples>",
1443+
"inputSchema": {
1444+
"type": "object",
1445+
"properties": {
1446+
"organizationSlug": {
1447+
"type": "string",
1448+
"description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
1449+
},
1450+
"regionUrl": {
1451+
"default": null,
1452+
"anyOf": [
1453+
{
1454+
"type": "string",
1455+
"description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
1456+
},
1457+
{
1458+
"type": "null"
1459+
}
1460+
]
1461+
},
1462+
"issueId": {
1463+
"type": "string",
1464+
"description": "The Issue ID. e.g. `PROJECT-1Z43`"
1465+
},
1466+
"issueUrl": {
1467+
"type": "string",
1468+
"format": "uri",
1469+
"description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43"
1470+
}
1471+
}
1472+
},
1473+
"requiredScopes": ["event:read"],
1474+
"skills": ["inspect", "triage"],
1475+
"surface": "catalog"
1476+
},
14401477
{
14411478
"name": "get_issue_details",
14421479
"description": "Get detailed information about a specific Sentry issue by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific issue ID (e.g., 'CLOUDFLARE-MCP-41', 'PROJECT-123')\n- Ask to 'explain [ISSUE-ID]', 'tell me about [ISSUE-ID]'\n- Want details/stacktrace/analysis for a known issue\n- Provide a Sentry issue URL\n\nDO NOT USE for:\n- General searching or listing issues (use search_issues)\n\nTRIGGER PATTERNS:\n- 'Explain ISSUE-123' → use get_issue_details\n- 'Tell me about PROJECT-456' → use get_issue_details\n- 'What happened in [issue URL]' → use get_issue_details\n\n<examples>\n### With Sentry URL (recommended - simplest approach)\n```\nget_issue_details(issueUrl='https://sentry.sentry.io/issues/6916805731/?project=4509062593708032&query=is%3Aunresolved')\n```\n\n### With issue ID and organization\n```\nget_issue_details(organizationSlug='my-organization', issueId='CLOUDFLARE-MCP-41')\n```\n\n### With event ID and organization\n```\nget_issue_details(organizationSlug='my-organization', eventId='c49541c747cb4d8aa3efb70ca5aba243')\n```\n</examples>\n\n<hints>\n- **IMPORTANT**: If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- When using issueUrl, all other parameters are automatically extracted - don't provide them separately\n- If using issueId (not URL), then organizationSlug is required\n</hints>",
@@ -2001,7 +2038,7 @@
20012038
},
20022039
{
20032040
"name": "get_sentry_resource",
2004-
"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>",
2041+
"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, 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>",
20052042
"inputSchema": {
20062043
"type": "object",
20072044
"properties": {
@@ -2011,15 +2048,14 @@
20112048
"format": "uri"
20122049
},
20132050
"resourceType": {
2014-
"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>`.",
2051+
"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 `snapshotImage` with `<snapshotId>:<image_file_name>`.",
20152052
"type": "string",
20162053
"enum": [
20172054
"issue",
20182055
"event",
20192056
"trace",
20202057
"span",
20212058
"ai_conversation",
2022-
"breadcrumbs",
20232059
"replay",
20242060
"monitor",
20252061
"snapshot",
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { eventFixture, mswServer } from "@sentry/mcp-server-mocks";
2+
import { http, HttpResponse } from "msw";
3+
import { afterEach, describe, expect, it } from "vitest";
4+
import getIssueBreadcrumbs from "./get-issue-breadcrumbs.js";
5+
6+
const context = {
7+
constraints: { organizationSlug: undefined },
8+
accessToken: "access-token",
9+
userId: "1",
10+
};
11+
12+
afterEach(() => {
13+
mswServer.resetHandlers();
14+
});
15+
16+
describe("get_issue_breadcrumbs", () => {
17+
it("returns breadcrumbs from the latest issue event", async () => {
18+
const result = await getIssueBreadcrumbs.handler(
19+
{
20+
organizationSlug: "sentry-mcp-evals",
21+
regionUrl: null,
22+
issueId: "CLOUDFLARE-MCP-41",
23+
},
24+
context,
25+
);
26+
27+
expect(result).toMatchInlineSnapshot(`
28+
"# Breadcrumbs for CLOUDFLARE-MCP-41
29+
30+
**Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
31+
**Total Breadcrumbs**: 4
32+
33+
\`\`\`
34+
2025-04-08T21:14:50.000Z info [fetch] GET /api/0/organizations/ [200] {"method":"GET","url":"/api/0/organizations/","status_code":200}
35+
2025-04-08T21:14:52.000Z warning [console] Deprecation warning: use v2 endpoint
36+
2025-04-08T21:14:55.000Z info [navigation] {"from":"/dashboard","to":"/settings"}
37+
2025-04-08T21:15:04.000Z error [console] Tool list_organizations is already registered
38+
\`\`\`
39+
40+
Breadcrumbs show the trail of events leading up to the error, in chronological order.
41+
Use \`get_sentry_resource(resourceType='issue', organizationSlug='...', resourceId='CLOUDFLARE-MCP-41')\` for full issue details."
42+
`);
43+
});
44+
45+
it("accepts an issue URL", async () => {
46+
const result = await getIssueBreadcrumbs.handler(
47+
{
48+
issueUrl:
49+
"https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41/",
50+
regionUrl: null,
51+
},
52+
context,
53+
);
54+
55+
expect(result).toContain("# Breadcrumbs for CLOUDFLARE-MCP-41");
56+
});
57+
58+
it("rejects issues outside the active project constraint", async () => {
59+
await expect(
60+
getIssueBreadcrumbs.handler(
61+
{
62+
organizationSlug: "sentry-mcp-evals",
63+
issueId: "CLOUDFLARE-MCP-41",
64+
regionUrl: null,
65+
},
66+
{
67+
...context,
68+
constraints: {
69+
organizationSlug: "sentry-mcp-evals",
70+
projectSlug: "frontend",
71+
},
72+
},
73+
),
74+
).rejects.toThrow(
75+
'Issue is outside the active project constraint. Expected project "frontend".',
76+
);
77+
});
78+
79+
it("handles an event with no breadcrumbs", async () => {
80+
mswServer.use(
81+
http.get(
82+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/",
83+
() =>
84+
HttpResponse.json({
85+
...eventFixture,
86+
entries: eventFixture.entries.filter(
87+
(entry: { type: string }) => entry.type !== "breadcrumbs",
88+
),
89+
}),
90+
{ once: true },
91+
),
92+
);
93+
94+
const result = await getIssueBreadcrumbs.handler(
95+
{
96+
organizationSlug: "sentry-mcp-evals",
97+
regionUrl: null,
98+
issueId: "CLOUDFLARE-MCP-41",
99+
},
100+
context,
101+
);
102+
103+
expect(result).toContain(
104+
"No breadcrumbs found in the latest event for this issue.",
105+
);
106+
});
107+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { setTag } from "@sentry/core";
2+
import { ApiNotFoundError } from "../../api-client";
3+
import { apiServiceFromContext } from "../../internal/tool-helpers/api";
4+
import { fetchAndFormatBreadcrumbs } from "../../internal/tool-helpers/breadcrumbs";
5+
import { defineTool } from "../../internal/tool-helpers/define";
6+
import { enhanceNotFoundError } from "../../internal/tool-helpers/enhance-error";
7+
import {
8+
ensureIssueWithinProjectConstraint,
9+
parseIssueParams,
10+
} from "../../internal/tool-helpers/issue";
11+
import {
12+
ParamIssueShortId,
13+
ParamIssueUrl,
14+
ParamOrganizationSlug,
15+
ParamRegionUrl,
16+
} from "../../schema";
17+
import type { ServerContext } from "../../types";
18+
19+
export default defineTool({
20+
name: "get_issue_breadcrumbs",
21+
skills: ["inspect", "triage"],
22+
requiredScopes: ["event:read"],
23+
description: [
24+
"Get the breadcrumb trail from the latest event for a Sentry issue.",
25+
"",
26+
"Use this tool when you need to:",
27+
"- See the user and application actions leading up to an error",
28+
"- Inspect navigation, console, HTTP, and other breadcrumb events",
29+
"- Reconstruct the immediate context before an issue occurred",
30+
"",
31+
"<examples>",
32+
"get_issue_breadcrumbs(organizationSlug='my-org', issueId='PROJECT-123')",
33+
"get_issue_breadcrumbs(issueUrl='https://my-org.sentry.io/issues/PROJECT-123/')",
34+
"</examples>",
35+
].join("\n"),
36+
inputSchema: {
37+
organizationSlug: ParamOrganizationSlug.optional(),
38+
regionUrl: ParamRegionUrl.nullable().default(null),
39+
issueId: ParamIssueShortId.optional(),
40+
issueUrl: ParamIssueUrl.optional(),
41+
},
42+
annotations: {
43+
readOnlyHint: true,
44+
openWorldHint: true,
45+
},
46+
async handler(params, context: ServerContext) {
47+
const parsed = parseIssueParams({
48+
issueUrl: params.issueUrl,
49+
issueId: params.issueId,
50+
organizationSlug:
51+
params.organizationSlug ?? context.constraints.organizationSlug,
52+
});
53+
const apiService = apiServiceFromContext(context, {
54+
regionUrl: params.regionUrl ?? context.constraints.regionUrl ?? undefined,
55+
});
56+
57+
setTag("organization.slug", parsed.organizationSlug);
58+
setTag("issue.id", parsed.issueId);
59+
60+
try {
61+
await ensureIssueWithinProjectConstraint({
62+
apiService,
63+
organizationSlug: parsed.organizationSlug,
64+
issueId: parsed.issueId,
65+
projectSlug: context.constraints.projectSlug,
66+
});
67+
return await fetchAndFormatBreadcrumbs(
68+
apiService,
69+
parsed.organizationSlug,
70+
parsed.issueId,
71+
);
72+
} catch (error) {
73+
if (error instanceof ApiNotFoundError) {
74+
throw enhanceNotFoundError(error, {
75+
organizationSlug: parsed.organizationSlug,
76+
issueId: parsed.issueId,
77+
});
78+
}
79+
throw error;
80+
}
81+
},
82+
});

0 commit comments

Comments
 (0)