diff --git a/src/mcp/github-file-ops-server.ts b/src/mcp/github-file-ops-server.ts index 4d61621b6..1845b06f3 100644 --- a/src/mcp/github-file-ops-server.ts +++ b/src/mcp/github-file-ops-server.ts @@ -8,7 +8,7 @@ import { resolve } from "path"; import { constants } from "fs"; import fetch from "node-fetch"; import { GITHUB_API_URL } from "../github/api/config"; -import { retryWithBackoff } from "../utils/retry"; +import { retryWithBackoff, NonRetryableError } from "../utils/retry"; import { validatePathWithinRepo } from "./path-validation"; type GitHubRef = { @@ -399,14 +399,11 @@ server.tool( throw permissionError; } - // For other errors, use the original message + // For non-403 errors, fail immediately without retry const error = new Error( `Failed to update reference: ${updateRefResponse.status} - ${errorText}`, ); - - // For non-403 errors, fail immediately without retry - console.error("Non-retryable error:", updateRefResponse.status); - throw error; + throw new NonRetryableError(error.message, error); } }, { @@ -615,14 +612,11 @@ server.tool( throw permissionError; } - // For other errors, use the original message + // For non-403 errors, fail immediately without retry const error = new Error( `Failed to update reference: ${updateRefResponse.status} - ${errorText}`, ); - - // For non-403 errors, fail immediately without retry - console.error("Non-retryable error:", updateRefResponse.status); - throw error; + throw new NonRetryableError(error.message, error); } }, { diff --git a/src/utils/retry.ts b/src/utils/retry.ts index bdcb54132..f029d6a1d 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -5,6 +5,21 @@ export type RetryOptions = { backoffFactor?: number; }; +/** + * Error class for errors that should not be retried. + * When thrown inside a retryWithBackoff operation, the retry loop + * will immediately rethrow without further attempts. + */ +export class NonRetryableError extends Error { + constructor( + message: string, + public readonly cause?: Error, + ) { + super(message); + this.name = "NonRetryableError"; + } +} + export async function retryWithBackoff( operation: () => Promise, options: RetryOptions = {}, @@ -24,6 +39,11 @@ export async function retryWithBackoff( console.log(`Attempt ${attempt} of ${maxAttempts}...`); return await operation(); } catch (error) { + // Non-retryable errors should fail immediately without further retries + if (error instanceof NonRetryableError) { + throw error.cause ?? error; + } + lastError = error instanceof Error ? error : new Error(String(error)); console.error(`Attempt ${attempt} failed:`, lastError.message);