actual-mcp-server 0.8.9 → 0.8.10
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 +1 -1
- package/dist/src/lib/actual-adapter.js +12 -1
- package/dist/src/lib/constants.js +18 -0
- package/dist/src/lib/server-version-guard.js +105 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ import { retry, isRetryableError } from './retry.js';
|
|
|
12
12
|
import { withOpTimeout } from './opTimeout.js';
|
|
13
13
|
import { notFoundMsg } from './errors.js';
|
|
14
14
|
import logger from '../logger.js';
|
|
15
|
+
import { checkServerVersionOnce } from './server-version-guard.js';
|
|
15
16
|
import config from '../config.js';
|
|
16
17
|
import { parseBudgetRegistry } from './budget-registry.js';
|
|
17
18
|
import { getPreferredBudgetSyncId, setPreferredBudgetSyncId, pickAllowedPreferredBudget } from './budget-preference-store.js';
|
|
@@ -229,11 +230,21 @@ function _enforceBudgetAcl(toolName) {
|
|
|
229
230
|
* In either mode `withApiLock` serialises against concurrent callers because
|
|
230
231
|
* `@actual-app/api` is a process-wide singleton.
|
|
231
232
|
*/
|
|
232
|
-
export async function withActualApi(
|
|
233
|
+
export async function withActualApi(rawOperation) {
|
|
233
234
|
// ACL enforcement BEFORE pool branching or lock acquisition (#156).
|
|
234
235
|
// Denial here means the lock is never acquired and no upstream resource is
|
|
235
236
|
// touched.
|
|
236
237
|
_enforceBudgetAcl();
|
|
238
|
+
// #276: on the FIRST successful op, warn (once) if the Actual server version is outside
|
|
239
|
+
// the range this build supports. It runs inside the op while the connection is live and
|
|
240
|
+
// reuses it via rawGetServerVersion (NOT the withActualApi-wrapped getServerVersion, which
|
|
241
|
+
// would re-enter the lock). The once-guard flips its flag synchronously, so this never
|
|
242
|
+
// repeats per session or op. It is advisory: it never throws and never blocks the op.
|
|
243
|
+
const operation = async () => {
|
|
244
|
+
const result = await rawOperation();
|
|
245
|
+
await checkServerVersionOnce(() => rawGetServerVersion(), logger);
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
237
248
|
const sessionId = _resolveSessionId();
|
|
238
249
|
if (_hasPooledConnection(sessionId)) {
|
|
239
250
|
// Pooled mode: skip init+shutdown.
|
|
@@ -81,3 +81,21 @@ export const MONTH_PATTERN = /^\d{4}-\d{2}$/;
|
|
|
81
81
|
* UUID pattern for ID validation
|
|
82
82
|
*/
|
|
83
83
|
export const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
84
|
+
/**
|
|
85
|
+
* #276: the Actual Budget SERVER version range this build's `@actual-app/api` is known
|
|
86
|
+
* to work with. HAND-MAINTAINED: `@actual-app/api` v26 exposes no supported-server range
|
|
87
|
+
* (verified: getServerVersion just proxies the server's build.version), so there is no
|
|
88
|
+
* upstream fact to derive this from and no #275-style drift guard is possible.
|
|
89
|
+
*
|
|
90
|
+
* UPDATE THIS when bumping `@actual-app/api`: set `minVersion` to the oldest server the new
|
|
91
|
+
* client supports, and `testedMaxMajor` to the newest server major it has been tested
|
|
92
|
+
* against. The check is ADVISORY only (the real compatibility contract is the migration
|
|
93
|
+
* check `@actual-app/api` runs at downloadBudget), so a wrong value here can at most produce
|
|
94
|
+
* a spurious or missing warning, never a broken deployment.
|
|
95
|
+
*/
|
|
96
|
+
export const SUPPORTED_ACTUAL_SERVER_RANGE = {
|
|
97
|
+
/** Oldest server version this client supports. Below this: "too old" warning. */
|
|
98
|
+
minVersion: '25.0.0',
|
|
99
|
+
/** Newest server MAJOR tested against `@actual-app/api` v26. Above this: "newer than tested". */
|
|
100
|
+
testedMaxMajor: 26,
|
|
101
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #276: warn (once) when the Actual Budget SERVER version is outside the range this build's
|
|
3
|
+
* `@actual-app/api` is known to work with.
|
|
4
|
+
*
|
|
5
|
+
* Reporting already existed (actual_server_get_version, actual_server_info) but nothing
|
|
6
|
+
* enforced or even flagged compatibility. This adds an ADVISORY startup-ish warning: the
|
|
7
|
+
* real compatibility contract is the migration check `@actual-app/api` runs at
|
|
8
|
+
* downloadBudget, so this warning never fails startup and never blocks an operation.
|
|
9
|
+
*
|
|
10
|
+
* Design (mirrors src/lib/node-version-guard.ts):
|
|
11
|
+
* - The comparator is PURE (no I/O, no logging), so its truth table is unit-testable.
|
|
12
|
+
* - It FAILS OPEN: an unparseable version produces no warning. A spurious warning on a
|
|
13
|
+
* healthy deployment would be worse than staying quiet.
|
|
14
|
+
* - The firing is a per-PROCESS once-guard: the flag is flipped SYNCHRONOUSLY before any
|
|
15
|
+
* await, so the check runs exactly once no matter how many ops or sessions occur, and it
|
|
16
|
+
* reuses the connection the triggering op already established (no extra auth burst,
|
|
17
|
+
* avoiding the #127/#134 class). There is NO boot-time probe: index.ts deliberately has
|
|
18
|
+
* no startup connection, and a naive boot call would double-init then be torn down by
|
|
19
|
+
* shutdownActualApi.
|
|
20
|
+
*/
|
|
21
|
+
import { SUPPORTED_ACTUAL_SERVER_RANGE } from './constants.js';
|
|
22
|
+
/** Parse `26.7.0` (or `v26.7.0`) into `[26, 7, 0]`. Returns null when unparseable. */
|
|
23
|
+
export function parseVersion(v) {
|
|
24
|
+
if (typeof v !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
const m = /^v?(\d+)\.(\d+)(?:\.(\d+))?/.exec(v.trim());
|
|
27
|
+
if (!m)
|
|
28
|
+
return null;
|
|
29
|
+
return [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)];
|
|
30
|
+
}
|
|
31
|
+
/** a < b as semver triples. */
|
|
32
|
+
function lessThan(a, b) {
|
|
33
|
+
for (let i = 0; i < 3; i++) {
|
|
34
|
+
if (a[i] !== b[i])
|
|
35
|
+
return a[i] < b[i];
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pure. Compare a running server version against the supported range.
|
|
41
|
+
* FAILS OPEN on anything unparseable (returns ok:true, no message), so a version string we
|
|
42
|
+
* did not anticipate never produces a false warning.
|
|
43
|
+
*/
|
|
44
|
+
export function checkServerVersion(running, range = SUPPORTED_ACTUAL_SERVER_RANGE) {
|
|
45
|
+
const have = parseVersion(running);
|
|
46
|
+
const min = parseVersion(range.minVersion);
|
|
47
|
+
if (!have || !min)
|
|
48
|
+
return { ok: true };
|
|
49
|
+
if (lessThan(have, min)) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: `Actual Budget server ${running} is older than the minimum this build supports ` +
|
|
53
|
+
`(>= ${range.minVersion}). Some tools may misbehave. Upgrade the Actual Budget server, ` +
|
|
54
|
+
'or pin an older actual-mcp-server. This is advisory: the server, not this warning, ' +
|
|
55
|
+
'enforces compatibility at budget download.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (have[0] > range.testedMaxMajor) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
message: `Actual Budget server ${running} is newer than this build was tested against ` +
|
|
62
|
+
`(tested up to major ${range.testedMaxMajor}.x). It will very likely work, but if a ` +
|
|
63
|
+
'tool behaves oddly, update actual-mcp-server. Advisory only.',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Per-process once-guard.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
let checked = false;
|
|
72
|
+
/**
|
|
73
|
+
* Run the compatibility check at most once per process. Call this from inside a successful
|
|
74
|
+
* `withActualApi` operation (while the connection is live), passing a function that reads the
|
|
75
|
+
* server version through the SAME live connection (rawGetServerVersion, not the withActualApi
|
|
76
|
+
* wrapped getServerVersion, to avoid re-entering the lock).
|
|
77
|
+
*
|
|
78
|
+
* NEVER throws. A read failure is a single `debug`; being outside the range is a single
|
|
79
|
+
* `warn`; being in range is silent. The `log` argument is the module logger so all output
|
|
80
|
+
* goes through the redaction-protected logger (never console.*), keeping stdio framing intact.
|
|
81
|
+
*/
|
|
82
|
+
export async function checkServerVersionOnce(readVersion, log) {
|
|
83
|
+
if (checked)
|
|
84
|
+
return;
|
|
85
|
+
checked = true; // set synchronously, before any await, so a concurrent op cannot double-fire
|
|
86
|
+
try {
|
|
87
|
+
const result = await readVersion();
|
|
88
|
+
if (!result || 'error' in result || typeof result.version !== 'string') {
|
|
89
|
+
log.debug('[server-version] could not read the Actual server version; skipping the compatibility check');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const verdict = checkServerVersion(result.version);
|
|
93
|
+
if (!verdict.ok && verdict.message) {
|
|
94
|
+
log.warn(verdict.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Advisory only: a failure to check must never surface as an error or affect the op.
|
|
99
|
+
log.debug('[server-version] compatibility check threw; ignored');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Test-only: reset the once-guard between cases. */
|
|
103
|
+
export function _resetForTests() {
|
|
104
|
+
checked = false;
|
|
105
|
+
}
|
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.
|
|
4
|
+
"version": "0.8.10",
|
|
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/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",
|
|
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/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",
|
|
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",
|