mcp-ynab
Version:
Model Context Protocol server for YNAB integration
306 lines (255 loc) • 10.6 kB
JavaScript
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;