actual-mcp-server 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -730,4 +730,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
730
730
 
731
731
  ---
732
732
 
733
- **Version:** 0.6.0 | **Tool Count:** 63 (verified LibreChat-compatible)
733
+ **Version:** 0.6.1 | **Tool Count:** 63 (verified LibreChat-compatible)
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -385,7 +385,8 @@ export async function importTransactions(accountId, txs) {
385
385
  }
386
386
  export async function createTransfer(params) {
387
387
  observability.incrementToolCall('actual.transfers.create').catch(() => { });
388
- return queueWriteOperation(async () => {
388
+ // ── Phase 1: validate + write ─────────────────────────────────────────────
389
+ const writeResult = await queueWriteOperation(async () => {
389
390
  if (params.from_account === params.to_account) {
390
391
  return { success: false, error: 'from_account and to_account must be different accounts.' };
391
392
  }
@@ -411,16 +412,30 @@ export async function createTransfer(params) {
411
412
  payee: transferPayee.id,
412
413
  ...(params.notes !== undefined && { notes: params.notes }),
413
414
  };
414
- const result = await withConcurrency(() => retry(() => rawAddTransactions(params.from_account, [sourceTx], { runTransfers: true }), { retries: 2, backoffMs: 200 }));
415
- let from_id = null;
416
- if (Array.isArray(result) && result.length > 0 && typeof result[0] === 'string' && result[0] !== 'ok') {
417
- from_id = result[0];
418
- }
419
- else if (result && typeof result === 'object' && 'id' in result) {
420
- from_id = result.id;
421
- }
422
- return { success: true, from_id, to_id: null };
415
+ await withConcurrency(() => retry(() => rawAddTransactions(params.from_account, [sourceTx], { runTransfers: true }), { retries: 2, backoffMs: 200 }));
416
+ return { success: true };
423
417
  });
418
+ if (!writeResult.success)
419
+ return writeResult;
420
+ // ── Phase 2: read-back in a fresh session (after write has synced) ────────
421
+ // A new withActualApi session downloads the budget from the server, which
422
+ // reflects the synced write, guaranteeing transfer_id is fully committed.
423
+ try {
424
+ return await withActualApi(async () => {
425
+ const txns = await withConcurrency(() => retry(() => rawGetTransactions(params.from_account, params.date, params.date), { retries: 2, backoffMs: 200 }));
426
+ // Find the most recently created transfer matching our amount.
427
+ // imported_id is not synced via Actual Budget CRDT, so we sort by
428
+ // sort_order descending and take the newest matching transfer instead.
429
+ const tx = (txns ?? [])
430
+ .filter((t) => t.amount === -Math.abs(params.amount) && t.transfer_id != null)
431
+ .sort((a, b) => (b.sort_order ?? 0) - (a.sort_order ?? 0))[0];
432
+ return { success: true, from_id: tx?.id ?? null, to_id: tx?.transfer_id ?? null };
433
+ });
434
+ }
435
+ catch {
436
+ // Transfer was created; IDs just can't be retrieved right now.
437
+ return { success: true, from_id: null, to_id: null };
438
+ }
424
439
  }
425
440
  export async function getTransactions(accountId, startDate, endDate) {
426
441
  return withActualApi(async () => {
@@ -13,7 +13,7 @@ import { z } from 'zod';
13
13
  import adapter from '../lib/actual-adapter.js';
14
14
  import { CommonSchemas } from '../lib/schemas/common.js';
15
15
  const InputSchema = z.object({
16
- startDate: CommonSchemas.date.optional().describe('Start date in YYYY-MM-DD format (default: first day of current month)'),
16
+ startDate: CommonSchemas.date.optional().describe('Start date in YYYY-MM-DD format (default: all-time)'),
17
17
  endDate: CommonSchemas.date.optional().describe('End date in YYYY-MM-DD format (default: today)'),
18
18
  accountId: CommonSchemas.accountId.optional().describe('Filter to a specific account ID (optional)'),
19
19
  includeTransactions: z.boolean().optional().default(false).describe('When true, include paginated transaction rows in the response (default: false)'),
@@ -37,14 +37,14 @@ const tool = {
37
37
  'Use limit (default 50, max 1000) and offset for pagination.',
38
38
  'Excludes: transfers, split-transaction parents, opening balance entries, off-budget accounts, and closed accounts.',
39
39
  'When accountId is provided, all fields are scoped to that account.',
40
+ 'Default date range is all-time (2000-01-01 to today); pass startDate/endDate to narrow.',
40
41
  'Note: the legacy summary.totalAmount field has been removed — totalAmount is now at the top level.',
41
42
  ].join(' '),
42
43
  inputSchema: InputSchema,
43
44
  call: async (args, _meta) => {
44
45
  const input = InputSchema.parse(args || {});
45
46
  const today = new Date();
46
- const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
47
- const startDate = input.startDate || firstDayOfMonth.toISOString().split('T')[0];
47
+ const startDate = input.startDate || '2000-01-01';
48
48
  const endDate = input.endDate || today.toISOString().split('T')[0];
49
49
  const accountId = input.accountId ?? undefined;
50
50
  // Full table scan when no accountId — reliably returns newly written transactions
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "actual-mcp-server",
3
3
  "displayName": "Actual MCP Server",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"