Skip to content

Commit 26a5040

Browse files
authored
feat(formatter): return structuredContent from get_issue_details (#1284)
1 parent b0f3cd9 commit 26a5040

4 files changed

Lines changed: 643 additions & 19 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3673,7 +3673,7 @@ export class SentryApiService {
36733673
): Promise<Event> {
36743674
const body = await this.requestJSON(
36753675
apiPath`/organizations/${organizationSlug}/issues/${issueId}/events/${eventId}/` +
3676-
`?llmFormat=markdown`,
3676+
`?llmFormat=json`,
36773677
undefined,
36783678
opts,
36793679
);

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

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,42 @@ export function formatFrameHeader(
180180
}
181181
}
182182

183+
/**
184+
* Whether the shared formatter covers this event type, and so whether its body should be used
185+
* instead of the local rendering.
186+
*
187+
* "default" is an error event without exception data, "generic" a performance regression or
188+
* metric issue, "csp" a Content Security Policy violation. Anything else (a transaction, most
189+
* notably) keeps the local path, which renders things the shared body does not carry such as
190+
* the fetched performance trace.
191+
*/
192+
/**
193+
* Whether to read the issue's metadata instead of its top level fields. Performance issues can
194+
* have various categories such as 'db_query', but the issueType starts with 'performance_'.
195+
*
196+
* It matters which side of this an issue falls on: metadata.value is a query pattern for a
197+
* performance issue and the exception message for an error, so reading it unconditionally
198+
* misnames the error text.
199+
*/
200+
export function isPerformanceIssueType(issue: {
201+
issueType?: string | null;
202+
issueCategory?: string | null;
203+
}): boolean {
204+
return (
205+
issue.issueType?.startsWith("performance_") === true ||
206+
issue.issueCategory === "performance"
207+
);
208+
}
209+
210+
export function usesSharedFormatterBody(event: { type?: unknown }): boolean {
211+
return (
212+
event.type === "error" ||
213+
event.type === "default" ||
214+
event.type === "generic" ||
215+
event.type === "csp"
216+
);
217+
}
218+
183219
/**
184220
* Formats a Sentry event into a structured markdown output.
185221
* Includes error messages, stack traces, request info, and contextual data.
@@ -1986,11 +2022,7 @@ export function formatIssueOutput({
19862022
}) {
19872023
let output = `# Issue ${issue.shortId} in **${organizationSlug}**\n\n`;
19882024

1989-
// Check if this is a performance issue based on issueCategory or issueType
1990-
// Performance issues can have various categories like 'db_query' but issueType starts with 'performance_'
1991-
const isPerformanceIssue =
1992-
issue.issueType?.startsWith("performance_") ||
1993-
issue.issueCategory === "performance";
2025+
const isPerformanceIssue = isPerformanceIssueType(issue);
19942026

19952027
if (isPerformanceIssue && issue.metadata) {
19962028
// For performance issues, use metadata for better context
@@ -2118,14 +2150,7 @@ export function formatIssueOutput({
21182150

21192151
output += `**Event ID**: ${event.id}\n`;
21202152
output += `**Type**: ${event.type}\n`;
2121-
// "default" type represents error events without exception data
2122-
// "generic" type represents performance regressions and metric-based issues
2123-
// "csp" type represents Content Security Policy violations
2124-
const isSharedFormatterType =
2125-
event.type === "error" ||
2126-
event.type === "default" ||
2127-
event.type === "generic" ||
2128-
event.type === "csp";
2153+
const isSharedFormatterType = usesSharedFormatterBody(event);
21292154
if (isSharedFormatterType) {
21302155
const typedEvent = event as
21312156
| z.infer<typeof ErrorEventSchema>
@@ -2140,7 +2165,13 @@ export function formatIssueOutput({
21402165
output += `**Message**:\n${event.message}\n`;
21412166
}
21422167
output += "\n";
2143-
if (isSharedFormatterType && event.formatted?.content) {
2168+
// only a markdown body belongs in this output; a json body is for structuredContent, and
2169+
// pasting it here would put a serialized object in the middle of the prose
2170+
if (
2171+
isSharedFormatterType &&
2172+
event.formatted?.format === "markdown" &&
2173+
event.formatted.content
2174+
) {
21442175
// the shared formatter body doesn't include the replay note — add it here to match formatEventOutput
21452176
output += formatIssueReplayOutput({
21462177
apiService,
@@ -2432,7 +2463,7 @@ function formatIssueReplayOutput({
24322463
return `${lines.join("\n")}\n\n`;
24332464
}
24342465

2435-
function getReplayIdFromEvent(event: Event): string | null {
2466+
export function getReplayIdFromEvent(event: Event): string | null {
24362467
const replayContext = event.contexts?.replay as
24372468
| Record<string, unknown>
24382469
| undefined;
@@ -2447,7 +2478,7 @@ function getReplayIdFromEvent(event: Event): string | null {
24472478
return normalizeReplayId(replayContextId ?? replayTagId);
24482479
}
24492480

2450-
function dedupeReplayIds(replayIds: string[]): string[] {
2481+
export function dedupeReplayIds(replayIds: string[]): string[] {
24512482
const normalizedReplayIds: string[] = [];
24522483
const seenReplayIds = new Set<string>();
24532484

0 commit comments

Comments
 (0)