UNPKG

mcp-ynab

Version:

Model Context Protocol server for YNAB integration

174 lines (157 loc) 6.37 kB
import { YnabClient } from '../../shared/ynab-client.js'; import { CacheManager } from '../../shared/cache-manager.js'; import { ErrorHandler } from '../../shared/error-handler.js'; const cache = new CacheManager(); const errorHandler = new ErrorHandler(); const toolDefinition = { name: 'get_transactions', description: 'Retrieve transactions with comprehensive filtering capabilities', inputSchema: { type: 'object', properties: { budget_id: { type: 'string', description: 'Specific budget ID (optional, defaults to default budget)' }, account_id: { type: 'string', description: 'Filter by specific account ID (optional)' }, category_id: { type: 'string', description: 'Filter by specific category ID (optional)' }, since_date: { type: 'string', description: 'Start date filter in YYYY-MM-DD format (optional)', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, until_date: { type: 'string', description: 'End date filter in YYYY-MM-DD format (optional)', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, limit: { type: 'integer', description: 'Maximum number of results to return (optional, default 100, max 1000)', minimum: 1, maximum: 1000, default: 100 } }, additionalProperties: false } }; const handler = async (params) => { try { const ynab = new YnabClient(); const { budget_id, account_id, category_id, since_date, until_date, limit = 100 } = params; // Generate cache key (shorter cache time for transactions) const cacheKey = cache.generateKey('transactions', { budget_id, account_id, category_id, since_date, until_date, limit }); return await cache.getOrSet(cacheKey, async () => { const budgetId = budget_id || await ynab.getDefaultBudgetId(); // Get transactions using the client const transactions = await ynab.getTransactions(budgetId, { accountId: account_id, categoryId: category_id, sinceDate: since_date, untilDate: until_date, limit: limit }); // Get additional data for enriching transaction details const [accounts, categories, payees] = await Promise.all([ ynab.getAccounts(budgetId), ynab.getCategories(budgetId), ynab.getPayees(budgetId) ]); // Create lookup maps for enrichment const accountMap = new Map(accounts.map(acc => [acc.id, acc])); const payeeMap = new Map(payees.map(payee => [payee.id, payee])); // Flatten categories from category groups for lookup const categoryMap = new Map(); categories.forEach(group => { if (group.categories) { group.categories.forEach(cat => { categoryMap.set(cat.id, { ...cat, group_name: group.name }); }); } }); // Transform transactions data for response const transactionsData = transactions.map(transaction => { const account = accountMap.get(transaction.account_id); const category = categoryMap.get(transaction.category_id); const payee = payeeMap.get(transaction.payee_id); return { id: transaction.id, date: transaction.date, amount: ynab.milliunitsToAmount(transaction.amount), cleared: transaction.cleared, approved: transaction.approved, payee_id: transaction.payee_id, payee_name: payee ? payee.name : 'Transfer', // Handle transfers category_id: transaction.category_id, category_name: category ? category.name : null, category_group_name: category ? category.group_name : null, account_id: transaction.account_id, account_name: account ? account.name : null, memo: transaction.memo || null, flag_color: transaction.flag_color || null, flag_name: transaction.flag_name || null, transfer_account_id: transaction.transfer_account_id || null, transfer_transaction_id: transaction.transfer_transaction_id || null, matched_transaction_id: transaction.matched_transaction_id || null, import_id: transaction.import_id || null, debt_transaction_type: transaction.debt_transaction_type || null }; }); // Sort by date descending (most recent first) transactionsData.sort((a, b) => new Date(b.date) - new Date(a.date)); // Calculate summary statistics const totalAmount = transactionsData.reduce((sum, t) => sum + t.amount, 0); const totalIncome = transactionsData.filter(t => t.amount > 0).reduce((sum, t) => sum + t.amount, 0); const totalExpenses = transactionsData.filter(t => t.amount < 0).reduce((sum, t) => sum + Math.abs(t.amount), 0); const clearedCount = transactionsData.filter(t => t.cleared === 'cleared').length; const unclearedCount = transactionsData.filter(t => t.cleared === 'uncleared').length; return { transactions: transactionsData, total_count: transactionsData.length, summary: { total_amount: totalAmount, total_income: totalIncome, total_expenses: totalExpenses, cleared_count: clearedCount, uncleared_count: unclearedCount, date_range: { earliest: transactionsData.length > 0 ? transactionsData[transactionsData.length - 1].date : null, latest: transactionsData.length > 0 ? transactionsData[0].date : null } }, filters_applied: { budget_id: budgetId, account_id: account_id || null, category_id: category_id || null, since_date: since_date || null, until_date: until_date || null, limit: limit } }; }, 2); // Cache for 2 minutes (transactions change frequently) } catch (error) { return errorHandler.formatForMCP(error); } }; export { toolDefinition, handler };