Skip to main content
Glama
dennisonbertram

Brex MCP Server

transactions.ts12.4 kB
/** * @file Transactions Resource Handler * @version 1.0.0 * @description Handles Brex transactions resource requests */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { ResourceTemplate } from "../models/resourceTemplate.js"; import { logDebug, logError } from "../utils/logger.js"; import { BrexClient } from "../services/brex/client.js"; import { isCardTransaction, isCashTransaction } from "../services/brex/transactions-types.js"; import { parseQueryParams } from "../models/common.js"; import { estimateTokens } from "../utils/responseLimiter.js"; // Get Brex client function getBrexClient(): BrexClient { return new BrexClient(); } // Define resource templates const cashTransactionsTemplate = new ResourceTemplate("brex://transactions/cash/{id}"); /** * Registers the transactions resource handler with the server * @param server The MCP server instance */ export function registerTransactionsResource(server: Server): void { server.registerCapabilities({ resources: { "brex://transactions/card/primary": { description: "Brex card transactions", mimeTypes: ["application/json"], }, "brex://transactions/cash/{id}": { description: "Brex cash transactions", mimeTypes: ["application/json"], } } }); // Use the standard approach with setRequestHandler server.setRequestHandler(ReadResourceRequestSchema, async (request, _extra) => { const uri = request.params.uri; // Check if this handler should process this URI if (!uri.startsWith("brex://transactions")) { return { handled: false }; // Not handled by this handler } logDebug(`Reading transactions resource: ${uri}`); // Get Brex client const brexClient = getBrexClient(); // Handle card transactions if (uri.includes("transactions/card/primary")) { // Extract query parameters const qp = parseQueryParams(uri); const options = { cursor: qp.cursor || undefined, limit: qp.limit ? parseInt(qp.limit, 10) : undefined, user_ids: qp.user_id ? [qp.user_id] : undefined } as any; try { logDebug("Fetching card transactions from Brex API", { options }); const transactions = await brexClient.getCardTransactions(options); // Validate transactions if (!transactions.items || !Array.isArray(transactions.items)) { throw new Error('Invalid card transactions data received'); } for (const transaction of transactions.items) { if (!isCardTransaction(transaction)) { logError(`Invalid card transaction data received: ${JSON.stringify(transaction)}`); throw new Error('Invalid card transaction data received'); } } logDebug(`Successfully fetched ${transactions.items.length} card transactions`); // Projection/limiting const fields = qp.fields ? qp.fields.split(',').map(s => s.trim()).filter(Boolean) : undefined; const summaryOnly = qp.summary_only === 'true'; const tooBig = estimateTokens(JSON.stringify(transactions.items)) > 24000; const summarized = summaryOnly || tooBig; // Optional client-side date filter: posted_at_start (ISO) if provided const clientFiltered = (() => { const start = qp.posted_at_start ? Date.parse(qp.posted_at_start) : NaN; if (!isNaN(start)) { return transactions.items.filter((t: any) => Date.parse(t.posted_at_date || t.posted_at || '') >= start); } return transactions.items; })(); const projected = summarized && fields && fields.length ? clientFiltered.map(t => project(t, fields)) : (summarized ? clientFiltered.map(t => project(t, DEFAULT_TX_FIELDS)) : clientFiltered); // Format response with pagination information const result = { items: projected, pagination: { hasMore: !!(transactions as any).next_cursor, nextCursor: (transactions as any).next_cursor }, meta: { summary_applied: summarized } }; return { contents: [{ uri: uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { logError(`Failed to fetch card transactions: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // Handle cash transactions if (uri.includes("transactions/cash")) { const params = cashTransactionsTemplate.parse(uri); if (!params.id) { return { error: { message: "Account ID is required for cash transactions endpoint", code: 400 } }; } // Extract query parameters const qp = parseQueryParams(uri); const options = { cursor: qp.cursor || undefined, limit: qp.limit ? parseInt(qp.limit, 10) : undefined, posted_at_start: qp.posted_at_start || undefined }; try { logDebug(`Fetching cash transactions for account ${params.id} from Brex API`, { options }); const transactions = await brexClient.getCashTransactions(params.id, options); // Validate transactions if (!transactions.items || !Array.isArray(transactions.items)) { throw new Error('Invalid cash transactions data received'); } for (const transaction of transactions.items) { if (!isCashTransaction(transaction)) { logError(`Invalid cash transaction data received: ${JSON.stringify(transaction)}`); throw new Error('Invalid cash transaction data received'); } } logDebug(`Successfully fetched ${transactions.items.length} cash transactions for account ${params.id}`); // Projection/limiting const fields = qp.fields ? qp.fields.split(',').map(s => s.trim()).filter(Boolean) : undefined; const summaryOnly = qp.summary_only === 'true'; const tooBig = estimateTokens(JSON.stringify(transactions.items)) > 24000; const summarized = summaryOnly || tooBig; const projected = summarized && fields && fields.length ? transactions.items.map(t => project(t, fields)) : (summarized ? transactions.items.map(t => project(t, DEFAULT_CASH_TX_FIELDS)) : transactions.items); // Format response with pagination information const result = { items: projected, pagination: { hasMore: !!(transactions as any).next_cursor, nextCursor: (transactions as any).next_cursor }, meta: { summary_applied: summarized } }; return { contents: [{ uri: uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }] }; } catch (error) { logError(`Failed to fetch cash transactions: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // If we get here, the URI wasn't handled properly return { error: { message: `Unsupported transactions endpoint: ${uri}`, code: 400 } }; }); } export function registerTransactionsCapabilities(server: Server): void { server.registerCapabilities({ resources: { "brex://transactions/card/primary": { description: "Brex primary card transactions (paginated)", mimeTypes: ["application/json"], }, "brex://transactions/cash/{id}": { description: "Brex cash transactions by account (paginated)", mimeTypes: ["application/json"], } } }); } export function canHandleTransactionsUri(uri: string): boolean { return uri.startsWith("brex://transactions"); } export async function readTransactionsUri(uri: string): Promise<any> { const brexClient = getBrexClient(); // Card transactions if (uri.includes("transactions/card/primary")) { const qp = parseQueryParams(uri); const options = { cursor: qp.cursor || undefined, limit: qp.limit ? parseInt(qp.limit, 10) : undefined, posted_at_start: qp.posted_at_start || undefined, user_ids: qp.user_id ? [qp.user_id] : undefined, expand: qp.expand ? [qp.expand] : undefined } as any; const transactions = await brexClient.getCardTransactions(options); if (!transactions.items || !Array.isArray(transactions.items)) throw new Error('Invalid card transactions data received'); for (const t of transactions.items) { if (!isCardTransaction(t)) throw new Error('Invalid card transaction data received'); } const fields = qp.fields ? qp.fields.split(',').map(s => s.trim()).filter(Boolean) : undefined; const summaryOnly = qp.summary_only === 'true'; const tooBig = estimateTokens(JSON.stringify(transactions.items)) > 24000; const summarized = summaryOnly || tooBig; const projected = summarized && fields && fields.length ? transactions.items.map(t => project(t, fields)) : (summarized ? transactions.items.map(t => project(t, DEFAULT_TX_FIELDS)) : transactions.items); const result = { items: projected, pagination: { hasMore: !!(transactions as any).next_cursor, nextCursor: (transactions as any).next_cursor }, meta: { summary_applied: summarized } }; return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] } as any; } // Cash transactions if (uri.includes("transactions/cash")) { const params = cashTransactionsTemplate.parse(uri); if (!params.id) return { error: { message: 'Account ID is required for cash transactions endpoint', code: 400 } } as any; const qp = parseQueryParams(uri); const options = { cursor: qp.cursor || undefined, limit: qp.limit ? parseInt(qp.limit, 10) : undefined } as any; const transactions = await brexClient.getCashTransactions(params.id, options); if (!transactions.items || !Array.isArray(transactions.items)) throw new Error('Invalid cash transactions data received'); for (const t of transactions.items) { if (!isCashTransaction(t)) throw new Error('Invalid cash transaction data received'); } const fields = qp.fields ? qp.fields.split(',').map(s => s.trim()).filter(Boolean) : undefined; const summaryOnly = qp.summary_only === 'true'; const tooBig = estimateTokens(JSON.stringify(transactions.items)) > 24000; const summarized = summaryOnly || tooBig; // Optional client-side date filter for cash const clientFiltered = (() => { const start = qp.posted_at_start ? Date.parse(qp.posted_at_start) : NaN; if (!isNaN(start)) { return transactions.items.filter((t: any) => Date.parse(t.posted_at_date || t.posted_at || '') >= start); } return transactions.items; })(); const projected = summarized && fields && fields.length ? clientFiltered.map(t => project(t, fields)) : (summarized ? clientFiltered.map(t => project(t, DEFAULT_CASH_TX_FIELDS)) : clientFiltered); const result = { items: projected, pagination: { hasMore: !!(transactions as any).next_cursor, nextCursor: (transactions as any).next_cursor }, meta: { summary_applied: summarized } }; return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] } as any; } return { error: { message: `Unsupported transactions endpoint: ${uri}`, code: 400 } } as any; } const DEFAULT_TX_FIELDS = [ 'id', 'posted_at', 'status', 'amount.amount', 'amount.currency', 'merchant.raw_descriptor' ]; const DEFAULT_CASH_TX_FIELDS = [ 'id', 'posted_at', 'amount.amount', 'amount.currency', 'description' ]; function project(src: any, fields: string[]): any { const out: any = {}; for (const f of fields) { const parts = f.split('.'); let cur: any = src; for (const p of parts) { cur = cur?.[p]; if (cur === undefined) break; } if (cur !== undefined) { let o: any = out; for (let i = 0; i < parts.length - 1; i++) { o[parts[i]] = o[parts[i]] ?? {}; o = o[parts[i]]; } o[parts[parts.length - 1]] = cur; } } return out; }

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