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
23 changes: 23 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ import {
RepositoryListSchema,
SearchAgentStartSchema,
SearchAgentStateSchema,
SeerCreditCheckSchema,
SentryAppComponentListSchema,
SentryAppExternalRequestOptionsSchema,
SentryAppInstallationListSchema,
Expand Down Expand Up @@ -254,6 +255,7 @@ import type {
ReplayList,
SearchAgentStart,
SearchAgentState,
SeerCreditOperation,
SentryAppComponentList,
SentryAppExternalRequestOptions,
SentryAppInstallationList,
Expand Down Expand Up @@ -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<boolean> {
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.
*
Expand Down
23 changes: 23 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp-core/src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import type {
AutofixRunStateSchema,
SearchAgentStartSchema,
SearchAgentStateSchema,
SeerCreditOperationSchema,
ClientKeyListSchema,
ClientKeySchema,
CommitListSchema,
Expand Down Expand Up @@ -243,6 +244,7 @@ export type AutofixRun = z.infer<typeof AutofixRunSchema>;
export type AutofixRunState = z.infer<typeof AutofixRunStateSchema>;
export type SearchAgentStart = z.infer<typeof SearchAgentStartSchema>;
export type SearchAgentState = z.infer<typeof SearchAgentStateSchema>;
export type SeerCreditOperation = z.infer<typeof SeerCreditOperationSchema>;
export type AssignedTo = z.infer<typeof AssignedToSchema>;
export type ReplayDetails = z.infer<typeof ReplayDetailsSchema>;
export type ReplayList = z.infer<typeof ReplayListResponseSchema>["data"];
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-core/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.");
Expand Down
49 changes: 47 additions & 2 deletions packages/mcp-core/src/internal/tool-helpers/seer.ts
Original file line number Diff line number Diff line change
@@ -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<AutofixRunState["autofix"]>;

Expand Down Expand Up @@ -85,6 +91,45 @@ 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.";

/**
* 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<boolean> {
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, "&amp;")
Expand Down
14 changes: 14 additions & 0 deletions packages/mcp-core/src/tools/catalog/analyze-issue-with-seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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";
}
Expand Down Expand Up @@ -215,6 +226,9 @@ export default defineTool({

if (status !== "completed") {
output += `\n**Status**: ${status}\n`;
output += getFailureReasonGuidance(
autofixState.autofix.failure_reason,
);
output += getHumanInterventionGuidance(status);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/mcp-server-mocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
Loading