Skip to main content
Glama
server.js9.01 kB
/** * YNAB MCP Server * Main entry point for the YNAB Model Context Protocol server * Uses the official MCP SDK for communication with Claude */ // Import MCP SDK components const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); // Import our modules const { logger } = require('./utils/logger'); const { ValidationError } = require('./utils/errorHandler'); const { startAuthentication, completeAuthentication, removeAuthentication, listAuthenticatedAccounts } = require('./auth/oauth'); const budgetsApi = require('./api/budgets'); const accountsApi = require('./api/accounts'); const categoriesApi = require('./api/categories'); const transactionsApi = require('./api/transactions'); const payeesApi = require('./api/payees'); const monthsApi = require('./api/months'); const scheduledTransactionsApi = require('./api/scheduledTransactions'); // Import tools from toolDefinitions to ensure consistency const { ynabTools } = require('./mcp/toolDefinitions'); // Handle authentication (both start and complete) async function handleAuthentication(params) { if (!params.email) { throw new ValidationError('Email parameter is required'); } // If auth_code is provided, complete authentication if (params.auth_code) { return await completeAuthentication(params.email, params.auth_code); } // Otherwise, start a new authentication flow return await startAuthentication(params.email); } // Main server class class YnabServer { constructor() { this.server = new Server( { name: "YNAB MCP Server", version: "0.1.0" }, { capabilities: { tools: { list: true, call: true } } } ); // Set up handlers this.setupHandlers(); this.setupSignalHandlers(); } /** * Set up signal handlers for graceful shutdown */ setupSignalHandlers() { // Handle graceful shutdown on SIGINT (Ctrl+C) and SIGTERM (Docker stop) process.on('SIGINT', this.shutdown.bind(this)); process.on('SIGTERM', this.shutdown.bind(this)); // Handle Docker-specific signals and Node.js IPC messages process.on('message', (msg) => { if (msg === 'shutdown') { this.shutdown(); } }); logger.info('Signal handlers registered for graceful shutdown'); } /** * Gracefully shutdown the server */ async shutdown() { logger.info('Received shutdown signal, closing YNAB MCP server...'); try { // Add any cleanup code here if needed // For example, closing database connections, etc. // Give a short time for cleanup and logging to complete setTimeout(() => { logger.info('YNAB MCP server shutdown complete'); process.exit(0); // Exit with success code so Docker will remove the container }, 500); } catch (error) { logger.error('Error during server shutdown:', error); process.exit(1); // Exit with error code } } setupHandlers() { // List tools handler this.server.setRequestHandler(ListToolsRequestSchema, async () => { logger.info('Handling ListTools request'); return { tools: ynabTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) }; }); // Call tool handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const toolName = request.params.name; const args = request.params.arguments || {}; logger.info(`Handling CallTool request for tool: ${toolName}`); // Map tool name to handler function let result; switch (toolName) { case 'list_ynab_accounts': result = await listAuthenticatedAccounts(); break; case 'authenticate_ynab_account': result = await handleAuthentication(args); break; case 'remove_ynab_account': result = await removeAuthentication(args.email); break; // Budget APIs case 'list_budgets': result = await budgetsApi.listBudgets(args); break; case 'get_budget': result = await budgetsApi.getBudget(args); break; // Account APIs case 'list_accounts': result = await accountsApi.listAccounts(args); break; case 'get_account': result = await accountsApi.getAccount(args); break; // Category APIs case 'list_categories': result = await categoriesApi.listCategories(args); break; case 'get_category': result = await categoriesApi.getCategory(args); break; case 'update_category': result = await categoriesApi.updateCategory(args); break; case 'assign_to_categories': result = await categoriesApi.assignToCategories(args); break; case 'get_recommended_allocations': result = await categoriesApi.getRecommendedAllocations(args); break; // Transaction APIs case 'list_transactions': result = await transactionsApi.listTransactions(args); break; case 'get_transaction': result = await transactionsApi.getTransaction(args); break; case 'create_transaction': result = await transactionsApi.createTransaction(args); break; case 'update_transaction': result = await transactionsApi.updateTransaction(args); break; case 'bulk_create_transactions': result = await transactionsApi.bulkCreateTransactions(args); break; // Scheduled Transaction APIs case 'list_scheduled_transactions': result = await scheduledTransactionsApi.listScheduledTransactions(args); break; case 'get_scheduled_transaction': result = await scheduledTransactionsApi.getScheduledTransaction(args); break; case 'create_scheduled_transaction': result = await scheduledTransactionsApi.createScheduledTransaction(args); break; case 'update_scheduled_transaction': result = await scheduledTransactionsApi.updateScheduledTransaction(args); break; case 'delete_scheduled_transaction': result = await scheduledTransactionsApi.deleteScheduledTransaction(args); break; // Payee APIs case 'list_payees': result = await payeesApi.listPayees(args); break; case 'get_payee': result = await payeesApi.getPayee(args); break; case 'get_payee_transactions': result = await payeesApi.getPayeeTransactions(args); break; // Month APIs case 'list_months': result = await monthsApi.listMonths(args); break; case 'get_month': result = await monthsApi.getMonth(args); break; default: throw new ValidationError(`Unknown tool: ${toolName}`); } // Format result according to MCP return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { logger.error('Error handling call tool request:', error); // Return error response return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: error.message, resolution: error.resolution || 'Please try again' }, null, 2) }], isError: true }; } }); } // Start the server async run() { try { logger.info('Starting YNAB MCP server...'); // Set up error handler this.server.onerror = (error) => { logger.error('Server error:', error); }; // Connect to stdio const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('YNAB MCP server running and connected via stdio'); } catch (error) { logger.error('Fatal server error:', error); throw error; } } } // Create and run server const server = new YnabServer(); server.run().catch(error => { logger.error('Failed to start server:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattweg/ynab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server