Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3218,7 +3218,7 @@ export class SentryApiService {
opts?: RequestOptions,
): Promise<Event> {
const body = await this.requestJSON(
`/organizations/${organizationSlug}/issues/${issueId}/events/${eventId}/`,
`/organizations/${organizationSlug}/issues/${issueId}/events/${eventId}/?llmFormat=markdown`,
undefined,
opts,
);
Expand Down Expand Up @@ -3983,7 +3983,7 @@ export class SentryApiService {
opts?: RequestOptions,
): Promise<AutofixRunState> {
const body = await this.requestJSON(
`/organizations/${organizationSlug}/issues/${issueId}/autofix/`,
`/organizations/${organizationSlug}/issues/${issueId}/autofix/?llmFormat=markdown`,
undefined,
opts,
);
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,8 @@ const BaseEventSchema = z.object({
_meta: z.unknown().optional(),
// dateReceived is when the server received the event (may not be present in all contexts)
dateReceived: z.string().datetime().optional(),
// shared-formatter output, present when the event endpoint is called with ?llmFormat
formatted: z.object({ format: z.string(), content: z.string() }).optional(),
});

export const ErrorEventSchema = BaseEventSchema.omit({
Expand Down Expand Up @@ -1231,6 +1233,8 @@ export const AutofixRunStateSchema = z.object({
})
.passthrough()
.nullable(),
// shared-formatter output, present when the autofix endpoint is called with ?llmFormat
formatted: z.object({ format: z.string(), content: z.string() }).optional(),
});

export const EventAttachmentSchema = z.object({
Expand Down
44 changes: 36 additions & 8 deletions packages/mcp-core/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getAutofixArtifactSummaries,
getStatusDisplayName,
isTerminalStatus,
wrapSeerContent,
} from "./tool-helpers/seer";
import { formatToolCallInstruction } from "./tool-helpers/tool-call-formatting";
import {
Expand Down Expand Up @@ -1926,6 +1927,16 @@ function formatSeerSummary(autofixState: AutofixRunState | undefined): string {
return "";
}

// Prefer the shared formatter's analysis when the endpoint provides it.
// Seer content is LLM-generated, so wrap it in the untrusted-data boundary.
if (autofixState.formatted?.content) {
const wrapped = wrapSeerContent(
autofixState.formatted.content,
autofixState.autofix.run_id,
);
return `## Seer Analysis\n\n${wrapped}\n`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seer status omitted with formatted content

Medium Severity

When autofixState.formatted.content is present, formatSeerSummary returns early and skips the existing status handling for in-progress, failed, and awaiting_user_input runs. analyze_issue_with_seer still appends that status after using formatted content, so get_issue_details can hide that Seer failed or needs input once the shared formatter rolls out.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e7f534f. Configure here.


const { autofix } = autofixState;
const parts: string[] = [];

Expand Down Expand Up @@ -2128,12 +2139,12 @@ export function formatIssueOutput({
// "default" type represents error events without exception data
// "generic" type represents performance regressions and metric-based issues
// "csp" type represents Content Security Policy violations
if (
const isSharedFormatterType =
event.type === "error" ||
event.type === "default" ||
event.type === "generic" ||
event.type === "csp"
) {
event.type === "csp";
if (isSharedFormatterType) {
const typedEvent = event as
| z.infer<typeof ErrorEventSchema>
| z.infer<typeof DefaultEventSchema>
Expand All @@ -2147,17 +2158,34 @@ export function formatIssueOutput({
output += `**Message**:\n${event.message}\n`;
}
output += "\n";
output += formatEventOutput(event, {
performanceTrace,
replaySummary: {
if (isSharedFormatterType && event.formatted?.content) {
// the shared formatter body doesn't include the replay note — add it here to match formatEventOutput
output += formatIssueReplayOutput({
apiService,
organizationSlug,
event,
relatedReplayIds,
experimentalMode: experimentalMode ?? false,
availableToolNames,
directToolNames,
},
});
});
const formattedContent = event.formatted.content;
output += formattedContent.endsWith("\n")
? formattedContent
: `${formattedContent}\n`;
} else {
output += formatEventOutput(event, {
performanceTrace,
replaySummary: {
apiService,
organizationSlug,
relatedReplayIds,
experimentalMode: experimentalMode ?? false,
availableToolNames,
directToolNames,
},
});
}

// Add Seer context if available
if (autofixState) {
Expand Down
14 changes: 14 additions & 0 deletions packages/mcp-core/src/internal/tool-helpers/seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ function wrapSeerAnalysisOutput({
return `<seer_analysis ${attrs.join(" ")}>\n${output.trimEnd()}\n</seer_analysis>\n`;
}

/**
* Wraps shared-formatter Seer analysis content in the provenance boundary,
* mirroring the tags getOutputForAutofixRun applies to MCP-rendered output.
* Seer content is LLM-generated, so it must be marked as untrusted data.
*/
export function wrapSeerContent(content: string, runId?: number): string {
return wrapSeerAnalysisOutput({
output: content,
runId,
step: "analysis",
includeProvenanceTags: true,
});
}

// Artifact data shapes from getsentry/sentry's
// `src/sentry/seer/autofix/artifact_schemas.py`. Fields are LLM-generated, so
// everything is treated as optional.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ describe("analyze_issue_with_seer", () => {
expect(result).toContain("The analysis has completed successfully.");
});

it("uses formatted.content from the autofix endpoint when present", async () => {
mswServer.use(
http.get(
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-FMT/autofix/",
() =>
HttpResponse.json({
autofix: { run_id: 42, status: "completed", blocks: [] },
formatted: {
format: "markdown",
content: "## Root Cause\n\nSHARED-AUTOFIX-MARKER",
},
}),
),
);

const result = await analyzeIssueWithSeer.handler(
{
organizationSlug: "sentry-mcp-evals",
regionUrl: null,
instruction: undefined,
issueId: "CLOUDFLARE-MCP-FMT",
issueUrl: undefined,
},
{
constraints: { organizationSlug: undefined },
accessToken: "access-token",
userId: "1",
},
);

expect(result).toContain("SHARED-AUTOFIX-MARKER"); // body from the shared /autofix/ formatter
// LLM-generated content is still wrapped in the untrusted-data boundary
expect(result).toContain('<seer_analysis run_id="42" step="analysis">');
expect(result).toContain("</seer_analysis>");
});

it("wraps completed Seer-authored sections with provenance tags", async () => {
mswServer.use(
http.get(
Expand Down
22 changes: 19 additions & 3 deletions packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isTerminalStatus,
getHumanInterventionGuidance,
getOutputForAutofixRun,
wrapSeerContent,
getActiveAutofixTodo,
getSeerUnsupportedIssueMessage,
isSeerSupportedIssue,
Expand Down Expand Up @@ -177,7 +178,12 @@ export default defineTool({
if (isTerminalStatus(existingStatus)) {
// Return results immediately, no polling needed
output += `## Analysis ${getStatusDisplayName(existingStatus)}\n\n`;
output += getOutputForAutofixRun(autofixState.autofix);
output += autofixState.formatted?.content
? wrapSeerContent(
autofixState.formatted.content,
autofixState.autofix.run_id,
)
: getOutputForAutofixRun(autofixState.autofix);

if (existingStatus !== "completed") {
output += `\n**Status**: ${existingStatus}\n`;
Expand Down Expand Up @@ -210,7 +216,12 @@ export default defineTool({
// Check if completed (terminal state)
if (isTerminalStatus(status)) {
output += `## Analysis ${getStatusDisplayName(status)}\n\n`;
output += getOutputForAutofixRun(autofixState.autofix);
output += autofixState.formatted?.content
? wrapSeerContent(
autofixState.formatted.content,
autofixState.autofix.run_id,
)
: getOutputForAutofixRun(autofixState.autofix);

if (status !== "completed") {
output += `\n**Status**: ${status}\n`;
Expand Down Expand Up @@ -279,7 +290,12 @@ export default defineTool({
// Show current progress
if (autofixState.autofix) {
output += `**Current Status**: ${getStatusDisplayName(autofixState.autofix.status)}\n\n`;
output += getOutputForAutofixRun(autofixState.autofix);
output += autofixState.formatted?.content
? wrapSeerContent(
autofixState.formatted.content,
autofixState.autofix.run_id,
)
: getOutputForAutofixRun(autofixState.autofix);
}

// Timeout reached
Expand Down
Loading
Loading