@aashari/mcp-server-aws-sso
Version:
Node.js/TypeScript MCP server for AWS Single Sign-On (SSO). Enables AI systems (LLMs) with tools to initiate SSO login (device auth flow), list accounts/roles, and securely execute AWS CLI commands using temporary credentials. Streamlines AI interaction w
121 lines (120 loc) • 4.92 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.withRetry = withRetry;
/**
* Retry utility with exponential backoff
*
* This module provides functions to handle retries with exponential backoff for AWS API requests
* that might return 429 (Too Many Requests) responses.
*/
const logger_util_js_1 = require("./logger.util.js");
const logger = logger_util_js_1.Logger.forContext('utils/retry.util.ts');
/**
* Default retry options
*/
const defaultRetryOptions = {
maxRetries: 5,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffFactor: 2.0,
retryCondition: (error) => {
// Default condition checks for 429 (TooManyRequests) errors
if (error && typeof error === 'object') {
if ('$metadata' in error && typeof error.$metadata === 'object') {
const metadata = error.$metadata;
return metadata.httpStatusCode === 429;
}
if ('name' in error && error.name === 'TooManyRequestsException') {
return true;
}
}
return false;
},
};
/**
* Sleep for a specified number of milliseconds
* @param ms Milliseconds to sleep
* @returns Promise that resolves after the specified time
*/
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Calculate delay for the next retry using exponential backoff with jitter
* @param attempt Current attempt number (0-based)
* @param options Retry options
* @returns Delay in milliseconds
*/
function calculateBackoff(attempt, options) {
// Calculate exponential backoff: initialDelay * (backoffFactor ^ attempt)
const exponentialDelay = options.initialDelayMs * Math.pow(options.backoffFactor, attempt);
// Apply jitter (±20%) to prevent thundering herd issues
const jitterFactor = 0.8 + Math.random() * 0.4; // 0.8 to 1.2
// Cap the delay at maxDelayMs
return Math.min(exponentialDelay * jitterFactor, options.maxDelayMs);
}
/**
* Execute an async operation with retry logic
* @param operation Async function to execute
* @param options Retry options
* @returns Promise resolving to the operation result
* @throws The last error encountered after all retries are exhausted
*/
async function withRetry(operation, options) {
const methodLogger = logger.forMethod('withRetry');
const mergedOptions = {
...defaultRetryOptions,
...options,
};
let lastError;
let attempt = 0;
while (attempt <= mergedOptions.maxRetries) {
try {
if (attempt > 0) {
methodLogger.debug(`Retry attempt ${attempt} of ${mergedOptions.maxRetries}`);
}
// Execute the operation
return await operation();
}
catch (error) {
lastError = error;
attempt++;
// Check if this is an authorization_pending error, which is expected during OAuth device flow
const isAuthorizationPending = error &&
typeof error === 'object' &&
'error' in error &&
error.error === 'authorization_pending';
// Check if we should retry based on error and max retries
if (attempt > mergedOptions.maxRetries ||
!mergedOptions.retryCondition(error)) {
// Either we're out of retries or this error doesn't qualify for retry
if (attempt > mergedOptions.maxRetries) {
methodLogger.warn(`Max retry attempts (${mergedOptions.maxRetries}) reached. Giving up.`);
}
else {
// Only log as debug for expected errors like authorization_pending
if (isAuthorizationPending) {
methodLogger.debug('Authorization pending, not a retry condition.');
}
else {
methodLogger.debug('Error does not match retry conditions. Not retrying.');
}
}
throw error;
}
// Calculate delay for next retry
const delayMs = calculateBackoff(attempt - 1, mergedOptions);
// Don't log authorization_pending as errors since they're expected
if (isAuthorizationPending) {
methodLogger.debug(`Authorization pending. Retrying in ${(delayMs / 1000).toFixed(2)}s (attempt ${attempt}/${mergedOptions.maxRetries})`);
}
else {
methodLogger.info(`Request failed with retriable error. Retrying in ${(delayMs / 1000).toFixed(2)}s (attempt ${attempt}/${mergedOptions.maxRetries})`, { error });
}
// Wait before next retry
await sleep(delayMs);
}
}
// This should never be reached because the last failed attempt will throw
throw lastError;
}