actual-mcp-server 0.6.56 → 0.7.3
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 +7 -7
- package/dist/package.json +4 -4
- package/dist/src/actualConnection.js +0 -2
- package/dist/src/actualToolsManager.js +1 -19
- package/dist/src/auth/setup.js +0 -4
- package/dist/src/index.js +1 -4
- package/dist/src/lib/ActualConnectionPool.js +0 -2
- package/dist/src/lib/ActualMCPConnection.js +0 -2
- package/dist/src/lib/actual-adapter.js +1 -28
- package/dist/src/lib/actual-schema.js +0 -9
- package/dist/src/lib/config-registry.js +1 -1
- package/dist/src/lib/constants.js +0 -43
- package/dist/src/lib/retry.js +0 -1
- package/dist/src/lib/toolFactory.js +0 -28
- package/dist/src/resources/accountsSummary.js +0 -2
- package/dist/src/server/httpServer.js +2 -2
- package/dist/src/tests/testMcpClient.js +5 -10
- package/dist/src/tests_adapter_runner.js +1 -1
- package/dist/src/tools/accounts_list.js +2 -1
- package/dist/src/tools/budget_updates_batch.js +0 -2
- 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_delete.js +0 -2
- package/dist/src/tools/category_groups_get.js +1 -1
- package/dist/src/tools/payees_get.js +1 -0
- package/dist/src/tools/rules_create_or_update.js +0 -2
- package/dist/src/tools/rules_delete.js +0 -2
- package/dist/src/tools/rules_get.js +1 -1
- package/dist/src/tools/schedules_delete.js +0 -2
- package/dist/src/tools/session_close.js +1 -1
- package/dist/src/utils.js +0 -51
- package/package.json +4 -4
- package/dist/src/lib/node-polyfills.js +0 -28
- package/dist/src/server/httpServer_testing.js +0 -401
- package/dist/src/server/streamable-http.js +0 -148
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/actual-mcp-server)
|
|
4
4
|
[](https://www.npmjs.com/package/actual-mcp-server)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://nodejs.org/)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://modelcontextprotocol.io/)
|
|
9
9
|
[](https://hub.docker.com/r/agigante80/actual-mcp-server)
|
|
10
10
|
[](https://hub.docker.com/r/agigante80/actual-mcp-server)
|
|
11
11
|
[](https://github.com/agigante80/actual-mcp-server/actions)
|
|
@@ -33,7 +33,7 @@ Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/
|
|
|
33
33
|
|
|
34
34
|
Most Actual Budget MCP implementations are simple stdio bridges designed for single-user, local use with Claude Desktop. This project goes further:
|
|
35
35
|
|
|
36
|
-
- **71 tools, the most comprehensive coverage available.** Accounts, transactions, categories, payees, tags, notes, rules, budgets, batch operations, bank sync, and more. Covers
|
|
36
|
+
- **71 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.
|
|
37
37
|
- **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.
|
|
38
38
|
- **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.
|
|
39
39
|
- **Multi-budget switching at runtime.** Configure multiple budget files and let the AI switch between them mid-conversation with `actual_budgets_switch`.
|
|
@@ -67,7 +67,7 @@ Most Actual Budget MCP implementations are simple stdio bridges designed for sin
|
|
|
67
67
|
|
|
68
68
|
- [Actual Budget](https://actualbudget.org/) server running (local or remote)
|
|
69
69
|
- Your **Budget Sync ID**: Actual → Settings → Show Advanced Settings → Sync ID
|
|
70
|
-
- **Node.js
|
|
70
|
+
- **Node.js 22+** (npm method) or **Docker**
|
|
71
71
|
|
|
72
72
|
### Option A: Docker (recommended)
|
|
73
73
|
|
|
@@ -282,7 +282,7 @@ For Claude Desktop (stdio), restart Claude after upgrading.
|
|
|
282
282
|
|
|
283
283
|
## Available Tools
|
|
284
284
|
|
|
285
|
-
**71 tools** across
|
|
285
|
+
**71 tools** across all categories. All tools use the `actual_<category>_<action>` naming convention.
|
|
286
286
|
|
|
287
287
|
### Accounts (7)
|
|
288
288
|
|
|
@@ -755,4 +755,4 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
755
755
|
|
|
756
756
|
---
|
|
757
757
|
|
|
758
|
-
**Version:** 0.
|
|
758
|
+
**Version:** 0.7.3 | **Tool Count:** 71 (verified LibreChat-compatible)
|
package/dist/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-mcp-server",
|
|
3
3
|
"displayName": "Actual MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=22.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
8
8
|
},
|
|
9
9
|
"description": "MCP server with 71 tools for AI-driven financial management with Actual Budget. HTTP and stdio transports for LibreChat, Claude Desktop, Cursor, VS Code, Gemini CLI",
|
|
@@ -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/
|
|
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",
|
|
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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"release:patch": "npm run version:bump -- patch",
|
|
58
58
|
"tool-count": "node scripts/tool-count.mjs",
|
|
59
59
|
"config-drift": "npm run build && node scripts/config-drift.mjs",
|
|
60
|
-
"knip": "knip
|
|
60
|
+
"knip": "knip"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@actual-app/api": "^26.6.0",
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// src/actualToolsManager.ts
|
|
2
|
-
// #232: load the navigator polyfill before @actual-app/api so the Node 20 global is defined first.
|
|
3
|
-
import './lib/node-polyfills.js';
|
|
4
2
|
import logger from './logger.js';
|
|
5
|
-
import { z } from 'zod';
|
|
6
3
|
import { formatZodError } from './lib/zod-error-format.js';
|
|
7
4
|
// ✅ List of tools already implemented in this class.
|
|
8
5
|
// Adding the tool name here is considered fully implemented.
|
|
@@ -122,21 +119,6 @@ const API_TOOL_MAP = {
|
|
|
122
119
|
getIDByName: 'actual_get_id_by_name',
|
|
123
120
|
getServerVersion: 'actual_server_get_version',
|
|
124
121
|
};
|
|
125
|
-
// Define Account schema for validation (simplified example)
|
|
126
|
-
const AccountSchema = z.object({
|
|
127
|
-
id: z.string().optional(),
|
|
128
|
-
name: z.string(),
|
|
129
|
-
type: z.string(), // Consider enum if you have fixed valid types
|
|
130
|
-
offbudget: z.boolean().optional().default(false),
|
|
131
|
-
closed: z.boolean().optional().default(false),
|
|
132
|
-
});
|
|
133
|
-
// Input schema for get_accounts - no args
|
|
134
|
-
const GetAccountsInputSchema = z.object({});
|
|
135
|
-
// Input schema for get_account_balance
|
|
136
|
-
const GetAccountBalanceInputSchema = z.object({
|
|
137
|
-
id: z.string().optional().describe('Account ID to get balance for (optional; defaults to first account)'),
|
|
138
|
-
cutoff: z.string().optional().describe('Optional ISO date string cutoff'),
|
|
139
|
-
});
|
|
140
122
|
class ActualToolsManager {
|
|
141
123
|
tools = new Map();
|
|
142
124
|
constructor() { }
|
|
@@ -144,7 +126,7 @@ class ActualToolsManager {
|
|
|
144
126
|
// Dynamically import all tool modules from src/tools/index.ts
|
|
145
127
|
const toolModules = (await import('./tools/index.js'));
|
|
146
128
|
let count = 0;
|
|
147
|
-
for (const [
|
|
129
|
+
for (const [_key, tool] of Object.entries(toolModules)) {
|
|
148
130
|
const t = tool;
|
|
149
131
|
if (t && t.name) {
|
|
150
132
|
this.tools.set(t.name, tool);
|
package/dist/src/auth/setup.js
CHANGED
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)
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
* For true concurrent multi-session support, the Actual Budget API would
|
|
11
11
|
* need to support multiple instances or we'd need request queuing.
|
|
12
12
|
*/
|
|
13
|
-
// #232: load the navigator polyfill before @actual-app/api so the Node 20 global is defined first.
|
|
14
|
-
import './node-polyfills.js';
|
|
15
13
|
import api from '@actual-app/api';
|
|
16
14
|
import logger from '../logger.js';
|
|
17
15
|
import config from '../config.js';
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// lib/ActualMCPConnection.ts
|
|
2
|
-
// #232: load the navigator polyfill before @actual-app/api so the Node 20 global is defined first.
|
|
3
|
-
import './node-polyfills.js';
|
|
4
2
|
import { EventEmitter } from 'events';
|
|
5
3
|
import * as actual from '@actual-app/api';
|
|
6
4
|
import actualToolsManager from '../actualToolsManager.js';
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// Must be the very first import — sets globalThis.navigator before @actual-app/api is evaluated.
|
|
2
|
-
// Required by @actual-app/api v26.3.0+ which uses navigator.platform at module load time.
|
|
3
|
-
import './node-polyfills.js';
|
|
4
1
|
import api from '@actual-app/api';
|
|
5
2
|
// @actual-app/api is a CJS package (no "type" field). In NodeNext/ESM context TypeScript
|
|
6
3
|
// cannot expose its named exports via static import syntax. At runtime the default import
|
|
@@ -11,7 +8,7 @@ const { addTransactions: rawAddTransactions, getAccounts: rawGetAccounts, import
|
|
|
11
8
|
} = api;
|
|
12
9
|
import { EventEmitter } from 'events';
|
|
13
10
|
import observability from '../observability.js';
|
|
14
|
-
import retry,
|
|
11
|
+
import { retry, isRetryableError } from './retry.js';
|
|
15
12
|
import { notFoundMsg } from './errors.js';
|
|
16
13
|
import logger from '../logger.js';
|
|
17
14
|
import config from '../config.js';
|
|
@@ -692,30 +689,6 @@ export function getConcurrencyState() {
|
|
|
692
689
|
writeConnectionReuses: writeConnectionReuseCount,
|
|
693
690
|
};
|
|
694
691
|
}
|
|
695
|
-
/**
|
|
696
|
-
* Sync local changes to the Actual Budget server.
|
|
697
|
-
*
|
|
698
|
-
* This function should be called after write operations (create, update, delete)
|
|
699
|
-
* to ensure changes are properly synced to the remote server. Without syncing,
|
|
700
|
-
* changes may only exist locally and could be lost.
|
|
701
|
-
*
|
|
702
|
-
* Note: Adds a small delay to ensure local changes are committed before syncing.
|
|
703
|
-
*/
|
|
704
|
-
async function syncToServer() {
|
|
705
|
-
try {
|
|
706
|
-
// Small delay to ensure local changes are committed to the database
|
|
707
|
-
// before attempting to sync to the server
|
|
708
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
709
|
-
// TypeScript doesn't recognize sync in the type definitions, but it exists at runtime
|
|
710
|
-
console.log('[SYNC] Calling api.sync()...');
|
|
711
|
-
await api.sync();
|
|
712
|
-
console.log('[SYNC] api.sync() completed successfully');
|
|
713
|
-
}
|
|
714
|
-
catch (err) {
|
|
715
|
-
// Log but don't throw - sync failures shouldn't break the operation
|
|
716
|
-
console.error('[SYNC] Sync to server failed:', err);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
692
|
/**
|
|
720
693
|
* Wrap a raw function with the standard adapter retry + concurrency behavior.
|
|
721
694
|
* Useful for tests that want to exercise retry behavior without calling the real raw methods.
|
|
@@ -211,12 +211,3 @@ export function isValidField(tableName, fieldName) {
|
|
|
211
211
|
export function isValidJoinPath(path) {
|
|
212
212
|
return path in JOIN_PATHS;
|
|
213
213
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Get field type information
|
|
216
|
-
*/
|
|
217
|
-
export function getFieldType(tableName, fieldName) {
|
|
218
|
-
const table = ACTUAL_SCHEMA[tableName];
|
|
219
|
-
if (!table || !(fieldName in table))
|
|
220
|
-
return null;
|
|
221
|
-
return table[fieldName].type;
|
|
222
|
-
}
|
|
@@ -40,7 +40,7 @@ export const RAW_ENV_ALLOWLIST = [
|
|
|
40
40
|
{ name: 'MCP_BRIDGE_DEBUG_TRANSPORT', reason: 'transport debug flag, read pre-config', readSite: 'index.ts, utils.ts' },
|
|
41
41
|
// Framework / internal: not user-facing config knobs.
|
|
42
42
|
{ name: 'NODE_ENV', reason: 'framework var; selects prod log format and prod behaviours', readSite: 'httpServer.ts, logger.ts (resolveLogConfig)' },
|
|
43
|
-
{ name: 'DEBUG', reason: 'framework debug toggle', readSite: 'utils.ts, index.ts
|
|
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
45
|
{ name: 'DOTENV_CONFIG_QUIET', reason: 'dotenv internal flag, not an operator knob', readSite: 'index.ts', documented: false },
|
|
46
46
|
{ name: 'VERSION', reason: 'build-time version arg, injected by the Docker build, not an operator knob', readSite: 'index.ts, server_info.ts', documented: false },
|
|
@@ -30,14 +30,6 @@ export const DEFAULT_CONCURRENCY_LIMIT = 5;
|
|
|
30
30
|
// ============================================================================
|
|
31
31
|
// TIMEOUTS
|
|
32
32
|
// ============================================================================
|
|
33
|
-
/**
|
|
34
|
-
* Default timeout for API operations in milliseconds
|
|
35
|
-
*/
|
|
36
|
-
export const DEFAULT_OPERATION_TIMEOUT_MS = 30000;
|
|
37
|
-
/**
|
|
38
|
-
* Timeout for server shutdown grace period
|
|
39
|
-
*/
|
|
40
|
-
export const SHUTDOWN_GRACE_PERIOD_MS = 5000;
|
|
41
33
|
/**
|
|
42
34
|
* How long (ms) to wait after calling rawRunBankSync for the SDK's background
|
|
43
35
|
* promise to surface a BankSyncError as an unhandledRejection.
|
|
@@ -57,10 +49,6 @@ export const WRITE_SESSION_DELAY_MS = 100;
|
|
|
57
49
|
// ============================================================================
|
|
58
50
|
// MCP SERVER
|
|
59
51
|
// ============================================================================
|
|
60
|
-
/**
|
|
61
|
-
* Default HTTP server bind host
|
|
62
|
-
*/
|
|
63
|
-
export const DEFAULT_BIND_HOST = 'localhost';
|
|
64
52
|
/**
|
|
65
53
|
* Default HTTP port for MCP server. Canonical default is 3600 (#230): it matches
|
|
66
54
|
* the listen-port fallback in src/index.ts, the Dockerfile EXPOSE/HEALTHCHECK, the
|
|
@@ -70,18 +58,6 @@ export const DEFAULT_BIND_HOST = 'localhost';
|
|
|
70
58
|
* not by a runtime import, so Knip cannot see the usage. Alive: removing it breaks the guard.
|
|
71
59
|
*/
|
|
72
60
|
export const DEFAULT_HTTP_PORT = 3600;
|
|
73
|
-
/**
|
|
74
|
-
* Default HTTP path for MCP server endpoint
|
|
75
|
-
*/
|
|
76
|
-
export const DEFAULT_HTTP_PATH = '/';
|
|
77
|
-
/**
|
|
78
|
-
* Server information
|
|
79
|
-
* Note: version is read from package.json at startup and passed as a parameter.
|
|
80
|
-
*/
|
|
81
|
-
export const SERVER_INFO = {
|
|
82
|
-
name: 'actual-budget-mcp',
|
|
83
|
-
description: 'MCP server for Actual Budget - 71 tools for finance management',
|
|
84
|
-
};
|
|
85
61
|
// ============================================================================
|
|
86
62
|
// VALIDATION & LIMITS
|
|
87
63
|
// ============================================================================
|
|
@@ -105,22 +81,3 @@ export const MONTH_PATTERN = /^\d{4}-\d{2}$/;
|
|
|
105
81
|
* UUID pattern for ID validation
|
|
106
82
|
*/
|
|
107
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;
|
|
108
|
-
// ============================================================================
|
|
109
|
-
// LOGGING
|
|
110
|
-
// ============================================================================
|
|
111
|
-
/**
|
|
112
|
-
* Log level for production environments
|
|
113
|
-
*/
|
|
114
|
-
export const PRODUCTION_LOG_LEVEL = 'info';
|
|
115
|
-
/**
|
|
116
|
-
* Log level for development environments
|
|
117
|
-
*/
|
|
118
|
-
export const DEVELOPMENT_LOG_LEVEL = 'debug';
|
|
119
|
-
/**
|
|
120
|
-
* Maximum number of log files to retain
|
|
121
|
-
*/
|
|
122
|
-
export const MAX_LOG_FILES = 14;
|
|
123
|
-
/**
|
|
124
|
-
* Maximum size of a single log file before rotation
|
|
125
|
-
*/
|
|
126
|
-
export const MAX_LOG_FILE_SIZE = '20m';
|
package/dist/src/lib/retry.js
CHANGED
|
@@ -79,31 +79,3 @@ export function createTool(config) {
|
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Batch create multiple tools with shared configuration
|
|
84
|
-
* Useful for creating related tools (CRUD operations) with common patterns
|
|
85
|
-
*
|
|
86
|
-
* @param baseConfig - Shared configuration for all tools
|
|
87
|
-
* @param tools - Array of tool-specific overrides
|
|
88
|
-
* @returns Array of tool definitions
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```typescript
|
|
92
|
-
* export const accountTools = createTools(
|
|
93
|
-
* { namePrefix: 'actual_accounts_' },
|
|
94
|
-
* [
|
|
95
|
-
* { name: 'create', schema: createSchema, handler: createHandler },
|
|
96
|
-
* { name: 'update', schema: updateSchema, handler: updateHandler },
|
|
97
|
-
* { name: 'delete', schema: deleteSchema, handler: deleteHandler },
|
|
98
|
-
* ]
|
|
99
|
-
* );
|
|
100
|
-
* ```
|
|
101
|
-
*/
|
|
102
|
-
export function createTools(baseConfig, tools) {
|
|
103
|
-
const { namePrefix = '', ...sharedConfig } = baseConfig;
|
|
104
|
-
return tools.map((toolConfig) => createTool({
|
|
105
|
-
...sharedConfig,
|
|
106
|
-
...toolConfig,
|
|
107
|
-
name: namePrefix + toolConfig.name,
|
|
108
|
-
}));
|
|
109
|
-
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// Example resource: summary of all accounts
|
|
2
|
-
// #232: load the navigator polyfill before @actual-app/api so the Node 20 global is defined first.
|
|
3
|
-
import '../lib/node-polyfills.js';
|
|
4
2
|
import api from '@actual-app/api';
|
|
5
3
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
4
|
const { getAccounts } = api;
|
|
@@ -37,7 +37,7 @@ 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
|
-
|
|
40
|
+
_bindHost = 'localhost', advertisedUrl) {
|
|
41
41
|
const app = express();
|
|
42
42
|
// Explicit body-size cap (#168). An oversized payload is rejected with HTTP 413
|
|
43
43
|
// rather than buffered unbounded. Tunable via MCP_HTTP_BODY_LIMIT (default 512kb).
|
|
@@ -170,7 +170,7 @@ bindHost = 'localhost', advertisedUrl) {
|
|
|
170
170
|
}
|
|
171
171
|
return true;
|
|
172
172
|
};
|
|
173
|
-
// Create a fresh Server instance
|
|
173
|
+
// Create a fresh Server instance per session
|
|
174
174
|
function createServerInstance() {
|
|
175
175
|
// ensure capabilities.tools is an object mapping tool name -> {}
|
|
176
176
|
const capabilitiesObj = capabilities && Object.keys(capabilities).length
|
|
@@ -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: {} };
|
|
@@ -41,7 +41,7 @@ runAdapterTests().catch((e) => {
|
|
|
41
41
|
});
|
|
42
42
|
// Concurrency and retry tests
|
|
43
43
|
import { callWithRetry, getConcurrencyState, setMaxConcurrency } from './lib/actual-adapter.js';
|
|
44
|
-
import retry from './lib/retry.js';
|
|
44
|
+
import { retry } from './lib/retry.js';
|
|
45
45
|
async function runConcurrencyAndRetryTests() {
|
|
46
46
|
console.log('Running concurrency and retry tests...');
|
|
47
47
|
// Concurrency test: set max concurrency to 1 and start 3 tasks that resolve after a delay.
|
|
@@ -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.",
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* - No wrapToolCall — uses direct call() pattern
|
|
18
18
|
* - Reuses exact same ConditionSchema / ActionSchema / FIELD_OPERATORS as rules_create.ts
|
|
19
19
|
*/
|
|
20
|
-
// #232: load the navigator polyfill before @actual-app/api so the Node 20 global is defined first.
|
|
21
|
-
import '../lib/node-polyfills.js';
|
|
22
20
|
import { z } from 'zod';
|
|
23
21
|
import adapter, { normalizeToId } from '../lib/actual-adapter.js';
|
|
24
22
|
import api from '@actual-app/api';
|
|
@@ -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/dist/src/utils.js
CHANGED
|
@@ -10,54 +10,3 @@ export function getLocalIp() {
|
|
|
10
10
|
}
|
|
11
11
|
return '127.0.0.1';
|
|
12
12
|
}
|
|
13
|
-
// new: transport logging helpers (enabled when --debug or DEBUG/MCP_BRIDGE_DEBUG_TRANSPORT set)
|
|
14
|
-
export function shouldDebugTransport() {
|
|
15
|
-
const argvDebug = process.argv.slice(2).includes('--debug');
|
|
16
|
-
const envDebug = Boolean(process.env.DEBUG) || process.env.MCP_BRIDGE_DEBUG_TRANSPORT === 'true' || process.env.LOG_LEVEL === 'debug';
|
|
17
|
-
return argvDebug || envDebug;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Log transport-level request/response objects when debug is enabled.
|
|
21
|
-
* - prefix: short label like "HTTP REQ" / "HTTP RES"
|
|
22
|
-
* - obj: any serializable data (headers, body, meta)
|
|
23
|
-
*
|
|
24
|
-
* This uses console.debug (falls through to console if no structured logger available).
|
|
25
|
-
*/
|
|
26
|
-
export function logTransport(prefix, obj) {
|
|
27
|
-
if (!shouldDebugTransport())
|
|
28
|
-
return;
|
|
29
|
-
const ts = new Date().toISOString();
|
|
30
|
-
let payload;
|
|
31
|
-
try {
|
|
32
|
-
payload = JSON.stringify(obj, replacer, 2);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
try {
|
|
36
|
-
payload = String(obj);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
payload = '[unserializable payload]';
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
// prefer console.debug so existing debug tooling (DEBUG env) can capture it
|
|
43
|
-
// format similar to other logs: "<iso> debug: [TRANSPORT] prefix: <json>"
|
|
44
|
-
// eslint-disable-next-line no-console
|
|
45
|
-
console.debug(`${ts} debug: [TRANSPORT] ${prefix}: ${payload}`);
|
|
46
|
-
}
|
|
47
|
-
// Helper replacer to avoid throwing on circular refs and to truncate very large buffers
|
|
48
|
-
function replacer(_key, value) {
|
|
49
|
-
// Limit Buffer/Uint8Array printing
|
|
50
|
-
if (value instanceof Uint8Array || (typeof Buffer !== 'undefined' && value instanceof Buffer)) {
|
|
51
|
-
const buf = value;
|
|
52
|
-
const len = buf.length ?? 0;
|
|
53
|
-
const preview = Array.prototype.slice.call(buf, 0, 64);
|
|
54
|
-
return { _type: 'Buffer', length: len, preview };
|
|
55
|
-
}
|
|
56
|
-
if (value instanceof Uint8Array) {
|
|
57
|
-
const buf = value;
|
|
58
|
-
const len = buf.length;
|
|
59
|
-
const preview = Array.prototype.slice.call(buf, 0, 64);
|
|
60
|
-
return { _type: 'Uint8Array', length: len, preview };
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
63
|
-
}
|