add_txns
Import multiple transactions at once for bulk entries like CSV or brokerage exports. Automatically sorts by date and returns a summary per ticker and type for confirmation.
Instructions
Bulk-insert transactions in one call. Use this for any import larger than ~5 rows (CSV / brokerage export / paste). Sorts by date ascending automatically — average cost depends on insertion order. Returns a per-ticker / per-type summary so you can confirm the import to the user. For one-off entries during conversation, prefer add_txn.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| transactions | Yes |
Implementation Reference
- apps/mcp/src/tools/mutate.ts:299-348 (registration)Registration of the 'add_txns' tool on the MCP server via server.tool(). Defines the tool name, description, and input schema.
server.tool( 'add_txns', 'Bulk-insert transactions in one call. Use this for any import larger than ~5 rows (CSV / brokerage export / paste). Sorts by date ascending automatically — average cost depends on insertion order. Returns a per-ticker / per-type summary so you can confirm the import to the user. For one-off entries during conversation, prefer add_txn.', { transactions: z .array( z.object({ ticker: z.string(), date: z.string().describe('YYYY-MM-DD'), type: z.enum(['buy', 'sell', 'deposit', 'dividend', 'tax']), shares: z.number().positive(), price: z.number().min(0), currency: z.string().default('USD'), reason: z.string().optional(), }), ) .min(1), }, async ({ transactions: txns }) => { const db = getDb(); const sorted = [...txns].sort((a, b) => a.date.localeCompare(b.date)); const byTicker = new Map<string, number>(); const byType = new Map<string, number>(); for (const t of sorted) { const ticker = t.ticker.toUpperCase(); db.insert(transactions) .values({ ticker, date: t.date, type: t.type, shares: t.shares, price: t.price, currency: t.currency ?? 'USD', reason: t.reason ?? null, }) .run(); byTicker.set(ticker, (byTicker.get(ticker) ?? 0) + 1); byType.set(t.type, (byType.get(t.type) ?? 0) + 1); } return ok({ inserted: sorted.length, first_date: sorted[0].date, last_date: sorted[sorted.length - 1].date, by_ticker: Object.fromEntries(byTicker), by_type: Object.fromEntries(byType), }); }, ); - apps/mcp/src/tools/mutate.ts:302-316 (schema)Input schema for add_txns: expects an array of transactions with fields ticker, date, type (enum), shares, price, currency (default USD), and optional reason.
{ transactions: z .array( z.object({ ticker: z.string(), date: z.string().describe('YYYY-MM-DD'), type: z.enum(['buy', 'sell', 'deposit', 'dividend', 'tax']), shares: z.number().positive(), price: z.number().min(0), currency: z.string().default('USD'), reason: z.string().optional(), }), ) .min(1), }, - apps/mcp/src/tools/mutate.ts:317-347 (handler)Handler function that bulk-inserts transactions. Sorts by date ascending, inserts each row into the 'transactions' table via Drizzle ORM, and returns a summary with inserted count, date range, by_ticker and by_type breakdowns.
async ({ transactions: txns }) => { const db = getDb(); const sorted = [...txns].sort((a, b) => a.date.localeCompare(b.date)); const byTicker = new Map<string, number>(); const byType = new Map<string, number>(); for (const t of sorted) { const ticker = t.ticker.toUpperCase(); db.insert(transactions) .values({ ticker, date: t.date, type: t.type, shares: t.shares, price: t.price, currency: t.currency ?? 'USD', reason: t.reason ?? null, }) .run(); byTicker.set(ticker, (byTicker.get(ticker) ?? 0) + 1); byType.set(t.type, (byType.get(t.type) ?? 0) + 1); } return ok({ inserted: sorted.length, first_date: sorted[0].date, last_date: sorted[sorted.length - 1].date, by_ticker: Object.fromEntries(byTicker), by_type: Object.fromEntries(byType), }); }, - apps/mcp/src/index.ts:19-21 (registration)Top-level registration: registerMutateTools(server) is called from index.ts, which registers both add_txn and add_txns on the McpServer.
registerPortfolioTools(server); registerReportTools(server); registerMutateTools(server); - apps/mcp/src/helpers.ts:67-91 (helper)Helper functions used by the handler: ok() for successful responses, err() for error responses.
export const ok = (data: unknown) => ({ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }], }); export const err = (msg: string) => ({ content: [{ type: 'text' as const, text: `Error: ${msg}` }], isError: true, }); export const toDateStr = (d: Date) => d.toISOString().slice(0, 10); type ConvertResult = { ok: true; usd: number } | { ok: false; error: string }; export const nativeToUsd = (amount: number, currency: string, date: string): ConvertResult => { const cur = currency.toUpperCase(); if (cur === 'USD') return { ok: true, usd: amount }; const row = getRepository().fx.getRateOnOrBefore(date, cur); if (!row || row.rate_to_usd == null) { return { ok: false, error: `No FX rate cached for ${cur} on or before ${date}. Run sync_fx_rates first, or pass currency="USD" with the converted amount.`, }; } return { ok: true, usd: Math.round(amount / row.rate_to_usd) }; };