UNPKG

actual-mcp

Version:

Actual Budget MCP server exposing API functionality

158 lines (156 loc) 6.86 kB
// ---------------------------- // RESOURCES // ---------------------------- import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import api from '@actual-app/api'; import { formatAmount, formatDate, getDateRange } from './utils.js'; import { initActualApi, shutdownActualApi } from './actual-api.js'; import { fetchAllAccounts } from './core/data/fetch-accounts.js'; export const setupResources = (server) => { /** * Handler for listing available resources (accounts) */ server.setRequestHandler(ListResourcesRequestSchema, async () => { try { await initActualApi(); const accounts = await fetchAllAccounts(); return { resources: accounts.map((account) => ({ uri: `actual://accounts/${account.id}`, name: account.name, description: `${account.name} (${account.type || 'Account'})${account.closed ? ' - CLOSED' : ''}`, mimeType: 'text/markdown', })), }; } catch (error) { console.error('Error listing resources:', error); throw error; } finally { await shutdownActualApi(); } }); /** * Handler for reading resources (account details and transactions) */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { await initActualApi(); const uri = request.params.uri; const url = new URL(uri); // Parse the path to determine what to return const pathParts = url.pathname.split('/').filter(Boolean); // If the path is just "accounts", return list of all accounts if (pathParts.length === 0 && url.hostname === 'accounts') { const accounts = await api.getAccounts(); const accountsText = accounts .map((account) => { const closed = account.closed ? ' (CLOSED)' : ''; const offBudget = account.offbudget ? ' (OFF BUDGET)' : ''; const balance = account.balance !== undefined ? ` - ${formatAmount(account.balance)}` : ''; return `- ${account.name}${closed}${offBudget}${balance} [ID: ${account.id}]`; }) .join('\n'); return { contents: [ { uri: uri, text: `# Actual Budget Accounts\n\n${accountsText}\n\nTotal Accounts: ${accounts.length}`, mimeType: 'text/markdown', }, ], }; } // If the path is "accounts/{id}", return account details if (pathParts.length === 1 && url.hostname === 'accounts') { const accountId = pathParts[0]; const accounts = await api.getAccounts(); const account = accounts.find((a) => a.id === accountId); if (!account) { return { contents: [ { uri: uri, text: `Error: Account with ID ${accountId} not found`, mimeType: 'text/plain', }, ], }; } const balance = await api.getAccountBalance(accountId); const formattedBalance = formatAmount(balance); const details = `# Account: ${account.name} ID: ${account.id} Type: ${account.type || 'Unknown'} Balance: ${formattedBalance} On Budget: ${!account.offbudget} Status: ${account.closed ? 'Closed' : 'Open'} To view transactions for this account, use the get-transactions tool.`; return { contents: [ { uri: uri, text: details, mimeType: 'text/markdown', }, ], }; } // If the path is "accounts/{id}/transactions", return transactions if (pathParts.length === 2 && pathParts[1] === 'transactions' && url.hostname === 'accounts') { const accountId = pathParts[0]; const { startDate, endDate } = getDateRange(); const transactions = await api.getTransactions(accountId, startDate, endDate); if (!transactions || transactions.length === 0) { return { contents: [ { uri: uri, text: `No transactions found for account ID ${accountId} between ${startDate} and ${endDate}`, mimeType: 'text/plain', }, ], }; } // Create a markdown table of transactions const header = '| Date | Payee | Category | Amount | Notes |\n| ---- | ----- | -------- | ------ | ----- |\n'; const rows = transactions .map((t) => { const amount = formatAmount(t.amount); const date = formatDate(t.date); const payee = t.payee_name || '(No payee)'; const category = t.category_name || '(Uncategorized)'; const notes = t.notes || ''; return `| ${date} | ${payee} | ${category} | ${amount} | ${notes} |`; }) .join('\n'); const text = `# Transactions for Account\n\nTime period: ${startDate} to ${endDate}\nTotal Transactions: ${transactions.length}\n\n${header}${rows}`; return { contents: [ { uri: uri, text: text, mimeType: 'text/markdown', }, ], }; } // If we don't recognize the URI pattern, return an error return { contents: [ { uri: uri, text: `Error: Unrecognized resource URI: ${uri}`, mimeType: 'text/plain', }, ], }; } catch (error) { console.error('Error reading resource:', error); throw error; } }); }; //# sourceMappingURL=resources.js.map