From d6462bccfdd2a7681569971ad56b34e23cea94c5 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 5 Nov 2026 13:51:48 -0800 Subject: [PATCH 1/4] feat(seer): Check Seer credits and show out-of-credits failures analyze_issue_with_seer checks the organization's Seer credits before starting a run, and tells the user when a run stopped because the organization ran out of credits. get_issue_details shows the same reason. A failed credit check never blocks a run. Refs CW-2330 --- packages/mcp-core/src/api-client/client.ts | 23 +++++++++ packages/mcp-core/src/api-client/schema.ts | 23 +++++++++ packages/mcp-core/src/api-client/types.ts | 2 + packages/mcp-core/src/internal/formatting.ts | 4 ++ .../src/internal/tool-helpers/seer.ts | 49 ++++++++++++++++++- .../tools/catalog/analyze-issue-with-seer.ts | 14 ++++++ packages/mcp-server-mocks/src/index.ts | 5 ++ 7 files changed, 118 insertions(+), 2 deletions(-) diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index f6b433942..c5f6bd5bc 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -180,6 +180,7 @@ import { RepositoryListSchema, SearchAgentStartSchema, SearchAgentStateSchema, + SeerCreditCheckSchema, SentryAppComponentListSchema, SentryAppExternalRequestOptionsSchema, SentryAppInstallationListSchema, @@ -254,6 +255,7 @@ import type { ReplayList, SearchAgentStart, SearchAgentState, + SeerCreditOperation, SentryAppComponentList, SentryAppExternalRequestOptions, SentryAppInstallationList, @@ -4742,6 +4744,27 @@ export class SentryApiService { return SearchAgentStateSchema.parse(body); } + // GET https://us.sentry.io/api/0/organizations/my-org/seer/credit-check/?operation=autofix_root_cause + async hasSeerCredits( + { + organizationSlug, + operation, + }: { + organizationSlug: string; + operation: SeerCreditOperation; + }, + opts?: RequestOptions, + ): Promise { + const queryParams = new URLSearchParams({ operation }); + const body = await this.requestJSON( + apiPath`/organizations/${organizationSlug}/seer/credit-check/` + + `?${queryParams.toString()}`, + undefined, + opts, + ); + return SeerCreditCheckSchema.parse(body).hasEnoughCredits; + } + /** * Retrieves high-level metadata about a trace. * diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index 725e3300b..a79ffe462 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -1353,6 +1353,7 @@ export const AutofixRunStateSchema = z.object({ run_id: z.number(), updated_at: z.string().nullable().optional(), status: AutofixStatusSchema, + failure_reason: z.string().nullable().optional(), blocks: z.array(AutofixBlockSchema).default([]), pending_user_input: z.unknown().nullable().optional(), repo_pr_states: z @@ -1367,6 +1368,28 @@ export const AutofixRunStateSchema = z.object({ .nullable(), }); +// Operations from Sentry's `SeerOperation` (`src/sentry/seer/credits.py`). +export const SeerCreditOperationSchema = z.enum([ + "agent_chat_message", + "autofix_root_cause", + "autofix_solution", + "autofix_code_changes", + "autofix_open_pr", + "autofix_pr_iteration", +]); + +/** + * Whether the organization has enough Seer credits to run an operation. + * + * Upstream source of truth in getsentry/sentry: + * - `src/sentry/seer/endpoints/organization_seer_credit_check.py` + */ +export const SeerCreditCheckSchema = z + .object({ + hasEnoughCredits: z.boolean(), + }) + .passthrough(); + /** * Schemas for Seer's search agent, which translates natural language into * Sentry search queries. diff --git a/packages/mcp-core/src/api-client/types.ts b/packages/mcp-core/src/api-client/types.ts index 18936720f..909070fe8 100644 --- a/packages/mcp-core/src/api-client/types.ts +++ b/packages/mcp-core/src/api-client/types.ts @@ -56,6 +56,7 @@ import type { AutofixRunStateSchema, SearchAgentStartSchema, SearchAgentStateSchema, + SeerCreditOperationSchema, ClientKeyListSchema, ClientKeySchema, CommitListSchema, @@ -243,6 +244,7 @@ export type AutofixRun = z.infer; export type AutofixRunState = z.infer; export type SearchAgentStart = z.infer; export type SearchAgentState = z.infer; +export type SeerCreditOperation = z.infer; export type AssignedTo = z.infer; export type ReplayDetails = z.infer; export type ReplayList = z.infer["data"]; diff --git a/packages/mcp-core/src/internal/formatting.ts b/packages/mcp-core/src/internal/formatting.ts index 2da18eb80..4966aeac1 100644 --- a/packages/mcp-core/src/internal/formatting.ts +++ b/packages/mcp-core/src/internal/formatting.ts @@ -47,6 +47,7 @@ import { getAutofixArtifactSummaries, getStatusDisplayName, isTerminalStatus, + SEER_OUT_OF_CREDITS_MESSAGE, } from "./tool-helpers/seer"; import { formatToolCallInstruction } from "./tool-helpers/tool-call-formatting"; import { isPlainObject } from "./type-guards"; @@ -2200,6 +2201,9 @@ function formatSeerSummary(autofixState: AutofixRunState | undefined): string { if (autofix.status === "error") { parts.push(""); parts.push("**Status:** Analysis failed."); + if (autofix.failure_reason === "out_of_credits") { + parts.push(SEER_OUT_OF_CREDITS_MESSAGE); + } } else if (autofix.status === "awaiting_user_input") { parts.push(""); parts.push("**Status:** Analysis paused - additional information needed."); diff --git a/packages/mcp-core/src/internal/tool-helpers/seer.ts b/packages/mcp-core/src/internal/tool-helpers/seer.ts index 0a98ca7fd..cd26014e8 100644 --- a/packages/mcp-core/src/internal/tool-helpers/seer.ts +++ b/packages/mcp-core/src/internal/tool-helpers/seer.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -import type { AutofixRunState } from "../../api-client/index"; -import type { Issue } from "../../api-client/types"; +import { + ApiNotFoundError, + ApiValidationError, + type AutofixRunState, + type SentryApiService, +} from "../../api-client/index"; +import type { Issue, SeerCreditOperation } from "../../api-client/types"; +import { logIssue, logWarn } from "../../telem/logging"; type AutofixRun = NonNullable; @@ -85,6 +91,45 @@ export function getHumanInterventionGuidance(status: string): string { return ""; } +export const SEER_OUT_OF_CREDITS_MESSAGE = + "Your organization doesn't have enough Seer credits for this. Add credits in Sentry to continue."; + +/** + * Explain why a failed run stopped, when Seer gives a reason we can explain. + */ +export function getFailureReasonGuidance( + failureReason: string | null | undefined, +): string { + if (failureReason === "out_of_credits") { + return `\n${SEER_OUT_OF_CREDITS_MESSAGE}\n`; + } + return ""; +} + +/** + * Whether the organization has enough Seer credits for the operation. + */ +export async function hasSeerCredits( + apiService: SentryApiService, + organizationSlug: string, + operation: SeerCreditOperation, +): Promise { + try { + return await apiService.hasSeerCredits({ organizationSlug, operation }); + } catch (error) { + if (error instanceof ApiValidationError || error instanceof z.ZodError) { + // Sentry rejected the operation or answered in a shape we don't expect, + // so our contract with Sentry drifted. + logIssue(error, { + contexts: { request: { organizationSlug, operation } }, + }); + } else if (!(error instanceof ApiNotFoundError)) { + logWarn(error, { extra: { organizationSlug, operation } }); + } + return true; + } +} + function escapeXmlAttribute(value: string | number): string { return String(value) .replace(/&/g, "&") 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 3788f0df0..8bca4706e 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 @@ -9,14 +9,17 @@ import { } from "../../internal/tool-helpers/issue"; import { getActiveAutofixTodo, + getFailureReasonGuidance, getHumanInterventionGuidance, getOutputForAutofixRun, getSeerUnsupportedIssueMessage, getStatusDisplayName, + hasSeerCredits, isSeerSupportedIssue, isTerminalStatus, SEER_INITIAL_RETRY_DELAY, SEER_MAX_RETRIES, + SEER_OUT_OF_CREDITS_MESSAGE, SEER_POLLING_INTERVAL, SEER_TIMEOUT, } from "../../internal/tool-helpers/seer"; @@ -141,6 +144,11 @@ export default defineTool({ // Step 2: Start analysis if none exists if (!autofixState.autofix) { + if (!(await hasSeerCredits(apiService, orgSlug, "autofix_root_cause"))) { + output += `## Analysis Not Started\n\n${SEER_OUT_OF_CREDITS_MESSAGE}\n`; + return output; + } + output += `Starting new analysis...\n\n`; const startResult = await apiService.startAutofix({ organizationSlug: orgSlug, @@ -182,6 +190,9 @@ export default defineTool({ if (existingStatus !== "completed") { output += `\n**Status**: ${existingStatus}\n`; + output += getFailureReasonGuidance( + autofixState.autofix.failure_reason, + ); output += getHumanInterventionGuidance(existingStatus); output += "\n"; } @@ -215,6 +226,9 @@ export default defineTool({ if (status !== "completed") { output += `\n**Status**: ${status}\n`; + output += getFailureReasonGuidance( + autofixState.autofix.failure_reason, + ); output += getHumanInterventionGuidance(status); } diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index c08a95e8f..ea6a924af 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -1318,6 +1318,11 @@ export const restHandlers = buildHandlers([ path: "/api/0/organizations/sentry-mcp-evals/issues/PEATED-A8/autofix/", fetch: () => HttpResponse.json({ run_id: 123 }), }, + { + method: "get", + path: "/api/0/organizations/sentry-mcp-evals/seer/credit-check/", + fetch: () => HttpResponse.json({ hasEnoughCredits: true }), + }, { method: "get", path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/autofix/", From 42b1ecf845f5e6bb439e3d876ec229776f227c85 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 5 Nov 2026 15:00:03 -0800 Subject: [PATCH 2/4] fix(seer): Match the out-of-credits message to Sentry's copy Say auto-reload, which is what the billing settings call it, and point to someone with billing access, since MCP users can have any role. Refs CW-2330 --- packages/mcp-core/src/internal/tool-helpers/seer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-core/src/internal/tool-helpers/seer.ts b/packages/mcp-core/src/internal/tool-helpers/seer.ts index cd26014e8..e9d647fef 100644 --- a/packages/mcp-core/src/internal/tool-helpers/seer.ts +++ b/packages/mcp-core/src/internal/tool-helpers/seer.ts @@ -92,7 +92,7 @@ export function getHumanInterventionGuidance(status: string): string { } export const SEER_OUT_OF_CREDITS_MESSAGE = - "Your organization doesn't have enough Seer credits for this. Add credits in Sentry to continue."; + "Your organization does not have enough Seer credits. Someone with billing access can turn on auto-reload in Sentry."; /** * Explain why a failed run stopped, when Seer gives a reason we can explain. From 100231178e7b139db475ebff17b4a8326836c6d5 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 9 Nov 2026 09:37:41 -0800 Subject: [PATCH 3/4] fix(seer): Rely on Sentry's 402 instead of a separate credit check Sentry checks Seer credits when analyze_issue_with_seer starts a run and answers 402 if there aren't enough, so the extra credit-check request isn't needed. The tool now turns a 402 from starting a run into an "Analysis Not Started" message instead of an error. Refs CW-2330 Co-Authored-By: Claude Opus 5.5 --- packages/mcp-core/src/api-client/client.ts | 23 -------- packages/mcp-core/src/api-client/schema.ts | 22 -------- packages/mcp-core/src/api-client/types.ts | 2 - .../src/internal/tool-helpers/seer.ts | 47 +++++----------- .../catalog/analyze-issue-with-seer.test.ts | 55 +++++++++++++++++++ .../tools/catalog/analyze-issue-with-seer.ts | 27 +++++---- packages/mcp-server-mocks/src/index.ts | 5 -- 7 files changed, 86 insertions(+), 95 deletions(-) diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index c5f6bd5bc..f6b433942 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -180,7 +180,6 @@ import { RepositoryListSchema, SearchAgentStartSchema, SearchAgentStateSchema, - SeerCreditCheckSchema, SentryAppComponentListSchema, SentryAppExternalRequestOptionsSchema, SentryAppInstallationListSchema, @@ -255,7 +254,6 @@ import type { ReplayList, SearchAgentStart, SearchAgentState, - SeerCreditOperation, SentryAppComponentList, SentryAppExternalRequestOptions, SentryAppInstallationList, @@ -4744,27 +4742,6 @@ export class SentryApiService { return SearchAgentStateSchema.parse(body); } - // GET https://us.sentry.io/api/0/organizations/my-org/seer/credit-check/?operation=autofix_root_cause - async hasSeerCredits( - { - organizationSlug, - operation, - }: { - organizationSlug: string; - operation: SeerCreditOperation; - }, - opts?: RequestOptions, - ): Promise { - const queryParams = new URLSearchParams({ operation }); - const body = await this.requestJSON( - apiPath`/organizations/${organizationSlug}/seer/credit-check/` + - `?${queryParams.toString()}`, - undefined, - opts, - ); - return SeerCreditCheckSchema.parse(body).hasEnoughCredits; - } - /** * Retrieves high-level metadata about a trace. * diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index a79ffe462..a19bcdd09 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -1368,28 +1368,6 @@ export const AutofixRunStateSchema = z.object({ .nullable(), }); -// Operations from Sentry's `SeerOperation` (`src/sentry/seer/credits.py`). -export const SeerCreditOperationSchema = z.enum([ - "agent_chat_message", - "autofix_root_cause", - "autofix_solution", - "autofix_code_changes", - "autofix_open_pr", - "autofix_pr_iteration", -]); - -/** - * Whether the organization has enough Seer credits to run an operation. - * - * Upstream source of truth in getsentry/sentry: - * - `src/sentry/seer/endpoints/organization_seer_credit_check.py` - */ -export const SeerCreditCheckSchema = z - .object({ - hasEnoughCredits: z.boolean(), - }) - .passthrough(); - /** * Schemas for Seer's search agent, which translates natural language into * Sentry search queries. diff --git a/packages/mcp-core/src/api-client/types.ts b/packages/mcp-core/src/api-client/types.ts index 909070fe8..18936720f 100644 --- a/packages/mcp-core/src/api-client/types.ts +++ b/packages/mcp-core/src/api-client/types.ts @@ -56,7 +56,6 @@ import type { AutofixRunStateSchema, SearchAgentStartSchema, SearchAgentStateSchema, - SeerCreditOperationSchema, ClientKeyListSchema, ClientKeySchema, CommitListSchema, @@ -244,7 +243,6 @@ export type AutofixRun = z.infer; export type AutofixRunState = z.infer; export type SearchAgentStart = z.infer; export type SearchAgentState = z.infer; -export type SeerCreditOperation = z.infer; export type AssignedTo = z.infer; export type ReplayDetails = z.infer; export type ReplayList = z.infer["data"]; diff --git a/packages/mcp-core/src/internal/tool-helpers/seer.ts b/packages/mcp-core/src/internal/tool-helpers/seer.ts index e9d647fef..3918a8f2b 100644 --- a/packages/mcp-core/src/internal/tool-helpers/seer.ts +++ b/packages/mcp-core/src/internal/tool-helpers/seer.ts @@ -1,12 +1,7 @@ import { z } from "zod"; -import { - ApiNotFoundError, - ApiValidationError, - type AutofixRunState, - type SentryApiService, -} from "../../api-client/index"; -import type { Issue, SeerCreditOperation } from "../../api-client/types"; -import { logIssue, logWarn } from "../../telem/logging"; +import type { AutofixRunState } from "../../api-client/index"; +import type { Issue } from "../../api-client/types"; +import { isPlainObject } from "../type-guards"; type AutofixRun = NonNullable; @@ -94,6 +89,18 @@ export function getHumanInterventionGuidance(status: string): string { export const SEER_OUT_OF_CREDITS_MESSAGE = "Your organization does not have enough Seer credits. Someone with billing access can turn on auto-reload in Sentry."; +/** + * Explain a 402 from starting a run. Sentry sends the credits 402 as + * `{ detail }`. The quota 402 is a bare string the API client can't parse, + * so it gets the fallback. + */ +export function getSeerPaymentRequiredMessage(responseBody: unknown): string { + if (isPlainObject(responseBody) && typeof responseBody.detail === "string") { + return responseBody.detail; + } + return "Your organization does not have enough Seer budget to start an analysis."; +} + /** * Explain why a failed run stopped, when Seer gives a reason we can explain. */ @@ -106,30 +113,6 @@ export function getFailureReasonGuidance( return ""; } -/** - * Whether the organization has enough Seer credits for the operation. - */ -export async function hasSeerCredits( - apiService: SentryApiService, - organizationSlug: string, - operation: SeerCreditOperation, -): Promise { - try { - return await apiService.hasSeerCredits({ organizationSlug, operation }); - } catch (error) { - if (error instanceof ApiValidationError || error instanceof z.ZodError) { - // Sentry rejected the operation or answered in a shape we don't expect, - // so our contract with Sentry drifted. - logIssue(error, { - contexts: { request: { organizationSlug, operation } }, - }); - } else if (!(error instanceof ApiNotFoundError)) { - logWarn(error, { extra: { organizationSlug, operation } }); - } - return true; - } -} - function escapeXmlAttribute(value: string | number): string { return String(value) .replace(/&/g, "&") 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 9cb6d0f24..7848b2ae0 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 @@ -439,6 +439,61 @@ describe("analyze_issue_with_seer", () => { }, ); + it.each([ + [ + "out of credits", + { + detail: + "Your organization does not have enough credits to start an Autofix run.", + }, + "Your organization does not have enough credits to start an Autofix run.", + ], + [ + "out of quota", + "No budget for Seer Autofix.", + "Your organization does not have enough Seer budget to start an analysis.", + ], + ])( + "explains why the run did not start when Sentry answers 402 (%s)", + async (_label, body, expected) => { + let getCallCount = 0; + + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-402/autofix/", + () => { + getCallCount++; + return HttpResponse.json({ autofix: null }); + }, + ), + http.post( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-402/autofix/", + () => HttpResponse.json(body, { status: 402 }), + ), + ); + + const result = await analyzeIssueWithSeer.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + issueId: "CLOUDFLARE-MCP-402", + instruction: undefined, + }, + { + constraints: { organizationSlug: undefined }, + accessToken: "access-token", + userId: "1", + }, + ); + + expect(result).toBe( + `# Seer Analysis for Issue CLOUDFLARE-MCP-402\n\n## Analysis Not Started\n\n${expected}\n`, + ); + // No polling after a refused start. + expect(getCallCount).toBe(1); + }, + ); + it("returns unsupported message for metric issues without calling Seer", async () => { let autofixRequests = 0; 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 8bca4706e..228c54a33 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 @@ -12,14 +12,13 @@ import { getFailureReasonGuidance, getHumanInterventionGuidance, getOutputForAutofixRun, + getSeerPaymentRequiredMessage, getSeerUnsupportedIssueMessage, getStatusDisplayName, - hasSeerCredits, isSeerSupportedIssue, isTerminalStatus, SEER_INITIAL_RETRY_DELAY, SEER_MAX_RETRIES, - SEER_OUT_OF_CREDITS_MESSAGE, SEER_POLLING_INTERVAL, SEER_TIMEOUT, } from "../../internal/tool-helpers/seer"; @@ -144,17 +143,23 @@ export default defineTool({ // Step 2: Start analysis if none exists if (!autofixState.autofix) { - if (!(await hasSeerCredits(apiService, orgSlug, "autofix_root_cause"))) { - output += `## Analysis Not Started\n\n${SEER_OUT_OF_CREDITS_MESSAGE}\n`; - return output; + let startResult: Awaited>; + try { + startResult = await apiService.startAutofix({ + organizationSlug: orgSlug, + issueId: parsedIssueId, + instruction: params.instruction, + }); + } catch (error) { + // Sentry answers 402 before starting a run when the org is out of + // Seer credits or quota. + if (error instanceof ApiError && error.status === 402) { + output += `## Analysis Not Started\n\n${getSeerPaymentRequiredMessage(error.responseBody)}\n`; + return output; + } + throw error; } - output += `Starting new analysis...\n\n`; - const startResult = await apiService.startAutofix({ - organizationSlug: orgSlug, - issueId: parsedIssueId, - instruction: params.instruction, - }); output += `Analysis started with Run ID: ${startResult.run_id}\n\n`; // Give it a moment to initialize diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index ea6a924af..c08a95e8f 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -1318,11 +1318,6 @@ export const restHandlers = buildHandlers([ path: "/api/0/organizations/sentry-mcp-evals/issues/PEATED-A8/autofix/", fetch: () => HttpResponse.json({ run_id: 123 }), }, - { - method: "get", - path: "/api/0/organizations/sentry-mcp-evals/seer/credit-check/", - fetch: () => HttpResponse.json({ hasEnoughCredits: true }), - }, { method: "get", path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/autofix/", From 5ee1dff79f088c380d49f088de8385ec57db21ae Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 9 Nov 2026 11:19:14 -0800 Subject: [PATCH 4/4] fix message copies --- packages/mcp-core/src/internal/formatting.ts | 4 +- .../src/internal/tool-helpers/seer.ts | 22 ++++++---- .../catalog/analyze-issue-with-seer.test.ts | 40 ++++++++++++++++++- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/mcp-core/src/internal/formatting.ts b/packages/mcp-core/src/internal/formatting.ts index 4966aeac1..7b62bc860 100644 --- a/packages/mcp-core/src/internal/formatting.ts +++ b/packages/mcp-core/src/internal/formatting.ts @@ -47,7 +47,7 @@ import { getAutofixArtifactSummaries, getStatusDisplayName, isTerminalStatus, - SEER_OUT_OF_CREDITS_MESSAGE, + SEER_CONTINUE_OUT_OF_CREDITS_MESSAGE, } from "./tool-helpers/seer"; import { formatToolCallInstruction } from "./tool-helpers/tool-call-formatting"; import { isPlainObject } from "./type-guards"; @@ -2202,7 +2202,7 @@ function formatSeerSummary(autofixState: AutofixRunState | undefined): string { parts.push(""); parts.push("**Status:** Analysis failed."); if (autofix.failure_reason === "out_of_credits") { - parts.push(SEER_OUT_OF_CREDITS_MESSAGE); + parts.push(SEER_CONTINUE_OUT_OF_CREDITS_MESSAGE); } } else if (autofix.status === "awaiting_user_input") { parts.push(""); diff --git a/packages/mcp-core/src/internal/tool-helpers/seer.ts b/packages/mcp-core/src/internal/tool-helpers/seer.ts index 3918a8f2b..629000feb 100644 --- a/packages/mcp-core/src/internal/tool-helpers/seer.ts +++ b/packages/mcp-core/src/internal/tool-helpers/seer.ts @@ -86,19 +86,25 @@ export function getHumanInterventionGuidance(status: string): string { return ""; } -export const SEER_OUT_OF_CREDITS_MESSAGE = - "Your organization does not have enough Seer credits. Someone with billing access can turn on auto-reload in Sentry."; +export const SEER_START_OUT_OF_CREDITS_MESSAGE = + "Your organization does not have enough credits to start an Autofix run. Someone with billing access can turn on auto-reload in Sentry."; + +export const SEER_CONTINUE_OUT_OF_CREDITS_MESSAGE = + "Your organization does not have enough credits to continue your Autofix run. Someone with billing access can turn on auto-reload in Sentry."; + +const SEER_NO_QUOTA_MESSAGE = + "Your organization does not have enough Seer quota to start an Autofix run."; /** - * Explain a 402 from starting a run. Sentry sends the credits 402 as - * `{ detail }`. The quota 402 is a bare string the API client can't parse, - * so it gets the fallback. + * Explain a 402 from starting a run. Sentry sends the out-of-credits 402 as + * `{ detail }` and the out-of-quota 402 as a bare string, which the API client + * can't parse. */ export function getSeerPaymentRequiredMessage(responseBody: unknown): string { if (isPlainObject(responseBody) && typeof responseBody.detail === "string") { - return responseBody.detail; + return SEER_START_OUT_OF_CREDITS_MESSAGE; } - return "Your organization does not have enough Seer budget to start an analysis."; + return SEER_NO_QUOTA_MESSAGE; } /** @@ -108,7 +114,7 @@ export function getFailureReasonGuidance( failureReason: string | null | undefined, ): string { if (failureReason === "out_of_credits") { - return `\n${SEER_OUT_OF_CREDITS_MESSAGE}\n`; + return `\n${SEER_CONTINUE_OUT_OF_CREDITS_MESSAGE}\n`; } return ""; } 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 7848b2ae0..1be03f4b0 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 @@ -446,12 +446,12 @@ describe("analyze_issue_with_seer", () => { detail: "Your organization does not have enough credits to start an Autofix run.", }, - "Your organization does not have enough credits to start an Autofix run.", + "Your organization does not have enough credits to start an Autofix run. Someone with billing access can turn on auto-reload in Sentry.", ], [ "out of quota", "No budget for Seer Autofix.", - "Your organization does not have enough Seer budget to start an analysis.", + "Your organization does not have enough Seer quota to start an Autofix run.", ], ])( "explains why the run did not start when Sentry answers 402 (%s)", @@ -494,6 +494,42 @@ describe("analyze_issue_with_seer", () => { }, ); + it("explains a run that stopped because the org ran out of credits", async () => { + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-CREDITS/autofix/", + () => + HttpResponse.json({ + autofix: { + run_id: 456, + status: "error", + failure_reason: "out_of_credits", + blocks: [], + }, + }), + ), + ); + + const result = await analyzeIssueWithSeer.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + issueId: "CLOUDFLARE-MCP-CREDITS", + instruction: undefined, + }, + { + constraints: { organizationSlug: undefined }, + accessToken: "access-token", + userId: "1", + }, + ); + + expect(result).toContain("## Analysis Failed"); + expect(result).toContain( + "Your organization does not have enough credits to continue your Autofix run. Someone with billing access can turn on auto-reload in Sentry.", + ); + }); + it("returns unsupported message for metric issues without calling Seer", async () => { let autofixRequests = 0;