report_flow
Aggregate cash flow entries by period to view monthly income, expenses, net flow, and savings rate.
Instructions
Aggregate cash flow entries by period. Returns monthly income, expenses, net flow, and savings rate sorted by period.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max number of periods to return (default: 36) |
Implementation Reference
- apps/mcp/src/tools/report.ts:48-86 (handler)The handler function for the 'report_flow' tool. It queries all flow entries from the database, aggregates them by period into income/expenses, sorts by period, limits results, and returns each period with income, expenses, net_flow, and savings_rate.
server.tool( 'report_flow', 'Aggregate cash flow entries by period. Returns monthly income, expenses, net flow, and savings rate sorted by period.', { limit: z .number() .int() .min(1) .max(120) .default(36) .describe('Max number of periods to return (default: 36)'), }, async ({ limit }) => { const db = getDb(); const rows = db.select().from(flowEntries).all(); const byPeriod = rows.reduce((map, { period, type, amount }) => { const prev = map.get(period) ?? { income: 0, expenses: 0 }; map.set(period, { income: prev.income + (type === 'income' ? amount : 0), expenses: prev.expenses + (type === 'expense' ? amount : 0), }); return map; }, new Map<string, { income: number; expenses: number }>()); const result = [...byPeriod.entries()] .sort(([a], [b]) => a.localeCompare(b)) .slice(-limit) .map(([period, { income, expenses }]) => ({ period, income, expenses, net_flow: income - expenses, savings_rate: income > 0 ? ((income - expenses) / income) * 100 : null, })); return ok(result); }, ); - apps/mcp/src/tools/report.ts:51-59 (schema)Input schema for report_flow: takes an optional 'limit' parameter (int, 1-120, default 36) controlling how many periods to return.
{ limit: z .number() .int() .min(1) .max(120) .default(36) .describe('Max number of periods to return (default: 36)'), }, - apps/mcp/src/index.ts:20-20 (registration)Registration of report tools (including report_flow) on the MCP server.
registerReportTools(server); - apps/mcp/src/tools/report.ts:8-196 (registration)Registration function that registers report_flow (and other report tools) on the MCP server.
export function registerReportTools(server: McpServer): void { server.tool( 'report_balance', 'Aggregate balance sheet entries by period. Returns monthly net worth trend sorted by period.', { limit: z .number() .int() .min(1) .max(120) .default(36) .describe('Max number of periods to return (default: 36)'), }, async ({ limit }) => { const db = getDb(); const rows = db.select().from(balanceEntries).all(); const byPeriod = rows.reduce((map, { period, type, amount }) => { const prev = map.get(period) ?? { assets: 0, liabilities: 0 }; map.set(period, { assets: prev.assets + (type === 'asset' ? amount : 0), liabilities: prev.liabilities + (type === 'liability' ? amount : 0), }); return map; }, new Map<string, { assets: number; liabilities: number }>()); const result = [...byPeriod.entries()] .sort(([a], [b]) => a.localeCompare(b)) .slice(-limit) .map(([period, { assets, liabilities }]) => ({ period, assets, liabilities, net_worth: assets - liabilities, })); return ok(result); }, ); server.tool( 'report_flow', 'Aggregate cash flow entries by period. Returns monthly income, expenses, net flow, and savings rate sorted by period.', { limit: z .number() .int() .min(1) .max(120) .default(36) .describe('Max number of periods to return (default: 36)'), }, async ({ limit }) => { const db = getDb(); const rows = db.select().from(flowEntries).all(); const byPeriod = rows.reduce((map, { period, type, amount }) => { const prev = map.get(period) ?? { income: 0, expenses: 0 }; map.set(period, { income: prev.income + (type === 'income' ? amount : 0), expenses: prev.expenses + (type === 'expense' ? amount : 0), }); return map; }, new Map<string, { income: number; expenses: number }>()); const result = [...byPeriod.entries()] .sort(([a], [b]) => a.localeCompare(b)) .slice(-limit) .map(([period, { income, expenses }]) => ({ period, income, expenses, net_flow: income - expenses, savings_rate: income > 0 ? ((income - expenses) / income) * 100 : null, })); return ok(result); }, ); server.tool( 'report_settle', 'Read-only summary for a period: balance sheet + cash flow entries with computed totals (net_worth, net_flow). Use this to review month-end settlement results after entries are recorded via add_balance / add_flow. Defaults to the current month.', { period: z.string().optional().describe('Period in YYYY-MM format (defaults to current month)'), }, async ({ period }) => { const db = getDb(); const targetPeriod = period ?? localPeriod(); const balEntries = db .select() .from(balanceEntries) .where(eq(balanceEntries.period, targetPeriod)) .all(); const flowEnts = db .select() .from(flowEntries) .where(eq(flowEntries.period, targetPeriod)) .all(); const sumBy = (entries: typeof balEntries | typeof flowEnts, type: string) => entries.filter((e) => e.type === type).reduce((s, e) => s + e.amount, 0); const total_assets = sumBy(balEntries, 'asset'); const total_liabilities = sumBy(balEntries, 'liability'); const total_income = sumBy(flowEnts, 'income'); const total_expenses = sumBy(flowEnts, 'expense'); return ok({ period: targetPeriod, balance: { entries: balEntries, total_assets, total_liabilities, net_worth: total_assets - total_liabilities, }, flow: { entries: flowEnts, total_income, total_expenses, net_flow: total_income - total_expenses, }, }); }, ); server.tool( 'report_combined', 'Both balance sheet and cash flow trends in one call. Equivalent to calling report_balance and report_flow with the same limit. **Primary entry point for wealth-trajectory questions** ("are my assets growing?", "is my net worth trending up?", "am I getting wealthier?") — portfolio value alone is insufficient because it ignores cash savings, debt paydown, and savings rate. Pair with show_snapshot when the user wants the market-driven slice of the trajectory.', { limit: z .number() .int() .min(1) .max(120) .default(36) .describe('Max number of periods per report (default: 36)'), }, async ({ limit }) => { const db = getDb(); const balRows = db.select().from(balanceEntries).all(); const flowRows = db.select().from(flowEntries).all(); const aggBal = [ ...balRows .reduce((map, { period, type, amount }) => { const prev = map.get(period) ?? { assets: 0, liabilities: 0 }; return map.set(period, { assets: prev.assets + (type === 'asset' ? amount : 0), liabilities: prev.liabilities + (type === 'liability' ? amount : 0), }); }, new Map<string, { assets: number; liabilities: number }>()) .entries(), ] .sort(([a], [b]) => a.localeCompare(b)) .slice(-limit) .map(([period, { assets, liabilities }]) => ({ period, assets, liabilities, net_worth: assets - liabilities, })); const aggFlow = [ ...flowRows .reduce((map, { period, type, amount }) => { const prev = map.get(period) ?? { income: 0, expenses: 0 }; return map.set(period, { income: prev.income + (type === 'income' ? amount : 0), expenses: prev.expenses + (type === 'expense' ? amount : 0), }); }, new Map<string, { income: number; expenses: number }>()) .entries(), ] .sort(([a], [b]) => a.localeCompare(b)) .slice(-limit) .map(([period, { income, expenses }]) => ({ period, income, expenses, net_flow: income - expenses, savings_rate: income > 0 ? ((income - expenses) / income) * 100 : null, })); return ok({ balance: aggBal, flow: aggFlow }); }, ); } - apps/mcp/src/tools/report.ts:5-6 (helper)Imports the flowEntries table and getDb helper used by report_flow to query the database.
import { balanceEntries, flowEntries, getDb } from '../db.ts'; import { ok } from '../helpers.ts';