mcp-ynab
Version:
Model Context Protocol server for YNAB integration
174 lines (157 loc) • 6.37 kB
JavaScript
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 };