actual-mcp-server 0.22.20 → 0.22.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/actualToolsManager.js +1 -0
- package/dist/src/index.js +19 -49
- package/dist/src/lib/actual-adapter.js +128 -70
- package/dist/src/lib/mcp-version.js +63 -0
- package/dist/src/lib/transportErrorLogger.js +5 -3
- package/dist/src/server/httpErrorHandler.js +109 -0
- package/dist/src/server/httpServer.js +10 -1
- package/dist/src/tools/rules_create_or_update.js +2 -1
- package/dist/src/tools/server_info.js +7 -24
- package/dist/src/tools/transactions_update_batch.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -835,7 +835,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
835
835
|
|
|
836
836
|
---
|
|
837
837
|
|
|
838
|
-
**Version:** 0.22.
|
|
838
|
+
**Version:** 0.22.25 | **Tool Count:** 83 (verified LibreChat-compatible)
|
|
839
839
|
|
|
840
840
|
## Sponsor
|
|
841
841
|
|
|
@@ -119,6 +119,7 @@ const API_TOOL_MAP = {
|
|
|
119
119
|
deletePayee: 'actual_payees_delete',
|
|
120
120
|
mergePayees: 'actual_payees_merge',
|
|
121
121
|
getPayeeRules: 'actual_payee_rules_get',
|
|
122
|
+
// #539: the tool covers this capability through per-item writes; it never calls batchBudgetUpdates.
|
|
122
123
|
batchBudgetUpdates: 'actual_budget_updates_batch',
|
|
123
124
|
holdBudgetForNextMonth: 'actual_budgets_holdForNextMonth',
|
|
124
125
|
resetBudgetHold: 'actual_budgets_resetHold',
|
package/dist/src/index.js
CHANGED
|
@@ -2,38 +2,25 @@
|
|
|
2
2
|
// source order, so this arms the Node floor check before zod, before the rejection
|
|
3
3
|
// allowlist, and before the handlers below. `npm start` and the documented
|
|
4
4
|
// `node dist/src/index.js --stdio` reach this file without going through bin/, so the
|
|
5
|
-
// guard cannot live in bin/ alone.
|
|
6
|
-
//
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
5
|
+
// guard cannot live in bin/ alone. It is a side-effect import: the module's top-level
|
|
6
|
+
// `enforceNodeVersion()` fires here, exactly once.
|
|
7
|
+
import './lib/node-version-guard.js';
|
|
8
|
+
import { getMcpServerVersion } from './lib/mcp-version.js';
|
|
9
|
+
import path from 'node:path';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { isKnownBenignRejection } from './lib/rejection-allowlist.js';
|
|
12
12
|
import { format } from 'node:util';
|
|
13
|
-
|
|
14
|
-
* #277:
|
|
13
|
+
/*
|
|
14
|
+
* #277 / #541: this module's version comes from `getMcpServerVersion()` (src/lib/mcp-version.ts),
|
|
15
|
+
* which `--version`, `--help`, the startup banner, the MCP initialize `serverInfo.version` and
|
|
16
|
+
* `actual_server_info` all share. It reads the ROOT package.json through
|
|
17
|
+
* `findRootPackageJson`, never the stale `dist/package.json` mirror that a dynamic JSON import
|
|
18
|
+
* once produced (#277), and it honours a build-time `VERSION`.
|
|
15
19
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* went stale between a version bump and the next build. `src/tools/server_info.ts` reads
|
|
20
|
-
* the ROOT manifest, so the two disagreed. Removing the import removes the mirror too.
|
|
21
|
-
*
|
|
22
|
-
* `findRootPackageJson` walks up to the outermost package.json named actual-mcp-server,
|
|
23
|
-
* skipping that mirror. It never throws (it swallows unreadable and malformed files), so
|
|
24
|
-
* the try/catch is belt-and-braces around a future change rather than a live path.
|
|
25
|
-
* Removing the import attributes also removes the last instance of the construct that
|
|
26
|
-
* crashed on Node 18 in #275.
|
|
20
|
+
* #541 reversed the gate's "leave `--version` unchanged": the old `readRootVersion()` here
|
|
21
|
+
* ignored `VERSION`, so a Docker image built as `X-dev-abc` printed plain `X` from
|
|
22
|
+
* `--version` while clients saw `X-dev-abc`. One reader means one string.
|
|
27
23
|
*/
|
|
28
|
-
function readRootVersion() {
|
|
29
|
-
try {
|
|
30
|
-
const manifest = findRootPackageJson(dirname(fileURLToPath(import.meta.url)));
|
|
31
|
-
return manifest?.version ?? process.env.VERSION ?? 'unknown';
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return process.env.VERSION ?? 'unknown';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
24
|
// Add global error handlers
|
|
38
25
|
process.on('unhandledRejection', (reason, promise) => {
|
|
39
26
|
console.error('=== UNHANDLED REJECTION ===');
|
|
@@ -140,7 +127,7 @@ if (!argsEarly.includes('--http') &&
|
|
|
140
127
|
}
|
|
141
128
|
if (argsEarly.includes('--help') || argsEarly.includes('-h') ||
|
|
142
129
|
argsEarly.includes('--version') || argsEarly.includes('-v')) {
|
|
143
|
-
const version =
|
|
130
|
+
const version = getMcpServerVersion();
|
|
144
131
|
if (argsEarly.includes('--version') || argsEarly.includes('-v')) {
|
|
145
132
|
console.log(version);
|
|
146
133
|
}
|
|
@@ -212,26 +199,9 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
212
199
|
const logger = loggerModule.default;
|
|
213
200
|
const { getLocalIp } = utilsModule;
|
|
214
201
|
const actualToolsManager = actualToolsManagerModule.default;
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
VERSION = readRootVersion();
|
|
219
|
-
// Append git commit hash for development builds
|
|
220
|
-
try {
|
|
221
|
-
const { execSync } = await import('child_process');
|
|
222
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
223
|
-
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
|
224
|
-
if (branch === 'develop' || branch !== 'main') {
|
|
225
|
-
VERSION = `${VERSION}-dev-${commitHash}`;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
// Git not available or not in a git repo, use version as-is
|
|
230
|
-
logger.debug('Could not determine git commit hash:', err);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Ensure VERSION is always a string (fallback to 0.1.0 if somehow still undefined)
|
|
234
|
-
const version = VERSION || '0.1.0';
|
|
202
|
+
// #541: the build-time VERSION, or the root package version plus a dev suffix on a
|
|
203
|
+
// checkout. Shared with actual_server_info so both report the identical string.
|
|
204
|
+
const version = getMcpServerVersion();
|
|
235
205
|
// now continue with the original logic (args, flags, usage, etc.)
|
|
236
206
|
const PORT = process.env.MCP_BRIDGE_PORT ? Number(process.env.MCP_BRIDGE_PORT) : 3600;
|
|
237
207
|
const HTTP_PATH = process.env.MCP_HTTP_PATH || '/http';
|
|
@@ -249,7 +219,7 @@ Docs & source: https://github.com/agigante80/actual-mcp-server`);
|
|
|
249
219
|
logger.error('❌ --http and --stdio are mutually exclusive. Pick one.');
|
|
250
220
|
process.exit(1);
|
|
251
221
|
}
|
|
252
|
-
logger.info(`🚀 Starting Actual MCP Server v${
|
|
222
|
+
logger.info(`🚀 Starting Actual MCP Server v${version}`);
|
|
253
223
|
// NOTE: Persistent connection disabled - using init/shutdown per operation pattern
|
|
254
224
|
// This ensures tombstone=0 for all created entities (they appear in UI)
|
|
255
225
|
// await connectToActual();
|
|
@@ -5,7 +5,7 @@ import api from '@actual-app/api';
|
|
|
5
5
|
// cannot expose its named exports via static import syntax. At runtime the default import
|
|
6
6
|
// IS module.exports, so all methods are accessible as properties.
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
-
const { addTransactions: rawAddTransactions, getAccounts: rawGetAccounts, importTransactions: rawImportTransactions, getTransactions: rawGetTransactions, getCategories: rawGetCategories, createCategory: rawCreateCategory, getPayees: rawGetPayees, getCommonPayees: rawGetCommonPayees, createPayee: rawCreatePayee, getBudgetMonths: rawGetBudgetMonths, getBudgetMonth: rawGetBudgetMonth, setBudgetAmount: rawSetBudgetAmount, createAccount: rawCreateAccount, updateAccount: rawUpdateAccount, getAccountBalance: rawGetAccountBalance, updateTransaction: rawUpdateTransaction, deleteTransaction: rawDeleteTransaction, updateCategory: rawUpdateCategory, deleteCategory: rawDeleteCategory, updatePayee: rawUpdatePayee, deletePayee: rawDeletePayee, deleteAccount: rawDeleteAccount, getRules: rawGetRules, createRule: rawCreateRule, updateRule: rawUpdateRule, deleteRule: rawDeleteRule, setBudgetCarryover: rawSetBudgetCarryover, closeAccount: rawCloseAccount, reopenAccount: rawReopenAccount, getCategoryGroups: rawGetCategoryGroups, createCategoryGroup: rawCreateCategoryGroup, updateCategoryGroup: rawUpdateCategoryGroup, deleteCategoryGroup: rawDeleteCategoryGroup, mergePayees: rawMergePayees, getPayeeRules: rawGetPayeeRules,
|
|
8
|
+
const { addTransactions: rawAddTransactions, getAccounts: rawGetAccounts, importTransactions: rawImportTransactions, getTransactions: rawGetTransactions, getCategories: rawGetCategories, createCategory: rawCreateCategory, getPayees: rawGetPayees, getCommonPayees: rawGetCommonPayees, createPayee: rawCreatePayee, getBudgetMonths: rawGetBudgetMonths, getBudgetMonth: rawGetBudgetMonth, setBudgetAmount: rawSetBudgetAmount, createAccount: rawCreateAccount, updateAccount: rawUpdateAccount, getAccountBalance: rawGetAccountBalance, updateTransaction: rawUpdateTransaction, deleteTransaction: rawDeleteTransaction, updateCategory: rawUpdateCategory, deleteCategory: rawDeleteCategory, updatePayee: rawUpdatePayee, deletePayee: rawDeletePayee, deleteAccount: rawDeleteAccount, getRules: rawGetRules, createRule: rawCreateRule, updateRule: rawUpdateRule, deleteRule: rawDeleteRule, setBudgetCarryover: rawSetBudgetCarryover, closeAccount: rawCloseAccount, reopenAccount: rawReopenAccount, getCategoryGroups: rawGetCategoryGroups, createCategoryGroup: rawCreateCategoryGroup, updateCategoryGroup: rawUpdateCategoryGroup, deleteCategoryGroup: rawDeleteCategoryGroup, mergePayees: rawMergePayees, getPayeeRules: rawGetPayeeRules, holdBudgetForNextMonth: rawHoldBudgetForNextMonth, resetBudgetHold: rawResetBudgetHold, runQuery: rawRunQuery, runBankSync: rawRunBankSync, getBudgets: rawGetBudgets, getIDByName: rawGetIDByName, getServerVersion: rawGetServerVersion, getSchedules: rawGetSchedules, createSchedule: rawCreateSchedule, updateSchedule: rawUpdateSchedule, deleteSchedule: rawDeleteSchedule, getTags: rawGetTags, createTag: rawCreateTag, updateTag: rawUpdateTag, deleteTag: rawDeleteTag, getNote: rawGetNote, updateNote: rawUpdateNote, exportBudget: rawExportBudget, importBudget: rawImportBudget, getPreferences: rawGetPreferences, getAccountGroups: rawGetAccountGroups, createAccountGroup: rawCreateAccountGroup, updateAccountGroup: rawUpdateAccountGroup, deleteAccountGroup: rawDeleteAccountGroup,
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
10
|
} = api;
|
|
11
11
|
import { EventEmitter } from 'events';
|
|
@@ -1878,17 +1878,29 @@ export async function setBudgetAmount(month, categoryId, amount) {
|
|
|
1878
1878
|
* full Playwright timeout when the upstream server's mutator queue stalled
|
|
1879
1879
|
* between cycles. The month read and the funds check run before either write.
|
|
1880
1880
|
*
|
|
1881
|
-
* NOT atomic and no rollback (#518).
|
|
1882
|
-
*
|
|
1883
|
-
* `
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
* `
|
|
1891
|
-
*
|
|
1881
|
+
* NOT atomic and no rollback (#518). The two writes are separate raw calls,
|
|
1882
|
+
* each awaited on its own: outside a batch bracket upstream applies each
|
|
1883
|
+
* write (`_sendMessages` then `applyMessages`) before the call returns and
|
|
1884
|
+
* rethrows an apply failure to us (read from the shipped upstream source, not
|
|
1885
|
+
* verified here by a read-back), so an apply failure is returned rather than
|
|
1886
|
+
* detached. If the second write fails or is not attempted, the first stays
|
|
1887
|
+
* applied and the thrown error says so (see below).
|
|
1888
|
+
*
|
|
1889
|
+
* COOPERATIVE DEADLINE between the two writes, for the same reason as
|
|
1890
|
+
* `setBudgetBatch`: `withOpTimeout` cannot cancel this callback, so without the
|
|
1891
|
+
* check an abandoned callback would still make the second write after the
|
|
1892
|
+
* caller was told the operation timed out.
|
|
1893
|
+
*
|
|
1894
|
+
* WHY NO `batchBudgetUpdates` BRACKET (#539). Upstream `api/batch-budget-start`
|
|
1895
|
+
* runs `batchMessages` un-awaited and `api/batch-budget-end` returns before the
|
|
1896
|
+
* queued messages are applied, so an apply failure under the bracket is a
|
|
1897
|
+
* rejection on a promise nobody holds: it reaches the `unhandledRejection`
|
|
1898
|
+
* handler and exits the server, after the tool already reported success. The
|
|
1899
|
+
* bracket did apply both writes in one sqlite transaction, so dropping it trades
|
|
1900
|
+
* that all-or-nothing apply for a partial transfer that is REPORTED; upstream
|
|
1901
|
+
* has no awaited bracket that keeps both. Do not reintroduce it (a unit test
|
|
1902
|
+
* guards this). The upstream trace is the `actual_budgets_transfer` and
|
|
1903
|
+
* `actual_budget_updates_batch` rows of `docs/audit/write-effect-audit.md`.
|
|
1892
1904
|
*/
|
|
1893
1905
|
export async function transferBudgetAmount(month, fromCategoryId, toCategoryId, amount) {
|
|
1894
1906
|
observability.incrementToolCall('actual.budgets.transfer').catch(() => { });
|
|
@@ -1896,6 +1908,9 @@ export async function transferBudgetAmount(month, fromCategoryId, toCategoryId,
|
|
|
1896
1908
|
// Inside processWriteQueue we already hold the api mutex in apiLock.ts and the api is
|
|
1897
1909
|
// initialised. Call raw functions only: adapter wrappers would re-enter
|
|
1898
1910
|
// queueWriteOperation / withActualApi and defeat the single-cycle goal.
|
|
1911
|
+
// The deadline clock starts here, before the read, because withOpTimeout times the whole
|
|
1912
|
+
// callback: starting it at the first write would let a slow read hide (#539 review round 2).
|
|
1913
|
+
const startedAt = Date.now();
|
|
1899
1914
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1900
1915
|
const budgetMonth = await rawGetBudgetMonth(month);
|
|
1901
1916
|
if (!budgetMonth?.categoryGroups) {
|
|
@@ -1916,10 +1931,31 @@ export async function transferBudgetAmount(month, fromCategoryId, toCategoryId,
|
|
|
1916
1931
|
if (prevFrom < amount) {
|
|
1917
1932
|
throw new Error(`Insufficient budget in source category. Available: ${prevFrom}, Requested: ${amount}`);
|
|
1918
1933
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
}
|
|
1934
|
+
// No retry on either write: a replayed amount write after a partial apply is not safe to
|
|
1935
|
+
// reason about, and the client re-reads instead.
|
|
1936
|
+
await withConcurrency(() => rawSetBudgetAmount(month, fromCategoryId, prevFrom - amount));
|
|
1937
|
+
const sourceDone = `Partial transfer: the source category ${fromCategoryId} was set from ${prevFrom} to ${prevFrom - amount}`;
|
|
1938
|
+
const readBack = 'Read back with actual_budgets_getMonth before retrying.';
|
|
1939
|
+
const deadlineMs = config.ACTUAL_OP_TIMEOUT_MS > 0 ? config.ACTUAL_OP_TIMEOUT_MS * BUDGET_BATCH_DEADLINE_FRACTION : 0;
|
|
1940
|
+
if (deadlineMs > 0 && Date.now() - startedAt > deadlineMs) {
|
|
1941
|
+
throw new Error(`${sourceDone}, but the target category ${toCategoryId} was not attempted (${BUDGET_BATCH_DEADLINE_ERROR}), ` +
|
|
1942
|
+
`so it is unchanged at ${prevTo}. ${readBack}`);
|
|
1943
|
+
}
|
|
1944
|
+
try {
|
|
1945
|
+
await withConcurrency(() => rawSetBudgetAmount(month, toCategoryId, prevTo + amount));
|
|
1946
|
+
}
|
|
1947
|
+
catch (error) {
|
|
1948
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1949
|
+
// An infrastructure error says nothing about whether the write landed, so only a
|
|
1950
|
+
// deterministic (domain) rejection may claim the target is unchanged.
|
|
1951
|
+
const target = _shouldDropPoolOnError(error)
|
|
1952
|
+
? `its outcome is unknown (it was ${prevTo})`
|
|
1953
|
+
: `it is unchanged at ${prevTo}`;
|
|
1954
|
+
// The original text stays INSIDE the message: isRetryableError classifies by message only
|
|
1955
|
+
// and never reads `cause`, and the write queue's self-heal (_shouldDropPoolOnError) depends
|
|
1956
|
+
// on that classification. Same reason as abortBatchOnInfrastructureError.
|
|
1957
|
+
throw new Error(`${sourceDone}, but setting the target category ${toCategoryId} failed, so ${target}: ${msg}. ${readBack}`, { cause: error });
|
|
1958
|
+
}
|
|
1923
1959
|
return {
|
|
1924
1960
|
transferred: amount,
|
|
1925
1961
|
fromCategory: { id: fromCategoryId, previousAmount: prevFrom, newAmount: prevFrom - amount },
|
|
@@ -2084,12 +2120,25 @@ export async function updateTransaction(id, fields) {
|
|
|
2084
2120
|
}
|
|
2085
2121
|
});
|
|
2086
2122
|
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Update many transactions in ONE write cycle, with a verdict per item. NOT atomic.
|
|
2125
|
+
*
|
|
2126
|
+
* COOPERATIVE DEADLINE (#538): same 0.75 fraction of ACTUAL_OP_TIMEOUT_MS as `createRulesBatch`
|
|
2127
|
+
* and `setBudgetBatch`, because `withOpTimeout` cannot cancel this callback: without it a slow
|
|
2128
|
+
* batch kept writing after its caller was told the operation timed out. The clock starts before
|
|
2129
|
+
* the existence query, so a slow read counts. Items not reached go to `failed` with
|
|
2130
|
+
* BUDGET_BATCH_DEADLINE_ERROR and were never sent upstream. A missing id keeps its not-found
|
|
2131
|
+
* error even past the deadline: it was never going to be written, and "not attempted" would
|
|
2132
|
+
* tell the client a retry could succeed. A raw update already in flight when the deadline
|
|
2133
|
+
* passes can still overrun; the check runs between items.
|
|
2134
|
+
*/
|
|
2087
2135
|
export async function updateTransactionBatch(updates) {
|
|
2088
2136
|
observability.incrementToolCall('actual.transactions.updateBatch').catch(() => { });
|
|
2089
2137
|
// All updates share one queueWriteOperation → one init/sync/shutdown cycle (issue #79).
|
|
2090
2138
|
// Sequential loop (not Promise.all) is intentional: concurrent rawUpdateTransaction calls
|
|
2091
2139
|
// within one session can interleave withMutation CRDT messages unpredictably.
|
|
2092
2140
|
return queueWriteOperation(async () => {
|
|
2141
|
+
const startedAt = Date.now();
|
|
2093
2142
|
// Nothing to do for an empty batch. The tool enforces min 1, but the adapter is a
|
|
2094
2143
|
// public export, so guard here and avoid an empty-$oneof existence query.
|
|
2095
2144
|
if (updates.length === 0)
|
|
@@ -2112,11 +2161,16 @@ export async function updateTransactionBatch(updates) {
|
|
|
2112
2161
|
}, { retries: 2, backoffMs: 200 }));
|
|
2113
2162
|
const succeeded = [];
|
|
2114
2163
|
const failed = [];
|
|
2164
|
+
const deadlineMs = config.ACTUAL_OP_TIMEOUT_MS > 0 ? config.ACTUAL_OP_TIMEOUT_MS * BUDGET_BATCH_DEADLINE_FRACTION : 0;
|
|
2115
2165
|
for (const [position, { id, fields }] of updates.entries()) {
|
|
2116
2166
|
if (!existing.has(id)) {
|
|
2117
2167
|
failed.push({ id, error: `Transaction "${id}" not found. Use actual_transactions_get to list transactions.` });
|
|
2118
2168
|
continue;
|
|
2119
2169
|
}
|
|
2170
|
+
if (deadlineMs > 0 && Date.now() - startedAt > deadlineMs) {
|
|
2171
|
+
failed.push({ id, error: BUDGET_BATCH_DEADLINE_ERROR });
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2120
2174
|
try {
|
|
2121
2175
|
await withConcurrency(() => retry(() => rawUpdateTransaction(id, fields), { retries: 0, backoffMs: 200, isRetryable: isRetryableError }));
|
|
2122
2176
|
succeeded.push({ id });
|
|
@@ -3013,23 +3067,32 @@ export async function getPayeeRules(payeeId) {
|
|
|
3013
3067
|
}
|
|
3014
3068
|
/** Error recorded for every item the cooperative deadline stopped before it was written. */
|
|
3015
3069
|
export const BUDGET_BATCH_DEADLINE_ERROR = 'not attempted: batch time budget exhausted';
|
|
3016
|
-
/** Fraction of ACTUAL_OP_TIMEOUT_MS the item loop may consume; the rest is left for
|
|
3070
|
+
/** Fraction of ACTUAL_OP_TIMEOUT_MS the item loop may consume; the rest is left for the trailing sync. */
|
|
3017
3071
|
const BUDGET_BATCH_DEADLINE_FRACTION = 0.75;
|
|
3018
3072
|
/**
|
|
3019
3073
|
* #516: set budget amounts and/or carryover flags for many (month, category) pairs in ONE
|
|
3020
3074
|
* write cycle, with a verdict per item. NOT atomic and no rollback: items applied before a
|
|
3021
|
-
* failure stay applied.
|
|
3022
|
-
*
|
|
3075
|
+
* failure stay applied.
|
|
3076
|
+
*
|
|
3077
|
+
* NO `batchBudgetUpdates` BRACKET (#539). Each raw write is awaited on its own. Outside a
|
|
3078
|
+
* bracket upstream applies the write (`_sendMessages` then `applyMessages`) before the call
|
|
3079
|
+
* returns and rethrows an apply failure (read from the shipped upstream source, not verified by
|
|
3080
|
+
* a read-back), so `succeeded` means the write call returned without error and a failed
|
|
3081
|
+
* apply lands on that item's verdict. Under the bracket (#516 to #539) upstream applied the
|
|
3082
|
+
* queued writes AFTER `batch-budget-end` returned, on a promise it never awaits, so an apply
|
|
3083
|
+
* failure was an unhandled rejection that exited the server after we had reported success. The
|
|
3084
|
+
* cost is one apply per write instead of one per batch; the server sync is a debounced timer
|
|
3085
|
+
* (`scheduleFullSync`), so it still runs once, except under NODE_ENV=test where upstream syncs on
|
|
3086
|
+
* every write. Do not reintroduce the bracket (a unit test guards this).
|
|
3023
3087
|
*
|
|
3024
3088
|
* TWO PHASES inside one `queueWriteOperation`:
|
|
3025
|
-
* 1. Before
|
|
3089
|
+
* 1. Before any write: read the categories listing and the budget months ONCE each, then
|
|
3026
3090
|
* decide a typed refusal per item. The guards apply to EVERY item, a carryover-only item
|
|
3027
3091
|
* included, because upstream `api/budget-set-amount` validates neither category nor month
|
|
3028
3092
|
* (it INSERTs an orphan `<month>-<category>` row) and the two fields should refuse alike.
|
|
3029
|
-
* 2.
|
|
3030
|
-
*
|
|
3031
|
-
*
|
|
3032
|
-
* Only `raw*` calls run there: an `adapter.*` call would nest the api lock and deadlock.
|
|
3093
|
+
* 2. Raw writes for the items that passed, each caught per item so one failure does not lose
|
|
3094
|
+
* the other verdicts. Only `raw*` calls run here: an `adapter.*` call would nest the api
|
|
3095
|
+
* lock and deadlock.
|
|
3033
3096
|
*
|
|
3034
3097
|
* INCOME CATEGORIES (maintainer decision): a carryover item whose category is an income
|
|
3035
3098
|
* category is refused in phase 1. Upstream `api/budget-set-carryover` rejects income
|
|
@@ -3037,18 +3100,18 @@ const BUDGET_BATCH_DEADLINE_FRACTION = 0.75;
|
|
|
3037
3100
|
* this check an item setting both would half-apply. Upstream's own rejection stays as a
|
|
3038
3101
|
* backstop and is reported per item.
|
|
3039
3102
|
*
|
|
3040
|
-
* RETRY: none. A refusal must never replay
|
|
3041
|
-
* the
|
|
3103
|
+
* RETRY: none. A refusal must never replay, an apply failure is deterministic, a retry would
|
|
3104
|
+
* spend the deadline, and infrastructure errors abort the batch instead; the client re-reads
|
|
3105
|
+
* (`actual_budgets_getMonth`) and retries.
|
|
3042
3106
|
*
|
|
3043
3107
|
* COOPERATIVE DEADLINE. The whole operation runs under one `withOpTimeout`, a `Promise.race`
|
|
3044
|
-
* that cannot cancel the callback
|
|
3045
|
-
*
|
|
3046
|
-
*
|
|
3047
|
-
*
|
|
3048
|
-
*
|
|
3049
|
-
* to `failed`. The 25% left over covers batch-budget-end and the sync.
|
|
3108
|
+
* that cannot cancel the callback, so an abandoned loop would keep writing after the caller was
|
|
3109
|
+
* told it timed out (as in `createRulesBatch`). So the loop checks, before each item write, the
|
|
3110
|
+
* time since this operation started against 75% of ACTUAL_OP_TIMEOUT_MS (skipped when it is 0,
|
|
3111
|
+
* the documented "no bound" setting) and stops writing once it is exceeded; the remaining items
|
|
3112
|
+
* go to `failed`. The 25% left over covers the drain's trailing sync.
|
|
3050
3113
|
* WHY NOT `queueWriteOperation`'s `timeoutMs` option: it would LENGTHEN the hold on the
|
|
3051
|
-
* process-global api lock, and it still would not
|
|
3114
|
+
* process-global api lock, and it still would not stop an abandoned loop from writing.
|
|
3052
3115
|
*
|
|
3053
3116
|
* `preservesListings` claim is safe: only raw budget amount and carryover writes run here.
|
|
3054
3117
|
*/
|
|
@@ -3058,7 +3121,7 @@ export async function setBudgetBatch(items) {
|
|
|
3058
3121
|
const startedAt = Date.now();
|
|
3059
3122
|
const succeeded = [];
|
|
3060
3123
|
const failed = [];
|
|
3061
|
-
// Phase 1: reads, then refusal decisions. Both reads happen BEFORE the
|
|
3124
|
+
// Phase 1: reads, then refusal decisions. Both reads happen BEFORE the first write.
|
|
3062
3125
|
const categories = await withConcurrency(() => retry(() => readDrainListing('categories', () => rawGetCategories()), { retries: 2, backoffMs: 200 }));
|
|
3063
3126
|
const categoryById = new Map();
|
|
3064
3127
|
for (const c of categories)
|
|
@@ -3085,43 +3148,38 @@ export async function setBudgetBatch(items) {
|
|
|
3085
3148
|
else
|
|
3086
3149
|
toWrite.push({ index, item });
|
|
3087
3150
|
});
|
|
3088
|
-
// Phase 2:
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
await rawSetBudgetAmount(item.month, item.categoryId, item.amount);
|
|
3102
|
-
amountWritten = true;
|
|
3103
|
-
}
|
|
3104
|
-
if (item.carryover !== undefined) {
|
|
3105
|
-
await rawSetBudgetCarryover(item.month, item.categoryId, item.carryover);
|
|
3106
|
-
}
|
|
3107
|
-
succeeded.push(ref);
|
|
3108
|
-
}
|
|
3109
|
-
catch (error) {
|
|
3110
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
3111
|
-
// Infrastructure errors stop the batch (see abortBatchOnInfrastructureError);
|
|
3112
|
-
// the throw leaves the bracket and upstream closes it in a `finally`.
|
|
3113
|
-
abortBatchOnInfrastructureError(error, {
|
|
3114
|
-
label: 'Budget',
|
|
3115
|
-
appliedCount: succeeded.length,
|
|
3116
|
-
total: items.length,
|
|
3117
|
-
applied: succeeded.map((x) => x.index).join(', '),
|
|
3118
|
-
message: msg,
|
|
3119
|
-
readBackTool: 'actual_budgets_getMonth',
|
|
3120
|
-
});
|
|
3121
|
-
failed.push({ ...ref, error: amountWritten ? `amount was applied, then the carryover failed: ${msg}` : msg });
|
|
3122
|
-
}
|
|
3151
|
+
// Phase 2: raw writes only, each awaited on its own (no bracket, see #539 above).
|
|
3152
|
+
const deadlineMs = config.ACTUAL_OP_TIMEOUT_MS > 0 ? config.ACTUAL_OP_TIMEOUT_MS * BUDGET_BATCH_DEADLINE_FRACTION : 0;
|
|
3153
|
+
for (const { index, item } of toWrite) {
|
|
3154
|
+
const ref = { index, month: item.month, categoryId: item.categoryId };
|
|
3155
|
+
if (deadlineMs > 0 && Date.now() - startedAt > deadlineMs) {
|
|
3156
|
+
failed.push({ ...ref, error: BUDGET_BATCH_DEADLINE_ERROR });
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
let amountWritten = false;
|
|
3160
|
+
try {
|
|
3161
|
+
if (item.amount !== undefined) {
|
|
3162
|
+
await withConcurrency(() => rawSetBudgetAmount(item.month, item.categoryId, item.amount));
|
|
3163
|
+
amountWritten = true;
|
|
3123
3164
|
}
|
|
3124
|
-
|
|
3165
|
+
if (item.carryover !== undefined) {
|
|
3166
|
+
await withConcurrency(() => rawSetBudgetCarryover(item.month, item.categoryId, item.carryover));
|
|
3167
|
+
}
|
|
3168
|
+
succeeded.push(ref);
|
|
3169
|
+
}
|
|
3170
|
+
catch (error) {
|
|
3171
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3172
|
+
// Infrastructure errors stop the batch (see abortBatchOnInfrastructureError).
|
|
3173
|
+
abortBatchOnInfrastructureError(error, {
|
|
3174
|
+
label: 'Budget',
|
|
3175
|
+
appliedCount: succeeded.length,
|
|
3176
|
+
total: items.length,
|
|
3177
|
+
applied: succeeded.map((x) => x.index).join(', '),
|
|
3178
|
+
message: msg,
|
|
3179
|
+
readBackTool: 'actual_budgets_getMonth',
|
|
3180
|
+
});
|
|
3181
|
+
failed.push({ ...ref, error: amountWritten ? `amount was applied, then the carryover failed: ${msg}` : msg });
|
|
3182
|
+
}
|
|
3125
3183
|
}
|
|
3126
3184
|
succeeded.sort((a, b) => a.index - b.index);
|
|
3127
3185
|
failed.sort((a, b) => a.index - b.index);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { findRootPackageJson } from './node-version-guard.js';
|
|
6
|
+
/**
|
|
7
|
+
* #541: the single source of this server's own version, shared by the MCP initialize
|
|
8
|
+
* `serverInfo.version` (src/index.ts) and the `actual_server_info` tool.
|
|
9
|
+
*
|
|
10
|
+
* A build-time `VERSION` (set by the Docker build arg and by CI) wins. Otherwise a
|
|
11
|
+
* developer checkout gets `<package version>-dev-<short hash>` when it is not on `main`.
|
|
12
|
+
*
|
|
13
|
+
* Git runs only when `.git` exists AT the package root, and always with that root as its
|
|
14
|
+
* cwd. The published npm package has no `.git`, and without that check `git rev-parse`
|
|
15
|
+
* would walk up from an install under some other repository (a consumer's node_modules)
|
|
16
|
+
* and report THAT repository's branch and hash. `existsSync` also accepts the `.git` FILE
|
|
17
|
+
* of a worktree or submodule.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveMcpServerVersion(baseVersion, env, root, runGit) {
|
|
20
|
+
if (env.VERSION && env.VERSION !== 'unknown')
|
|
21
|
+
return env.VERSION;
|
|
22
|
+
// #546: no manifest was found, so there is no version to qualify; 'unknown-dev-<hash>'
|
|
23
|
+
// would read as a real one.
|
|
24
|
+
if (baseVersion === 'unknown')
|
|
25
|
+
return baseVersion;
|
|
26
|
+
if (!existsSync(join(root, '.git')))
|
|
27
|
+
return baseVersion;
|
|
28
|
+
try {
|
|
29
|
+
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
30
|
+
const hash = runGit(['rev-parse', '--short', 'HEAD']);
|
|
31
|
+
if (!hash)
|
|
32
|
+
return baseVersion;
|
|
33
|
+
// A detached HEAD reports 'HEAD', which is not main, so it gets the suffix too.
|
|
34
|
+
return branch !== 'main' ? `${baseVersion}-dev-${hash}` : baseVersion;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Git missing, not a repository, or the timeout fired: report the plain version.
|
|
38
|
+
return baseVersion;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let cached;
|
|
42
|
+
/**
|
|
43
|
+
* This package's (the MCP server's) version, computed once per process. Unrelated to the
|
|
44
|
+
* adapter's `getServerVersion()`, which returns the ACTUAL BUDGET server version.
|
|
45
|
+
*/
|
|
46
|
+
export function getMcpServerVersion() {
|
|
47
|
+
if (cached !== undefined)
|
|
48
|
+
return cached;
|
|
49
|
+
// dist/src/lib/ to the package root. This one root is both the `.git` check directory
|
|
50
|
+
// and git's cwd; never process.cwd(), which is wherever the server was started from.
|
|
51
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
52
|
+
const base = findRootPackageJson(root)?.version ?? 'unknown';
|
|
53
|
+
// stderr is discarded so a "not a git repository" line never reaches the stdio channel
|
|
54
|
+
// or the log, and the timeout keeps a hung git from blocking startup.
|
|
55
|
+
const runGit = (args) => execFileSync('git', args, {
|
|
56
|
+
cwd: root,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
59
|
+
timeout: 2000,
|
|
60
|
+
}).trim();
|
|
61
|
+
cached = resolveMcpServerVersion(base, process.env, root, runGit);
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
import { createModuleLogger } from './loggerFactory.js';
|
|
15
15
|
const MAX_DETAIL_CODE_POINTS = 200;
|
|
16
16
|
const CUT_CHARS = /[{["'`]/;
|
|
17
|
-
// Control (C0, C1), line/paragraph separators
|
|
18
|
-
//
|
|
17
|
+
// Control (C0, C1), line/paragraph separators, bidi format characters, and (#535) the
|
|
18
|
+
// invisible marks that can hide or reorder text in a log line: the Arabic letter mark,
|
|
19
|
+
// zero-width space/joiners, LRM/RLM and the BOM. Written with escapes, as src/logger.ts
|
|
20
|
+
// does: a raw U+2028 in source would terminate the literal.
|
|
19
21
|
// eslint-disable-next-line no-control-regex
|
|
20
|
-
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
22
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g;
|
|
21
23
|
const OMITTED = '[payload omitted]';
|
|
22
24
|
/** Pure: turn an arbitrary thrown value into a bounded, payload-free, single-line string. */
|
|
23
25
|
export function sanitizeTransportErrorDetail(value) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/server/httpErrorHandler.ts
|
|
2
|
+
//
|
|
3
|
+
// #543: the final Express error layer for the HTTP transport. Without it, an error falls
|
|
4
|
+
// through to Express's default handler, and from express 5.3.0 that handler logs the WHOLE
|
|
5
|
+
// error object with console.error (5.2.1 logged err.stack). body-parser 2.3.0 attaches the
|
|
6
|
+
// raw request text to a JSON parse error as `err.body`, and express.json runs before any
|
|
7
|
+
// auth, so an unauthenticated malformed POST would put its raw body in the logs. The default
|
|
8
|
+
// handler also renders the stack into the response under NODE_ENV=development.
|
|
9
|
+
//
|
|
10
|
+
// Contract:
|
|
11
|
+
// - The response NEVER carries err.message, err.body or err.stack, whatever NODE_ENV is.
|
|
12
|
+
// - The log NEVER carries request body content: not err.body, and not err.message either,
|
|
13
|
+
// because the V8 JSON SyntaxError message quotes the input text. Only an allowlist of
|
|
14
|
+
// fields is logged.
|
|
15
|
+
// - next(err) is NEVER called: app.handle wires finalhandler with onerror: logerror, which
|
|
16
|
+
// is the console.error(err) path this module exists to close.
|
|
17
|
+
//
|
|
18
|
+
// The handler also absorbs any non-MCPAuth error that mcp-auth's bearerAuth rethrows, with
|
|
19
|
+
// the same fixed 500 and the same allowlisted log.
|
|
20
|
+
import { STATUS_CODES } from 'node:http';
|
|
21
|
+
import { createModuleLogger } from '../lib/loggerFactory.js';
|
|
22
|
+
import { resolveRequestId } from '../logger.js';
|
|
23
|
+
const log = createModuleLogger('HTTP');
|
|
24
|
+
const JSONRPC_PARSE_ERROR = -32700;
|
|
25
|
+
const JSONRPC_INVALID_REQUEST = -32600;
|
|
26
|
+
const JSONRPC_INTERNAL_ERROR = -32603;
|
|
27
|
+
/**
|
|
28
|
+
* True when the request targets the MCP endpoint: the pathname of `originalUrl` (the query
|
|
29
|
+
* string removed) equals `mcpPath` or starts with `mcpPath + '/'`. Never a bare prefix match:
|
|
30
|
+
* with an MCP path of `/mcp`, `/mcp-info` is NOT the MCP endpoint and gets the plain shape.
|
|
31
|
+
*
|
|
32
|
+
* #545: compared case-insensitively, because this server never enables Express's
|
|
33
|
+
* `case sensitive routing`, so `/MCP` reaches the MCP route and must get its envelope too.
|
|
34
|
+
*/
|
|
35
|
+
function isMcpRequestPath(originalUrl, mcpPath) {
|
|
36
|
+
const q = originalUrl.indexOf('?');
|
|
37
|
+
const pathname = (q === -1 ? originalUrl : originalUrl.slice(0, q)).toLowerCase();
|
|
38
|
+
const lowered = mcpPath.toLowerCase();
|
|
39
|
+
const base = lowered.length > 1 && lowered.endsWith('/') ? lowered.slice(0, -1) : lowered;
|
|
40
|
+
// A root MCP path ('/') makes every request an MCP request.
|
|
41
|
+
if (base === '/' || base === '')
|
|
42
|
+
return true;
|
|
43
|
+
return pathname === base || pathname.startsWith(`${base}/`);
|
|
44
|
+
}
|
|
45
|
+
export function createHttpErrorHandler({ mcpPath }) {
|
|
46
|
+
// Four parameters: Express identifies an error handler by its arity.
|
|
47
|
+
return function httpErrorHandler(err, req, res, _next) {
|
|
48
|
+
const e = err !== null && typeof err === 'object' ? err : {};
|
|
49
|
+
const type = typeof e.type === 'string' ? e.type : undefined;
|
|
50
|
+
// The ticket's first cut mapped everything except parse and size errors to 500. That was
|
|
51
|
+
// reversed in review: body-parser raises other CLIENT errors before auth (unsupported
|
|
52
|
+
// charset or encoding: 415; a corrupt gzip body or an aborted request: 400), each marked
|
|
53
|
+
// expose with its own 4xx status. Reporting those as 500 and logging them at error level
|
|
54
|
+
// let any caller fire 5xx alerts at will. So an exposed 4xx keeps its status; only the
|
|
55
|
+
// status is taken from the error, never its message.
|
|
56
|
+
let status = 500;
|
|
57
|
+
if (type === 'entity.parse.failed')
|
|
58
|
+
status = 400;
|
|
59
|
+
else if (type === 'entity.too.large')
|
|
60
|
+
status = 413;
|
|
61
|
+
else {
|
|
62
|
+
const s = typeof e.status === 'number' ? e.status : e.statusCode;
|
|
63
|
+
if (e.expose === true && Number.isInteger(s) && s >= 400 && s <= 499)
|
|
64
|
+
status = s;
|
|
65
|
+
}
|
|
66
|
+
const mcp = isMcpRequestPath(req.originalUrl || req.url || '', mcpPath);
|
|
67
|
+
const meta = {
|
|
68
|
+
type,
|
|
69
|
+
status,
|
|
70
|
+
method: req.method,
|
|
71
|
+
// The pathname only: a query string can carry anything.
|
|
72
|
+
path: (req.originalUrl || '').split('?')[0],
|
|
73
|
+
requestId: resolveRequestId(req.get('x-correlation-id')),
|
|
74
|
+
};
|
|
75
|
+
if (status === 500) {
|
|
76
|
+
if (typeof e.name === 'string')
|
|
77
|
+
meta.errorName = e.name;
|
|
78
|
+
if (typeof e.code === 'string' || typeof e.code === 'number')
|
|
79
|
+
meta.errorCode = e.code;
|
|
80
|
+
}
|
|
81
|
+
if (res.headersSent) {
|
|
82
|
+
log.warn('Request error after the response started; closing the connection', meta);
|
|
83
|
+
req.socket?.destroy();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (status === 500)
|
|
87
|
+
log.error('Unhandled request error', undefined, meta);
|
|
88
|
+
else
|
|
89
|
+
log.warn('Rejected request body', meta);
|
|
90
|
+
let message = 'Internal error';
|
|
91
|
+
if (type === 'entity.parse.failed')
|
|
92
|
+
message = mcp ? 'Parse error' : 'Invalid JSON body';
|
|
93
|
+
else if (status === 413)
|
|
94
|
+
message = 'Request body too large';
|
|
95
|
+
else if (status !== 500)
|
|
96
|
+
message = STATUS_CODES[status] ?? 'Bad Request';
|
|
97
|
+
if (mcp) {
|
|
98
|
+
let code = JSONRPC_INVALID_REQUEST;
|
|
99
|
+
if (type === 'entity.parse.failed')
|
|
100
|
+
code = JSONRPC_PARSE_ERROR;
|
|
101
|
+
else if (status === 500)
|
|
102
|
+
code = JSONRPC_INTERNAL_ERROR;
|
|
103
|
+
res.status(status).json({ jsonrpc: '2.0', error: { code, message }, id: null });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
res.status(status).json({ error: message });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -35,6 +35,7 @@ import { buildToolListEntries } from '../lib/tool-list-entry.js';
|
|
|
35
35
|
// transports can reach it (stdio could not, which was the whole of #452). Re-exported here
|
|
36
36
|
// unchanged so existing importers and tests are unaffected.
|
|
37
37
|
import { classifyInitFailure } from '../lib/init-failure.js';
|
|
38
|
+
import { createHttpErrorHandler } from './httpErrorHandler.js';
|
|
38
39
|
export { classifyInitFailure };
|
|
39
40
|
export { requestContext };
|
|
40
41
|
// Resolve the authenticated principal for the per-principal budget preference
|
|
@@ -821,9 +822,17 @@ advertisedUrl) {
|
|
|
821
822
|
res.status(204).end();
|
|
822
823
|
return;
|
|
823
824
|
}
|
|
824
|
-
res.
|
|
825
|
+
// #543: the charset is spelled out because res.send appends it, and express 5.3.0
|
|
826
|
+
// appends it AFTER version=0.0.4 where 5.2.1 put it before. Stated in full, the header
|
|
827
|
+
// is passed through unchanged on both, so scrapers see the same value.
|
|
828
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8; version=0.0.4');
|
|
825
829
|
res.send(txt);
|
|
826
830
|
});
|
|
831
|
+
// #543: the FINAL layer, mounted globally (express.json above is global). Without it an
|
|
832
|
+
// error reaches Express's default handler, which from express 5.3.0 logs the whole error
|
|
833
|
+
// object, raw request body included, and under NODE_ENV=development renders the stack.
|
|
834
|
+
// Keep this the last app.use: anything registered after it is never reached on error.
|
|
835
|
+
app.use(createHttpErrorHandler({ mcpPath: httpPath }));
|
|
827
836
|
// config validation (#169) guarantees both paths are set when HTTPS is on, so
|
|
828
837
|
// the non-null assertions are safe. Wrap the reads so a missing/unreadable
|
|
829
838
|
// file fails with an actionable message naming the env vars and paths instead
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Adapted for this project's conventions:
|
|
17
17
|
* - No wrapToolCall: uses the direct call() pattern
|
|
18
|
-
* -
|
|
18
|
+
* - Shares ConditionSchema with rules_create.ts (src/lib/schemas/rules.ts); ActionSchema and the
|
|
19
|
+
* FIELD_OPERATORS map are private copies of the same shape (#540 found them identical)
|
|
19
20
|
*
|
|
20
21
|
* #376: the read-match-write cycle lives in `adapter.upsertRule` (identity rules in
|
|
21
22
|
* `src/lib/rule-matching.ts`). This tool owns the schema, the operator/UUID validation and
|
|
@@ -4,40 +4,23 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import actualToolsManager from '../actualToolsManager.js';
|
|
6
6
|
import { INSTALLED_API_VERSION, resolveInstalledVersion } from '../lib/installed-api-version.js';
|
|
7
|
+
import { getMcpServerVersion } from '../lib/mcp-version.js';
|
|
7
8
|
/** Resolved once at module load, not per call: the walk does blocking file reads
|
|
8
9
|
* and this tool can be called in a loop. The SDK exports no root path, so the
|
|
9
10
|
* resolvable subpath is passed explicitly (#445). */
|
|
10
11
|
const MCP_SDK_RESOLVED = resolveInstalledVersion('@modelcontextprotocol/sdk', '@modelcontextprotocol/sdk/server/index.js');
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = dirname(__filename);
|
|
13
|
-
//
|
|
14
|
+
// Name, description and declared dependencies come from package.json. The version does
|
|
15
|
+
// NOT: it comes from getMcpServerVersion() at call time (#541), so it matches the MCP
|
|
16
|
+
// initialize version and importing the tool registry never spawns git.
|
|
14
17
|
let packageInfo;
|
|
15
18
|
try {
|
|
16
19
|
const packagePath = join(__dirname, '../../../package.json');
|
|
17
|
-
|
|
18
|
-
packageInfo = JSON.parse(packageJson);
|
|
19
|
-
// Use VERSION environment variable if available (set by Docker build or CI/CD)
|
|
20
|
-
// This includes development metadata like: 0.2.4-dev-abc1234
|
|
21
|
-
if (process.env.VERSION && process.env.VERSION !== 'unknown') {
|
|
22
|
-
packageInfo.version = process.env.VERSION;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
// Fallback: Try to append git commit hash for local development builds
|
|
26
|
-
try {
|
|
27
|
-
const { execSync } = require('child_process');
|
|
28
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', cwd: join(__dirname, '../../..') }).trim();
|
|
29
|
-
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8', cwd: join(__dirname, '../../..') }).trim();
|
|
30
|
-
if (branch === 'develop' || branch === 'development' || branch !== 'main') {
|
|
31
|
-
packageInfo.version = `${packageInfo.version}-dev-${commitHash}`;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch (gitErr) {
|
|
35
|
-
// Git not available or not in a git repo, use package.json version as-is
|
|
36
|
-
}
|
|
37
|
-
}
|
|
20
|
+
packageInfo = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
38
21
|
}
|
|
39
22
|
catch (error) {
|
|
40
|
-
packageInfo = {
|
|
23
|
+
packageInfo = { name: 'actual-mcp-server', description: 'MCP server for Actual Budget' };
|
|
41
24
|
}
|
|
42
25
|
const InputSchema = z.object({}).strict();
|
|
43
26
|
const tool = {
|
|
@@ -61,7 +44,7 @@ Use this to check server status, verify version compatibility, or debug issues.`
|
|
|
61
44
|
return {
|
|
62
45
|
server: {
|
|
63
46
|
name: packageInfo.name,
|
|
64
|
-
version:
|
|
47
|
+
version: getMcpServerVersion(),
|
|
65
48
|
description: packageInfo.description,
|
|
66
49
|
transport: process.env.MCP_STDIO_MODE === 'true' ? 'stdio' : 'http',
|
|
67
50
|
},
|
|
@@ -37,7 +37,7 @@ const InputSchema = z.object({
|
|
|
37
37
|
});
|
|
38
38
|
const tool = {
|
|
39
39
|
name: 'actual_transactions_update_batch',
|
|
40
|
-
description: `Update multiple transactions in a single call. Accepts up to 50 {id, fields} pairs. Each update is applied independently and NOT atomically: items applied before a failure stay applied. A missing id or an update Actual rejects is reported per item in failed, and the rest continue. If the call itself fails (lost connection, out of memory, timeout), the error names the ids already applied; read the transactions back with actual_transactions_get BEFORE retrying. Splits are not supported here: a subtransactions field is ignored in batch. Use actual_transactions_update to edit an existing split's children.
|
|
40
|
+
description: `Update multiple transactions in a single call. Accepts up to 50 {id, fields} pairs. Each update is applied independently and NOT atomically: items applied before a failure stay applied. A missing id or an update Actual rejects is reported per item in failed, and the rest continue. If the call itself fails (lost connection, out of memory, timeout), the error names the ids already applied; read the transactions back with actual_transactions_get BEFORE retrying. An item whose error says "not attempted" was never sent, because the batch ran out of time; read back before retrying it too, since others may have been applied. Splits are not supported here: a subtransactions field is ignored in batch. Use actual_transactions_update to edit an existing split's children.
|
|
41
41
|
|
|
42
42
|
Returns: { succeeded: [{id}], failed: [{id, error}], total, successCount, failureCount }
|
|
43
43
|
|
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.22.
|
|
4
|
+
"version": "0.22.25",
|
|
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/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.test.js",
|
|
33
|
+
"test:unit-js": "node tests/unit/transactions_create.test.js && node tests/unit/transactions_aggregate.test.js && node tests/unit/account_flow_summary.test.js && node tests/unit/recurring_expenses_summary.test.js && node tests/unit/generated_tools.smoke.test.js &&node tests/unit/schema_validation.test.js && node tests/unit/schema_json_openai_compat.test.js && node tests/unit/config_https_validation.test.js && node tests/unit/config_data_dir.test.js && node tests/unit/config_drift.test.js && node tests/unit/port_alignment.test.js && node tests/unit/dockerfile_data_dir_alignment.test.js && node tests/unit/unraid_template_alignment.test.js && node tests/unit/entrypoint_invariants.test.js && node tests/unit/e2e_spec_collection.test.js && node tests/unit/compose_profile_sync.test.js && node tests/unit/advertised_tools_sync.test.js && node tests/unit/new_tool_completeness.test.js && node tests/unit/knip_config.test.js && node tests/unit/readme_stats_sync.test.js && node tests/unit/config_insecure_upstream.test.js && node tests/unit/workflow_release_guards.test.js && node tests/unit/report_train_failure.test.js && node tests/unit/unit_chain_membership.test.js && node tests/unit/api_surface_drift.test.js && node tests/unit/write_effect_audit_staleness.test.js && node tests/unit/write_effect_audit_membership.test.js && node tests/unit/tool_annotations.test.js && node tests/unit/tool_id_schema_drift.test.js && node tests/unit/filter_id_resolution.test.js && node tests/unit/filter_id_tool_wiring.test.js && node tests/unit/api_coverage_baseline.test.js && node tests/unit/train_preflight.test.js && node tests/unit/verify_published_artifacts.test.js && node tests/unit/report_train_stale.test.js && node tests/unit/stdio_framing_check.test.js && node tests/unit/server_info.test.js && node tests/unit/transfers_create.test.js && node tests/unit/bot_target_branch.test.js && node tests/unit/auth_posture.test.js && node tests/unit/auth_posture_wiring.test.js && node tests/unit/auth-acl.test.js && node tests/unit/bug76.test.js && node tests/unit/budgets_setAmount.test.js && node tests/unit/adapter_set_budget_batch.test.js && node tests/unit/budget_updates_batch.test.js && node tests/unit/rules_create.test.js && node tests/unit/adapter_rule_reference_guard.test.js && node tests/unit/rules_create_batch.test.js && node tests/unit/budgets_hold_for_next_month.test.js && node tests/unit/budgets_transfer.test.js && node tests/unit/transactions_uncategorized.test.js && node tests/unit/httpServer_session_init.test.js && node tests/unit/httpServer_session_not_found.test.js && node tests/unit/httpServer_init_failure_report.test.js && node tests/unit/stdio_init_failure_report.test.js && node tests/unit/manual_mcp_client_retry.test.js && node tests/unit/manual_mcp_client_session.test.js && node tests/unit/manual_mcp_client_circuit.test.js && node tests/unit/manual_runner_killswitch.test.js && node tests/unit/adapter_auth_rate_limit.test.js && node tests/unit/adapter_session_reuse.test.js && node tests/unit/retry_classifier.test.js && node tests/unit/adapter_module_surface.test.js && node tests/unit/adapter_nonidempotent_no_retry.test.js && node tests/unit/pool_liveness.test.js && node tests/unit/pool_shutdown_all.test.js && node tests/unit/query_where_operators.test.js && node tests/unit/query_run_validation.test.js && node tests/unit/adapter_with_write_session.test.js && node tests/unit/category_groups_delete.test.js && node tests/unit/rules_delete.test.js && node tests/unit/schedules_delete.test.js && node tests/unit/accounts_delete.test.js && node tests/unit/update_tools_not_found.test.js && node tests/unit/accounts_close.test.js && node tests/unit/accounts_reopen.test.js && node tests/unit/payees_delete.test.js && node tests/unit/payees_merge.test.js && node tests/unit/rules_create_or_update.test.js && node tests/unit/unhandled-rejection.test.js && node tests/unit/rejection-allowlist-purity.test.js && node tests/unit/httpServer_bearer_auth.test.js && node tests/unit/session_ownership.test.js && node tests/unit/httpServer_oidc_audience.test.js && node tests/unit/httpServer_oidc_auth_verification.test.js && node tests/unit/httpServer_oidc_jwks_discovery.test.js && node tests/unit/oidc_resource_identifier.test.js && node tests/unit/httpServer_oidc_resource_metadata.test.js && node tests/unit/oidc_error_cause.test.js && node tests/unit/httpServer_oidc_error_body.test.js && node tests/unit/oidc_token_source.test.js && node tests/unit/config_oidc_assertion_refines.test.js && node tests/unit/httpServer_oidc_assertion_mode.test.js && node tests/unit/httpServer_body_limit.test.js && node tests/unit/httpErrorHandler.test.js && node tests/unit/httpServer_error_handler_wiring.test.js && node tests/unit/httpServer_trust_proxy.test.js && node tests/unit/httpServer_batch_cap.test.js && node tests/unit/dotenv_stdout_quiet.test.js && node tests/unit/httpServer_bind_host.test.js && node tests/unit/adapter_write_pool_cooperation.test.js && node tests/unit/adapter_drain_listing_cache.test.js && node tests/unit/budget_acl_enforcement.test.js && node tests/unit/budget_selection_precondition.test.js && node tests/unit/budget_load_abandonment.test.js && node tests/unit/budget_loader_postcondition.test.js && node tests/unit/budget_loader_import_allowlist.test.js && node tests/unit/budget_import_tracking.test.js && node tests/unit/pool_shutdown_under_lock.test.js && node tests/unit/api_lock_affinity.test.js && node tests/unit/commit_message_closing_keyword.test.js && node tests/unit/write_queue_stranding.test.js && node tests/unit/write_queue_stranded_bound.test.js && node tests/unit/budget_preference_store.test.js && node tests/unit/budget_preference_restore.test.js && node tests/unit/tags_list.test.js && node tests/unit/tags_create.test.js && node tests/unit/tags_update.test.js && node tests/unit/tags_delete.test.js && node tests/unit/notes_get.test.js && node tests/unit/notes_update.test.js && node tests/unit/payees_common_list.test.js && node tests/unit/transactions_update_guard.test.js && node tests/unit/transactions_update_splits.test.js && node tests/unit/transactions_update_split_convert.test.js && node tests/unit/entities_search.test.js && node tests/unit/transactions_import_schema.test.js && node tests/unit/tool_schema_shape.test.js && node tests/unit/zod_error_format.test.js && node tests/unit/transactions_update_batch.test.js && node tests/unit/tool_count_sync.test.js && node tests/unit/stdio_transport.test.js && node tests/unit/stdio_budget_switch.test.js && node tests/unit/logger_structured.test.js && node tests/unit/logger_redaction.test.js && node tests/unit/logger_correlation.test.js && node tests/unit/check_coverage.test.js && node tests/unit/adapter_op_timeout.test.js && node tests/unit/adapter_write_queue_wakeup.test.js && node tests/unit/ci_workflow_guards.test.js && node tests/unit/node_version_guard.test.js && node tests/unit/node_version_drift.test.js && node tests/unit/playwright_version_drift.test.js && node tests/unit/observability_smoke.test.js && node tests/unit/residue_pattern.test.js && node tests/unit/batch_uncategorized_cleanup.test.js && node tests/unit/dual_transport_gate.test.js && node tests/unit/manual_assertions_use_helper.test.js && node tests/unit/server_version_guard.test.js && node tests/unit/server_version_probe_outside_op_timeout.test.js && node tests/unit/payee_rules_reference_filter.test.js && node tests/unit/oidc_metadata_discovery.test.js && node tests/unit/httpServer_oauth_metadata_route.test.js && node tests/unit/httpServer_mcp_info_route.test.js && node tests/unit/residue_sweep_balance_account.test.js && node tests/unit/tool_factory.test.js && node tests/unit/budget_registry_parse.test.js && node tests/unit/preferences_get.test.js && node tests/unit/budgets_export.test.js && node tests/unit/budgets_import.test.js && node tests/unit/budgets_import_syncid.test.js && node tests/unit/budget_acl_dynamic.test.js && node tests/unit/budget_acl_identity_map.test.js && node tests/unit/budget_acl_userinfo.test.js && node tests/unit/acl_e2e_fixture_contract.test.js && node tests/unit/budget_acl_middleware.test.js && node tests/unit/rules_stage.test.js && node tests/unit/oidc_scope_list.test.js && node tests/unit/get_context.test.js && node tests/unit/contributor_docs.test.js && node tests/unit/toolsets.test.js && node tests/unit/toolsets_entrypoint.test.js && node tests/unit/tools_list_size_budget.test.js && node tests/unit/rules_update_schedules_schema.test.js && node tests/unit/source_text_helper.test.js && node tests/unit/transport_onerror.test.js && node tests/unit/adapter_transaction_batch_abort.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",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
73
73
|
"debug": "^4.4.3",
|
|
74
74
|
"dotenv": "^18.0.6",
|
|
75
|
-
"express": "^5.
|
|
75
|
+
"express": "^5.3.0",
|
|
76
76
|
"fuse.js": "^7.4.2",
|
|
77
77
|
"jose": "^6.2.12",
|
|
78
78
|
"mcp-auth": "^0.2.0",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@playwright/test": "^1.63.0",
|
|
88
88
|
"@types/express": "^5.0.3",
|
|
89
89
|
"@types/node": "^26.6.4",
|
|
90
|
-
"knip": "^6.
|
|
90
|
+
"knip": "^6.41.0",
|
|
91
91
|
"node-fetch": "^3.3.2",
|
|
92
92
|
"tsconfig-paths": "^4.2.0",
|
|
93
93
|
"typescript": "^7.0.2"
|