actual-mcp-server 0.22.7 → 0.22.11
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 +13 -12
- package/dist/src/actualToolsManager.js +1 -0
- package/dist/src/index.js +19 -6
- package/dist/src/lib/actual-adapter.js +140 -18
- package/dist/src/lib/config-registry.js +0 -1
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/schemas/common.js +1 -1
- package/dist/src/lib/tool-annotations.js +1 -0
- package/dist/src/tools/get_context.js +49 -0
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/transactions_update.js +13 -9
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/
|
|
|
24
24
|
┌─────────────┐ MCP/HTTP ┌──────────────────┐ Actual API ┌──────────────┐
|
|
25
25
|
│ LibreChat │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
|
|
26
26
|
│ LobeChat │ │ Server │ │ Budget │
|
|
27
|
-
│ (remote) │ │ (
|
|
27
|
+
│ (remote) │ │ (82 tools) │ │ Server │
|
|
28
28
|
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
29
29
|
|
|
30
30
|
┌─────────────┐ MCP/stdio ┌──────────────────┐ Actual API ┌──────────────┐
|
|
31
31
|
│ Claude │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
|
|
32
32
|
│ Desktop │ │ Server │ │ Budget │
|
|
33
|
-
│ (local) │ │ (
|
|
33
|
+
│ (local) │ │ (82 tools) │ │ Server │
|
|
34
34
|
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -38,14 +38,14 @@ Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/
|
|
|
38
38
|
|
|
39
39
|
Most Actual Budget MCP implementations are simple stdio bridges designed for single-user, local use with Claude Desktop. This project goes further:
|
|
40
40
|
|
|
41
|
-
- **
|
|
41
|
+
- **82 tools, the most comprehensive coverage available.** Accounts, transactions, categories, payees, tags, notes, rules, budgets, batch operations, bank sync, and more. Covers the reachable Actual Budget API with no genuine gaps.
|
|
42
42
|
- **HTTP and stdio transport.** Runs as a real remote server for LibreChat/LobeChat (`--http`), or as a direct local process for Claude Desktop (`--stdio`). No Docker or HTTP server is needed for local use.
|
|
43
43
|
- **6 exclusive ActualQL-powered tools.** Search and summarise transactions by month, amount, category, or payee using Actual Budget's native query engine. Aggregated results, no raw data dumped into the AI context window.
|
|
44
44
|
- **Multi-budget switching at runtime.** Configure multiple budget files and let the AI switch between them mid-conversation with `actual_budgets_switch`. Works on both transports: HTTP keys the active budget to the MCP session, and stdio (Claude Desktop, Claude Code, Cursor) gets a synthetic per-process session so a switch is scoped to that process rather than shared globally (#348).
|
|
45
45
|
- **Multi-user ready with OIDC.** Secure every session with JWKS-validated JWTs and per-user budget ACLs. No shared tokens required.
|
|
46
46
|
- **Production-grade reliability on both transports.** HTTP connection pooling (up to 15 concurrent sessions), and a long-lived stdio process that logs in ONCE and reuses that connection for every tool call instead of re-authenticating per call, so Claude Desktop and Claude Code stay fast and a burst of calls no longer risks a per-call login storm against the upstream limiter. Automatic retry with exponential backoff, and a full test suite (unit + E2E + integration).
|
|
47
47
|
|
|
48
|
-
> **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All
|
|
48
|
+
> **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All 82 tools tested end-to-end. Any MCP-compatible client should work.
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
@@ -182,7 +182,7 @@ Add to `claude_desktop_config.json` (see [docs/guides/MCP_CLIENTS_SETUP.md](docs
|
|
|
182
182
|
}
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
> **No token needed.** stdio runs as a local process owned by your user. The transport itself is the security boundary. All
|
|
185
|
+
> **No token needed.** stdio runs as a local process owned by your user. The transport itself is the security boundary. All 82 tools are available.
|
|
186
186
|
>
|
|
187
187
|
> **`MCP_BRIDGE_DATA_DIR` should be an absolute path.** Without one, the data directory resolves relative to wherever the client spawns the process, which can be unpredictable. The directory is created automatically on first run.
|
|
188
188
|
|
|
@@ -308,7 +308,7 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
308
308
|
|
|
309
309
|
## Available Tools
|
|
310
310
|
|
|
311
|
-
**
|
|
311
|
+
**82 tools** across all categories. All tools use the `actual_<category>_<action>` naming convention.
|
|
312
312
|
|
|
313
313
|
### Accounts (12)
|
|
314
314
|
|
|
@@ -337,10 +337,10 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
337
337
|
| `actual_transactions_filter` | Filter with advanced criteria |
|
|
338
338
|
| `actual_transactions_create` | Create new transaction(s), including splits (pass a `subtransactions` array) |
|
|
339
339
|
| `actual_transactions_import` | Import and reconcile transactions |
|
|
340
|
-
| `actual_transactions_update` | Update a transaction, or edit the children of an existing split via `subtransactions` |
|
|
340
|
+
| `actual_transactions_update` | Update a transaction, split a plain one in place, or edit the children of an existing split via `subtransactions` |
|
|
341
341
|
| `actual_transactions_delete` | Delete a transaction |
|
|
342
342
|
|
|
343
|
-
> **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`
|
|
343
|
+
> **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` with `subtransactions` edits the children of a transaction that is already a split, or splits a plain transaction IN PLACE: it keeps its id, `imported_id` and cleared state, so a bank-synced transaction can be split without breaking import matching. Its own category is cleared, and a child that names no category inherits it. A split child cannot itself be split. 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.
|
|
344
344
|
|
|
345
345
|
**Utility (2)**
|
|
346
346
|
|
|
@@ -453,12 +453,13 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
453
453
|
|
|
454
454
|
`actual_budget_updates_batch`: batch multiple budget updates in one call
|
|
455
455
|
|
|
456
|
-
### Server Information & Lookup (
|
|
456
|
+
### Server Information & Lookup (5)
|
|
457
457
|
|
|
458
458
|
| Tool | Description |
|
|
459
459
|
|------|-------------|
|
|
460
460
|
| `actual_server_info` | Server status, version, build info |
|
|
461
461
|
| `actual_server_get_version` | Actual Budget server version |
|
|
462
|
+
| `actual_get_context` | Retrieve open accounts, category groups with nested categories, and payees (name-sorted, capped by `payeeLimit`) in a single call |
|
|
462
463
|
| `actual_get_id_by_name` | Resolve an exact name → UUID for accounts, categories, payees |
|
|
463
464
|
| `actual_entities_search` | Find accounts/categories/payees by a name pattern (contains/startsWith/endsWith/exact/fuzzy). Fixes "payee not found" from a partial or mistyped name |
|
|
464
465
|
|
|
@@ -601,7 +602,7 @@ stdio is the simplest way to connect Claude Desktop directly to Actual Budget. T
|
|
|
601
602
|
- No auth token. Process ownership is the security boundary.
|
|
602
603
|
- All logs go to stderr so they never corrupt the JSON-RPC framing on stdout
|
|
603
604
|
- The process exits when stdin closes (Claude Desktop shutting down)
|
|
604
|
-
- All
|
|
605
|
+
- All 82 tools are available, identical to HTTP mode
|
|
605
606
|
|
|
606
607
|
**Start manually to verify:**
|
|
607
608
|
|
|
@@ -704,7 +705,7 @@ See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-m
|
|
|
704
705
|
| Command | What It Tests | Requires Live Server |
|
|
705
706
|
|---------|---------------|---------------------|
|
|
706
707
|
| `npm run build` | TypeScript compilation | No |
|
|
707
|
-
| `npm run test:unit-js` |
|
|
708
|
+
| `npm run test:unit-js` | 82-tool smoke, schema validation, auth ACL | No |
|
|
708
709
|
| `npm run test:adapter` | Adapter, retry logic, concurrency | No |
|
|
709
710
|
| `npm run test:e2e` | MCP protocol compliance (Playwright) | No |
|
|
710
711
|
| `npm run test:e2e:docker:full` | Full stack integration | Yes (Docker) |
|
|
@@ -788,7 +789,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
788
789
|
|
|
789
790
|
---
|
|
790
791
|
|
|
791
|
-
**Version:** 0.22.
|
|
792
|
+
**Version:** 0.22.11 | **Tool Count:** 82 (verified LibreChat-compatible)
|
|
792
793
|
|
|
793
794
|
## Sponsor
|
|
794
795
|
|
|
@@ -85,6 +85,7 @@ const IMPLEMENTED_TOOLS = [
|
|
|
85
85
|
'actual_budgets_export',
|
|
86
86
|
'actual_budgets_import',
|
|
87
87
|
'actual_preferences_get',
|
|
88
|
+
'actual_get_context',
|
|
88
89
|
];
|
|
89
90
|
// 🔑 Mapping of Actual API function names → your MCP tool names
|
|
90
91
|
// This allows us to compare what exists in the API vs. what it has been wrapped.
|
package/dist/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// guard cannot live in bin/ alone. Importing the named export does not change that:
|
|
6
6
|
// the module's top-level `enforceNodeVersion()` still fires here, exactly once.
|
|
7
7
|
import { findRootPackageJson } from './lib/node-version-guard.js';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
8
|
+
import path, { dirname } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { isKnownBenignRejection } from './lib/rejection-allowlist.js';
|
|
@@ -83,10 +83,6 @@ process.on('uncaughtException', (error) => {
|
|
|
83
83
|
});
|
|
84
84
|
// Minimal early help handling before any side-effectful modules (prevents dotenv from running on --help)
|
|
85
85
|
const argsEarly = process.argv.slice(2);
|
|
86
|
-
// dotenv v17 outputs diagnostic text to stdout by default (console.log).
|
|
87
|
-
// Suppress it unconditionally — in stdio mode stdout is the JSON-RPC channel
|
|
88
|
-
// (non-JSON corrupts the framing), and in other modes the diagnostic is noise.
|
|
89
|
-
process.env.DOTENV_CONFIG_QUIET = 'true';
|
|
90
86
|
// Set MCP_STDIO_MODE before the async IIFE so it is in place when src/logger.ts is first
|
|
91
87
|
// imported. The Winston Console transport reads this env var at construction time to decide
|
|
92
88
|
// whether to route all output to stderr (required in stdio mode — stdout writes corrupt JSON-RPC).
|
|
@@ -148,7 +144,24 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
148
144
|
// Load dotenv here (dynamic import) only when not running with --help
|
|
149
145
|
if (!argsEarly.includes('--help')) {
|
|
150
146
|
const dotenv = await import('dotenv');
|
|
151
|
-
dotenv
|
|
147
|
+
// #501: this runs BEFORE ./logger.js hijacks console.*, so anything dotenv prints
|
|
148
|
+
// lands on the real stdout (the JSON-RPC channel under --stdio) and skips the #220
|
|
149
|
+
// redaction. Explicit options beat every DOTENV_* environment knob, so each is pinned:
|
|
150
|
+
// quiet and debug keep both streams empty, override keeps a secret already in the
|
|
151
|
+
// environment authoritative over the file, encoding stops DOTENV_ENCODING from silently
|
|
152
|
+
// skipping the file (a bogus value loads nothing and reports it only in the discarded
|
|
153
|
+
// result), fast keeps DOTENV_FAST from swapping in the alternate parser (no input was
|
|
154
|
+
// found that the two parse differently, so this pin is defensive rather than tested
|
|
155
|
+
// by behaviour), and path keeps the cwd-relative .env
|
|
156
|
+
// that npx, `node dist/src/index.js` and Docker (WORKDIR /app) all rely on.
|
|
157
|
+
dotenv.config({
|
|
158
|
+
quiet: true,
|
|
159
|
+
debug: false,
|
|
160
|
+
override: false,
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
fast: false,
|
|
163
|
+
path: path.resolve(process.cwd(), '.env'),
|
|
164
|
+
});
|
|
152
165
|
}
|
|
153
166
|
// Enable verbose debug output when --debug is passed
|
|
154
167
|
if (argsEarly.includes('--debug')) {
|
|
@@ -760,7 +760,7 @@ async function shutdownActualApi(opts) {
|
|
|
760
760
|
setApiInitialized(false);
|
|
761
761
|
}
|
|
762
762
|
}
|
|
763
|
-
import { BANK_SYNC_SETTLE_MS, WRITE_SESSION_DELAY_MS } from './constants.js';
|
|
763
|
+
import { BANK_SYNC_SETTLE_MS, SPLIT_VERIFY_INTERVAL_MS, SPLIT_VERIFY_TIMEOUT_MS, WRITE_SESSION_DELAY_MS } from './constants.js';
|
|
764
764
|
// Concurrency limiter extracted to ./actual-adapter/concurrency.ts (#166).
|
|
765
765
|
// Imported for internal use (every method wraps its raw call in withConcurrency)
|
|
766
766
|
// and setMaxConcurrency is re-exported.
|
|
@@ -1646,6 +1646,81 @@ export async function getPayees() {
|
|
|
1646
1646
|
return await withConcurrency(() => retry(() => rawGetPayees(), { retries: 2, backoffMs: 200 }));
|
|
1647
1647
|
});
|
|
1648
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* #484: accounts, category groups and payees in ONE withActualApi session, so one api lock cycle
|
|
1651
|
+
* and one consistent snapshot. The reads are raw and sequential: an `adapter.get*` call here would
|
|
1652
|
+
* nest a session inside this one and deadlock until ACTUAL_OP_TIMEOUT_MS, and Promise.all would buy
|
|
1653
|
+
* nothing because every call serialises on the one api singleton anyway.
|
|
1654
|
+
*/
|
|
1655
|
+
export async function getContext(options) {
|
|
1656
|
+
return withActualApi(async () => {
|
|
1657
|
+
observability.incrementToolCall('actual.context.get').catch(() => { });
|
|
1658
|
+
const includeAccounts = options?.includeAccounts ?? true;
|
|
1659
|
+
const includeCategories = options?.includeCategories ?? true;
|
|
1660
|
+
const includePayees = options?.includePayees ?? true;
|
|
1661
|
+
const includeClosed = options?.includeClosed ?? false;
|
|
1662
|
+
const payeeLimit = options?.payeeLimit ?? 500;
|
|
1663
|
+
const result = {};
|
|
1664
|
+
// Payees need the account listing even when accounts were not requested: upstream getPayees()
|
|
1665
|
+
// drops the transfer payees of DELETED accounts but not of CLOSED ones, so hiding closed
|
|
1666
|
+
// accounts means hiding their transfer payees here too.
|
|
1667
|
+
const closedIds = new Set();
|
|
1668
|
+
if (includeAccounts || (includePayees && !includeClosed)) {
|
|
1669
|
+
const rawAccounts = await withConcurrency(() => retry(() => rawGetAccounts(), { retries: 2, backoffMs: 200 }));
|
|
1670
|
+
const all = Array.isArray(rawAccounts) ? rawAccounts : [];
|
|
1671
|
+
for (const a of all)
|
|
1672
|
+
if (a.closed)
|
|
1673
|
+
closedIds.add(String(a.id));
|
|
1674
|
+
if (includeAccounts) {
|
|
1675
|
+
result.accounts = all
|
|
1676
|
+
.filter((a) => includeClosed || !a.closed)
|
|
1677
|
+
.map((a) => ({
|
|
1678
|
+
id: String(a.id),
|
|
1679
|
+
name: String(a.name ?? ''),
|
|
1680
|
+
offbudget: Boolean(a.offbudget),
|
|
1681
|
+
closed: Boolean(a.closed),
|
|
1682
|
+
}));
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (includeCategories) {
|
|
1686
|
+
// No options: upstream then returns hidden groups and categories too, flagged by `hidden`.
|
|
1687
|
+
const rawGroups = await withConcurrency(() => retry(() => rawGetCategoryGroups(), { retries: 2, backoffMs: 200 }));
|
|
1688
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1689
|
+
result.categoryGroups = (Array.isArray(rawGroups) ? rawGroups : []).map((g) => ({
|
|
1690
|
+
id: String(g.id),
|
|
1691
|
+
name: String(g.name ?? ''),
|
|
1692
|
+
is_income: Boolean(g.is_income),
|
|
1693
|
+
hidden: Boolean(g.hidden),
|
|
1694
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1695
|
+
categories: (Array.isArray(g.categories) ? g.categories : []).map((c) => ({
|
|
1696
|
+
id: String(c.id),
|
|
1697
|
+
name: String(c.name ?? ''),
|
|
1698
|
+
hidden: Boolean(c.hidden),
|
|
1699
|
+
})),
|
|
1700
|
+
}));
|
|
1701
|
+
}
|
|
1702
|
+
if (includePayees) {
|
|
1703
|
+
const rawPayees = await withConcurrency(() => retry(() => rawGetPayees(), { retries: 2, backoffMs: 200 }));
|
|
1704
|
+
// Name order, case-insensitive, id as the tie-break, so the cut is deterministic. Transfer
|
|
1705
|
+
// payees count toward the cap. Recency is not an option: payees carry no last-used field.
|
|
1706
|
+
const list = (Array.isArray(rawPayees) ? rawPayees : [])
|
|
1707
|
+
.map((p) => ({
|
|
1708
|
+
id: String(p.id),
|
|
1709
|
+
name: String(p.name ?? ''),
|
|
1710
|
+
transfer_acct: p.transfer_acct != null ? String(p.transfer_acct) : null,
|
|
1711
|
+
}))
|
|
1712
|
+
.filter((p) => includeClosed || p.transfer_acct === null || !closedIds.has(p.transfer_acct))
|
|
1713
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1714
|
+
result.payees = list.slice(0, payeeLimit);
|
|
1715
|
+
result.payeesTruncated = list.length > payeeLimit;
|
|
1716
|
+
result.payeeTotal = list.length;
|
|
1717
|
+
if (result.payeesTruncated) {
|
|
1718
|
+
result.hint = `Showing ${result.payees.length} of ${list.length} payees. Use actual_entities_search or actual_get_id_by_name to find the rest.`;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return result;
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1649
1724
|
/**
|
|
1650
1725
|
* #388: turn an optional FILTER id that is actually a NAME into a refusal that names the id.
|
|
1651
1726
|
*
|
|
@@ -1875,7 +1950,7 @@ export async function updateTransaction(id, fields) {
|
|
|
1875
1950
|
// so an update that changed nothing would otherwise be reported as success. A
|
|
1876
1951
|
// targeted ActualQL query by id keeps this cheap (indexed lookup). #305 extends
|
|
1877
1952
|
// the select to is_parent + amount so the split guards below can run off the
|
|
1878
|
-
// same read (single source; no second out-of-queue read).
|
|
1953
|
+
// same read (single source; no second out-of-queue read); #489 adds is_child.
|
|
1879
1954
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1880
1955
|
const { q } = (await import('@actual-app/api'));
|
|
1881
1956
|
const rows = await withConcurrency(() => retry(async () => {
|
|
@@ -1885,34 +1960,80 @@ export async function updateTransaction(id, fields) {
|
|
|
1885
1960
|
// (which is exactly what broke editing a split). `splits: 'all'` returns
|
|
1886
1961
|
// regular transactions, split parents, and split children as flat rows, so
|
|
1887
1962
|
// the existence + is_parent + amount read is correct for every kind.
|
|
1888
|
-
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id }).select(['id', 'is_parent', 'amount'])));
|
|
1963
|
+
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' }).filter({ id }).select(['id', 'is_parent', 'is_child', 'amount', 'category'])));
|
|
1889
1964
|
return Array.isArray(res?.data) ? res.data : [];
|
|
1890
1965
|
}, { retries: 2, backoffMs: 200 }));
|
|
1891
1966
|
if (rows.length === 0) {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
//
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1898
|
-
// is
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
//
|
|
1967
|
+
// #489: a typed refusal (nothing was written), wording kept byte-identical to what callers saw.
|
|
1968
|
+
throw new NotFoundRefusal('Transaction', id, 'actual_transactions_get', undefined, `Transaction "${id}" not found. Use actual_transactions_get to list transactions.`);
|
|
1969
|
+
}
|
|
1970
|
+
// Split guards, BEFORE the raw write:
|
|
1971
|
+
// (a) a CHILD cannot itself be split: upstream nests one level only.
|
|
1972
|
+
// (b) child amounts must sum to the effective parent amount. The API does not
|
|
1973
|
+
// enforce this; the ground truth is the caller's new `amount` if supplied,
|
|
1974
|
+
// else the stored amount read above.
|
|
1975
|
+
// #489 lifted #305's third rule, which refused a PLAIN target: "converting a plain
|
|
1976
|
+
// transaction into a split via updateTransaction is broken in 26.7.0 (it strands orphan
|
|
1977
|
+
// children)". That is no longer true. Upstream #8207 (`shared/transactions.ts`) converts in
|
|
1978
|
+
// place through makeChild, and a live probe at 26.9.0 confirmed it persists across a fresh
|
|
1979
|
+
// download with the SAME id and `imported_id`, and keeps a transfer balanced by replacing the
|
|
1980
|
+
// single counterpart with one per child. Reusing that path is why #489 is not a create plus
|
|
1981
|
+
// delete: raw `addTransactions` does not run transfers while `deleteTransaction` does, so the
|
|
1982
|
+
// orchestration deleted a transfer's counterpart, and it re-ran rules on the new parent.
|
|
1902
1983
|
const subs = fields?.subtransactions;
|
|
1984
|
+
const row = rows[0];
|
|
1985
|
+
const converting = Boolean(subs) && row.is_parent !== true;
|
|
1903
1986
|
if (subs) {
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
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.`);
|
|
1987
|
+
if (row.is_child === true) {
|
|
1988
|
+
throw new OutOfRangeRefusal(`Transaction "${id}" is a split child and cannot itself be split. Edit its parent's subtransactions with actual_transactions_update instead.`, id);
|
|
1907
1989
|
}
|
|
1908
1990
|
const providedAmount = fields.amount;
|
|
1909
1991
|
const parentAmount = providedAmount != null ? providedAmount : row.amount;
|
|
1910
1992
|
const sum = subtransactionsSum(subs);
|
|
1911
1993
|
if (sum !== parentAmount) {
|
|
1912
|
-
throw new
|
|
1994
|
+
throw new OutOfRangeRefusal(`Subtransactions must sum to the parent amount. Expected ${parentAmount}, got ${sum}.`, String(sum));
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
// #489: a converted parent keeps its old category upstream (the update path nulls a parent's
|
|
1998
|
+
// category only when `account` is in the diff), while the Actual UI shows a split parent with
|
|
1999
|
+
// none. Clear it in the SAME write, and hand its category to every child that names none, so
|
|
2000
|
+
// the children still inherit it exactly as upstream's makeChild would have done.
|
|
2001
|
+
let payload = fields;
|
|
2002
|
+
if (converting) {
|
|
2003
|
+
const f = fields;
|
|
2004
|
+
const inherited = 'category' in f ? f.category : (row.category ?? null);
|
|
2005
|
+
payload = {
|
|
2006
|
+
...f,
|
|
2007
|
+
category: null,
|
|
2008
|
+
subtransactions: f.subtransactions.map((c) => ('category' in c ? c : { ...c, category: inherited })),
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
await withConcurrency(() => retry(() => rawUpdateTransaction(id, payload), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
2012
|
+
if (!converting)
|
|
2013
|
+
return {};
|
|
2014
|
+
// #489: verify AFTER the write. Upstream resolves before the conversion lands (see
|
|
2015
|
+
// SPLIT_VERIFY_TIMEOUT_MS), so success is only claimed once the parent reads as a split
|
|
2016
|
+
// with every requested child. Polling here, inside the drain, also guarantees the change is
|
|
2017
|
+
// local before the drain's api.sync(). A retry after a timeout is safe: the row is then a
|
|
2018
|
+
// split, so a second call EDITS its children rather than creating anything new.
|
|
2019
|
+
const expected = subs.length;
|
|
2020
|
+
const deadline = Date.now() + SPLIT_VERIFY_TIMEOUT_MS;
|
|
2021
|
+
for (;;) {
|
|
2022
|
+
const res = (await rawRunQuery(q('transactions').options({ splits: 'all' })
|
|
2023
|
+
.filter({ $or: [{ id }, { parent_id: id }] })
|
|
2024
|
+
.select(['id', 'is_parent', 'parent_id'])));
|
|
2025
|
+
const data = Array.isArray(res?.data) ? res.data : [];
|
|
2026
|
+
const parent = data.find((r) => r.id === id);
|
|
2027
|
+
const children = data.filter((r) => r.parent_id === id).map((r) => r.id);
|
|
2028
|
+
if (parent?.is_parent === true && children.length === expected) {
|
|
2029
|
+
return { split: { parent: id, children } };
|
|
2030
|
+
}
|
|
2031
|
+
if (Date.now() >= deadline) {
|
|
2032
|
+
throw new Error(`Transaction "${id}" was sent to be split into ${expected} parts, but the split was not visible after ${SPLIT_VERIFY_TIMEOUT_MS}ms (found ${children.length} children), so the outcome is unknown. ` +
|
|
2033
|
+
'Read it back with actual_transactions_get before retrying: if it is already a split, a retry edits its children rather than duplicating them.');
|
|
1913
2034
|
}
|
|
2035
|
+
await new Promise((resolve) => setTimeout(resolve, SPLIT_VERIFY_INTERVAL_MS));
|
|
1914
2036
|
}
|
|
1915
|
-
await withConcurrency(() => retry(() => rawUpdateTransaction(id, fields), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
1916
2037
|
});
|
|
1917
2038
|
}
|
|
1918
2039
|
export async function updateTransactionBatch(updates) {
|
|
@@ -3571,5 +3692,6 @@ export default {
|
|
|
3571
3692
|
exportBudget,
|
|
3572
3693
|
importBudget,
|
|
3573
3694
|
getPreferences,
|
|
3695
|
+
getContext,
|
|
3574
3696
|
notifications,
|
|
3575
3697
|
};
|
|
@@ -42,7 +42,6 @@ export const RAW_ENV_ALLOWLIST = [
|
|
|
42
42
|
{ name: 'NODE_ENV', reason: 'framework var; selects prod log format and prod behaviours', readSite: 'httpServer.ts, logger.ts (resolveLogConfig)' },
|
|
43
43
|
{ name: 'DEBUG', reason: 'framework debug toggle', readSite: 'utils.ts, index.ts' },
|
|
44
44
|
{ name: 'LOG_LEVEL', reason: 'debug-detection toggle (distinct from MCP_BRIDGE_LOG_LEVEL, the winston level)', readSite: 'utils.ts' },
|
|
45
|
-
{ name: 'DOTENV_CONFIG_QUIET', reason: 'dotenv internal flag, not an operator knob', readSite: 'index.ts', documented: false },
|
|
46
45
|
{ name: 'VERSION', reason: 'build-time version arg, injected by the Docker build, not an operator knob', readSite: 'index.ts, server_info.ts', documented: false },
|
|
47
46
|
// Domain-time: read after config loads; future candidates for schema promotion.
|
|
48
47
|
{ name: 'ACTUAL_API_CONCURRENCY', reason: 'adapter concurrency tuning; future schema-promotion candidate', readSite: 'lib/actual-adapter/concurrency.ts' },
|
|
@@ -46,6 +46,18 @@ export const BANK_SYNC_SETTLE_MS = 30_000;
|
|
|
46
46
|
* session at the cost of slightly higher latency.
|
|
47
47
|
*/
|
|
48
48
|
export const WRITE_SESSION_DELAY_MS = 100;
|
|
49
|
+
/**
|
|
50
|
+
* #489: how long (ms) to wait for a plain-to-split conversion to become visible.
|
|
51
|
+
*
|
|
52
|
+
* Upstream `api/transaction-update` returns `handlers['transactions-batch-update'](diff)['updated']`
|
|
53
|
+
* without awaiting it (verified at @actual-app/api 26.9.0), so the write lands AFTER the call
|
|
54
|
+
* resolves. Measured live, a read issued immediately after still sees the plain row. The adapter
|
|
55
|
+
* therefore reads the split back, polling every SPLIT_VERIFY_INTERVAL_MS, and refuses to report
|
|
56
|
+
* success if it has not appeared within this bound. A local SQLite write, so milliseconds in
|
|
57
|
+
* practice; the bound only decides how long a genuinely lost write takes to be reported.
|
|
58
|
+
*/
|
|
59
|
+
export const SPLIT_VERIFY_TIMEOUT_MS = 2000;
|
|
60
|
+
export const SPLIT_VERIFY_INTERVAL_MS = 25;
|
|
49
61
|
// ============================================================================
|
|
50
62
|
// MCP SERVER
|
|
51
63
|
// ============================================================================
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Common validation schemas used across MCP tools for Actual Budget.
|
|
5
5
|
* These schemas provide consistent validation, better error messages,
|
|
6
|
-
* and reduce duplication across the
|
|
6
|
+
* and reduce duplication across the 82 tool definitions.
|
|
7
7
|
*/
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { MAX_NAME_LENGTH, MAX_NOTES_LENGTH, DATE_PATTERN, MONTH_PATTERN, UUID_PATTERN } from '../constants.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createTool } from '../lib/toolFactory.js';
|
|
3
|
+
import adapter from '../lib/actual-adapter.js';
|
|
4
|
+
/**
|
|
5
|
+
* actual_get_context (#484)
|
|
6
|
+
*
|
|
7
|
+
* One call that returns the ids an assistant needs before it can do anything else: accounts,
|
|
8
|
+
* category groups with their categories, and payees, read in a single api session.
|
|
9
|
+
*
|
|
10
|
+
* The name follows the actual_get_id_by_name precedent: a cross-entity snapshot has no single
|
|
11
|
+
* domain noun for actual_{domain}_{action}. Read-only.
|
|
12
|
+
*/
|
|
13
|
+
const schema = z
|
|
14
|
+
.object({
|
|
15
|
+
includeAccounts: z.boolean().optional().default(true)
|
|
16
|
+
.describe('Include accounts (default: true).'),
|
|
17
|
+
includeCategories: z.boolean().optional().default(true)
|
|
18
|
+
.describe('Include category groups with their categories, hidden ones flagged (default: true).'),
|
|
19
|
+
includePayees: z.boolean().optional().default(true)
|
|
20
|
+
.describe('Include payees (default: true).'),
|
|
21
|
+
includeClosed: z.boolean().optional().default(false)
|
|
22
|
+
.describe('Include closed accounts and their transfer payees (default: false).'),
|
|
23
|
+
payeeLimit: z.number().int().min(1).max(2000).optional().default(500)
|
|
24
|
+
.describe('Maximum payees to return, 1 to 2000 (default: 500). Payees are sorted by name; when more exist, payeesTruncated is true and payeeTotal gives the full count. Use actual_entities_search or actual_get_id_by_name for the rest.'),
|
|
25
|
+
})
|
|
26
|
+
.refine((v) => v.includeAccounts || v.includeCategories || v.includePayees, {
|
|
27
|
+
message: 'At least one of includeAccounts, includeCategories or includePayees must be true.',
|
|
28
|
+
path: ['includeAccounts'],
|
|
29
|
+
});
|
|
30
|
+
export default createTool({
|
|
31
|
+
name: 'actual_get_context',
|
|
32
|
+
description: 'Read the budget structure and entity UUIDs (accounts, category groups with nested categories, and payees) in a single call. ' +
|
|
33
|
+
'Call this first to bootstrap context so you have the ids needed for transactions, rules, or budget operations without several list calls. ' +
|
|
34
|
+
'A section you exclude is omitted from the response rather than returned empty. Read-only.',
|
|
35
|
+
schema,
|
|
36
|
+
handler: async (input) => {
|
|
37
|
+
return await adapter.getContext(input);
|
|
38
|
+
},
|
|
39
|
+
examples: [
|
|
40
|
+
{
|
|
41
|
+
description: 'Bootstrap budget context with open accounts, categories, and up to 500 payees',
|
|
42
|
+
input: {},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
description: 'Fetch accounts and categories without reading payees',
|
|
46
|
+
input: { includePayees: false },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
package/dist/src/tools/index.js
CHANGED
|
@@ -80,3 +80,4 @@ export { default as tags_delete } from './tags_delete.js';
|
|
|
80
80
|
export { default as notes_get } from './notes_get.js';
|
|
81
81
|
export { default as notes_update } from './notes_update.js';
|
|
82
82
|
export { default as preferences_get } from './preferences_get.js';
|
|
83
|
+
export { default as get_context } from './get_context.js';
|
|
@@ -24,24 +24,28 @@ const InputSchema = z.object({
|
|
|
24
24
|
transfer_id: z.string().nullable().optional().describe('Transfer transaction ID if this is a transfer'),
|
|
25
25
|
cleared: z.boolean().nullable().optional().describe('Whether transaction is cleared'),
|
|
26
26
|
reconciled: z.boolean().nullable().optional().describe('Whether transaction is reconciled'),
|
|
27
|
-
// #305: edit the children of an
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// the
|
|
31
|
-
//
|
|
27
|
+
// #305: edit the children of an existing split. #489: or split a PLAIN transaction in
|
|
28
|
+
// place (same id, so imported_id and bank-sync matching are kept). The child amounts must
|
|
29
|
+
// sum to the parent amount, and a split child cannot itself be split; both are enforced in
|
|
30
|
+
// the adapter pre-flight (it reads is_parent, is_child and amount), because the stored
|
|
31
|
+
// amount is not part of this input.
|
|
32
32
|
subtransactions: CommonSchemas.subtransactions
|
|
33
33
|
.optional()
|
|
34
|
-
.describe('
|
|
34
|
+
.describe('Split children. On a plain transaction this splits it in place (same id, import fields kept); on a split it replaces the children. Amounts must sum to the parent amount.'),
|
|
35
35
|
}).describe('Fields to update'),
|
|
36
36
|
});
|
|
37
37
|
const tool = {
|
|
38
38
|
name: 'actual_transactions_update',
|
|
39
|
-
description: 'Update an existing transaction in Actual Budget. Provide the transaction ID and the fields you want to update.'
|
|
39
|
+
description: 'Update an existing transaction in Actual Budget. Provide the transaction ID and the fields you want to update. ' +
|
|
40
|
+
'To split an existing transaction across categories in one call, pass fields.subtransactions (amounts summing to the transaction amount): ' +
|
|
41
|
+
'it is split in place, keeping its id and import fields, and the result lists the new child ids. ' +
|
|
42
|
+
'The parent\'s own category is cleared; a child that names no category inherits it.',
|
|
40
43
|
inputSchema: InputSchema,
|
|
41
44
|
call: async (args, _meta) => {
|
|
42
45
|
const input = InputSchema.parse(args || {});
|
|
43
|
-
await adapter.updateTransaction(input.id, input.fields);
|
|
44
|
-
|
|
46
|
+
const split = (await adapter.updateTransaction(input.id, input.fields))?.split;
|
|
47
|
+
// #489: name the children only when a split was CREATED, so the common edit keeps its shape.
|
|
48
|
+
return split ? { success: true, split } : { success: true };
|
|
45
49
|
},
|
|
46
50
|
};
|
|
47
51
|
export default tool;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.22.
|
|
4
|
+
"version": "0.22.11",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
8
8
|
},
|
|
9
|
-
"description": "MCP server with
|
|
9
|
+
"description": "MCP server with 82 tools for AI-driven financial management with Actual Budget. HTTP and stdio transports for LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
|
|
10
10
|
"homepage": "https://github.com/agigante80/actual-mcp-server#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -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/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.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/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.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/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.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_hold_for_next_month.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/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.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/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.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/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.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/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.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/stdio_budget_switch.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/playwright_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/server_version_probe_outside_op_timeout.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 && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.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/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.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/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.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_hold_for_next_month.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/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.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/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.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/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.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/transactions_update_split_convert.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/stdio_budget_switch.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/playwright_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/server_version_probe_outside_op_timeout.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 && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.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",
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@actual-app/api": "^26.9.0",
|
|
72
|
-
"@modelcontextprotocol/sdk": "^1.30.
|
|
72
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
73
73
|
"debug": "^4.4.3",
|
|
74
|
-
"dotenv": "^
|
|
74
|
+
"dotenv": "^18.0.3",
|
|
75
75
|
"express": "^5.2.1",
|
|
76
76
|
"fuse.js": "^7.4.2",
|
|
77
77
|
"jose": "^6.2.12",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@playwright/test": "^1.63.0",
|
|
88
88
|
"@types/express": "^5.0.3",
|
|
89
|
-
"@types/node": "^26.6.
|
|
90
|
-
"knip": "^6.
|
|
89
|
+
"@types/node": "^26.6.3",
|
|
90
|
+
"knip": "^6.38.0",
|
|
91
91
|
"node-fetch": "^3.3.2",
|
|
92
92
|
"tsconfig-paths": "^4.2.0",
|
|
93
93
|
"typescript": "^7.0.2"
|