mcp-ynab
Version:
Model Context Protocol server for YNAB integration
113 lines (97 loc) • 4.23 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_categories',
description: 'Get all budget categories with current balances, budgeted amounts, and goal information',
inputSchema: {
type: 'object',
properties: {
budget_id: {
type: 'string',
description: 'Specific budget ID (optional, defaults to default budget)'
},
group_id: {
type: 'string',
description: 'Filter by specific category group ID (optional)'
},
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, group_id, month } = params;
// Generate cache key
const cacheKey = cache.generateKey('categories', { budget_id, group_id, month });
return await cache.getOrSet(cacheKey, async () => {
const budgetId = budget_id || await ynab.getDefaultBudgetId();
const monthToUse = month || new Date().toISOString().substring(0, 10);
// Get categories
const categoryGroups = await ynab.getCategories(budgetId, monthToUse);
// Filter by group if specified
let filteredGroups = categoryGroups;
if (group_id) {
filteredGroups = categoryGroups.filter(group => group.id === group_id);
}
// Transform data for response
const transformedGroups = filteredGroups.map(group => {
// Skip hidden/internal category groups
if (group.name.startsWith('Internal Master Category') || group.hidden) {
return null;
}
const categories = group.categories
.filter(category => !category.hidden && !category.deleted)
.map(category => {
// Calculate goal percentage
let goalPercentageComplete = 0;
if (category.goal_target && category.goal_target > 0) {
goalPercentageComplete = Math.min(
Math.max((category.balance / category.goal_target) * 100, 0),
100
);
}
return {
id: category.id,
name: category.name,
budgeted: ynab.milliunitsToAmount(category.budgeted),
activity: ynab.milliunitsToAmount(category.activity),
balance: ynab.milliunitsToAmount(category.balance),
goal_type: category.goal_type || null,
goal_target: category.goal_target ? ynab.milliunitsToAmount(category.goal_target) : null,
goal_target_month: category.goal_target_month || null,
goal_percentage_complete: Math.round(goalPercentageComplete),
note: category.note || null,
original_category_group_id: category.original_category_group_id || null
};
});
return {
id: group.id,
name: group.name,
categories: categories,
categories_count: categories.length,
total_budgeted: categories.reduce((sum, cat) => sum + cat.budgeted, 0),
total_activity: categories.reduce((sum, cat) => sum + cat.activity, 0),
total_balance: categories.reduce((sum, cat) => sum + cat.balance, 0)
};
}).filter(group => group !== null); // Remove null groups
return {
month: monthToUse,
category_groups: transformedGroups,
total_groups: transformedGroups.length,
total_categories: transformedGroups.reduce((sum, group) => sum + group.categories_count, 0)
};
}, 10); // Cache for 10 minutes
} catch (error) {
return errorHandler.formatForMCP(error);
}
};
export { toolDefinition, handler };