Skip to main content
Glama
freshbooks-client.ts5.56 kB
/** * FreshBooks Client Wrapper * * Wraps the FreshBooks SDK with: * - Automatic token refresh * - Error normalization to MCP format * - Rate limiting with exponential backoff * - Request logging */ import { Client } from '@freshbooks/api'; import { FreshBooksOAuth } from '../auth/oauth.js'; import { handleError, MCPErrorCode } from '../errors/index.js'; import { logger } from '../utils/logger.js'; import type { MCPError, ErrorContext } from '../errors/types.js'; /** * Wrapper around FreshBooks SDK Client * * Provides automatic token refresh, error handling, and logging. */ export class FreshBooksClientWrapper { private oauth: FreshBooksOAuth; private client: Client | null = null; private currentAccountId: string | null = null; // Rate limiting configuration private static readonly MAX_RETRIES = 3; private static readonly BASE_BACKOFF_MS = 1000; constructor(oauth: FreshBooksOAuth) { this.oauth = oauth; } /** * Get or create FreshBooks client with valid token */ private async getClient(): Promise<Client> { try { // Get valid access token (auto-refreshes if needed) const accessToken = await this.oauth.getValidToken(); // Always create a new client instance with the current token // The FreshBooks SDK requires the token to be provided at construction this.client = new Client(accessToken, { apiUrl: 'https://api.freshbooks.com', }); logger.debug('Created new FreshBooks client'); return this.client; } catch (error) { logger.error('Failed to get FreshBooks client', error); throw handleError(error, { operation: 'getClient' }); } } /** * Execute an API call with automatic retry and error handling * * @param operation Name of the operation for logging * @param apiCall Function that makes the API call * @returns API response */ async executeWithRetry<T>( operation: string, apiCall: (client: Client) => Promise<T> ): Promise<T> { let lastError: Error | unknown; const requestId = this.generateRequestId(); for (let attempt = 1; attempt <= FreshBooksClientWrapper.MAX_RETRIES; attempt++) { try { logger.debug(`Executing ${operation}`, { requestId, attempt, accountId: this.currentAccountId, }); const startTime = Date.now(); const client = await this.getClient(); const result = await apiCall(client); const duration = Date.now() - startTime; logger.info(`${operation} completed`, { requestId, duration, attempt, }); return result; } catch (error) { lastError = error; // Check if error is recoverable and should be retried const mcpError = this.normalizeError(error, operation, requestId); const shouldRetry = this.shouldRetry(mcpError, attempt); logger.warn(`${operation} failed`, { requestId, attempt, errorCode: mcpError.code, recoverable: mcpError.data.recoverable, willRetry: shouldRetry, }); if (!shouldRetry) { throw mcpError; } // Exponential backoff before retry const backoffMs = FreshBooksClientWrapper.BASE_BACKOFF_MS * Math.pow(2, attempt - 1); await this.sleep(backoffMs); } } // All retries exhausted logger.error(`${operation} failed after ${FreshBooksClientWrapper.MAX_RETRIES} attempts`, lastError, { requestId, }); throw this.normalizeError(lastError, operation, requestId); } /** * Set the active account ID for API calls */ setAccountId(accountId: string): void { this.currentAccountId = accountId; logger.debug('Set active account', { accountId }); } /** * Get the current account ID */ getAccountId(): string | null { return this.currentAccountId; } /** * Normalize API errors to MCP format */ private normalizeError(error: unknown, operation: string, requestId: string): MCPError { const context: ErrorContext = { operation, requestId, }; if (this.currentAccountId !== null) { context.accountId = this.currentAccountId; } return handleError(error, context); } /** * Determine if an error should be retried */ private shouldRetry(error: MCPError, attempt: number): boolean { // Don't retry if we've exhausted attempts if (attempt >= FreshBooksClientWrapper.MAX_RETRIES) { return false; } // Only retry recoverable errors if (!error.data.recoverable) { return false; } // Retry rate limit and network errors const retryableCodes = [ MCPErrorCode.RATE_LIMITED, MCPErrorCode.NETWORK_ERROR, MCPErrorCode.TIMEOUT, MCPErrorCode.SERVICE_UNAVAILABLE, ]; return retryableCodes.includes(error.code); } /** * Sleep for specified milliseconds */ private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Generate unique request ID for tracking */ private generateRequestId(): string { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 8); return `req_${timestamp}_${random}`; } /** * Access the underlying FreshBooks client * Use with caution - prefer executeWithRetry for automatic error handling */ async getUnderlyingClient(): Promise<Client> { return this.getClient(); } }

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/Good-Samaritan-Software-LLC/freshbooks-mcp'

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