actual-mcp-server 0.11.0 → 0.11.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
|
@@ -1302,7 +1302,20 @@ export async function updateRule(id, fields) {
|
|
|
1302
1302
|
const fieldsObj = fields;
|
|
1303
1303
|
const rule = {
|
|
1304
1304
|
id,
|
|
1305
|
-
|
|
1305
|
+
// #342: `??` is WRONG for stage, and only for stage. null is a MEANINGFUL
|
|
1306
|
+
// value here (Actual's normal stage), not an absent one, so
|
|
1307
|
+
// `fieldsObj.stage ?? existingRule.stage` silently discards an explicit
|
|
1308
|
+
// `stage: null` and leaves the rule where it was. That made it impossible
|
|
1309
|
+
// to move a rule OUT of 'pre' or 'post' back to the normal stage, and it
|
|
1310
|
+
// failed silently: the call still returned success.
|
|
1311
|
+
//
|
|
1312
|
+
// Decide on PRESENCE of the key instead. The tool clones its input with
|
|
1313
|
+
// JSON.parse(JSON.stringify(...)), which preserves an explicit null key, so
|
|
1314
|
+
// `in` distinguishes "not supplied" from "supplied as null" correctly.
|
|
1315
|
+
//
|
|
1316
|
+
// The other three fields keep `??` deliberately: none of them treats null
|
|
1317
|
+
// as a distinct legal value, so nullish-coalescing is the right merge there.
|
|
1318
|
+
stage: 'stage' in fieldsObj ? fieldsObj.stage : existingRule.stage,
|
|
1306
1319
|
conditionsOp: fieldsObj.conditionsOp ?? existingRule.conditionsOp,
|
|
1307
1320
|
conditions: fieldsObj.conditions ?? existingRule.conditions ?? [],
|
|
1308
1321
|
actions: fieldsObj.actions ?? existingRule.actions ?? [],
|
|
@@ -33,7 +33,29 @@ const FIELD_OPERATORS = {
|
|
|
33
33
|
'date': { type: 'date', operators: ['is', 'gte', 'lte', 'gt', 'lt'] },
|
|
34
34
|
};
|
|
35
35
|
const InputSchema = z.object({
|
|
36
|
-
|
|
36
|
+
// #342: three stages, and the default one is `null`, not a string.
|
|
37
|
+
//
|
|
38
|
+
// Verified against Actual's validator (transaction-rules.ts) and a live server:
|
|
39
|
+
// it accepts exactly 'pre', 'post' and null, and REJECTS the literal "default"
|
|
40
|
+
// with `Invalid rule stage: default`. The published API reference says
|
|
41
|
+
// pre/default/post, which is wrong in both directions (reported upstream as
|
|
42
|
+
// actualbudget/actual#8682).
|
|
43
|
+
//
|
|
44
|
+
// The default here MUST be null and must be SENT. Two traps:
|
|
45
|
+
// - Defaulting to 'pre' (what this did until #342) silently puts every
|
|
46
|
+
// MCP-created rule ahead of every rule the user made in the UI, because
|
|
47
|
+
// Actual runs stages in the order pre, default, post. No error, no warning.
|
|
48
|
+
// - Simply making it .optional() and omitting the key does NOT work: on a
|
|
49
|
+
// create the validator always runs and rejects `undefined` with
|
|
50
|
+
// `Invalid rule stage: undefined`.
|
|
51
|
+
stage: z
|
|
52
|
+
.enum(['pre', 'post'])
|
|
53
|
+
.nullable()
|
|
54
|
+
.optional()
|
|
55
|
+
.default(null)
|
|
56
|
+
.describe('When to apply the rule. null (the default) is Actual\'s normal stage, the one a rule gets in the UI ' +
|
|
57
|
+
'when no stage is chosen. "pre" runs before the default stage, "post" after. Leave unset unless you ' +
|
|
58
|
+
'specifically need the rule to out-rank or defer to the user\'s existing rules.'),
|
|
37
59
|
conditionsOp: z.enum(['and', 'or']).optional().default('and').describe('How to combine multiple conditions'),
|
|
38
60
|
conditions: z.array(ConditionSchema).describe('Array of conditions that must be met for the rule to apply'),
|
|
39
61
|
actions: z.array(ActionSchema).describe('Array of actions to perform when conditions are met'),
|
|
@@ -49,10 +71,11 @@ IMPORTANT Field Types:
|
|
|
49
71
|
- "notes", "description" (string) - for text matching. Supports: contains, matches, doesNotContain, is, isNot
|
|
50
72
|
- "amount", "date" (number/date) - supports: is, gte, lte, gt, lt
|
|
51
73
|
|
|
52
|
-
Stage
|
|
74
|
+
Stage: omit it (the normal stage, recommended), or 'pre' to run before the user's own rules, or 'post' to run after.
|
|
75
|
+
Do NOT pass "default" as a string; Actual rejects it. The normal stage is null, which is what omitting gives you.
|
|
53
76
|
Action operators: 'set', 'set-split-amount', 'link-schedule', 'append-notes'.
|
|
54
77
|
|
|
55
|
-
Example
|
|
78
|
+
Example (no stage, so the rule lands in the normal stage alongside the user's own rules): {conditionsOp: "and", conditions: [{field: "imported_payee", op: "contains", value: "Amazon"}], actions: [{op: "set", field: "category", value: "category-uuid"}]}`,
|
|
56
79
|
inputSchema: InputSchema,
|
|
57
80
|
call: async (args, _meta) => {
|
|
58
81
|
// Zod validation errors are formatted centrally by actualToolsManager (#206).
|
|
@@ -55,7 +55,17 @@ const FIELD_OPERATORS = {
|
|
|
55
55
|
'date': { type: 'date', operators: ['is', 'gte', 'lte', 'gt', 'lt'] },
|
|
56
56
|
};
|
|
57
57
|
const InputSchema = z.object({
|
|
58
|
-
|
|
58
|
+
// #342: see the long note in rules_create.ts. null is Actual's DEFAULT stage;
|
|
59
|
+
// the literal "default" is rejected by the validator. No .default() here on
|
|
60
|
+
// purpose: this tool upserts, so "not supplied" must mean "leave the matched
|
|
61
|
+
// rule's stage alone", which is decided at the merge below. The create path
|
|
62
|
+
// supplies null explicitly.
|
|
63
|
+
stage: z
|
|
64
|
+
.enum(['pre', 'post'])
|
|
65
|
+
.nullable()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('When to apply the rule. null is Actual\'s normal stage (what the UI gives a rule with no stage chosen); ' +
|
|
68
|
+
'"pre" runs before it, "post" after. On an update, omitting this leaves the existing stage unchanged.'),
|
|
59
69
|
conditionsOp: z.enum(['and', 'or']).optional().default('and').describe('How to combine multiple conditions'),
|
|
60
70
|
conditions: z.array(ConditionSchema).describe('Array of conditions that must be met'),
|
|
61
71
|
actions: z.array(ActionSchema).describe('Array of actions to perform when conditions match'),
|
|
@@ -102,7 +112,9 @@ const tool = {
|
|
|
102
112
|
|
|
103
113
|
Matching logic: a rule is considered a "match" when it has the same set of conditions (field + op + value triples) and the same conditionsOp ("and"/"or"). Condition order is irrelevant.
|
|
104
114
|
|
|
105
|
-
When a match is found: the rule's actions
|
|
115
|
+
When a match is found: the rule's actions are REPLACED with the new values. The stage is replaced ONLY if you
|
|
116
|
+
supply one; omit stage to leave the matched rule in whatever stage it is already in. On a newly created rule,
|
|
117
|
+
an omitted stage means the normal stage (null), the same one the UI assigns.
|
|
106
118
|
When no match exists: a new rule is created.
|
|
107
119
|
|
|
108
120
|
IMPORTANT Field Types:
|
|
@@ -176,7 +188,13 @@ Returns: { id, created: boolean } — created=true if new rule was created, fals
|
|
|
176
188
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
177
189
|
const merged = {
|
|
178
190
|
id: matchedRule.id,
|
|
179
|
-
|
|
191
|
+
// #342: `??` is WRONG for stage. null is a MEANINGFUL value here (Actual's
|
|
192
|
+
// default stage), not an absent one, so `ruleData.stage ?? existing` would
|
|
193
|
+
// silently discard an explicit `stage: null` and keep the old stage,
|
|
194
|
+
// making it impossible to move a rule back to the default stage. Decide on
|
|
195
|
+
// PRESENCE of the key instead. Every other field below is fine with `??`
|
|
196
|
+
// because none of them treats null as a distinct legal value.
|
|
197
|
+
stage: 'stage' in ruleData ? ruleData.stage : matchedRule.stage,
|
|
180
198
|
conditionsOp: ruleData.conditionsOp ?? matchedRule.conditionsOp,
|
|
181
199
|
conditions: ruleData.conditions ?? matchedRule.conditions ?? [],
|
|
182
200
|
actions: ruleData.actions ?? matchedRule.actions ?? [],
|
|
@@ -185,7 +203,13 @@ Returns: { id, created: boolean } — created=true if new rule was created, fals
|
|
|
185
203
|
return { id: matchedRule.id, created: false };
|
|
186
204
|
}
|
|
187
205
|
else {
|
|
188
|
-
// CREATE new rule
|
|
206
|
+
// CREATE new rule.
|
|
207
|
+
// #342: stage has no Zod default on this tool (see the schema note), so an
|
|
208
|
+
// omitted stage arrives as undefined. Actual's validator ALWAYS runs on a
|
|
209
|
+
// create and rejects undefined with `Invalid rule stage: undefined`, so the
|
|
210
|
+
// key must be present. null is the correct value: Actual's default stage.
|
|
211
|
+
if (ruleData.stage === undefined)
|
|
212
|
+
ruleData.stage = null;
|
|
189
213
|
const rawId = await rawCreateRule(ruleData);
|
|
190
214
|
return { id: normalizeToId(rawId), created: true };
|
|
191
215
|
}
|
|
@@ -28,7 +28,17 @@ const FIELD_OPERATORS = {
|
|
|
28
28
|
const InputSchema = z.object({
|
|
29
29
|
id: z.string().describe('Rule ID to update'),
|
|
30
30
|
fields: z.object({
|
|
31
|
-
|
|
31
|
+
// #342: null is Actual's DEFAULT stage; the literal "default" is rejected by
|
|
32
|
+
// its validator. Deliberately NO .default() here: this is a partial update, so
|
|
33
|
+
// omitting stage must leave the existing one alone. Actual only validates
|
|
34
|
+
// stage on an update when the key is present, so omitting it is safe (unlike
|
|
35
|
+
// on a create, where undefined is rejected).
|
|
36
|
+
stage: z
|
|
37
|
+
.enum(['pre', 'post'])
|
|
38
|
+
.nullable()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('When to apply the rule. null is Actual\'s normal stage (what the UI gives a rule with no stage chosen); ' +
|
|
41
|
+
'"pre" runs before it, "post" after. Omit to leave the rule\'s current stage unchanged.'),
|
|
32
42
|
conditionsOp: z.enum(['and', 'or']).optional().describe('How to combine multiple conditions'),
|
|
33
43
|
conditions: z.array(ConditionSchema).optional().describe('New array of conditions'),
|
|
34
44
|
actions: z.array(ActionSchema).optional().describe('New array of actions'),
|
|
@@ -45,7 +55,9 @@ IMPORTANT Field Types:
|
|
|
45
55
|
- "notes", "description" (string) - for text matching. Supports: contains, matches, doesNotContain, is, isNot
|
|
46
56
|
- "amount", "date" (number/date) - supports: is, gte, lte, gt, lt
|
|
47
57
|
|
|
48
|
-
Stage
|
|
58
|
+
Stage: omit it to leave the rule where it is. Pass null to move it to the normal stage (where UI-created
|
|
59
|
+
rules live), 'pre' to run before that stage, or 'post' to run after. Do NOT pass "default" as a string;
|
|
60
|
+
Actual rejects it.
|
|
49
61
|
Action operators: 'set', 'set-split-amount', 'link-schedule', 'append-notes'.
|
|
50
62
|
|
|
51
63
|
Do not include the rule ID in the fields object - it is provided separately.`,
|
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.11.
|
|
4
|
+
"version": "0.11.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/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.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_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 && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_middleware.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/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.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_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 && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.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",
|