actual-mcp-server 0.8.17 → 0.9.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
@@ -8,8 +8,8 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/actual-mcp-server)](https://www.npmjs.com/package/actual-mcp-server)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen)](https://nodejs.org/)
11
- [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue)](https://www.typescriptlang.org/)
12
- [![MCP Protocol](https://img.shields.io/badge/MCP-1.29-orange)](https://modelcontextprotocol.io/)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue)](https://www.typescriptlang.org/)
12
+ [![MCP Protocol](https://img.shields.io/badge/MCP-1.30-orange)](https://modelcontextprotocol.io/)
13
13
  [![Docker Pulls](https://img.shields.io/docker/pulls/agigante80/actual-mcp-server)](https://hub.docker.com/r/agigante80/actual-mcp-server)
14
14
  [![Docker Image Size](https://img.shields.io/docker/image-size/agigante80/actual-mcp-server/latest)](https://hub.docker.com/r/agigante80/actual-mcp-server)
15
15
  [![Unraid Community Apps](https://img.shields.io/badge/Unraid-Community%20Apps-f15a2c?logo=unraid&logoColor=white)](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)
@@ -326,11 +326,13 @@ For Claude Desktop (stdio), restart Claude after upgrading.
326
326
  |------|-------------|
327
327
  | `actual_transactions_get` | Get transactions for an account |
328
328
  | `actual_transactions_filter` | Filter with advanced criteria |
329
- | `actual_transactions_create` | Create new transaction(s) |
329
+ | `actual_transactions_create` | Create new transaction(s), including splits (pass a `subtransactions` array) |
330
330
  | `actual_transactions_import` | Import and reconcile transactions |
331
- | `actual_transactions_update` | Update a transaction |
331
+ | `actual_transactions_update` | Update a transaction, or edit the children of an existing split via `subtransactions` |
332
332
  | `actual_transactions_delete` | Delete a transaction |
333
333
 
334
+ > **Split transactions:** pass a `subtransactions` array (each child needs an `amount` in integer cents; `category` and `notes` are optional). The child amounts must sum to the parent `amount` (the server does not enforce this, so the tool does). A split parent carries no category of its own: put categories on the children. `actual_transactions_update` can edit the children of a transaction that is ALREADY a split; converting a plain transaction into a split via update is not supported (create it as a split instead). Note: `actual_transactions_import` forwards a `subtransactions` array to the API but does NOT apply the sum check, so use `actual_transactions_create` for guaranteed-balanced splits.
335
+
334
336
  **Utility (1)**
335
337
 
336
338
  | Tool | Description |
@@ -447,7 +449,7 @@ All configuration is via environment variables. Copy `.env.example` to `.env` to
447
449
  |----------|---------|----------|-------------|
448
450
  | **Actual Budget Connection** ||||
449
451
  | `ACTUAL_SERVER_URL` | _(none)_ | Yes | URL of your Actual Budget server. Use the same URL you type in your browser: `http://localhost:5006` (local), `http://192.168.1.x:5006` (network), `https://actual.yourdomain.com` (domain), or `http://actual:5006` (container name if on the same Docker network) |
450
- | `ACTUAL_PASSWORD` | _(none)_ | Yes | Password for Actual Budget server |
452
+ | `ACTUAL_PASSWORD` | _(none)_ | Yes | Login password for your Actual Budget **server**, the one you type on its own login screen. Not the same as `ACTUAL_BUDGET_PASSWORD` (which decrypts an E2E-encrypted budget) and unrelated to `AUTH_PROVIDER=oidc` (which controls how MCP *clients* authenticate to this server, not how this server authenticates to Actual). This server always signs in to Actual with a password, so upstream password login must stay enabled |
451
453
  | `ACTUAL_BUDGET_SYNC_ID` | _(none)_ | Yes | Budget Sync ID from Actual (Settings then Sync ID) |
452
454
  | `ACTUAL_BUDGET_PASSWORD` | _(none)_ | No | Optional encryption password for encrypted budgets |
453
455
  | `ALLOW_INSECURE_UPSTREAM` | `false` | No | Allow an `http://` upstream even when `ACTUAL_BUDGET_PASSWORD` is set (#161). Off by default so a plaintext upstream plus an encryption password is refused |
@@ -727,4 +729,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
727
729
 
728
730
  ---
729
731
 
730
- **Version:** 0.8.17 | **Tool Count:** 71 (verified LibreChat-compatible)
732
+ **Version:** 0.9.1 | **Tool Count:** 71 (verified LibreChat-compatible)
@@ -1,3 +1,4 @@
1
+ import { subtransactionsSum } from './schemas/common.js';
1
2
  import api from '@actual-app/api';
2
3
  // @actual-app/api is a CJS package (no "type" field). In NodeNext/ESM context TypeScript
3
4
  // cannot expose its named exports via static import syntax. At runtime the default import
@@ -1075,16 +1076,45 @@ export async function updateTransaction(id, fields) {
1075
1076
  return queueWriteOperation(async () => {
1076
1077
  // Pre-flight existence check (#212): the raw API silently no-ops on a missing id,
1077
1078
  // so an update that changed nothing would otherwise be reported as success. A
1078
- // targeted ActualQL query by id keeps this cheap (indexed lookup).
1079
+ // targeted ActualQL query by id keeps this cheap (indexed lookup). #305 extends
1080
+ // the select to is_parent + amount so the split guards below can run off the
1081
+ // same read (single source; no second out-of-queue read).
1079
1082
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1080
1083
  const { q } = (await import('@actual-app/api'));
1081
- const found = await withConcurrency(() => retry(async () => {
1082
- const res = (await rawRunQuery(q('transactions').filter({ id }).select(['id'])));
1083
- return Array.isArray(res?.data) && res.data.length > 0;
1084
+ const rows = await withConcurrency(() => retry(async () => {
1085
+ // #305: `.options({ splits: 'all' })` is REQUIRED. The default transactions
1086
+ // query excludes split PARENT rows, so a plain filter(id) returns 0 rows for
1087
+ // a split parent and this pre-flight would wrongly report it "not found"
1088
+ // (which is exactly what broke editing a split). `splits: 'all'` returns
1089
+ // regular transactions, split parents, and split children as flat rows, so
1090
+ // the existence + is_parent + amount read is correct for every kind.
1091
+ const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id }).select(['id', 'is_parent', 'amount'])));
1092
+ return Array.isArray(res?.data) ? res.data : [];
1084
1093
  }, { retries: 2, backoffMs: 200 }));
1085
- if (!found) {
1094
+ if (rows.length === 0) {
1086
1095
  throw new Error(`Transaction "${id}" not found. Use actual_transactions_get to list transactions.`);
1087
1096
  }
1097
+ // #305: split-edit guards, BEFORE the raw write. Two rules:
1098
+ // (a) subtransactions may only edit a transaction that is ALREADY a split;
1099
+ // converting a plain transaction into a split via updateTransaction is
1100
+ // broken in @actual-app/api 26.7.0 (it strands orphan children), so it
1101
+ // is rejected here rather than silently corrupting data.
1102
+ // (b) child amounts must sum to the effective parent amount. The API does
1103
+ // not enforce this; the ground truth is the caller's new `amount` if
1104
+ // supplied, else the stored amount read above.
1105
+ const subs = fields?.subtransactions;
1106
+ if (subs) {
1107
+ const row = rows[0];
1108
+ if (row.is_parent !== true) {
1109
+ throw new Error(`Transaction "${id}" is not a split. Converting a plain transaction into a split is not supported here; create the split with actual_transactions_create instead.`);
1110
+ }
1111
+ const providedAmount = fields.amount;
1112
+ const parentAmount = providedAmount != null ? providedAmount : row.amount;
1113
+ const sum = subtransactionsSum(subs);
1114
+ if (sum !== parentAmount) {
1115
+ throw new Error(`Subtransactions must sum to the parent amount. Expected ${parentAmount}, got ${sum}.`);
1116
+ }
1117
+ }
1088
1118
  await withConcurrency(() => retry(() => rawUpdateTransaction(id, fields), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
1089
1119
  });
1090
1120
  }
@@ -1105,7 +1135,13 @@ export async function updateTransactionBatch(updates) {
1105
1135
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1106
1136
  const { q } = (await import('@actual-app/api'));
1107
1137
  const existing = await withConcurrency(() => retry(async () => {
1108
- const res = (await rawRunQuery(q('transactions').filter({ id: { $oneof: ids } }).select(['id'])));
1138
+ // `.options({ splits: 'all' })` for the same reason as the single-update and
1139
+ // delete pre-flights (#305): the default query omits split PARENT rows, so a
1140
+ // batch touching a split parent would route a perfectly valid id to failed[]
1141
+ // as "not found". Splits themselves stay unsupported in batch (FieldsSchema
1142
+ // strips `subtransactions`); this only ensures a split parent's OTHER fields
1143
+ // are updatable here.
1144
+ const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id: { $oneof: ids } }).select(['id'])));
1109
1145
  return new Set(Array.isArray(res?.data) ? res.data.map((r) => r.id) : []);
1110
1146
  }, { retries: 2, backoffMs: 200 }));
1111
1147
  const succeeded = [];
@@ -1136,7 +1172,12 @@ export async function deleteTransaction(id) {
1136
1172
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1137
1173
  const { q } = (await import('@actual-app/api'));
1138
1174
  const found = await withConcurrency(() => retry(async () => {
1139
- const res = (await rawRunQuery(q('transactions').filter({ id }).select(['id'])));
1175
+ // `.options({ splits: 'all' })` is REQUIRED, same as the update pre-flight
1176
+ // above (#305): the default transactions query excludes split PARENT rows,
1177
+ // so without it a split parent reads as non-existent and this check throws
1178
+ // "not found" before the raw delete is ever reached. The effect was that a
1179
+ // split could be created through the tools but never deleted through them.
1180
+ const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id }).select(['id'])));
1140
1181
  return Array.isArray(res?.data) && res.data.length > 0;
1141
1182
  }, { retries: 2, backoffMs: 200 }));
1142
1183
  if (!found) {
@@ -163,6 +163,42 @@ export const offBudgetSchema = z
163
163
  .optional()
164
164
  .describe('Whether the account is off-budget');
165
165
  // ============================================================================
166
+ // SPLIT (SUBTRANSACTION) SCHEMAS (#305)
167
+ // ============================================================================
168
+ /**
169
+ * A single split child. Only `amount` is required; `category` and `notes` are
170
+ * optional. Child `account`/`date`/`cleared`/`reconciled` are ALWAYS derived
171
+ * from the parent by @actual-app/api's `makeChild` (26.7.0), and `payee_name`
172
+ * is not resolved on children, so extra fields are rejected here (`.strict()`)
173
+ * to keep the contract honest rather than silently ignored.
174
+ */
175
+ export const subtransactionSchema = z
176
+ .object({
177
+ amount: amountCentsSchema,
178
+ category: categoryIdSchema.optional(),
179
+ notes: notesSchema,
180
+ })
181
+ .strict();
182
+ /**
183
+ * The `subtransactions` array on a split. Bounded per the project convention of
184
+ * bounding every array input (cf. transactions_update_batch, entities_search
185
+ * `.max(100)`, transactions_uncategorized `.max(1000)`).
186
+ */
187
+ export const subtransactionsArraySchema = z
188
+ .array(subtransactionSchema)
189
+ .min(1, 'A split needs at least one subtransaction')
190
+ .max(100, 'A split cannot exceed 100 subtransactions')
191
+ .describe('Split children; each amount is integer cents and they must sum to the parent amount');
192
+ /**
193
+ * Pure sum helper: the single source of CALCULATION for the split amount
194
+ * invariant (#305). Enforced at two sites (create tool at the Zod layer where
195
+ * the parent amount is in the input; update adapter against
196
+ * `fields.amount ?? dbAmount`) so the arithmetic cannot drift between them.
197
+ */
198
+ export function subtransactionsSum(subs) {
199
+ return subs.reduce((total, s) => total + s.amount, 0);
200
+ }
201
+ // ============================================================================
166
202
  // COMPOSITE SCHEMAS
167
203
  // ============================================================================
168
204
  /**
@@ -192,6 +228,9 @@ export const CommonSchemas = {
192
228
  reconciled: reconciledSchema,
193
229
  closed: closedSchema,
194
230
  offBudget: offBudgetSchema,
231
+ // Splits (#305)
232
+ subtransaction: subtransactionSchema,
233
+ subtransactions: subtransactionsArraySchema,
195
234
  };
196
235
  // ============================================================================
197
236
  // EXAMPLES
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
- import { CommonSchemas } from '../lib/schemas/common.js';
4
- const InputSchema = z.object({
3
+ import { CommonSchemas, subtransactionsSum } from '../lib/schemas/common.js';
4
+ const InputSchema = z
5
+ .object({
5
6
  account: CommonSchemas.accountId,
6
7
  date: CommonSchemas.date,
7
8
  amount: CommonSchemas.amountCents,
@@ -11,6 +12,31 @@ const InputSchema = z.object({
11
12
  category: CommonSchemas.categoryId.optional(),
12
13
  cleared: CommonSchemas.cleared,
13
14
  imported_id: z.string().optional().describe('Original imported transaction ID'),
15
+ subtransactions: CommonSchemas.subtransactions
16
+ .optional()
17
+ .describe('Split this transaction: child amounts (integer cents) must sum to `amount`. Put categories on the children, not the parent.'),
18
+ })
19
+ // #305: cross-field split rules. The API does NOT enforce the sum invariant
20
+ // (a mismatch is stored and only flagged in the app), and a split parent
21
+ // carries no category (it moves to the children), so both are enforced here.
22
+ .superRefine((val, ctx) => {
23
+ if (!val.subtransactions)
24
+ return;
25
+ if (val.category != null) {
26
+ ctx.addIssue({
27
+ code: 'custom',
28
+ path: ['category'],
29
+ message: 'A split parent carries no category; put categories on the subtransactions instead.',
30
+ });
31
+ }
32
+ const sum = subtransactionsSum(val.subtransactions);
33
+ if (sum !== val.amount) {
34
+ ctx.addIssue({
35
+ code: 'custom',
36
+ path: ['subtransactions'],
37
+ message: `Subtransactions must sum to the parent amount. Expected ${val.amount}, got ${sum}.`,
38
+ });
39
+ }
14
40
  });
15
41
  const tool = {
16
42
  name: 'actual_transactions_create',
@@ -18,11 +44,15 @@ const tool = {
18
44
  inputSchema: InputSchema,
19
45
  call: async (args, _meta) => {
20
46
  const input = InputSchema.parse(args || {});
47
+ // #305: a split parent must carry is_parent so @actual-app/api links the
48
+ // children (without it the subtransactions are ignored). The sum invariant
49
+ // and the no-parent-category rule were already enforced by the schema above.
50
+ const payload = input.subtransactions ? { ...input, is_parent: true } : input;
21
51
  try {
22
52
  // Use addTransactions - it reliably creates transactions.
23
53
  // Note: API may return "ok" string instead of a UUID depending on server version.
24
54
  // "ok" is a valid success indicator — the transaction WAS created.
25
- const result = await adapter.addTransactions(input);
55
+ const result = await adapter.addTransactions(payload);
26
56
  if (!result || result.length === 0) {
27
57
  return {
28
58
  success: false,
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import adapter from '../lib/actual-adapter.js';
3
+ import { CommonSchemas } from '../lib/schemas/common.js';
3
4
  const InputSchema = z.object({
4
5
  id: z.string().optional().describe('Transaction ID to update (optional for smoke tests, required for actual usage)'),
5
6
  fields: z.object({
@@ -15,6 +16,14 @@ const InputSchema = z.object({
15
16
  transfer_id: z.string().nullable().optional().describe('Transfer transaction ID if this is a transfer'),
16
17
  cleared: z.boolean().nullable().optional().describe('Whether transaction is cleared'),
17
18
  reconciled: z.boolean().nullable().optional().describe('Whether transaction is reconciled'),
19
+ // #305: edit the children of an EXISTING split. The child amounts must sum
20
+ // to the parent amount; the target must already be a split. Both are
21
+ // enforced in the adapter pre-flight (it reads is_parent + amount), because
22
+ // the parent amount is not part of this input. Converting a plain
23
+ // transaction into a split here is rejected (unsupported by the API).
24
+ subtransactions: CommonSchemas.subtransactions
25
+ .optional()
26
+ .describe('Replace the children of an existing split; amounts must sum to the parent amount. To create a split, use actual_transactions_create.'),
18
27
  }).describe('Fields to update'),
19
28
  });
20
29
  const tool = {
@@ -36,7 +36,7 @@ const InputSchema = z.object({
36
36
  });
37
37
  const tool = {
38
38
  name: 'actual_transactions_update_batch',
39
- description: `Update multiple transactions in a single call. Accepts up to 50 {id, fields} pairs. Each update is applied independently: partial failures are reported per-item so you know exactly which succeeded and which failed.
39
+ description: `Update multiple transactions in a single call. Accepts up to 50 {id, fields} pairs. Each update is applied independently: partial failures are reported per-item so you know exactly which succeeded and which failed. Splits are not supported here: a subtransactions field is ignored in batch. Use actual_transactions_update to edit an existing split's children.
40
40
 
41
41
  Returns: { succeeded: [{id}], failed: [{id, error}], total, successCount, failureCount }
42
42
 
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.8.17",
4
+ "version": "0.9.1",
5
5
  "engines": {
6
6
  "node": ">=22.0.0",
7
7
  "npm": ">=10.0.0"
@@ -30,7 +30,7 @@
30
30
  "verify-tools": "npm run build && node scripts/verify-tools.js",
31
31
  "check:coverage": "node scripts/list-actual-api-methods.mjs",
32
32
  "direct-sync": "node scripts/direct-sync/bank-sync-direct.mjs",
33
- "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js",
33
+ "test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/generated_tools.smoke.test.js && node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/payees_delete.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js",
34
34
  "test:adapter": "npm run build && node dist/src/tests_adapter_runner.js",
35
35
  "test:e2e": "npx playwright test",
36
36
  "test:e2e:docker": "./tests/e2e/run-docker-e2e.sh",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@actual-app/api": "^26.7.0",
68
- "@modelcontextprotocol/sdk": "^1.29.0",
68
+ "@modelcontextprotocol/sdk": "^1.30.0",
69
69
  "debug": "^4.4.3",
70
70
  "dotenv": "^17.4.2",
71
71
  "express": "^5.2.1",
@@ -86,7 +86,7 @@
86
86
  "knip": "^6.16.1",
87
87
  "node-fetch": "^3.3.2",
88
88
  "tsconfig-paths": "^4.2.0",
89
- "typescript": "^6.0.3"
89
+ "typescript": "^7.0.2"
90
90
  },
91
91
  "license": "MIT",
92
92
  "keywords": [