Skip to main content
Glama
bradcstevens

Copilot Studio Agent Direct Line MCP Server

by bradcstevens
retry.ts9.72 kB
/** * Retry logic with exponential backoff and advanced error handling */ import { AxiosError } from 'axios'; import type { RetryStrategy } from '../types/errors.js'; /** * Error classification */ export enum ErrorType { NETWORK = 'network', AUTHENTICATION = 'authentication', RATE_LIMIT = 'rate_limit', SERVER = 'server', CLIENT = 'client', UNKNOWN = 'unknown', } /** * Classify an error * @param error - Error to classify * @returns Error type */ export function classifyError(error: unknown): ErrorType { // Check if it's an Axios error using the isAxiosError property (works with mocks too) if (error && typeof error === 'object' && 'isAxiosError' in error && error.isAxiosError) { const axiosError = error as AxiosError; if (!axiosError.response) { return ErrorType.NETWORK; } const status = axiosError.response.status; if (status === 401 || status === 403) { return ErrorType.AUTHENTICATION; } if (status === 429) { return ErrorType.RATE_LIMIT; } if (status >= 500) { return ErrorType.SERVER; } if (status >= 400) { return ErrorType.CLIENT; } } return ErrorType.UNKNOWN; } /** * Check if an error is retryable * @param error - Error to check * @returns True if error is retryable */ export function isRetryableError(error: unknown): boolean { const errorType = classifyError(error); return ( errorType === ErrorType.NETWORK || errorType === ErrorType.RATE_LIMIT || errorType === ErrorType.SERVER ); } /** * Retry options */ export interface RetryOptions { maxRetries?: number; baseDelay?: number; maxDelay?: number; onRetry?: (attempt: number, error: unknown) => void; } /** * Calculate exponential backoff delay * @param attempt - Current attempt number (0-based) * @param baseDelay - Base delay in milliseconds * @param maxDelay - Maximum delay in milliseconds * @returns Delay in milliseconds */ export function calculateBackoff( attempt: number, baseDelay: number = 1000, maxDelay: number = 4000 ): number { const delay = baseDelay * Math.pow(2, attempt); return Math.min(delay, maxDelay); } /** * Sleep for specified milliseconds * @param ms - Milliseconds to sleep */ export function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Retry a function with exponential backoff * @param fn - Function to retry * @param options - Retry options * @returns Result of function * @throws Last error if all retries fail */ export async function retryWithBackoff<T>( fn: () => Promise<T>, options: RetryOptions = {} ): Promise<T> { const { maxRetries = 3, baseDelay = 1000, maxDelay = 4000, onRetry } = options; let lastError: unknown; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Don't retry if it's the last attempt or error is not retryable if (attempt === maxRetries || !isRetryableError(error)) { break; } // Calculate backoff delay const delay = calculateBackoff(attempt, baseDelay, maxDelay); // Call retry callback if provided if (onRetry) { onRetry(attempt + 1, error); } console.warn( `[Retry] Attempt ${attempt + 1}/${maxRetries} failed, retrying in ${delay}ms...`, { error: error instanceof Error ? error.message : String(error), errorType: classifyError(error), } ); // Wait before retrying await sleep(delay); } } throw lastError; } /** * Default retry strategy for general operations */ export const DEFAULT_RETRY_STRATEGY: RetryStrategy = { maxRetries: 3, initialDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, retryableStatuses: [408, 429, 500, 502, 503, 504], }; /** * Add jitter to delay to prevent thundering herd * Uses full jitter: random value between 0 and calculated delay */ function addJitter(delay: number): number { return Math.random() * delay; } /** * Calculate delay for next retry with exponential backoff */ function calculateDelayWithStrategy(attempt: number, strategy: RetryStrategy): number { const exponentialDelay = strategy.initialDelay * Math.pow(strategy.backoffMultiplier, attempt); const cappedDelay = Math.min(exponentialDelay, strategy.maxDelay); return addJitter(cappedDelay); } /** * Check if error is retryable based on strategy */ function isRetryableWithStrategy(error: Error, strategy: RetryStrategy): boolean { // Check if error has an HTTP status code if ('status' in error && typeof error.status === 'number') { return strategy.retryableStatuses?.includes(error.status) ?? false; } // Check for network-related errors if ( error.name === 'NetworkError' || error.message.includes('ECONNRESET') || error.message.includes('ETIMEDOUT') || error.message.includes('ECONNREFUSED') ) { return true; } return false; } /** * Extended retry options */ export interface ExtendedRetryOptions { strategy?: Partial<RetryStrategy>; onRetry?: (attempt: number, error: Error, delay: number) => void; shouldRetry?: (error: Error) => boolean; } /** * Retry with custom strategy and jitter * * @param fn - Async function to retry * @param options - Extended retry options * @returns Result of the function * @throws Last error if all retries exhausted */ export async function retryWithStrategy<T>( fn: () => Promise<T>, options: ExtendedRetryOptions = {} ): Promise<T> { const strategy: RetryStrategy = { ...DEFAULT_RETRY_STRATEGY, ...options.strategy, }; let lastError: Error | undefined; let attempt = 0; while (attempt <= strategy.maxRetries) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Check if we should retry this error const shouldRetryError = options.shouldRetry?.(lastError) ?? isRetryableWithStrategy(lastError, strategy); // If not retryable or we've exhausted retries, throw if (!shouldRetryError || attempt >= strategy.maxRetries) { throw lastError; } // Calculate delay and wait const delay = calculateDelayWithStrategy(attempt, strategy); // Call retry callback if provided options.onRetry?.(attempt + 1, lastError, delay); console.warn( `[RetryStrategy] Attempt ${attempt + 1}/${strategy.maxRetries} failed, retrying in ${Math.round(delay)}ms...`, { error: lastError.message, errorName: lastError.name, } ); await sleep(delay); attempt++; } } // Should never reach here, but TypeScript requires it throw lastError || new Error('Retry failed with unknown error'); } /** * Retry specifically for OAuth token operations * Includes OAuth-specific error handling */ export async function retryOAuthOperation<T>( fn: () => Promise<T>, options: Omit<ExtendedRetryOptions, 'strategy'> & { strategy?: Partial<RetryStrategy>; } = {} ): Promise<T> { const oauthStrategy: RetryStrategy = { maxRetries: 3, initialDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, retryableStatuses: [500, 502, 503, 504], // Don't retry auth errors (401, 403) ...options.strategy, }; return retryWithStrategy(fn, { ...options, strategy: oauthStrategy, shouldRetry: (error) => { // Don't retry if custom shouldRetry returns false if (options.shouldRetry && !options.shouldRetry(error)) { return false; } // Don't retry authentication/authorization errors if ( error.name === 'AuthenticationError' || error.name === 'AuthorizationError' || error.name === 'OAuthError' || ('status' in error && (error.status === 401 || error.status === 403)) ) { return false; } // Don't retry invalid_grant errors (user needs to re-authenticate) if (error.message.includes('invalid_grant')) { return false; } // Retry network and server errors return isRetryableWithStrategy(error, oauthStrategy); }, }); } /** * Retry with circuit breaker pattern * Adds circuit breaker awareness to retry logic * * @param fn - Async function to retry * @param options - Retry options * @param circuitBreakerCheck - Optional function to check circuit breaker state * @returns Result of the function */ export async function retryWithCircuitBreaker<T>( fn: () => Promise<T>, options: ExtendedRetryOptions = {}, circuitBreakerCheck?: () => boolean ): Promise<T> { // Wrap function with circuit breaker check const wrappedFn = async (): Promise<T> => { // Check circuit breaker before each attempt if (circuitBreakerCheck && !circuitBreakerCheck()) { const error = new Error('Circuit breaker is open'); error.name = 'CircuitBreakerError'; throw error; } return fn(); }; return retryWithStrategy(wrappedFn, options); } /** * Retry with deadline - stop retrying after specified time */ export async function retryWithDeadline<T>( fn: () => Promise<T>, deadlineMs: number, options: ExtendedRetryOptions = {} ): Promise<T> { const startTime = Date.now(); return retryWithStrategy(fn, { ...options, shouldRetry: (error) => { // Check if we've exceeded deadline if (Date.now() - startTime >= deadlineMs) { return false; } // Use custom shouldRetry if provided if (options.shouldRetry) { return options.shouldRetry(error); } return true; }, }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bradcstevens/copilot-studio-agent-direct-line-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server