Skip to main content
Glama
shahlaukik

Money Manager MCP Server

by shahlaukik
index.ts19.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError as SdkMcpError, } from "@modelcontextprotocol/sdk/types.js"; import { loadConfig, type Config } from "./config/index.js"; import { createHttpClient, type HttpClient } from "./client/http-client.js"; import { wrapError, ValidationError } from "./errors/index.js"; import { ToolSchemas, type ToolName, safeValidateToolInput, } from "./schemas/index.js"; import { executeToolHandler } from "./tools/handlers.js"; import packageJson from "../package.json" with { type: "json" }; /** * Tool definitions for the MCP server * Each tool maps to a Money Manager API endpoint */ const TOOL_DEFINITIONS = [ // Initialization { name: "init_get_data", description: "Retrieves initial application data including categories, payment types, asset groups, and multi-book configuration.", inputSchema: { type: "object" as const, properties: { mbid: { type: "string", description: "Optional: Money book ID", }, }, }, }, // Transactions { name: "transaction_list", description: "Lists transactions within a date range.", inputSchema: { type: "object" as const, properties: { startDate: { type: "string", description: "Start date (YYYY-MM-DD)", }, endDate: { type: "string", description: "End date (YYYY-MM-DD)", }, mbid: { type: "string", description: "Money book ID", }, assetId: { type: "string", description: "Optional: Filter by asset ID", }, }, required: ["startDate", "endDate", "mbid"], }, }, { name: "transaction_create", description: "Creates a new income or expense transaction.", inputSchema: { type: "object" as const, properties: { mbDate: { type: "string", description: "Transaction date (YYYY-MM-DD)", }, assetId: { type: "string", description: "Asset/Account ID" }, payType: { type: "string", description: "Payment type name" }, mcid: { type: "string", description: "Category ID" }, mbCategory: { type: "string", description: "Category name" }, mbCash: { type: "number", description: "Amount" }, inOutCode: { type: "string", enum: ["0", "1"], description: "0=Income, 1=Expense", }, inOutType: { type: "string", description: "Transaction type name" }, mcscid: { type: "string", description: "Optional: Subcategory ID" }, subCategory: { type: "string", description: "Optional: Subcategory name", }, mbContent: { type: "string", description: "Optional: Description" }, mbDetailContent: { type: "string", description: "Optional: Detailed notes", }, }, required: [ "mbDate", "assetId", "payType", "mcid", "mbCategory", "mbCash", "inOutCode", "inOutType", ], }, }, { name: "transaction_update", description: "Updates an existing transaction.", inputSchema: { type: "object" as const, properties: { id: { type: "string", description: "Transaction ID" }, mbDate: { type: "string", description: "Transaction date (YYYY-MM-DD)", }, assetId: { type: "string", description: "Asset/Account ID" }, payType: { type: "string", description: "Payment type name" }, mcid: { type: "string", description: "Category ID" }, mbCategory: { type: "string", description: "Category name" }, mbCash: { type: "number", description: "Amount" }, inOutCode: { type: "string", description: "Transaction type code" }, inOutType: { type: "string", description: "Transaction type name" }, mcscid: { type: "string", description: "Optional: Subcategory ID" }, subCategory: { type: "string", description: "Optional: Subcategory name", }, mbContent: { type: "string", description: "Optional: Description" }, mbDetailContent: { type: "string", description: "Optional: Detailed notes", }, }, required: [ "id", "mbDate", "assetId", "payType", "mcid", "mbCategory", "mbCash", "inOutCode", "inOutType", ], }, }, { name: "transaction_delete", description: "Deletes one or more transactions.", inputSchema: { type: "object" as const, properties: { ids: { type: "array", items: { type: "string" }, description: "Array of transaction IDs to delete", }, }, required: ["ids"], }, }, // Summary { name: "summary_get_period", description: "Retrieves financial summary statistics for a date range.", inputSchema: { type: "object" as const, properties: { startDate: { type: "string", description: "Start date (YYYY-MM-DD)" }, endDate: { type: "string", description: "End date (YYYY-MM-DD)" }, }, required: ["startDate", "endDate"], }, }, { name: "summary_export_excel", description: "Exports transaction data to Excel file. The server returns an HTML-based Excel format. Use .xls extension for best compatibility (if .xlsx is provided, it will be auto-corrected to .xls with a warning).", inputSchema: { type: "object" as const, properties: { startDate: { type: "string", description: "Start date (YYYY-MM-DD)" }, endDate: { type: "string", description: "End date (YYYY-MM-DD)" }, mbid: { type: "string", description: "Money book ID" }, assetId: { type: "string", description: "Optional: Filter by asset ID", }, inOutType: { type: "string", description: "Optional: Filter by transaction type", }, outputPath: { type: "string", description: "Local path to save the Excel file (use .xls extension for best compatibility)", }, }, required: ["startDate", "endDate", "mbid", "outputPath"], }, }, // Assets { name: "asset_list", description: "Retrieves all assets in a hierarchical structure.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "asset_create", description: "Creates a new asset/account.", inputSchema: { type: "object" as const, properties: { assetGroupId: { type: "string", description: "Asset group ID" }, assetGroupName: { type: "string", description: "Asset group name" }, assetName: { type: "string", description: "Asset name" }, assetMoney: { type: "number", description: "Initial balance" }, linkAssetId: { type: "string", description: "Optional: Linked asset ID", }, linkAssetName: { type: "string", description: "Optional: Linked asset name", }, }, required: ["assetGroupId", "assetGroupName", "assetName", "assetMoney"], }, }, { name: "asset_update", description: "Modifies an existing asset.", inputSchema: { type: "object" as const, properties: { assetId: { type: "string", description: "Asset ID" }, assetGroupId: { type: "string", description: "Asset group ID" }, assetGroupName: { type: "string", description: "Asset group name" }, assetName: { type: "string", description: "Asset name" }, assetMoney: { type: "number", description: "Current balance" }, linkAssetId: { type: "string", description: "Optional: Linked asset ID", }, linkAssetName: { type: "string", description: "Optional: Linked asset name", }, }, required: [ "assetId", "assetGroupId", "assetGroupName", "assetName", "assetMoney", ], }, }, { name: "asset_delete", description: "Removes an asset.", inputSchema: { type: "object" as const, properties: { assetId: { type: "string", description: "Asset ID to delete" }, }, required: ["assetId"], }, }, // Credit Cards { name: "card_list", description: "Retrieves all credit cards in a hierarchical structure.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "card_create", description: "Creates a new credit card.", inputSchema: { type: "object" as const, properties: { cardName: { type: "string", description: "Credit card name" }, linkAssetId: { type: "string", description: "Linked payment asset ID" }, linkAssetName: { type: "string", description: "Linked payment asset name", }, notPayMoney: { type: "number", description: "Unpaid balance (negative value)", }, jungsanDay: { type: "number", description: "Optional: Balance calculation day (1-31)", }, paymentDay: { type: "number", description: "Optional: Payment due day (1-31)", }, }, required: ["cardName", "linkAssetId", "linkAssetName", "notPayMoney"], }, }, { name: "card_update", description: "Modifies an existing credit card.", inputSchema: { type: "object" as const, properties: { assetId: { type: "string", description: "Card asset ID" }, cardName: { type: "string", description: "Credit card name" }, linkAssetId: { type: "string", description: "Linked payment asset ID" }, linkAssetName: { type: "string", description: "Linked payment asset name", }, jungsanDay: { type: "number", description: "Optional: Balance calculation day (1-31)", }, paymentDay: { type: "number", description: "Optional: Payment due day (1-31)", }, }, required: ["assetId", "cardName", "linkAssetId", "linkAssetName"], }, }, // Transfers { name: "transfer_create", description: "Transfers money between two assets.", inputSchema: { type: "object" as const, properties: { moveDate: { type: "string", description: "Transfer date (YYYY-MM-DD)" }, fromAssetId: { type: "string", description: "Source asset ID" }, fromAssetName: { type: "string", description: "Source asset name" }, toAssetId: { type: "string", description: "Destination asset ID" }, toAssetName: { type: "string", description: "Destination asset name" }, moveMoney: { type: "number", description: "Transfer amount" }, moneyContent: { type: "string", description: "Optional: Description" }, mbDetailContent: { type: "string", description: "Optional: Detailed notes", }, }, required: [ "moveDate", "fromAssetId", "fromAssetName", "toAssetId", "toAssetName", "moveMoney", ], }, }, { name: "transfer_update", description: "Modifies an existing transfer. WARNING: The server creates a new transfer with a NEW ID instead of updating in-place. The old ID will no longer exist after update. Use transaction_list to get the new ID if needed.", inputSchema: { type: "object" as const, properties: { id: { type: "string", description: "Transfer transaction ID" }, moveDate: { type: "string", description: "Transfer date (YYYY-MM-DD)" }, fromAssetId: { type: "string", description: "Source asset ID" }, fromAssetName: { type: "string", description: "Source asset name" }, toAssetId: { type: "string", description: "Destination asset ID" }, toAssetName: { type: "string", description: "Destination asset name" }, moveMoney: { type: "number", description: "Transfer amount" }, moneyContent: { type: "string", description: "Optional: Description" }, mbDetailContent: { type: "string", description: "Optional: Detailed notes", }, }, required: [ "id", "moveDate", "fromAssetId", "fromAssetName", "toAssetId", "toAssetName", "moveMoney", ], }, }, // Dashboard { name: "dashboard_get_overview", description: "Retrieves dashboard overview with asset trends and portfolio breakdown.", inputSchema: { type: "object" as const, properties: {}, }, }, { name: "dashboard_get_asset_chart", description: "Retrieves historical chart data for a specific asset.", inputSchema: { type: "object" as const, properties: { assetId: { type: "string", description: "Asset ID" }, }, required: ["assetId"], }, }, // Backup - DISABLED: These tools are dangerous and not recommended for use via MCP // { // name: 'backup_download', // description: 'Downloads the SQLite database backup.', // inputSchema: { // type: 'object' as const, // properties: { // outputPath: { type: 'string', description: 'Local path to save the backup file' }, // }, // required: ['outputPath'], // }, // }, // { // name: 'backup_restore', // description: 'Restores from a SQLite database backup file.', // inputSchema: { // type: 'object' as const, // properties: { // filePath: { type: 'string', description: 'Path to the SQLite backup file' }, // }, // required: ['filePath'], // }, // }, ]; /** * Money Manager MCP Server */ class MoneyManagerMcpServer { private server: Server; private httpClient: HttpClient | null = null; private config: Config | null = null; constructor() { this.server = new Server( { name: "money-manager-mcp", version: packageJson.version, }, { capabilities: { tools: {}, }, }, ); this.setupHandlers(); this.setupErrorHandling(); } /** * Sets up request handlers for the MCP server */ private setupHandlers(): void { // Handle tool listing this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOL_DEFINITIONS, }; }); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Validate tool name if (!this.isValidToolName(name)) { throw new SdkMcpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}`, ); } // Validate input using Zod schema const validationResult = safeValidateToolInput(name, args); if (!validationResult.success) { const error = ValidationError.fromZodError(validationResult.error); return { content: [ { type: "text" as const, text: JSON.stringify({ success: false, error: { code: error.code, message: error.message, details: error.details, }, }), }, ], isError: true, }; } try { // Execute the tool (placeholder - will be implemented in Phase 2) const result = await this.executeTool(name, validationResult.data); return { content: [ { type: "text" as const, text: JSON.stringify(result), }, ], }; } catch (error) { const mcpError = wrapError(error); return { content: [ { type: "text" as const, text: JSON.stringify({ success: false, error: { code: mcpError.code, message: mcpError.message, retryable: mcpError.retryable, details: mcpError.details, }, }), }, ], isError: true, }; } }); } /** * Sets up error handling for the server */ private setupErrorHandling(): void { this.server.onerror = (error) => { console.error("[MCP Server Error]", error); }; // Handle graceful shutdown process.on("SIGINT", async () => { console.log("\nReceived SIGINT, shutting down gracefully..."); await this.shutdown(); process.exit(0); }); process.on("SIGTERM", async () => { console.log("\nReceived SIGTERM, shutting down gracefully..."); await this.shutdown(); process.exit(0); }); } /** * Checks if a tool name is valid */ private isValidToolName(name: string): name is ToolName { return name in ToolSchemas; } /** * Executes a tool by delegating to the appropriate handler */ private async executeTool(name: ToolName, args: unknown): Promise<unknown> { // Ensure HTTP client is initialized if (!this.httpClient || !this.config) { throw new Error("Server not initialized. Call start() first."); } // Execute the tool handler return executeToolHandler(this.httpClient, name, args); } /** * Starts the MCP server */ async start(): Promise<void> { try { // Parse command line arguments const args = process.argv.slice(2); let customBaseUrl: string | undefined; for (let i = 0; i < args.length; i++) { if (args[i] === "--baseUrl" && args[i + 1]) { customBaseUrl = args[i + 1]; break; } } // Load configuration this.config = await loadConfig(); // Override baseUrl if provided via command line if (customBaseUrl) { this.config.server.baseUrl = customBaseUrl; } console.error( `[MCP Server] Configuration loaded. Base URL: ${this.config.server.baseUrl}`, ); // Create HTTP client this.httpClient = createHttpClient(this.config); console.error("[MCP Server] HTTP client initialized."); // Start the server with stdio transport const transport = new StdioServerTransport(); await this.server.connect(transport); console.error( "[MCP Server] Money Manager MCP Server started successfully.", ); } catch (error) { console.error("[MCP Server] Failed to start server:", error); throw error; } } /** * Shuts down the server gracefully */ async shutdown(): Promise<void> { try { await this.server.close(); console.error("[MCP Server] Server closed."); } catch (error) { console.error("[MCP Server] Error during shutdown:", error); } } } /** * Main entry point */ async function main(): Promise<void> { const server = new MoneyManagerMcpServer(); await server.start(); } // Run the server main().catch((error) => { console.error("[MCP Server] Fatal error:", error); process.exit(1); });

Implementation Reference

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/shahlaukik/money-manager-mcp'

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