actual-mcp-server 0.7.2 → 0.7.4
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 +12 -69
- package/dist/package.json +2 -2
- package/dist/src/actualToolsManager.js +2 -17
- package/dist/src/index.js +4 -6
- package/dist/src/lib/actual-adapter.js +0 -24
- package/dist/src/server/httpServer.js +7 -3
- package/dist/src/tests/testMcpClient.js +5 -10
- package/dist/src/tools/accounts_list.js +2 -1
- package/dist/src/tools/budgets_getMonth.js +1 -0
- package/dist/src/tools/budgets_getMonths.js +1 -0
- package/dist/src/tools/categories_create.js +1 -0
- package/dist/src/tools/categories_get.js +1 -0
- package/dist/src/tools/category_groups_get.js +1 -1
- package/dist/src/tools/payees_get.js +1 -0
- package/dist/src/tools/rules_get.js +1 -1
- package/dist/src/tools/session_close.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="unraid/actual-mcp-icon.png" alt="Actual MCP Server icon" width="128" height="128">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Actual MCP Server</h1>
|
|
2
6
|
|
|
3
7
|
[](https://www.npmjs.com/package/actual-mcp-server)
|
|
4
8
|
[](https://www.npmjs.com/package/actual-mcp-server)
|
|
@@ -51,10 +55,9 @@ Most Actual Budget MCP implementations are simple stdio bridges designed for sin
|
|
|
51
55
|
- [Available Tools](#available-tools)
|
|
52
56
|
- [Configuration](#configuration)
|
|
53
57
|
- [Multi-Budget Switching](#multi-budget-switching)
|
|
54
|
-
- [Transport & Authentication](#transport
|
|
58
|
+
- [Transport & Authentication](#transport-authentication)
|
|
55
59
|
- [Testing](#testing)
|
|
56
60
|
- [Documentation](#documentation)
|
|
57
|
-
- [Comparison with Similar Projects](#comparison-with-similar-projects)
|
|
58
61
|
- [Contributing](#contributing)
|
|
59
62
|
- [License](#license)
|
|
60
63
|
- [Disclaimer](#disclaimer)
|
|
@@ -634,74 +637,14 @@ See [`tests/manual/README.md`](tests/manual/README.md) and [`tests/e2e/README.md
|
|
|
634
637
|
| [docs/SECURITY_AND_PRIVACY.md](docs/SECURITY_AND_PRIVACY.md) | Auth models, threat model, hardening |
|
|
635
638
|
| [docs/TESTING_AND_RELIABILITY.md](docs/TESTING_AND_RELIABILITY.md) | Test strategy, coverage, reliability patterns |
|
|
636
639
|
| [docs/NEW_TOOL_CHECKLIST.md](docs/NEW_TOOL_CHECKLIST.md) | Step-by-step guide for adding a new MCP tool |
|
|
637
|
-
| [CONTRIBUTING.md](CONTRIBUTING.md) | Development setup, code standards, PR process |
|
|
640
|
+
| [CONTRIBUTING.md](.github/CONTRIBUTING.md) | Development setup, code standards, PR process |
|
|
638
641
|
| [.env.example](.env.example) | Fully annotated environment variable reference |
|
|
639
642
|
|
|
640
643
|
---
|
|
641
644
|
|
|
642
|
-
## Comparison with Similar Projects
|
|
643
|
-
|
|
644
|
-
Several MCP servers exist for personal finance management. Here's how this project compares with the closest alternatives.
|
|
645
|
-
|
|
646
|
-
> **Snapshot date**: 2026-03-20. Versions and features reflect each project's latest release at that date.
|
|
647
|
-
|
|
648
|
-
### Feature Comparison
|
|
649
|
-
|
|
650
|
-
| Feature | **agigante80/actual-mcp-server** | [s-stefanov/actual-mcp](https://github.com/s-stefanov/actual-mcp) | [henfrydls/actual-budget-mcp](https://github.com/henfrydls/actual-budget-mcp) | [WGDevelopment/ynab-mcp-server](https://github.com/WGDevelopment/ynab-mcp-server) |
|
|
651
|
-
|---|---|---|---|---|
|
|
652
|
-
| **Version** | v0.4.26 | v1.11.1 | v0.2.0 | v0.1.0 |
|
|
653
|
-
| **Budget App** | Actual Budget (self-hosted) | Actual Budget (self-hosted) | Actual Budget (self-hosted) | YNAB (cloud, subscription) |
|
|
654
|
-
| **Language** | TypeScript / Node.js | TypeScript / Node.js | TypeScript / Node.js | Python |
|
|
655
|
-
| **Tool Count** | **71** | ~22 | 18 | 9 |
|
|
656
|
-
| **Setup & Distribution** |||||
|
|
657
|
-
| **Transport** | HTTP + stdio | STDIO + SSE option | STDIO | STDIO |
|
|
658
|
-
| **Docker support** | ✅ Full (image + Compose) | ✅ Image only | ❌ | ❌ |
|
|
659
|
-
| **Published package (npx/pip)** | ✅ `npx actual-mcp-server` | ✅ `npx actual-mcp` | ✅ `npx actual-budget-mcp` | ✅ `pip install ynab-mcp` |
|
|
660
|
-
| **Security & Access** |||||
|
|
661
|
-
| **Authentication** | ✅ Bearer token + OIDC (JWKS) | ⚠️ Optional Bearer token | ❌ None (local only) | ✅ OS keyring / env var |
|
|
662
|
-
| **Read-only mode** | ❌ All tools always available | ✅ Write requires `--enable-write` flag | ❌ | ✅ Most tools are read-only |
|
|
663
|
-
| **Multi-budget switching** | ✅ Runtime switch via tool | ❌ | ❌ | ✅ (YNAB natively multi-budget) |
|
|
664
|
-
| **Production & Reliability** |||||
|
|
665
|
-
| **Connection pooling** | ✅ Up to 15 concurrent sessions | ❌ | ❌ | ❌ |
|
|
666
|
-
| **Retry / backoff** | ✅ 3 attempts, exponential backoff | ❌ | ❌ | ❌ |
|
|
667
|
-
| **Automated test suite** | ✅ Unit + E2E + integration | ❌ | ❌ | ❌ |
|
|
668
|
-
| **Transactions** |||||
|
|
669
|
-
| **Create / update / delete** | ✅ | ✅ | ✅ | ✅ |
|
|
670
|
-
| **Import & reconcile** | ✅ `actual_transactions_import` | ❌ | ❌ | ❌ |
|
|
671
|
-
| **Scheduled / recurring** | ❌ (planned) | ❌ | ❌ | ❌ |
|
|
672
|
-
| **Analysis & Reporting** |||||
|
|
673
|
-
| **ActualQL custom queries** | ✅ 6 exclusive tools + `actual_query_run` | ❌ | ❌ | N/A |
|
|
674
|
-
| **Summary by category / payee** | ✅ | ✅ spending-by-category | ✅ | ❌ |
|
|
675
|
-
| **Spending projections / forecast** | ❌ | ❌ | ✅ end-of-month forecast | ❌ |
|
|
676
|
-
| **Budget vs actual comparison** | ✅ via `actual_budgets_getMonth` | ❌ | ✅ dedicated tool | ✅ month summary |
|
|
677
|
-
| **Bank sync** | ✅ GoCardless / SimpleFIN | ❌ | ✅ | ❌ (YNAB handles sync natively) |
|
|
678
|
-
| **Budget Management** |||||
|
|
679
|
-
| **Set / transfer / carryover / hold** | ✅ Full (10 tools) | ❌ | ✅ Partial | ✅ Partial |
|
|
680
|
-
| **Batch budget updates** | ✅ `actual_budget_updates_batch` | ❌ | ❌ | ❌ |
|
|
681
|
-
| **Accounts, Payees & Rules** |||||
|
|
682
|
-
| **Account lifecycle (close/reopen)** | ✅ | ❌ | ❌ | N/A |
|
|
683
|
-
| **Payee merging** | ✅ `actual_payees_merge` | ❌ | ❌ | N/A |
|
|
684
|
-
| **Payee rules management** | ✅ Full CRUD | ✅ Full CRUD | ❌ | N/A |
|
|
685
|
-
| **UX & Usability** |||||
|
|
686
|
-
| **Natural language date parsing** | ❌ YYYY-MM-DD required | ❌ | ✅ "last month", "yesterday" | ❌ |
|
|
687
|
-
| **Bilingual support** | ❌ | ❌ | ✅ English + Spanish | ❌ |
|
|
688
|
-
| **Auto name → UUID resolution** | ⚠️ Explicit tool (`actual_get_id_by_name`) | ❌ | ✅ Automatic in all tools | ❌ |
|
|
689
|
-
| **AI prompt templates** | ❌ | ✅ financial-insights, budget-review | ❌ | ❌ |
|
|
690
|
-
| **Tested AI clients** | LibreChat, LobeChat | Claude Desktop, Codex | Claude Desktop, Cursor, VS Code | Claude Code |
|
|
691
|
-
| **License** | MIT | MIT | MIT | MIT |
|
|
692
|
-
|
|
693
|
-
### When to choose which project
|
|
694
|
-
|
|
695
|
-
- **This project**: best for production deployments, multi-user environments (OIDC), LibreChat/LobeChat, Docker-native setup, or for Claude Desktop users who want a native stdio connection without any HTTP server overhead.
|
|
696
|
-
- **s-stefanov/actual-mcp**: the original implementation; good for Claude Desktop with STDIO transport, AI-generated prompt templates, and built-in read-only mode.
|
|
697
|
-
- **henfrydls/actual-budget-mcp**: best for Spanish-speaking users, Cursor/VS Code integration, or when you want natural-language dates, automatic name resolution, and spending forecasts without any server setup.
|
|
698
|
-
- **WGDevelopment/ynab-mcp-server**: only option if you're a YNAB user; privacy-first design with OS keyring token storage and local-LLM focus.
|
|
699
|
-
|
|
700
|
-
---
|
|
701
|
-
|
|
702
645
|
## Contributing
|
|
703
646
|
|
|
704
|
-
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code standards, and the PR process.
|
|
647
|
+
Contributions are welcome! See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, code standards, and the PR process.
|
|
705
648
|
|
|
706
649
|
Quick flow:
|
|
707
650
|
1. Fork → `git checkout -b feature/my-feature`
|
|
@@ -714,9 +657,9 @@ Quick flow:
|
|
|
714
657
|
## Architecture
|
|
715
658
|
|
|
716
659
|
- **Runtime**: Node.js 22 (Alpine Linux in Docker)
|
|
717
|
-
- **Language**: TypeScript
|
|
718
|
-
- **MCP SDK**: `@modelcontextprotocol/sdk`
|
|
719
|
-
- **Actual API**: `@actual-app/api`
|
|
660
|
+
- **Language**: TypeScript (ESM, NodeNext module resolution)
|
|
661
|
+
- **MCP SDK**: `@modelcontextprotocol/sdk`
|
|
662
|
+
- **Actual API**: `@actual-app/api`
|
|
720
663
|
- **Validation**: Zod (runtime types + JSON Schema for tool inputs)
|
|
721
664
|
- **Transports**: Express + StreamableHTTP (`--http`) · `StdioServerTransport` (`--stdio`)
|
|
722
665
|
- **Logging**: Winston with daily rotation (all output routed to stderr in stdio mode)
|
|
@@ -755,4 +698,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
755
698
|
|
|
756
699
|
---
|
|
757
700
|
|
|
758
|
-
**Version:** 0.7.
|
|
701
|
+
**Version:** 0.7.4 | **Tool Count:** 71 (verified LibreChat-compatible)
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.4",
|
|
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/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/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_body_limit.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",
|
|
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/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/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_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",
|
|
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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
// src/actualToolsManager.ts
|
|
1
2
|
import logger from './logger.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
3
|
import { formatZodError } from './lib/zod-error-format.js';
|
|
4
4
|
// ✅ List of tools already implemented in this class.
|
|
5
5
|
// Adding the tool name here is considered fully implemented.
|
|
@@ -119,21 +119,6 @@ const API_TOOL_MAP = {
|
|
|
119
119
|
getIDByName: 'actual_get_id_by_name',
|
|
120
120
|
getServerVersion: 'actual_server_get_version',
|
|
121
121
|
};
|
|
122
|
-
// Define Account schema for validation (simplified example)
|
|
123
|
-
const AccountSchema = z.object({
|
|
124
|
-
id: z.string().optional(),
|
|
125
|
-
name: z.string(),
|
|
126
|
-
type: z.string(), // Consider enum if you have fixed valid types
|
|
127
|
-
offbudget: z.boolean().optional().default(false),
|
|
128
|
-
closed: z.boolean().optional().default(false),
|
|
129
|
-
});
|
|
130
|
-
// Input schema for get_accounts - no args
|
|
131
|
-
const GetAccountsInputSchema = z.object({});
|
|
132
|
-
// Input schema for get_account_balance
|
|
133
|
-
const GetAccountBalanceInputSchema = z.object({
|
|
134
|
-
id: z.string().optional().describe('Account ID to get balance for (optional; defaults to first account)'),
|
|
135
|
-
cutoff: z.string().optional().describe('Optional ISO date string cutoff'),
|
|
136
|
-
});
|
|
137
122
|
class ActualToolsManager {
|
|
138
123
|
tools = new Map();
|
|
139
124
|
constructor() { }
|
|
@@ -141,7 +126,7 @@ class ActualToolsManager {
|
|
|
141
126
|
// Dynamically import all tool modules from src/tools/index.ts
|
|
142
127
|
const toolModules = (await import('./tools/index.js'));
|
|
143
128
|
let count = 0;
|
|
144
|
-
for (const [
|
|
129
|
+
for (const [_key, tool] of Object.entries(toolModules)) {
|
|
145
130
|
const t = tool;
|
|
146
131
|
if (t && t.name) {
|
|
147
132
|
this.tools.set(t.name, tool);
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { isKnownBenignRejection } from './lib/rejection-allowlist.js';
|
|
3
3
|
// Add global error handlers
|
|
4
|
-
let isHandlingQueryError = false;
|
|
5
4
|
process.on('unhandledRejection', (reason, promise) => {
|
|
6
5
|
console.error('=== UNHANDLED REJECTION ===');
|
|
7
6
|
console.error('Promise:', promise);
|
|
@@ -125,16 +124,14 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
125
124
|
import('./actualConnection.js'),
|
|
126
125
|
import('./lib/ActualMCPConnection.js'),
|
|
127
126
|
]);
|
|
128
|
-
const [{ startHttpServer }, { startStdioServer }, loggerModule,
|
|
127
|
+
const [{ startHttpServer }, { startStdioServer }, loggerModule, utilsModule, actualToolsManagerModule,] = await Promise.all([
|
|
129
128
|
import('./server/httpServer.js'),
|
|
130
129
|
import('./server/stdioServer.js'),
|
|
131
130
|
import('./logger.js'),
|
|
132
|
-
import('os'),
|
|
133
131
|
import('./utils.js'),
|
|
134
132
|
import('./actualToolsManager.js'),
|
|
135
133
|
]);
|
|
136
134
|
const logger = loggerModule.default;
|
|
137
|
-
const os = osModule;
|
|
138
135
|
const { getLocalIp } = utilsModule;
|
|
139
136
|
const actualToolsManager = actualToolsManagerModule.default;
|
|
140
137
|
// Load version from environment (Docker build-time) or package.json (local dev)
|
|
@@ -248,8 +245,9 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
248
245
|
// If requested, run the MCP client-side tests now that all variables are ready
|
|
249
246
|
if (useTestMcpClient) {
|
|
250
247
|
logger.info('⚙️ --test-mcp-client specified, starting HTTP server and running client-side MCP tests...');
|
|
251
|
-
// start server in http mode
|
|
252
|
-
|
|
248
|
+
// start server in http mode. Bind all interfaces by default (#239): advertisedUrl
|
|
249
|
+
// uses the LAN IP (getLocalIp), so the self-test client must be able to reach it.
|
|
250
|
+
await startHttpServer(mcp, PORT, HTTP_PATH, capabilities, implementedTools, SERVER_DESCRIPTION, SERVER_INSTRUCTIONS, toolSchemas, version, process.env.MCP_BRIDGE_BIND_HOST || '0.0.0.0', advertisedUrl);
|
|
253
251
|
// dynamic import to avoid circular at top
|
|
254
252
|
const { testMcpClient } = await import('./tests/testMcpClient.js');
|
|
255
253
|
try {
|
|
@@ -689,30 +689,6 @@ export function getConcurrencyState() {
|
|
|
689
689
|
writeConnectionReuses: writeConnectionReuseCount,
|
|
690
690
|
};
|
|
691
691
|
}
|
|
692
|
-
/**
|
|
693
|
-
* Sync local changes to the Actual Budget server.
|
|
694
|
-
*
|
|
695
|
-
* This function should be called after write operations (create, update, delete)
|
|
696
|
-
* to ensure changes are properly synced to the remote server. Without syncing,
|
|
697
|
-
* changes may only exist locally and could be lost.
|
|
698
|
-
*
|
|
699
|
-
* Note: Adds a small delay to ensure local changes are committed before syncing.
|
|
700
|
-
*/
|
|
701
|
-
async function syncToServer() {
|
|
702
|
-
try {
|
|
703
|
-
// Small delay to ensure local changes are committed to the database
|
|
704
|
-
// before attempting to sync to the server
|
|
705
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
706
|
-
// TypeScript doesn't recognize sync in the type definitions, but it exists at runtime
|
|
707
|
-
console.log('[SYNC] Calling api.sync()...');
|
|
708
|
-
await api.sync();
|
|
709
|
-
console.log('[SYNC] api.sync() completed successfully');
|
|
710
|
-
}
|
|
711
|
-
catch (err) {
|
|
712
|
-
// Log but don't throw - sync failures shouldn't break the operation
|
|
713
|
-
console.error('[SYNC] Sync to server failed:', err);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
692
|
/**
|
|
717
693
|
* Wrap a raw function with the standard adapter retry + concurrency behavior.
|
|
718
694
|
* Useful for tests that want to exercise retry behavior without calling the real raw methods.
|
|
@@ -37,7 +37,8 @@ serverDescription, // was passed by index.ts
|
|
|
37
37
|
serverInstructions, // was passed by index.ts
|
|
38
38
|
toolSchemas, // was passed by index.ts
|
|
39
39
|
version, // server version from package.json
|
|
40
|
-
bindHost = 'localhost',
|
|
40
|
+
bindHost = 'localhost', // #239: interface to bind; forwarded to listen()
|
|
41
|
+
advertisedUrl) {
|
|
41
42
|
const app = express();
|
|
42
43
|
// Explicit body-size cap (#168). An oversized payload is rejected with HTTP 413
|
|
43
44
|
// rather than buffered unbounded. Tunable via MCP_HTTP_BODY_LIMIT (default 512kb).
|
|
@@ -580,11 +581,14 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
580
581
|
`MCP_HTTPS_CERT=${config.MCP_HTTPS_CERT}, MCP_HTTPS_KEY=${config.MCP_HTTPS_KEY}. Cause: ${cause}`);
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
|
-
const listener = (tlsOptions ? https.createServer(tlsOptions, app) : app).listen(port, () => {
|
|
584
|
+
const listener = (tlsOptions ? https.createServer(tlsOptions, app) : app).listen(port, bindHost, () => {
|
|
584
585
|
const advertised = advertisedUrl || `${scheme}://${serverIp}:${port}${httpPath}`;
|
|
586
|
+
// #239: when bound to a specific (non all-interfaces) host the server is NOT on
|
|
587
|
+
// loopback, so point the health hint at the actual bind host instead of localhost.
|
|
588
|
+
const healthHost = (!bindHost || bindHost === '0.0.0.0' || bindHost === '::') ? 'localhost' : bindHost;
|
|
585
589
|
console.info(`MCP Streamable HTTP Server listening on ${port}`);
|
|
586
590
|
console.info(`📨 MCP endpoint: ${advertised}`);
|
|
587
|
-
console.info(`❤️ Health check: ${scheme}
|
|
591
|
+
console.info(`❤️ Health check: ${scheme}://${healthHost}:${port}/health`);
|
|
588
592
|
if (config.AUTH_PROVIDER === 'oidc') {
|
|
589
593
|
logger.info(`🔒 OIDC authentication enabled (JWT Bearer token required — issuer: ${config.OIDC_ISSUER})`);
|
|
590
594
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export async function testMcpClient(advertisedUrl,
|
|
1
|
+
export async function testMcpClient(advertisedUrl, _port, httpPath) {
|
|
2
2
|
// Try builtin fetch or fallback to node-fetch if necessary
|
|
3
3
|
const globalFetch = globalThis.fetch;
|
|
4
4
|
const fetchAny = globalFetch ?? (await import('node-fetch')).default;
|
|
@@ -82,14 +82,10 @@ export async function testMcpClient(advertisedUrl, port, httpPath) {
|
|
|
82
82
|
throw new Error('initialize result.capabilities missing or not object');
|
|
83
83
|
}
|
|
84
84
|
// Accept either a tools array or derive tools from capabilities.tools object
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else if (initResult['capabilities'] && typeof initResult['capabilities']['tools'] === 'object') {
|
|
90
|
-
initTools = Object.keys(initResult['capabilities']['tools']);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
85
|
+
const hasTools = Array.isArray(initResult['tools'])
|
|
86
|
+
|| (initResult['capabilities'] != null
|
|
87
|
+
&& typeof initResult['capabilities']['tools'] === 'object');
|
|
88
|
+
if (!hasTools) {
|
|
93
89
|
throw new Error('initialize result.tools missing or not array and capabilities.tools not present');
|
|
94
90
|
}
|
|
95
91
|
// serverInstructions may be a string, an object { instructions }, or absent.
|
|
@@ -106,7 +102,6 @@ export async function testMcpClient(advertisedUrl, port, httpPath) {
|
|
|
106
102
|
else {
|
|
107
103
|
console.warn('Warning: serverInstructions present but in unexpected shape — continuing tests');
|
|
108
104
|
}
|
|
109
|
-
// replace usages below with initTools where needed
|
|
110
105
|
console.info('Initialize OK: capabilities/tools/serverInstructions present');
|
|
111
106
|
// call tools/list (JSON-RPC) to verify RPC path works
|
|
112
107
|
const listPayload = { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
const InputSchema = z.object({});
|
|
4
|
+
// RESPONSE_TYPE: Account[]
|
|
4
5
|
const tool = {
|
|
5
6
|
name: 'actual_accounts_list',
|
|
6
7
|
description: "List all accounts in Actual Budget including checking, savings, credit cards, and investment accounts. Returns account ID, name, balance (in cents), on-budget/off-budget status, and open/closed state.",
|
|
7
8
|
inputSchema: InputSchema,
|
|
8
|
-
call: async (
|
|
9
|
+
call: async (_args, _meta) => {
|
|
9
10
|
// Single API session: getAccounts + all balances in one withActualApi cycle.
|
|
10
11
|
// Calling getAccountBalance() per-account would open N separate sessions and
|
|
11
12
|
// overwhelm the Actual server with concurrent init/shutdown cycles.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
const InputSchema = z.object({ month: z.string().optional() });
|
|
4
|
+
// RESPONSE_TYPE: BudgetMonth
|
|
4
5
|
const tool = {
|
|
5
6
|
name: 'actual_budgets_getMonth',
|
|
6
7
|
description: "Get budget data for a specific month in YYYY-MM format (e.g., '2025-12'). Returns all categories with their budgeted amounts, actual spending, and available balances for the month.",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
const InputSchema = z.object({});
|
|
4
|
+
// RESPONSE_TYPE: array
|
|
4
5
|
const tool = {
|
|
5
6
|
name: 'actual_budgets_getMonths',
|
|
6
7
|
description: "Get a list of all available budget months with summary data. Returns an array of months showing total budgeted, spent, and income for each month. Useful for viewing budget history and trends over time.",
|
|
@@ -6,6 +6,7 @@ const InputSchema = z.object({
|
|
|
6
6
|
group_id: CommonSchemas.categoryGroupId,
|
|
7
7
|
is_income: z.boolean().nullable().optional(),
|
|
8
8
|
}).passthrough(); // Allow other fields to pass through
|
|
9
|
+
// RESPONSE_TYPE: string
|
|
9
10
|
const tool = {
|
|
10
11
|
name: 'actual_categories_create',
|
|
11
12
|
description: "Create a category. REQUIRED: group_id (category group UUID). Use actual_category_groups_get to find available groups. Optional: is_income (boolean, defaults to false for expense categories).",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
const InputSchema = z.object({});
|
|
4
|
+
// RESPONSE_TYPE: Category[]
|
|
4
5
|
const tool = {
|
|
5
6
|
name: 'actual_categories_get',
|
|
6
7
|
description: 'List all budget categories organized by category groups. Categories are spending/income buckets (e.g., Groceries, Rent, Salary) used for budgeting and transaction categorization. Returns both grouped view and flat list with category IDs, names, and group assignments.',
|
|
@@ -5,7 +5,7 @@ const tool = {
|
|
|
5
5
|
name: 'actual_category_groups_get',
|
|
6
6
|
description: `List all category groups in Actual Budget. Category groups organize related categories together (e.g., "Monthly Bills" group contains "Rent", "Utilities", "Internet" categories). Each group has an ID, name, and optional properties.`,
|
|
7
7
|
inputSchema: InputSchema,
|
|
8
|
-
call: async (
|
|
8
|
+
call: async (_args, _meta) => {
|
|
9
9
|
const groups = await adapter.getCategoryGroups();
|
|
10
10
|
return { groups };
|
|
11
11
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import adapter from '../lib/actual-adapter.js';
|
|
3
3
|
const InputSchema = z.object({});
|
|
4
|
+
// RESPONSE_TYPE: Payee[]
|
|
4
5
|
const tool = {
|
|
5
6
|
name: 'actual_payees_get',
|
|
6
7
|
description: "List all payees in Actual Budget. Payees represent merchants, service providers, individuals, or other entities you transact with. Returns payee ID, name, and optional transfer account information for internal transfers.",
|
|
@@ -5,7 +5,7 @@ const tool = {
|
|
|
5
5
|
name: 'actual_rules_get',
|
|
6
6
|
description: `List all budget rules in Actual Budget. Rules automate transaction categorization and other budget operations based on conditions. Each rule has conditions (e.g., payee matches "Amazon") and actions (e.g., set category to "Shopping").`,
|
|
7
7
|
inputSchema: InputSchema,
|
|
8
|
-
call: async (
|
|
8
|
+
call: async (_args, _meta) => {
|
|
9
9
|
const rules = await adapter.getRules();
|
|
10
10
|
return { rules };
|
|
11
11
|
},
|
|
@@ -9,7 +9,7 @@ const tool = {
|
|
|
9
9
|
name: 'actual_session_close',
|
|
10
10
|
description: 'Close an idle MCP session to free up connection slots. Useful when you get "Max concurrent sessions reached" errors. Only closes sessions other than the current one.',
|
|
11
11
|
inputSchema: InputSchema,
|
|
12
|
-
call: async (args,
|
|
12
|
+
call: async (args, _meta) => {
|
|
13
13
|
const input = InputSchema.parse(args || {});
|
|
14
14
|
const stats = connectionPool.getStats();
|
|
15
15
|
if (stats.totalSessions === 0) {
|
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.7.
|
|
4
|
+
"version": "0.7.4",
|
|
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/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/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_body_limit.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",
|
|
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/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/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_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",
|
|
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",
|