UNPKG

mcp-ynab

Version:

Model Context Protocol server for YNAB integration

125 lines (104 loc) 4.42 kB
import { YnabClient } from '../../shared/ynab-client.js'; import { CacheManager } from '../../shared/cache-manager.js'; import { ErrorHandler } from '../../shared/error-handler.js'; import logger from '../../shared/logger.js'; const cache = new CacheManager(); const errorHandler = new ErrorHandler(); const toolDefinition = { name: 'get_budget_summary', description: 'Get a comprehensive snapshot of budget health including totals, account counts, and key metrics', inputSchema: { type: 'object', properties: { budget_id: { type: 'string', description: 'Specific budget ID (optional, defaults to default budget)' }, month: { type: 'string', description: 'Specific month in YYYY-MM-DD format (optional, defaults to current month)', pattern: '^\\d{4}-\\d{2}-\\d{2}$' } }, additionalProperties: false } }; const handler = async (params) => { try { const ynab = new YnabClient(); const { budget_id, month } = params; // Generate cache key const cacheKey = cache.generateKey('budget_summary', { budget_id, month }); return await cache.getOrSet(cacheKey, async () => { const budgetId = budget_id || await ynab.getDefaultBudgetId(); // Get budget basic info const budget = await ynab.getBudgetById(budgetId); // Get current month data or specified month // Use YnabClient's utility to get current month in proper format (YYYY-MM-01) const monthToUse = month || ynab.getCurrentMonthInISOFormat(); const currentMonth = await ynab.getMonth(budgetId, monthToUse); // Get categories to calculate totals const categoryGroups = await ynab.getCategories(budgetId, monthToUse); // Defensive check for undefined/null categoryGroups if (!categoryGroups || !Array.isArray(categoryGroups)) { await logger.error('CategoryGroups is not a valid array', { categoryGroups }); throw new Error('Failed to retrieve valid category groups from YNAB API'); } // Get accounts const accounts = await ynab.getAccounts(budgetId); // Calculate category totals let totalBudgeted = 0; let totalActivity = 0; let totalAvailable = 0; let categoriesCount = 0; let overspentCategoriesCount = 0; let goalsCount = 0; let underfundedGoalsCount = 0; categoryGroups.forEach(group => { if (group.categories) { group.categories.forEach(category => { // Skip internal categories if (category.name.startsWith('Internal Master Category')) { return; } categoriesCount++; totalBudgeted += category.budgeted; totalActivity += category.activity; totalAvailable += category.balance; // Check for overspending (negative balance) if (category.balance < 0) { overspentCategoriesCount++; } // Check for goals if (category.goal_type && category.goal_type !== 'NEED') { goalsCount++; // Check if goal is underfunded if (category.goal_target && category.balance < category.goal_target) { underfundedGoalsCount++; } } }); } }); // Count accounts (excluding closed accounts) const activeAccounts = accounts.filter(account => !account.closed); return { budget_name: budget.name, current_month: monthToUse, to_be_budgeted: ynab.milliunitsToAmount(currentMonth.to_be_budgeted), total_budgeted: ynab.milliunitsToAmount(totalBudgeted), total_activity: ynab.milliunitsToAmount(totalActivity), total_available: ynab.milliunitsToAmount(totalAvailable), accounts_count: activeAccounts.length, categories_count: categoriesCount, goals_count: goalsCount, overspent_categories_count: overspentCategoriesCount, underfunded_goals_count: underfundedGoalsCount, age_of_money: currentMonth.age_of_money || 0 }; }, 5); // Cache for 5 minutes since this is frequently accessed } catch (error) { return errorHandler.formatForMCP(error); } }; export { toolDefinition, handler };