Skip to main content
Glama
dennisonbertram

Brex MCP Server

client.ts39.4 kB
/** * @file Brex API Client * @version 1.0.0 * @status STABLE - DO NOT MODIFY WITHOUT TESTS * @lastModified 2024-03-19 * * Brex API client implementation * * IMPORTANT: * - Add tests for any new API endpoints * - Handle rate limiting and errors appropriately * * Functionality: * - Fetch accounts and transactions * - Fetch and manage expenses * - Upload and manage receipts * - Manage budgets and spend limits * - Handle authentication and errors * - Implement rate limiting */ import axios, { AxiosInstance } from 'axios'; import { appConfig } from '../../config/index.js'; import { logError, logInfo, logWarn, logDebug } from '../../utils/logger.js'; import { BrexAccount, BrexTransaction, BrexPaginatedResponse } from './types.js'; import { CardAccount, CashAccount, PageCashAccount, PageStatement, PageCardTransaction, PageCashTransaction } from './transactions-types.js'; import { Expense, SimpleExpense, ExpensesResponse, ListExpensesParams, UpdateExpenseRequest, ReceiptMatchRequest, ReceiptUploadRequest, CreateAsyncFileUploadResponse, ExpenseStatus } from './expenses-types.js'; import { Budget, BudgetListParams, BudgetsResponse, CreateBudgetRequest, UpdateBudgetRequest, SpendLimit, SpendLimitListParams, SpendLimitsResponse, CreateSpendLimitRequest, UpdateSpendLimitRequest, BudgetProgram, BudgetProgramListParams, BudgetProgramsResponse, CreateBudgetProgramRequest, UpdateBudgetProgramRequest } from '../../models/budget.js'; export class BrexClient { private client: AxiosInstance; constructor() { if (!appConfig.brex.apiKey) { logWarn('Brex API key is not set. API requests will fail with 401 Unauthorized.'); } this.client = axios.create({ baseURL: appConfig.brex.apiUrl, headers: { 'Authorization': `Bearer ${appConfig.brex.apiKey}`, 'Content-Type': 'application/json', 'User-Agent': 'mcp-brex/0.2.2 (+https://github.com/dennisonbertram/mcp-brex)' }, }); // Attach a request interceptor to add Idempotency-Key for writes if missing this.client.interceptors.request.use((config) => { const method = (config.method || 'get').toUpperCase(); // Generate a simple idempotency key for POST/PUT if caller didn't specify one if ((method === 'POST' || method === 'PUT') && config.headers) { const headers = config.headers as Record<string, string>; if (!('Idempotency-Key' in headers) && !('idempotency-key' in headers)) { try { // Use crypto.randomUUID when available; otherwise, fallback const key = (globalThis as any).crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`; headers['Idempotency-Key'] = key; } catch { headers['Idempotency-Key'] = `${Date.now()}-${Math.random().toString(36).slice(2)}`; } } } return config; }); // Add response interceptor for logging this.client.interceptors.response.use( (response) => { logInfo('Brex API request successful', { method: response.config.method, url: response.config.url, status: response.status, }); return response; }, async (error) => { // Handle rate limiting with simple retry/backoff const status = error?.response?.status; const cfg: any = error?.config || {}; if (status === 429 && cfg) { cfg.__retryCount = cfg.__retryCount || 0; if (cfg.__retryCount < 2) { cfg.__retryCount += 1; let delayMs = 1000; const ra = error.response.headers?.['retry-after']; if (ra) { const asInt = parseInt(ra, 10); if (!Number.isNaN(asInt)) { delayMs = asInt * 1000; } else { const raDate = Date.parse(ra); if (!Number.isNaN(raDate)) { delayMs = Math.max(0, raDate - Date.now()); } } } logWarn(`Brex API rate limited (429). Retrying in ${delayMs}ms...`, { url: cfg.url, attempt: cfg.__retryCount }); await new Promise((res) => setTimeout(res, delayMs)); return this.client.request(cfg); } } if (axios.isAxiosError(error) && error.response?.status === 401) { logError(`Brex API authentication failed: Invalid or expired API key. Check your BREX_API_KEY in .env file.`, { method: error.config?.method, url: error.config?.url, status: error.response?.status, }); } else { logError(error, { method: error.config?.method, url: error.config?.url, status: error.response?.status, }); } throw error; } ); } // Accounts API async getAccounts(): Promise<BrexPaginatedResponse<BrexAccount>> { try { logDebug('Fetching all accounts (cash and card)'); // Fetch both cash and card accounts in parallel const [cashAccountsResponse, cardAccounts] = await Promise.all([ this.client.get('/v2/accounts/cash'), this.getCardAccounts() ]); const accounts: BrexAccount[] = []; // Add cash accounts if (cashAccountsResponse.data && cashAccountsResponse.data.items) { for (const cashAccount of cashAccountsResponse.data.items) { accounts.push({ id: cashAccount.id, name: cashAccount.name, type: 'CASH' as const, currency: cashAccount.current_balance.currency || 'USD', balance: { amount: cashAccount.current_balance.amount, currency: cashAccount.current_balance.currency || 'USD' }, status: cashAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE' }); } } // Add card accounts for (const cardAccount of cardAccounts) { accounts.push({ id: cardAccount.id, name: `Card Account ${cardAccount.id}`, type: 'CARD' as const, currency: cardAccount.current_balance?.currency || 'USD', balance: { amount: cardAccount.current_balance?.amount || 0, currency: cardAccount.current_balance?.currency || 'USD' }, status: cardAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE' }); } return { items: accounts, hasMore: false // We fetched all accounts }; } catch (error) { this.handleApiError(error, 'GET', '/accounts'); throw error; } } async getAccount(accountId: string): Promise<BrexAccount> { try { // Determine account type based on ID prefix if (accountId.startsWith('dpacc_')) { // This is a cash account - use the cash endpoint logDebug(`Fetching cash account ${accountId}`); const response = await this.client.get(`/v2/accounts/cash/${accountId}`); const cashAccount = response.data; // Transform to BrexAccount format return { id: cashAccount.id, name: cashAccount.name, type: 'CASH' as const, currency: cashAccount.current_balance.currency || 'USD', balance: { amount: cashAccount.current_balance.amount, currency: cashAccount.current_balance.currency || 'USD' }, status: cashAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE' }; } else if (accountId.startsWith('cuacc_')) { // This is a card account - get from the card accounts list logDebug(`Fetching card account ${accountId}`); const cardAccounts = await this.getCardAccounts(); const cardAccount = cardAccounts.find(account => account.id === accountId); if (!cardAccount) { throw new Error(`Card account ${accountId} not found`); } // Transform to BrexAccount format return { id: cardAccount.id, name: `Card Account ${cardAccount.id}`, // Card accounts don't have names type: 'CARD' as const, currency: cardAccount.current_balance?.currency || 'USD', balance: { amount: cardAccount.current_balance?.amount || 0, currency: cardAccount.current_balance?.currency || 'USD' }, status: cardAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE' }; } else { throw new Error(`Unknown account ID format: ${accountId}. Expected account ID to start with 'dpacc_' (cash) or 'cuacc_' (card)`); } } catch (error) { this.handleApiError(error, 'GET', `/account/${accountId}`); throw error; } } /** * Get card accounts * @returns List of card accounts */ async getCardAccounts(): Promise<CardAccount[]> { try { logDebug('Fetching card accounts from Brex API'); const response = await this.client.get('/v2/accounts/card'); logDebug(`Successfully fetched ${response.data.length} card accounts`); return response.data; } catch (error) { this.handleApiError(error, 'GET', '/v2/accounts/card'); throw error; } } /** * Get primary card account statements * @param cursor Pagination cursor * @param limit Number of items per page * @returns Paginated list of statements */ async getPrimaryCardStatements(cursor?: string, limit?: number): Promise<PageStatement> { try { const params: Record<string, unknown> = {}; if (cursor) params.cursor = cursor; if (limit) params.limit = limit; logDebug('Fetching primary card account statements from Brex API'); const response = await this.client.get('/v2/accounts/card/primary/statements', { params }); logDebug(`Successfully fetched ${response.data.items.length} primary card statements`); return response.data; } catch (error) { this.handleApiError(error, 'GET', '/v2/accounts/card/primary/statements'); throw error; } } /** * Get cash accounts * @returns Paginated list of cash accounts */ async getCashAccounts(): Promise<PageCashAccount> { try { logDebug('Fetching cash accounts from Brex API'); const response = await this.client.get('/v2/accounts/cash'); logDebug(`Successfully fetched ${response.data.items.length} cash accounts`); return response.data; } catch (error) { this.handleApiError(error, 'GET', '/v2/accounts/cash'); throw error; } } /** * Get primary cash account * @returns Primary cash account */ async getPrimaryCashAccount(): Promise<CashAccount> { try { logDebug('Fetching primary cash account from Brex API'); const response = await this.client.get('/v2/accounts/cash/primary'); logDebug('Successfully fetched primary cash account'); return response.data; } catch (error) { this.handleApiError(error, 'GET', '/v2/accounts/cash/primary'); throw error; } } /** * Get cash account by ID * @param id Account ID * @returns Cash account */ async getCashAccountById(id: string): Promise<CashAccount> { try { logDebug(`Fetching cash account ${id} from Brex API`); const response = await this.client.get(`/v2/accounts/cash/${id}`); logDebug(`Successfully fetched cash account ${id}`); return response.data; } catch (error) { this.handleApiError(error, 'GET', `/v2/accounts/cash/${id}`); throw error; } } /** * Get cash account statements * @param id Account ID * @param cursor Pagination cursor * @param limit Number of items per page * @returns Paginated list of statements */ async getCashAccountStatements(id: string, cursor?: string, limit?: number): Promise<PageStatement> { try { const params: Record<string, unknown> = {}; if (cursor) params.cursor = cursor; if (limit) params.limit = limit; logDebug(`Fetching statements for cash account ${id} from Brex API`); const response = await this.client.get(`/v2/accounts/cash/${id}/statements`, { params }); logDebug(`Successfully fetched ${response.data.items.length} statements for cash account ${id}`); return response.data; } catch (error) { this.handleApiError(error, 'GET', `/v2/accounts/cash/${id}/statements`); throw error; } } /** * Get card transactions * @param options Options for fetching card transactions * @returns Paginated list of card transactions */ async getCardTransactions(options?: { cursor?: string; limit?: number; user_ids?: string[]; }): Promise<PageCardTransaction> { try { const params: Record<string, unknown> = {}; if (options) { if (options.cursor) params.cursor = options.cursor; if (options.limit) params.limit = options.limit; if (options.user_ids && options.user_ids.length > 0) params.user_ids = options.user_ids; // Note: posted_at_start and expand are NOT supported for card transactions; omit to avoid 400s } logDebug('Fetching card transactions from Brex API'); const response = await this.client.get('/v2/transactions/card/primary', { params }); logDebug(`Successfully fetched ${response.data.items.length} card transactions`); return response.data; } catch (error) { this.handleApiError(error, 'GET', '/v2/transactions/card/primary'); throw error; } } /** * Get cash transactions * @param id Account ID * @param options Options for fetching cash transactions * @returns Paginated list of cash transactions */ async getCashTransactions(id: string, options?: { cursor?: string; limit?: number; }): Promise<PageCashTransaction> { try { const params: Record<string, unknown> = {}; if (options) { if (options.cursor) params.cursor = options.cursor; if (options.limit) params.limit = options.limit; // Note: posted_at_start and expand are NOT supported for cash transactions; omit to avoid 400s } logDebug(`Fetching cash transactions for account ${id} from Brex API`); const response = await this.client.get(`/v2/transactions/cash/${id}`, { params }); logDebug(`Successfully fetched ${response.data.items.length} cash transactions for account ${id}`); return response.data; } catch (error) { this.handleApiError(error, 'GET', `/v2/transactions/cash/${id}`); throw error; } } // Transactions API async getTransactions( accountId: string, cursor?: string, limit: number = 50 ): Promise<BrexPaginatedResponse<BrexTransaction>> { try { logDebug(`Fetching transactions for account ${accountId}`); if (accountId.startsWith('dpacc_')) { // Cash account - use cash transactions endpoint const response = await this.getCashTransactions(accountId, { cursor, limit }); // Transform to BrexTransaction format const transactions: BrexTransaction[] = response.items.map((tx) => ({ id: tx.id, date: tx.posted_at_date, description: tx.description, amount: { amount: tx.amount?.amount || 0, currency: tx.amount?.currency || 'USD' }, status: 'POSTED' as const, type: (tx.amount?.amount || 0) >= 0 ? 'CREDIT' : 'DEBIT', category: tx.type, accountId: accountId })); return { items: transactions, nextCursor: response.next_cursor, hasMore: Boolean(response.next_cursor) }; } else if (accountId.startsWith('cuacc_')) { // Card account - use card transactions endpoint const response = await this.getCardTransactions({ cursor, limit }); // Transform to BrexTransaction format const transactions: BrexTransaction[] = response.items.map((tx) => ({ id: tx.id, date: tx.posted_at_date, description: tx.description, amount: { amount: tx.amount.amount, currency: tx.amount.currency || 'USD' }, status: 'POSTED' as const, type: 'DEBIT' as const, // Card transactions are typically debits category: tx.type, merchant: tx.merchant ? { name: tx.merchant.raw_descriptor, category: tx.merchant.mcc } : undefined, accountId: accountId })); return { items: transactions, nextCursor: response.next_cursor, hasMore: Boolean(response.next_cursor) }; } else { throw new Error(`Unknown account ID format: ${accountId}. Expected account ID to start with 'dpacc_' (cash) or 'cuacc_' (card)`); } } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } throw error; } } async getTransaction(_transactionId: string): Promise<BrexTransaction> { try { // This is a fallback implementation since we couldn't find a direct transaction endpoint throw new Error('Individual transaction endpoint not available'); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } throw error; } } // User API async getCurrentUser(): Promise<any> { try { const response = await this.client.get('/v2/users/me'); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } throw error; } } // Expenses API async getExpenses(params?: ListExpensesParams): Promise<ExpensesResponse> { try { logDebug('Fetching expenses from Brex API'); const query: Record<string, unknown> = {}; if (params) { if (params.expand !== undefined) query['expand[]'] = params.expand; if (params.user_id) query['user_id[]'] = params.user_id; if (params.parent_expense_id) query['parent_expense_id[]'] = params.parent_expense_id; if (params.budget_id) query['budget_id[]'] = params.budget_id; if (params.spending_entity_id) query['spending_entity_id[]'] = params.spending_entity_id; if (params.expense_type) query['expense_type[]'] = params.expense_type; if (params.status) query['status[]'] = params.status; if (params.payment_status) query['payment_status[]'] = params.payment_status; if (params.purchased_at_start) query.purchased_at_start = params.purchased_at_start; if (params.purchased_at_end) query.purchased_at_end = params.purchased_at_end; if (params.updated_at_start) query.updated_at_start = params.updated_at_start; if (params.updated_at_end) query.updated_at_end = params.updated_at_end; if (params.load_custom_fields !== undefined) query.load_custom_fields = params.load_custom_fields; if (params.cursor) query.cursor = params.cursor; if (params.limit) query.limit = params.limit; } const response = await this.client.get('/v1/expenses', { params: query }); const data = (response.data || {}) as { items?: unknown[]; next_cursor?: string }; const normalized = { items: Array.isArray(data.items) ? data.items : [], nextCursor: data.next_cursor || '', hasMore: Boolean(data.next_cursor) } as ExpensesResponse; return normalized; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } throw error; } } async getExpense(expenseId: string, params?: { expand?: string[], load_custom_fields?: boolean }): Promise<Expense> { try { logDebug(`Fetching expense ${expenseId} from Brex API`); const response = await this.client.get(`/v1/expenses/${expenseId}`, { params }); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } else if (axios.isAxiosError(error) && error.response?.status === 404) { throw new Error(`Expense with ID ${expenseId} not found`); } throw error; } } // Card Expenses API async getCardExpenses(params?: ListExpensesParams): Promise<ExpensesResponse> { try { logDebug('Fetching card expenses from Brex API'); try { // Build query with correct bracketed array keys - respect user's expand choices const query: Record<string, unknown> = {}; if (params?.expand !== undefined) query['expand[]'] = params.expand; if (params?.user_id) query['user_id[]'] = params.user_id; if (params?.parent_expense_id) query['parent_expense_id[]'] = params.parent_expense_id; if (params?.budget_id) query['budget_id[]'] = params.budget_id; if (params?.spending_entity_id) query['spending_entity_id[]'] = params.spending_entity_id; // Do NOT send expense_type to /card endpoint; filter is implied if (params?.status) query['status[]'] = params.status; if (params?.payment_status) query['payment_status[]'] = params.payment_status; if (params?.purchased_at_start) query.purchased_at_start = params.purchased_at_start; if (params?.purchased_at_end) query.purchased_at_end = params.purchased_at_end; if (params?.updated_at_start) query.updated_at_start = params.updated_at_start; if (params?.updated_at_end) query.updated_at_end = params.updated_at_end; if (params?.load_custom_fields !== undefined) query.load_custom_fields = params.load_custom_fields; if (params?.cursor) query.cursor = params.cursor; if (params?.limit) query.limit = params.limit; const response = await this.client.get('/v1/expenses/card', { params: query }); const data = (response.data || {}) as { items?: unknown[]; next_cursor?: string }; if (!data || !Array.isArray(data.items)) { logWarn('Unexpected response format from Brex Card Expenses API'); } const normalized = { items: Array.isArray(data.items) ? data.items : [], nextCursor: data.next_cursor || '', hasMore: Boolean(data.next_cursor) } as ExpensesResponse; return normalized; } catch (apiError) { logError(`Brex Card Expenses API error: ${apiError instanceof Error ? apiError.message : String(apiError)}`); // Return empty but valid response return { items: [], nextCursor: '', hasMore: false }; } } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { logWarn(`Brex API authentication failed: Please check your API key in the .env file`); } // Always return a valid response return { items: [], nextCursor: '', hasMore: false }; } } async getCardExpense(expenseId: string, params?: { expand?: string[], load_custom_fields?: boolean }): Promise<Expense> { try { logDebug(`Fetching card expense ${expenseId} from Brex API`); try { const response = await this.client.get(`/v1/expenses/card/${expenseId}`, { params }); // Handle potential response format issues if (!response.data || typeof response.data !== 'object') { logWarn(`Unexpected response format for card expense ${expenseId}`); // Create a minimal valid expense object return { id: expenseId, updated_at: new Date().toISOString(), status: ExpenseStatus.DRAFT, ...(response.data && typeof response.data === 'object' ? response.data : {}) }; } // Ensure the response has the required fields if (!response.data.id) { logDebug(`Adding id to card expense response: ${expenseId}`); response.data.id = expenseId; } if (!response.data.updated_at) { logDebug(`Adding updated_at to card expense response`); response.data.updated_at = new Date().toISOString(); } // Log the response structure logDebug(`Card expense response structure: ${JSON.stringify(Object.keys(response.data))}`) return response.data; } catch (apiError) { logError(`Brex Card Expense API error: ${apiError instanceof Error ? apiError.message : String(apiError)}`); // Return a valid minimal expense object return { id: expenseId, updated_at: new Date().toISOString(), status: ExpenseStatus.DRAFT, memo: `Error: ${apiError instanceof Error ? apiError.message : String(apiError)}` }; } } catch (error) { logWarn(`General error fetching card expense ${expenseId}: ${error instanceof Error ? error.message : String(error)}`); // Always return a valid expense object return { id: expenseId, updated_at: new Date().toISOString(), status: ExpenseStatus.DRAFT, memo: `Error: ${error instanceof Error ? error.message : String(error)}` }; } } async updateCardExpense(expenseId: string, updateData: UpdateExpenseRequest): Promise<SimpleExpense> { try { logDebug(`Updating card expense ${expenseId} in Brex API`); const response = await this.client.put(`/v1/expenses/card/${expenseId}`, updateData); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } else if (axios.isAxiosError(error) && error.response?.status === 404) { throw new Error(`Card expense with ID ${expenseId} not found`); } throw error; } } // Receipt API async createReceiptMatch(request: ReceiptMatchRequest): Promise<CreateAsyncFileUploadResponse> { try { logDebug('Creating receipt match in Brex API'); const response = await this.client.post('/v1/expenses/card/receipt_match', request); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } throw error; } } async createReceiptUpload(expenseId: string, request: ReceiptUploadRequest): Promise<CreateAsyncFileUploadResponse> { try { logDebug(`Creating receipt upload for expense ${expenseId} in Brex API`); const response = await this.client.post(`/v1/expenses/card/${expenseId}/receipt_upload`, request); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { throw new Error(`Brex API authentication failed: Please check your API key in the .env file`); } else if (axios.isAxiosError(error) && error.response?.status === 404) { throw new Error(`Card expense with ID ${expenseId} not found`); } throw error; } } // File upload helper async uploadFileToS3(url: string, fileContent: Buffer, contentType: string): Promise<void> { try { logDebug('Uploading file to pre-signed S3 URL'); await axios.put(url, fileContent, { headers: { 'Content-Type': contentType, }, }); logDebug('File uploaded successfully'); } catch (error) { logError('Failed to upload file to S3', { error }); throw new Error('Failed to upload file: ' + (error instanceof Error ? error.message : String(error))); } } // Budget API methods /** * Get a list of budgets * @param params Optional parameters for filtering and pagination * @returns Paginated list of budgets */ async getBudgets(params?: BudgetListParams): Promise<BudgetsResponse> { try { logDebug('Fetching budgets from Brex API', { params }); return await this.get<BudgetsResponse>('/v2/budgets', params as unknown as Record<string, any>); } catch (error) { logError(`Error fetching budgets: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Get a single budget by ID * @param budgetId Budget ID * @returns Budget object */ async getBudget(budgetId: string): Promise<Budget> { try { logDebug(`Fetching budget ${budgetId} from Brex API`); return await this.get<Budget>(`/v2/budgets/${budgetId}`); } catch (error) { logError(`Error fetching budget ${budgetId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Create a new budget * @param data Budget creation request data * @returns Created budget */ async createBudget(data: CreateBudgetRequest): Promise<Budget> { try { logDebug('Creating new budget in Brex API'); return await this.post<Budget>('/v2/budgets', data); } catch (error) { logError(`Error creating budget: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Update an existing budget * @param budgetId Budget ID * @param data Budget update request data * @returns Updated budget */ async updateBudget(budgetId: string, data: UpdateBudgetRequest): Promise<Budget> { try { logDebug(`Updating budget ${budgetId} in Brex API`); return await this.put<Budget>(`/v2/budgets/${budgetId}`, data); } catch (error) { logError(`Error updating budget ${budgetId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Archive a budget * @param budgetId Budget ID * @returns Archived budget */ async archiveBudget(budgetId: string): Promise<Budget> { try { logDebug(`Archiving budget ${budgetId} in Brex API`); return await this.post<Budget>(`/v2/budgets/${budgetId}/archive`, {}); } catch (error) { logError(`Error archiving budget ${budgetId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // Spend Limits API methods /** * Get a list of spend limits * @param params Optional parameters for filtering and pagination * @returns Paginated list of spend limits */ async getSpendLimits(params?: SpendLimitListParams): Promise<SpendLimitsResponse> { try { logDebug('Fetching spend limits from Brex API', { params }); return await this.get<SpendLimitsResponse>('/v2/spend_limits', params as unknown as Record<string, any>); } catch (error) { logError(`Error fetching spend limits: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Get a single spend limit by ID * @param spendLimitId Spend limit ID * @returns Spend limit object */ async getSpendLimit(spendLimitId: string): Promise<SpendLimit> { try { logDebug(`Fetching spend limit ${spendLimitId} from Brex API`); return await this.get<SpendLimit>(`/v2/spend_limits/${spendLimitId}`); } catch (error) { logError(`Error fetching spend limit ${spendLimitId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Create a new spend limit * @param data Spend limit creation request data * @returns Created spend limit */ async createSpendLimit(data: CreateSpendLimitRequest): Promise<SpendLimit> { try { logDebug('Creating new spend limit in Brex API'); return await this.post<SpendLimit>('/v2/spend_limits', data); } catch (error) { logError(`Error creating spend limit: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Update an existing spend limit * @param spendLimitId Spend limit ID * @param data Spend limit update request data * @returns Updated spend limit */ async updateSpendLimit(spendLimitId: string, data: UpdateSpendLimitRequest): Promise<SpendLimit> { try { logDebug(`Updating spend limit ${spendLimitId} in Brex API`); return await this.put<SpendLimit>(`/v2/spend_limits/${spendLimitId}`, data); } catch (error) { logError(`Error updating spend limit ${spendLimitId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Archive a spend limit * @param spendLimitId Spend limit ID * @returns Archived spend limit */ async archiveSpendLimit(spendLimitId: string): Promise<SpendLimit> { try { logDebug(`Archiving spend limit ${spendLimitId} in Brex API`); return await this.post<SpendLimit>(`/v2/spend_limits/${spendLimitId}/archive`, {}); } catch (error) { logError(`Error archiving spend limit ${spendLimitId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // Budget Programs API methods /** * Get a list of budget programs * @param params Optional parameters for filtering and pagination * @returns Paginated list of budget programs */ async getBudgetPrograms(params?: BudgetProgramListParams): Promise<BudgetProgramsResponse> { try { logDebug('Fetching budget programs from Brex API', { params }); return await this.get<BudgetProgramsResponse>('/v1/budget_programs', params as unknown as Record<string, any>); } catch (error) { logError(`Error fetching budget programs: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Get a single budget program by ID * @param programId Budget program ID * @returns Budget program object */ async getBudgetProgram(programId: string): Promise<BudgetProgram> { try { logDebug(`Fetching budget program ${programId} from Brex API`); return await this.get<BudgetProgram>(`/v1/budget_programs/${programId}`); } catch (error) { logError(`Error fetching budget program ${programId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Create a new budget program * @param data Budget program creation request data * @returns Created budget program */ async createBudgetProgram(data: CreateBudgetProgramRequest): Promise<BudgetProgram> { try { logDebug('Creating new budget program in Brex API'); return await this.post<BudgetProgram>('/v1/budget_programs', data); } catch (error) { logError(`Error creating budget program: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Update an existing budget program * @param programId Budget program ID * @param data Budget program update request data * @returns Updated budget program */ async updateBudgetProgram(programId: string, data: UpdateBudgetProgramRequest): Promise<BudgetProgram> { try { logDebug(`Updating budget program ${programId} in Brex API`); return await this.put<BudgetProgram>(`/v1/budget_programs/${programId}`, data); } catch (error) { logError(`Error updating budget program ${programId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Delete a budget program * @param programId Budget program ID * @returns Void */ async deleteBudgetProgram(programId: string): Promise<void> { try { logDebug(`Deleting budget program ${programId} from Brex API`); await this.client.delete(`/v1/budget_programs/${programId}`); } catch (error) { logError(`Error deleting budget program ${programId}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // General API methods for flexible endpoint access /** * Generic GET method for Brex API * @param endpoint API endpoint path (starting with /) * @param params Optional query parameters * @returns Response data */ async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> { try { logDebug(`Making GET request to Brex API endpoint: ${endpoint}`, { params }); const response = await this.client.get(endpoint, { params }); return response.data as T; } catch (error) { this.handleApiError(error, 'GET', endpoint); throw error; } } /** * Generic POST method for Brex API * @param endpoint API endpoint path (starting with /) * @param data Request body data * @param params Optional query parameters * @returns Response data */ async post<T = any>(endpoint: string, data: unknown, params?: Record<string, any>): Promise<T> { try { logDebug(`Making POST request to Brex API endpoint: ${endpoint}`, { params }); const response = await this.client.post(endpoint, data, { params }); return response.data as T; } catch (error) { this.handleApiError(error, 'POST', endpoint); throw error; } } /** * Generic PUT method for Brex API * @param endpoint API endpoint path (starting with /) * @param data Request body data * @param params Optional query parameters * @returns Response data */ async put<T = any>(endpoint: string, data: unknown, params?: Record<string, any>): Promise<T> { try { logDebug(`Making PUT request to Brex API endpoint: ${endpoint}`, { params }); const response = await this.client.put(endpoint, data, { params }); return response.data as T; } catch (error) { this.handleApiError(error, 'PUT', endpoint); throw error; } } /** * Handle API errors consistently * @param error The error object * @param method HTTP method used * @param endpoint API endpoint path */ private handleApiError(error: unknown, method: string, endpoint: string): void { if (axios.isAxiosError(error)) { if (error.response?.status === 401) { logError(`Brex API authentication failed: Please check your API key in the .env file`, { method, endpoint, status: error.response.status }); } else if (error.response?.status === 404) { logError(`Resource not found at ${endpoint}`, { method, endpoint, status: error.response.status }); } else { logError(`Brex API error: ${error.response?.data?.message || error.message}`, { method, endpoint, status: error.response?.status }); } } else { logError(`Unexpected error during ${method} request to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`); } } }

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/dennisonbertram/mcp-brex'

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