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;