actual-mcp-server 0.6.37 → 0.6.41

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
@@ -750,4 +750,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
750
750
 
751
751
  ---
752
752
 
753
- **Version:** 0.6.37 | **Tool Count:** 70 (verified LibreChat-compatible)
753
+ **Version:** 0.6.41 | **Tool Count:** 70 (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.37",
4
+ "version": "0.6.41",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -71,14 +71,6 @@
71
71
  "optionalDependencies": {
72
72
  "prom-client": "*"
73
73
  },
74
- "overrides": {
75
- "ajv": "8.18.0",
76
- "qs": "6.14.2",
77
- "fast-uri": "^3.1.2"
78
- },
79
- "comments": {
80
- "security-overrides": "ajv>=8.18.0 (CVE alert #21), qs>=6.14.2 (alert #17), fast-uri>=3.1.2 (GHSA-q3j6-qgpj-74h6 + GHSA-v39h-62p7-jpjc)"
81
- },
82
74
  "devDependencies": {
83
75
  "@playwright/test": "^1.60.0",
84
76
  "@types/express": "^5.0.3",
@@ -1037,6 +1037,18 @@ export async function updateTransactionBatch(updates) {
1037
1037
  export async function deleteTransaction(id) {
1038
1038
  observability.incrementToolCall('actual.transactions.delete').catch(() => { });
1039
1039
  return queueWriteOperation(async () => {
1040
+ // Pre-flight existence check: the raw API silently no-ops on a missing id and would
1041
+ // otherwise report success for a delete that removed nothing. A targeted ActualQL
1042
+ // query by id keeps this cheap (indexed lookup, not a full-table scan).
1043
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1044
+ const { q } = (await import('@actual-app/api'));
1045
+ const found = await withConcurrency(() => retry(async () => {
1046
+ const res = (await rawRunQuery(q('transactions').filter({ id }).select(['id'])));
1047
+ return Array.isArray(res?.data) && res.data.length > 0;
1048
+ }, { retries: 2, backoffMs: 200 }));
1049
+ if (!found) {
1050
+ throw new Error(`Transaction "${id}" not found. Use actual_transactions_get to list transactions.`);
1051
+ }
1040
1052
  // Non-idempotent: do not retry (#165). A retry after a lost-response would
1041
1053
  // re-issue the delete against an already-removed record and surface a
1042
1054
  // confusing "not found" even though the first attempt succeeded.
@@ -20,10 +20,9 @@ const tool = {
20
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
21
  const groupExists = groups.some((g) => g.id === input.id);
22
22
  if (!groupExists) {
23
- return {
24
- error: notFoundMsg('Category group', input.id, 'actual_category_groups_get'),
25
- success: false,
26
- };
23
+ // Throw (not return {success:false}) so a non-existent id surfaces as an MCP
24
+ // error, consistent with every other delete tool's not-found behavior.
25
+ throw new Error(notFoundMsg('Category group', input.id, 'actual_category_groups_get'));
27
26
  }
28
27
  await rawDeleteCategoryGroup(input.id);
29
28
  return { success: true };
@@ -1,8 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
- import api from '@actual-app/api';
4
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
- const { deletePayee: rawDeletePayee } = api;
6
3
  const InputSchema = z.object({
7
4
  id: z.string().describe('Payee ID to delete'),
8
5
  });
@@ -12,12 +9,13 @@ const tool = {
12
9
  inputSchema: InputSchema,
13
10
  call: async (args, _meta) => {
14
11
  const input = InputSchema.parse(args || {});
15
- // Single-write tool, but uses withWriteSession for consistency with the other
16
- // delete tools and to share the same lock-cycle invariant (#142).
17
- return await adapter.withWriteSession(async () => {
18
- await rawDeletePayee(input.id);
19
- return { success: true };
20
- });
12
+ // Route through the guarded adapter method (pre-flight existence check), not the
13
+ // raw api.deletePayee. The raw call throws a cryptic "Cannot destructure property
14
+ // 'transfer_acct' of null" on a non-existent id; adapter.deletePayee returns an
15
+ // actionable "Payee not found" instead. adapter.deletePayee already runs inside a
16
+ // single write-queue cycle, preserving the #142 lock invariant.
17
+ await adapter.deletePayee(input.id);
18
+ return { success: true };
21
19
  },
22
20
  };
23
21
  export default tool;
@@ -20,10 +20,9 @@ const tool = {
20
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
21
  const ruleExists = allRules.some((r) => r.id === input.id);
22
22
  if (!ruleExists) {
23
- return {
24
- error: notFoundMsg('Rule', input.id, 'actual_rules_get'),
25
- success: false,
26
- };
23
+ // Throw (not return {success:false}) so a non-existent id surfaces as an MCP
24
+ // error, consistent with every other delete tool's not-found behavior.
25
+ throw new Error(notFoundMsg('Rule', input.id, 'actual_rules_get'));
27
26
  }
28
27
  await rawDeleteRule(input.id);
29
28
  return { success: true };
@@ -23,10 +23,9 @@ const tool = {
23
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
24
  const scheduleExists = schedules.some((s) => s.id === input.id);
25
25
  if (!scheduleExists) {
26
- return {
27
- error: notFoundMsg('Schedule', input.id, 'actual_schedules_get'),
28
- success: false,
29
- };
26
+ // Throw (not return {success:false}) so a non-existent id surfaces as an MCP
27
+ // error, consistent with every other delete tool's not-found behavior.
28
+ throw new Error(notFoundMsg('Schedule', input.id, 'actual_schedules_get'));
30
29
  }
31
30
  try {
32
31
  await rawDeleteSchedule(input.id);
@@ -35,10 +34,9 @@ const tool = {
35
34
  catch (err) {
36
35
  const msg = err instanceof Error ? err.message : String(err);
37
36
  if (msg.includes('NOT NULL constraint') || msg.includes('messages_crdt')) {
38
- return {
39
- error: constraintErrorMsg('Schedule', input.id, 'actual_schedules_get'),
40
- success: false,
41
- };
37
+ // Translate the raw SQLite constraint error into an actionable message, thrown
38
+ // for the same consistency reason as the not-found case above.
39
+ throw new Error(constraintErrorMsg('Schedule', input.id, 'actual_schedules_get'));
42
40
  }
43
41
  throw err;
44
42
  }
@@ -6,9 +6,8 @@ const InputSchema = z.object({
6
6
  const tool = {
7
7
  name: 'actual_transactions_delete',
8
8
  description: 'Delete a transaction from Actual Budget by its ID. ' +
9
- 'Note: if the transaction does not exist, the call will still return { success: true } ' +
10
- 'because the Actual Budget API does not distinguish between a successful delete and a no-op. ' +
11
- 'Use actual_transactions_filter to verify the transaction exists before deleting. ' +
9
+ 'If the transaction does not exist, the call returns an actionable "not found" error ' +
10
+ '(the adapter verifies existence first, since the raw Actual Budget API silently no-ops on a missing id). ' +
12
11
  'This permanently removes the transaction and updates account balances accordingly. This operation cannot be undone.',
13
12
  inputSchema: InputSchema,
14
13
  call: async (args, _meta) => {
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.37",
4
+ "version": "0.6.41",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
7
7
  "npm": ">=10.0.0"
@@ -71,14 +71,6 @@
71
71
  "optionalDependencies": {
72
72
  "prom-client": "*"
73
73
  },
74
- "overrides": {
75
- "ajv": "8.18.0",
76
- "qs": "6.14.2",
77
- "fast-uri": "^3.1.2"
78
- },
79
- "comments": {
80
- "security-overrides": "ajv>=8.18.0 (CVE alert #21), qs>=6.14.2 (alert #17), fast-uri>=3.1.2 (GHSA-q3j6-qgpj-74h6 + GHSA-v39h-62p7-jpjc)"
81
- },
82
74
  "devDependencies": {
83
75
  "@playwright/test": "^1.60.0",
84
76
  "@types/express": "^5.0.3",