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
2 changes: 1 addition & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
"tools": [
{
"name": "analyze_issue_with_seer",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n- Pass runId when you already hold a Seer run id (for example from a seer.* webhook) so the tool refuses to return a different run's results or start a new run\n</hints>",
"requiredScopes": []
},
{
Expand Down
8 changes: 7 additions & 1 deletion packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
},
{
"name": "analyze_issue_with_seer",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n- Pass runId when you already hold a Seer run id (for example from a seer.* webhook) so the tool refuses to return a different run's results or start a new run\n</hints>",
"inputSchema": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -143,6 +143,12 @@
"instruction": {
"type": "string",
"description": "Optional custom instruction for the AI analysis"
},
"runId": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"description": "Optional Seer run ID to confirm, e.g. from a seer.* webhook payload. When set, the tool never starts a new run and returns a mismatch message if the issue's current run differs."
}
}
},
Expand Down
128 changes: 128 additions & 0 deletions packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,53 @@ describe("analyze_issue_with_seer", () => {
expect(result).toContain("</seer_analysis>");
});

it("returns a mismatch message when runId differs from the current run", async () => {
const result = await analyzeIssueWithSeer.handler(
{
organizationSlug: "sentry-mcp-evals",
regionUrl: null,
instruction: undefined,
issueId: "CLOUDFLARE-MCP-45",
issueUrl: undefined,
runId: 99,
},
{
constraints: {
organizationSlug: undefined,
},
accessToken: "access-token",
userId: "1",
},
);

expect(result).toContain("No analysis found for Run ID 99.");
expect(result).toContain("current analysis is Run ID 13.");
expect(result).not.toContain("## Analysis Complete");
});

it("returns the run when runId matches the current run", async () => {
const result = await analyzeIssueWithSeer.handler(
{
organizationSlug: "sentry-mcp-evals",
regionUrl: null,
instruction: undefined,
issueId: "CLOUDFLARE-MCP-45",
issueUrl: undefined,
runId: 13,
},
{
constraints: {
organizationSlug: undefined,
},
accessToken: "access-token",
userId: "1",
},
);

expect(result).toContain("Found existing analysis (Run ID: 13)");
expect(result).toContain("## Analysis Complete");
});

it("wraps completed Seer-authored sections with provenance tags", async () => {
mswServer.use(
http.get(
Expand Down Expand Up @@ -347,6 +394,87 @@ describe("analyze_issue_with_seer", () => {
expect(result).toContain("Processing: Analyzing the issue...");
});

describe("with runId while polling", () => {
const processing = {
...autofixStateFixture,
autofix: {
...autofixStateFixture.autofix,
run_id: 13,
status: "processing",
blocks: [],
},
};

function callWithRunId() {
return analyzeIssueWithSeer.handler(
{
organizationSlug: "sentry-mcp-evals",
regionUrl: null,
instruction: undefined,
issueId: "CLOUDFLARE-MCP-TIMEOUT",
runId: 13,
},
{
constraints: {
organizationSlug: undefined,
},
accessToken: "access-token",
userId: "1",
},
);
}

it("stops when a newer run replaces the requested one", async () => {
let polls = 0;
mswServer.use(
http.get(
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-TIMEOUT/autofix/",
() => {
polls++;
return HttpResponse.json(
polls === 1
? processing
: {
...processing,
autofix: {
...autofixStateFixture.autofix,
run_id: 14,
status: "completed",
},
},
);
},
),
);

const promise = callWithRunId();
await vi.runAllTimersAsync();
const result = await promise;

expect(result).toContain("No analysis found for Run ID 13.");
expect(result).toContain("current analysis is Run ID 14.");
expect(result).not.toContain("## Analysis Complete");
});

it("keeps runId in the follow-up call after a timeout", async () => {
mswServer.use(
http.get(
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-TIMEOUT/autofix/",
() => HttpResponse.json(processing),
),
);

const promise = callWithRunId();
await vi.advanceTimersByTimeAsync(6 * 60 * 1000);
const result = await promise;

expect(result).toContain("## Analysis Timed Out");
expect(result).toContain(
'analyze_issue_with_seer(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-TIMEOUT", runId=13)',
);
});
});

it("handles consecutive polling errors", async () => {
let pollAttempts = 0;
const inProgressState = {
Expand Down
50 changes: 41 additions & 9 deletions packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"- If the user provides an issueUrl, extract it and use that parameter alone",
"- The analysis includes actual code snippets and fixes, not just error descriptions",
"- Results are cached - subsequent calls return instantly",
"- Pass runId when you already hold a Seer run id (for example from a seer.* webhook) so the tool refuses to return a different run's results or start a new run",
"</hints>",
].join("\n"),
inputSchema: {
Expand All @@ -85,6 +86,14 @@
.string()
.describe("Optional custom instruction for the AI analysis")
.optional(),
runId: z
.number()
.int()
.positive()
.describe(
"Optional Seer run ID to confirm, e.g. from a seer.* webhook payload. When set, the tool never starts a new run and returns a mismatch message if the issue's current run differs.",
)
.optional(),
},
annotations: {
readOnlyHint: false,
Expand Down Expand Up @@ -119,6 +128,25 @@
}

let output = `# Seer Analysis for Issue ${parsedIssueId}\n\n`;
const runIdArg =
params.runId !== undefined ? `, runId=${params.runId}` : "";
const retryCall = params.issueUrl
? `analyze_issue_with_seer(issueUrl="${params.issueUrl}"${runIdArg})`
: `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}"${runIdArg})`;
// Sentry only exposes an issue's current run, and a newer run can replace
// the requested one while this tool polls.
const runMismatch = (currentRunId: number | undefined): string | null => {
if (params.runId === undefined || currentRunId === params.runId) {
return null;
}
return (
`No analysis found for Run ID ${params.runId}.\n` +
(currentRunId === undefined
? `The issue has no Seer analysis yet.\n`
: `The issue's current analysis is Run ID ${currentRunId}.\n`) +
`Sentry only exposes an issue's current run, so call again without runId to work with that run instead.\n`
);
};

Check warning on line 149 in packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts

View check run for this annotation

@sentry/warden / warden: code-review

[YXA-H7S] Timeout path can return a different run after the final refresh (additional location)

If the final poll refresh finds a different current run as the timeout expires, the timeout path renders that run's content without checking `runId`. Callers can therefore receive another run's results instead of the promised mismatch message.

// Step 1: Check if analysis already exists
let autofixState = await retryWithBackoff(
Expand All @@ -139,6 +167,11 @@
},
);

const initialMismatch = runMismatch(autofixState.autofix?.run_id);
if (initialMismatch) {
return output + initialMismatch;
}

Comment thread
sergical marked this conversation as resolved.
// Step 2: Start analysis if none exists
if (!autofixState.autofix) {
output += `Starting new analysis...\n\n`;
Expand Down Expand Up @@ -204,13 +237,16 @@
if (!autofixState.autofix) {
output += `Error: Analysis state lost. Please try again by running:\n`;
output += `\`\`\`\n`;
output += params.issueUrl
? `analyze_issue_with_seer(issueUrl="${params.issueUrl}")`
: `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}")`;
output += retryCall;
output += `\n\`\`\`\n`;
return output;
}

const pollMismatch = runMismatch(autofixState.autofix.run_id);
if (pollMismatch) {
return output + pollMismatch;
}

Check warning on line 248 in packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts

View check run for this annotation

@sentry/warden / warden: code-review

Timeout path can return a different run after the final refresh

If the final poll refresh finds a different current run as the timeout expires, the timeout path renders that run's content without checking `runId`. Callers can therefore receive another run's results instead of the promised mismatch message.

const status = autofixState.autofix.status;

// Check if completed (terminal state)
Expand Down Expand Up @@ -275,9 +311,7 @@
output += `Error: ${error instanceof Error ? error.message : String(error)}\n\n`;
output += `You can check the status later by running the same command again:\n`;
output += `\`\`\`\n`;
output += params.issueUrl
? `analyze_issue_with_seer(issueUrl="${params.issueUrl}")`
: `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}")`;
output += retryCall;
output += `\n\`\`\`\n`;
return output;
}
Expand All @@ -288,27 +322,25 @@
}

// Show current progress
if (autofixState.autofix) {
output += `**Current Status**: ${getStatusDisplayName(autofixState.autofix.status)}\n\n`;
output += autofixState.formatted?.content
? wrapSeerContent(
autofixState.formatted.content,
autofixState.autofix.run_id,
)
: getOutputForAutofixRun(autofixState.autofix);
}

// Timeout reached
output += `\n## Analysis Timed Out\n\n`;
output += `The analysis is taking longer than expected (>${SEER_TIMEOUT / 1000}s).\n\n`;

output += `\nYou can check the status later by running the same command again:\n`;
output += `\`\`\`\n`;
output += params.issueUrl
? `analyze_issue_with_seer(issueUrl="${params.issueUrl}")`
: `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}")`;
output += retryCall;
output += `\n\`\`\`\n`;

return output;

Check warning on line 344 in packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts

View check run for this annotation

@sentry/warden / warden: code-review

[YXA-H7S] Timeout path can return a different run after the final refresh (additional location)

If the final poll refresh finds a different current run as the timeout expires, the timeout path renders that run's content without checking `runId`. Callers can therefore receive another run's results instead of the promised mismatch message.
},
});
Loading