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
10 changes: 10 additions & 0 deletions docs/contributing/tool-responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ When changing Sentry API endpoint usage, validate the upstream behavior in
should model what Sentry returns, but tool responses should model what users
need.

### Issue Details

`get_issue_details` includes the issue's suspect commit when available, with
its SHA, message, author, and source. Structured responses expose
`suspectCommit`; Markdown responses include a `Suspect Commit` section with
the same data. Without a commit, the structured field is `null` and Markdown
omits the section. Commit lookup is optional: failures do not prevent issue
details from loading, but unexpected server or response-validation failures
are reported to Sentry.

## Structured Content

MCP tools may expose `structuredContent` alongside generated text `content`.
Expand Down
27 changes: 27 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
ClientKeyListSchema,
ClientKeySchema,
CommitListSchema,
CommittersResponseSchema,
DashboardListSchema,
DashboardSchema,
DeployListSchema,
Expand Down Expand Up @@ -118,6 +119,7 @@ import type {
ClientKey,
ClientKeyList,
CommitList,
CommitterList,
Dashboard,
DashboardListItem,
DeployList,
Expand Down Expand Up @@ -2792,6 +2794,31 @@ export class SentryApiService {
return CommitListSchema.parse(body);
}

/**
* Retrieves the current suspect commit for the event's issue, grouped by committer.
* This reflects the issue's current suspect commit, not its state when the event occurred.
* Unlike release commits, this response actually populates `suspectCommitType`.
*/
async getEventCommitters(
{
organizationSlug,
projectSlug,
eventId,
}: {
organizationSlug: string;
projectSlug: string;
eventId: string;
},
opts?: RequestOptions,
): Promise<CommitterList> {
const body = await this.requestJSON(
apiPath`/projects/${organizationSlug}/${projectSlug}/events/${eventId}/committers/`,
undefined,
opts,
);
return CommittersResponseSchema.parse(body).committers;
}

async listMonitors(
{
organizationSlug,
Expand Down
12 changes: 12 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,7 @@ export const CommitSchema = z
message: z.string().nullable().optional(),
dateCreated: z.string().datetime().nullable().optional(),
pullRequest: z.record(z.string(), z.unknown()).nullable().optional(),
// The event committers endpoint populates this; release commits usually return an empty string.
suspectCommitType: z.string().optional(),
author: ApiActorSchema.nullable().optional(),
repository: z
Expand All @@ -808,6 +809,17 @@ export const CommitSchema = z

export const CommitListSchema = z.array(CommitSchema);

export const CommitterSchema = z
.object({
author: ApiActorSchema.nullable().optional(),
commits: CommitListSchema,
})
.passthrough();

export const CommittersResponseSchema = z.object({
committers: z.array(CommitterSchema),
});

export const IssueActivitySchema = z
.object({
id: ApiResourceIdSchema,
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-core/src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import type {
ClientKeySchema,
CommitListSchema,
CommitSchema,
CommittersResponseSchema,
DashboardListItemSchema,
DashboardSchema,
DashboardWidgetSchema,
Expand Down Expand Up @@ -200,6 +201,9 @@ export type MetricAlertRuleList = z.infer<typeof MetricAlertRuleListSchema>;
export type ReleaseList = z.infer<typeof ReleaseListSchema>;
export type DeployList = z.infer<typeof DeployListSchema>;
export type CommitList = z.infer<typeof CommitListSchema>;
export type CommitterList = z.infer<
typeof CommittersResponseSchema
>["committers"];
export type IssueList = z.infer<typeof IssueListSchema>;
export type IssueActivityList = z.infer<
typeof IssueActivityListResponseSchema
Expand Down
35 changes: 35 additions & 0 deletions packages/mcp-core/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
import { ThreadsEntrySchema } from "../api-client";
import type {
AutofixRunState,
CommitterList,
Event,
ExternalIssueList,
GenericEvent,
Expand Down Expand Up @@ -1984,6 +1985,22 @@ function formatSeerSummary(autofixState: AutofixRunState | undefined): string {
return `${parts.join("\n")}\n\n`;
}

/** Projects the suspect commit consistently for structured and markdown issue details. */
export function getSuspectCommit(committers: CommitterList | undefined) {
// The endpoint currently returns the issue's latest suspect commit, grouped by author.
const committer = committers?.[0];
const commit = committer?.commits[0];
if (!commit) {
return null;
}
return {
id: String(commit.id),
message: commit.message,
author: committer.author?.name ?? committer.author?.email,
suspectCommitType: commit.suspectCommitType,
};
}

/**
* Formats a Sentry issue with its latest event into comprehensive markdown output.
* Includes issue metadata, event details, and usage instructions.
Expand All @@ -2002,6 +2019,7 @@ export function formatIssueOutput({
relatedReplayIds,
aiConversations,
codeLocation,
committers,
experimentalMode,
availableToolNames,
directToolNames,
Expand All @@ -2016,6 +2034,7 @@ export function formatIssueOutput({
relatedReplayIds?: string[];
aiConversations?: AIConversationReference[];
codeLocation?: CodeLocation;
committers?: CommitterList;
experimentalMode?: boolean;
availableToolNames?: ReadonlySet<string>;
directToolNames?: ReadonlySet<string>;
Expand Down Expand Up @@ -2091,6 +2110,22 @@ export function formatIssueOutput({
output += formatCodeLocation(codeLocation);
}

const suspectCommit = getSuspectCommit(committers);
if (suspectCommit) {
output += "## Suspect Commit\n\n";
output += `**SHA**: \`${suspectCommit.id}\`\n`;
if (suspectCommit.message) {
output += `**Message**: ${suspectCommit.message}\n`;
}
if (suspectCommit.author) {
output += `**Author**: ${suspectCommit.author}\n`;
}
if (suspectCommit.suspectCommitType) {
output += `**Source**: ${suspectCommit.suspectCommitType}\n`;
}
output += "\n";
}

output += "## Event Details\n\n";

// Check if this is an unsupported event type
Expand Down
6 changes: 3 additions & 3 deletions packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
},
{
"name": "get_issue_details",
"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>",
"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>",
"requiredScopes": ["event:read"]
},
{
Expand Down Expand Up @@ -239,7 +239,7 @@
},
{
"name": "get_issue_details",
"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>",
"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>",
"requiredScopes": ["event:read"]
},
{
Expand Down Expand Up @@ -365,7 +365,7 @@
},
{
"name": "get_issue_details",
"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>",
"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>",
"requiredScopes": ["event:read"]
},
{
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3753,7 +3753,7 @@
},
{
"name": "get_issue_details",
"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>",
"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>",
"inputSchema": {
"type": "object",
"properties": {
Expand Down
Loading
Loading