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