UNPKG

mcp-ynab

Version:

Model Context Protocol server for YNAB integration

306 lines (255 loc) 10.6 kB
import { YnabClient } from './ynab-client.js'; import { ErrorHandler } from './error-handler.js'; /** * Prompt Executor - Executes multi-step prompts by coordinating MCP tools */ export class PromptExecutor { constructor() { this.ynabClient = new YnabClient(); this.errorHandler = new ErrorHandler(); this.toolRegistry = new Map(); this.setupToolRegistry(); } /** * Register all available MCP tools for prompt execution */ setupToolRegistry() { // Map tool names to their execution functions // This will be dynamically populated based on available tools const tools = { // Budget Overview Tools 'get_budget_summary': this.executeBudgetSummary.bind(this), 'get_budget_months': this.executeBudgetMonths.bind(this), 'get_categories': this.executeCategories.bind(this), 'list_budgets': this.executeListBudgets.bind(this), // Account Tools 'get_accounts': this.executeAccounts.bind(this), 'get_account_details': this.executeAccountDetails.bind(this), // Transaction Analysis Tools 'get_transactions': this.executeTransactions.bind(this), 'search_transactions': this.executeSearchTransactions.bind(this), 'get_payees': this.executePayees.bind(this), 'get_scheduled_transactions': this.executeScheduledTransactions.bind(this), // Analysis & Reporting Tools 'get_spending_report': this.executeSpendingReport.bind(this), 'get_cash_flow_analysis': this.executeCashFlowAnalysis.bind(this), 'get_goals_status': this.executeGoalsStatus.bind(this), 'get_net_worth_trend': this.executeNetWorthTrend.bind(this), 'get_overspending_analysis': this.executeOverspendingAnalysis.bind(this), 'get_underspending_analysis': this.executeUnderspendingAnalysis.bind(this) }; for (const [name, handler] of Object.entries(tools)) { this.toolRegistry.set(name, handler); } } /** * Execute a prompt with given parameters * @param {Object} prompt - The prompt definition * @param {Object} parameters - Parameters for the prompt * @returns {Object} Execution results */ async executePrompt(prompt, parameters = {}) { const executionStart = Date.now(); const results = { promptName: prompt.name, description: prompt.description, parameters, executedAt: new Date(), steps: [], toolResults: {}, summary: {}, executionTimeMs: 0, success: true, errors: [] }; try { // Executing prompt silently for MCP compatibility // Execute each step for (let i = 0; i < prompt.steps.length; i++) { const step = prompt.steps[i]; const stepStart = Date.now(); // Executing step silently const stepResult = { stepNumber: i + 1, description: step, executedAt: new Date(), success: true, executionTimeMs: 0, data: null, error: null }; try { // Determine which tools to execute for this step const toolsForStep = this.determineToolsForStep(step, prompt.tools_used); // Execute tools for this step for (const toolName of toolsForStep) { if (!this.toolRegistry.has(toolName)) { throw new Error(`Unknown tool: ${toolName}`); } // Executing tool silently const toolHandler = this.toolRegistry.get(toolName); const toolResult = await toolHandler(parameters); results.toolResults[toolName] = toolResult; stepResult.data = { ...stepResult.data, [toolName]: toolResult }; } stepResult.executionTimeMs = Date.now() - stepStart; results.steps.push(stepResult); } catch (error) { stepResult.success = false; stepResult.error = error.message; stepResult.executionTimeMs = Date.now() - stepStart; results.steps.push(stepResult); results.errors.push(`Step ${i + 1}: ${error.message}`); // Error in step, continuing with other steps } } // Generate summary based on collected data results.summary = this.generateSummary(prompt, results.toolResults); } catch (error) { results.success = false; results.errors.push(`Prompt execution failed: ${error.message}`); // Prompt execution error } results.executionTimeMs = Date.now() - executionStart; // Prompt execution completed return results; } /** * Determine which tools should be executed for a given step * This is a simple heuristic - in practice, you might want more sophisticated mapping */ determineToolsForStep(stepDescription, availableTools) { const step = stepDescription.toLowerCase(); const toolsForStep = []; // Simple keyword matching to determine relevant tools const toolKeywords = { 'get_budget_summary': ['budget summary', 'health', 'overview', 'metrics'], 'get_categories': ['category', 'categories', 'balance', 'goal'], 'get_accounts': ['account', 'balance', 'net worth'], 'get_overspending_analysis': ['overspend', 'over spending', 'overspent'], 'get_goals_status': ['goal', 'savings', 'progress'], 'get_cash_flow_analysis': ['cash flow', 'income', 'expenses'], 'get_spending_report': ['spending', 'spend', 'expense'], 'get_payees': ['payee', 'vendor', 'merchant'], 'get_scheduled_transactions': ['scheduled', 'recurring', 'upcoming'], 'get_net_worth_trend': ['net worth', 'asset', 'debt'], 'get_underspending_analysis': ['underspend', 'unused', 'optimization'], 'get_transactions': ['transaction', 'history'], 'search_transactions': ['search', 'find', 'filter'], 'get_account_details': ['account detail', 'reconcil'], 'get_budget_months': ['month', 'period'], 'list_budgets': ['budget', 'list'] }; // Find matching tools based on keywords for (const [toolName, keywords] of Object.entries(toolKeywords)) { if (availableTools.includes(toolName)) { if (keywords.some(keyword => step.includes(keyword))) { toolsForStep.push(toolName); } } } // If no specific matches, return a sensible default based on step position if (toolsForStep.length === 0 && availableTools.length > 0) { // For first step, usually want summary/overview if (step.includes('get') || step.includes('overview') || step.includes('summary')) { if (availableTools.includes('get_budget_summary')) { toolsForStep.push('get_budget_summary'); } } } return toolsForStep; } /** * Generate a summary based on the collected tool results */ generateSummary(prompt, toolResults) { const summary = { promptType: prompt.name, executedTools: Object.keys(toolResults), keyInsights: [], recommendations: [], dataPoints: {} }; // Extract key data points from tool results if (toolResults.get_budget_summary) { const budgetData = toolResults.get_budget_summary; summary.dataPoints.budgetHealth = { toBeBudgeted: budgetData.to_be_budgeted || 0, totalAccounts: budgetData.accounts?.length || 0, ageOfMoney: budgetData.age_of_money || 0 }; } if (toolResults.get_overspending_analysis) { const overspendData = toolResults.get_overspending_analysis; if (overspendData.overspending_categories?.length > 0) { summary.keyInsights.push(`Found ${overspendData.overspending_categories.length} categories with overspending patterns`); } } if (toolResults.get_goals_status) { const goalsData = toolResults.get_goals_status; if (goalsData.goals?.length > 0) { const activeGoals = goalsData.goals.filter(g => !g.completed); summary.dataPoints.goalsProgress = { totalGoals: goalsData.goals.length, activeGoals: activeGoals.length, averageProgress: activeGoals.length > 0 ? activeGoals.reduce((sum, g) => sum + (g.progress_percentage || 0), 0) / activeGoals.length : 0 }; } } return summary; } // Tool execution methods - these will call the actual MCP tools // For now, they return placeholder data, but in full implementation // they would import and call the actual tool handlers async executeBudgetSummary(params) { // TODO: Import and call actual get-budget-summary tool return { placeholder: 'budget_summary_data', tool: 'get_budget_summary' }; } async executeBudgetMonths(params) { return { placeholder: 'budget_months_data', tool: 'get_budget_months' }; } async executeCategories(params) { return { placeholder: 'categories_data', tool: 'get_categories' }; } async executeListBudgets(params) { return { placeholder: 'budgets_list_data', tool: 'list_budgets' }; } async executeAccounts(params) { return { placeholder: 'accounts_data', tool: 'get_accounts' }; } async executeAccountDetails(params) { return { placeholder: 'account_details_data', tool: 'get_account_details' }; } async executeTransactions(params) { return { placeholder: 'transactions_data', tool: 'get_transactions' }; } async executeSearchTransactions(params) { return { placeholder: 'search_results_data', tool: 'search_transactions' }; } async executePayees(params) { return { placeholder: 'payees_data', tool: 'get_payees' }; } async executeScheduledTransactions(params) { return { placeholder: 'scheduled_transactions_data', tool: 'get_scheduled_transactions' }; } async executeSpendingReport(params) { return { placeholder: 'spending_report_data', tool: 'get_spending_report' }; } async executeCashFlowAnalysis(params) { return { placeholder: 'cash_flow_data', tool: 'get_cash_flow_analysis' }; } async executeGoalsStatus(params) { return { placeholder: 'goals_status_data', tool: 'get_goals_status' }; } async executeNetWorthTrend(params) { return { placeholder: 'net_worth_trend_data', tool: 'get_net_worth_trend' }; } async executeOverspendingAnalysis(params) { return { placeholder: 'overspending_analysis_data', tool: 'get_overspending_analysis' }; } async executeUnderspendingAnalysis(params) { return { placeholder: 'underspending_analysis_data', tool: 'get_underspending_analysis' }; } } export default PromptExecutor;