From d6462bccfdd2a7681569971ad56b34e23cea94c5 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 5 Nov 2026 13:51:48 -0800 Subject: [PATCH 1/2] 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/2] 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.