Skip to content

Commit 6494584

Browse files
hasnaintypesbetegoncodex
authored
feat(tools): expose the suspect commit in get_issue_details (#1339)
## Summary Fixes #1249 by calling the events/committers endpoint using the event ID already fetched and exposing the suspect commit in both structured and Markdown responses from `get_issue_details`. ### Key Changes - Added `getEventCommitters` to the API client, backed by `CommitterSchema` and `CommittersResponseSchema`. - Include the suspect commit's SHA, message, author, and source in structured output and the Markdown fallback, including explicit event lookups. - Keep commit lookup optional: unavailable commit data does not prevent issue details from loading, while unexpected server or response-validation failures are reported to Sentry. - Cover both output formats, event selection, absent commits, and expected and unexpected lookup failures with regression tests. ### Breaking Changes - None --------- Co-authored-by: betegon <miguelbetegongarcia@gmail.com> Co-authored-by: GPT-6 <noreply@openai.com>
1 parent c13c685 commit 6494584

10 files changed

Lines changed: 392 additions & 32 deletions

File tree

‎docs/contributing/tool-responses.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ When changing Sentry API endpoint usage, validate the upstream behavior in
115115
should model what Sentry returns, but tool responses should model what users
116116
need.
117117

118+
### Issue Details
119+
120+
`get_issue_details` includes the issue's suspect commit when available, with
121+
its SHA, message, author, and source. Structured responses expose
122+
`suspectCommit`; Markdown responses include a `Suspect Commit` section with
123+
the same data. Without a commit, the structured field is `null` and Markdown
124+
omits the section. Commit lookup is optional: failures do not prevent issue
125+
details from loading, but unexpected server or response-validation failures
126+
are reported to Sentry.
127+
118128
## Structured Content
119129

120130
MCP tools may expose `structuredContent` alongside generated text `content`.

‎packages/mcp-core/src/api-client/client.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
ClientKeyListSchema,
5353
ClientKeySchema,
5454
CommitListSchema,
55+
CommittersResponseSchema,
5556
DashboardListSchema,
5657
DashboardSchema,
5758
DeployListSchema,
@@ -120,6 +121,7 @@ import type {
120121
ClientKey,
121122
ClientKeyList,
122123
CommitList,
124+
CommitterList,
123125
Dashboard,
124126
DashboardListItem,
125127
DeployList,
@@ -2936,6 +2938,31 @@ export class SentryApiService {
29362938
return CommitListSchema.parse(body);
29372939
}
29382940

2941+
/**
2942+
* Retrieves the current suspect commit for the event's issue, grouped by committer.
2943+
* This reflects the issue's current suspect commit, not its state when the event occurred.
2944+
* Unlike release commits, this response actually populates `suspectCommitType`.
2945+
*/
2946+
async getEventCommitters(
2947+
{
2948+
organizationSlug,
2949+
projectSlug,
2950+
eventId,
2951+
}: {
2952+
organizationSlug: string;
2953+
projectSlug: string;
2954+
eventId: string;
2955+
},
2956+
opts?: RequestOptions,
2957+
): Promise<CommitterList> {
2958+
const body = await this.requestJSON(
2959+
apiPath`/projects/${organizationSlug}/${projectSlug}/events/${eventId}/committers/`,
2960+
undefined,
2961+
opts,
2962+
);
2963+
return CommittersResponseSchema.parse(body).committers;
2964+
}
2965+
29392966
async listMonitors(
29402967
{
29412968
organizationSlug,

‎packages/mcp-core/src/api-client/schema.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@ export const CommitSchema = z
794794
message: z.string().nullable().optional(),
795795
dateCreated: z.string().datetime().nullable().optional(),
796796
pullRequest: z.record(z.string(), z.unknown()).nullable().optional(),
797+
// The event committers endpoint populates this; release commits usually return an empty string.
797798
suspectCommitType: z.string().optional(),
798799
author: ApiActorSchema.nullable().optional(),
799800
repository: z
@@ -808,6 +809,17 @@ export const CommitSchema = z
808809

809810
export const CommitListSchema = z.array(CommitSchema);
810811

812+
export const CommitterSchema = z
813+
.object({
814+
author: ApiActorSchema.nullable().optional(),
815+
commits: CommitListSchema,
816+
})
817+
.passthrough();
818+
819+
export const CommittersResponseSchema = z.object({
820+
committers: z.array(CommitterSchema),
821+
});
822+
811823
export const IssueActivitySchema = z
812824
.object({
813825
id: ApiResourceIdSchema,

‎packages/mcp-core/src/api-client/types.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import type {
5858
ClientKeySchema,
5959
CommitListSchema,
6060
CommitSchema,
61+
CommittersResponseSchema,
6162
DashboardListItemSchema,
6263
DashboardSchema,
6364
DashboardWidgetSchema,
@@ -248,6 +249,9 @@ export type MetricAlertRuleList = z.infer<typeof MetricAlertRuleListSchema>;
248249
export type ReleaseList = z.infer<typeof ReleaseListSchema>;
249250
export type DeployList = z.infer<typeof DeployListSchema>;
250251
export type CommitList = z.infer<typeof CommitListSchema>;
252+
export type CommitterList = z.infer<
253+
typeof CommittersResponseSchema
254+
>["committers"];
251255
export type IssueList = z.infer<typeof IssueListSchema>;
252256
export type IssueActivityList = z.infer<
253257
typeof IssueActivityListResponseSchema

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
import { ThreadsEntrySchema } from "../api-client";
2222
import type {
2323
AutofixRunState,
24+
CommitterList,
2425
Event,
2526
ExternalIssueList,
2627
GenericEvent,
@@ -1984,6 +1985,22 @@ function formatSeerSummary(autofixState: AutofixRunState | undefined): string {
19841985
return `${parts.join("\n")}\n\n`;
19851986
}
19861987

1988+
/** Projects the suspect commit consistently for structured and markdown issue details. */
1989+
export function getSuspectCommit(committers: CommitterList | undefined) {
1990+
// The endpoint currently returns the issue's latest suspect commit, grouped by author.
1991+
const committer = committers?.[0];
1992+
const commit = committer?.commits[0];
1993+
if (!commit) {
1994+
return null;
1995+
}
1996+
return {
1997+
id: String(commit.id),
1998+
message: commit.message,
1999+
author: committer.author?.name ?? committer.author?.email,
2000+
suspectCommitType: commit.suspectCommitType,
2001+
};
2002+
}
2003+
19872004
/**
19882005
* Formats a Sentry issue with its latest event into comprehensive markdown output.
19892006
* Includes issue metadata, event details, and usage instructions.
@@ -2002,6 +2019,7 @@ export function formatIssueOutput({
20022019
relatedReplayIds,
20032020
aiConversations,
20042021
codeLocation,
2022+
committers,
20052023
experimentalMode,
20062024
availableToolNames,
20072025
directToolNames,
@@ -2016,6 +2034,7 @@ export function formatIssueOutput({
20162034
relatedReplayIds?: string[];
20172035
aiConversations?: AIConversationReference[];
20182036
codeLocation?: CodeLocation;
2037+
committers?: CommitterList;
20192038
experimentalMode?: boolean;
20202039
availableToolNames?: ReadonlySet<string>;
20212040
directToolNames?: ReadonlySet<string>;
@@ -2091,6 +2110,22 @@ export function formatIssueOutput({
20912110
output += formatCodeLocation(codeLocation);
20922111
}
20932112

2113+
const suspectCommit = getSuspectCommit(committers);
2114+
if (suspectCommit) {
2115+
output += "## Suspect Commit\n\n";
2116+
output += `**SHA**: \`${suspectCommit.id}\`\n`;
2117+
if (suspectCommit.message) {
2118+
output += `**Message**: ${suspectCommit.message}\n`;
2119+
}
2120+
if (suspectCommit.author) {
2121+
output += `**Author**: ${suspectCommit.author}\n`;
2122+
}
2123+
if (suspectCommit.suspectCommitType) {
2124+
output += `**Source**: ${suspectCommit.suspectCommitType}\n`;
2125+
}
2126+
output += "\n";
2127+
}
2128+
20942129
output += "## Event Details\n\n";
20952130

20962131
// Check if this is an unsupported event type

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
},
100100
{
101101
"name": "get_issue_details",
102-
"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>",
102+
"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- Want the suspect commit's SHA, message, author, and source when available\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>",
103103
"requiredScopes": ["event:read"]
104104
},
105105
{
@@ -249,7 +249,7 @@
249249
},
250250
{
251251
"name": "get_issue_details",
252-
"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>",
252+
"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- Want the suspect commit's SHA, message, author, and source when available\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>",
253253
"requiredScopes": ["event:read"]
254254
},
255255
{
@@ -375,7 +375,7 @@
375375
},
376376
{
377377
"name": "get_issue_details",
378-
"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>",
378+
"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- Want the suspect commit's SHA, message, author, and source when available\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>",
379379
"requiredScopes": ["event:read"]
380380
},
381381
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4908,7 +4908,7 @@
49084908
},
49094909
{
49104910
"name": "get_issue_details",
4911-
"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>",
4911+
"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- Want the suspect commit's SHA, message, author, and source when available\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>",
49124912
"inputSchema": {
49134913
"type": "object",
49144914
"properties": {

0 commit comments

Comments
 (0)