diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index ad3b73447..1dc8f2f43 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -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\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\n\n\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", + "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\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\n\n\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", "requiredScopes": [] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index e686c09ba..7758aff35 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -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\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\n\n\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", + "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\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\n\n\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", "inputSchema": { "type": "object", "properties": { @@ -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." } } }, diff --git a/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.test.ts b/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.test.ts index 3803f0ef3..c553b821d 100644 --- a/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.test.ts +++ b/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.test.ts @@ -93,6 +93,53 @@ describe("analyze_issue_with_seer", () => { expect(result).toContain(""); }); + 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( @@ -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 = { diff --git a/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts b/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts index 20da93674..b167b34e4 100644 --- a/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts +++ b/packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts @@ -74,6 +74,7 @@ export default defineTool({ "- 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", "", ].join("\n"), inputSchema: { @@ -85,6 +86,14 @@ export default defineTool({ .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, @@ -119,6 +128,25 @@ export default defineTool({ } 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` + ); + }; // Step 1: Check if analysis already exists let autofixState = await retryWithBackoff( @@ -139,6 +167,11 @@ export default defineTool({ }, ); + const initialMismatch = runMismatch(autofixState.autofix?.run_id); + if (initialMismatch) { + return output + initialMismatch; + } + // Step 2: Start analysis if none exists if (!autofixState.autofix) { output += `Starting new analysis...\n\n`; @@ -204,13 +237,16 @@ export default defineTool({ 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; + } + const status = autofixState.autofix.status; // Check if completed (terminal state) @@ -275,9 +311,7 @@ export default defineTool({ 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; } @@ -304,9 +338,7 @@ export default defineTool({ 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;