actual-mcp-server 0.16.11 → 0.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -775,7 +775,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
775
775
|
|
|
776
776
|
---
|
|
777
777
|
|
|
778
|
-
**Version:** 0.16.
|
|
778
|
+
**Version:** 0.16.13 | **Tool Count:** 74 (verified LibreChat-compatible)
|
|
779
779
|
|
|
780
780
|
## Sponsor
|
|
781
781
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// calls and string parsing, with no module state or side effects. parseWhereClause
|
|
4
4
|
// is re-exported from actual-adapter.ts and is unit-tested directly. The #178
|
|
5
5
|
// operator support (LIKE / NOT LIKE / IS NULL, throw-on-unsupported) lives here.
|
|
6
|
+
import { getFieldType } from '../actual-schema.js';
|
|
6
7
|
// Strip a single pair of surrounding quotes from a SQL value literal.
|
|
7
8
|
function _stripWhereQuotes(s) {
|
|
8
9
|
return s.trim().replace(/^['"]|['"]$/g, '');
|
|
@@ -17,7 +18,38 @@ function _coerceWhereValue(s) {
|
|
|
17
18
|
const n = Number(v);
|
|
18
19
|
return isNaN(n) ? v : n;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
// #420: coerce a SQL literal to a real JavaScript boolean, for a column the schema types as
|
|
22
|
+
// boolean. ActualQL requires a genuine JS boolean here: its compiler only tags a value as
|
|
23
|
+
// `boolean` from `typeof value === 'boolean'`, and `castInput` has no string-to-boolean or
|
|
24
|
+
// integer-to-boolean branch, so `"true"` and `1` are both rejected with the exact errors #420
|
|
25
|
+
// reports.
|
|
26
|
+
//
|
|
27
|
+
// Accepts `true`/`false` case-insensitively AND `1`/`0`. The integer form is accepted on purpose:
|
|
28
|
+
// the tool advertises SQL, and every mainstream engine (SQLite, which is Actual's own store, plus
|
|
29
|
+
// MySQL/MariaDB/SQL Server/Postgres) treats `1`/`0` as booleans. Refusing it would make this tool
|
|
30
|
+
// stricter than the database it models. A value that is neither throws, naming the column and the
|
|
31
|
+
// value, because a bad boolean literal is a caller mistake we can describe precisely.
|
|
32
|
+
function _coerceBoolean(field, s) {
|
|
33
|
+
const v = _stripWhereQuotes(s).toLowerCase();
|
|
34
|
+
if (v === 'true' || v === '1')
|
|
35
|
+
return true;
|
|
36
|
+
if (v === 'false' || v === '0')
|
|
37
|
+
return false;
|
|
38
|
+
throw new Error(`Invalid boolean value for column "${field}": ${JSON.stringify(_stripWhereQuotes(s))}. ` +
|
|
39
|
+
`Use true or false (1 or 0 are also accepted).`);
|
|
40
|
+
}
|
|
41
|
+
// Whether the schema types `field` (a plain or joined column) as boolean, given the base table.
|
|
42
|
+
// An unknown field is NOT boolean, so its value is left untouched: this is what keeps a string
|
|
43
|
+
// comparison like `notes = 'true'` a string, and the query validator handles the unknown-field
|
|
44
|
+
// case separately.
|
|
45
|
+
function _isBooleanField(tableName, field) {
|
|
46
|
+
return tableName !== undefined && getFieldType(tableName, field) === 'boolean';
|
|
47
|
+
}
|
|
48
|
+
// `tableName` is the FROM table, used to look up column types so boolean literals can be coerced
|
|
49
|
+
// (#420). It is optional only so the many existing unit tests that assert non-boolean behaviour
|
|
50
|
+
// need not all pass it; the production call site in actual-adapter.ts always does. When it is
|
|
51
|
+
// absent, no field resolves as boolean and the pre-#420 behaviour is exactly preserved.
|
|
52
|
+
export function parseWhereClause(query, whereClause, tableName) {
|
|
21
53
|
// OR is not supported. Detect it up front and fail loudly. Without this guard
|
|
22
54
|
// a clause like `amount = 100 OR amount < 0` is left as a single fragment by
|
|
23
55
|
// the AND-splitter, and the comparison regex's greedy value capture swallows
|
|
@@ -68,8 +100,20 @@ export function parseWhereClause(query, whereClause) {
|
|
|
68
100
|
const inMatch = trimmedCondition.match(/^([\w.]+)\s+IN\s+\((.+)\)$/i);
|
|
69
101
|
if (inMatch) {
|
|
70
102
|
const [, field, valuesStr] = inMatch;
|
|
71
|
-
const
|
|
72
|
-
|
|
103
|
+
const rawValues = valuesStr.split(',');
|
|
104
|
+
if (_isBooleanField(tableName, field)) {
|
|
105
|
+
// #420: `$oneof` STRINGIFIES every element (upstream `compiler.ts` emits
|
|
106
|
+
// `'${String(id)}'`), so `is_parent IN (true, false)` would compile to
|
|
107
|
+
// `IN ('true','false')` against a 0/1 column and silently match nothing, which is worse
|
|
108
|
+
// than the loud error we produce today. Expand to `$or` of equalities instead: each inner
|
|
109
|
+
// equality then routes through `castInput(..., 'boolean')` and works. `$or` is a real
|
|
110
|
+
// ActualQL condition key (compiler.ts handles `field === '$or'` via `compileOr`).
|
|
111
|
+
const orConditions = rawValues.map((v) => ({ [field]: _coerceBoolean(field, v) }));
|
|
112
|
+
query = query.filter({ $or: orConditions });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
query = query.filter({ [field]: { $oneof: rawValues.map(_coerceWhereValue) } });
|
|
116
|
+
}
|
|
73
117
|
continue;
|
|
74
118
|
}
|
|
75
119
|
// Comparison operators: field >= value, field = value, etc.
|
|
@@ -86,7 +130,12 @@ export function parseWhereClause(query, whereClause) {
|
|
|
86
130
|
'!=': '$ne',
|
|
87
131
|
};
|
|
88
132
|
const actualOp = operatorMap[operator];
|
|
89
|
-
|
|
133
|
+
// #420: on a boolean column the literal must reach ActualQL as a real JS boolean, not the
|
|
134
|
+
// string "true" or the number 1. Only = and != are meaningful for a boolean; the ordering
|
|
135
|
+
// operators are left to their normal path, where ActualQL will reject them for a boolean as
|
|
136
|
+
// it does today, rather than us inventing an ordering the column does not have.
|
|
137
|
+
const isBool = _isBooleanField(tableName, field) && (actualOp === '$eq' || actualOp === '$ne');
|
|
138
|
+
const finalValue = isBool ? _coerceBoolean(field, valueStr) : _coerceWhereValue(valueStr);
|
|
90
139
|
if (actualOp === '$eq') {
|
|
91
140
|
// Simple equality can use the direct field: value shorthand.
|
|
92
141
|
query = query.filter({ [field]: finalValue });
|
|
@@ -248,6 +248,35 @@ function _hasPooledConnection(sessionId) {
|
|
|
248
248
|
return false; // singleton was shut down by some other path
|
|
249
249
|
return connectionPool.hasConnection(sessionId);
|
|
250
250
|
}
|
|
251
|
+
// #419: is this whole process a stdio server? The --stdio flag sets MCP_STDIO_MODE
|
|
252
|
+
// in src/index.ts BEFORE any import, it is the RAW_ENV_ALLOWLIST-registered process
|
|
253
|
+
// marker (config-registry.ts), and server_info.ts reads exactly this to report the
|
|
254
|
+
// transport. A stdio process is one long-lived single-user session for its whole
|
|
255
|
+
// lifetime, so keeping the api singleton alive between ops is a PROCESS fact. It is
|
|
256
|
+
// deliberately NOT read from the ambient request context: the write drain calls
|
|
257
|
+
// shutdownActualApi OUTSIDE the per-op requestContext.run (see processWriteQueue),
|
|
258
|
+
// in a context that belongs to an unrelated enqueuer, which is the #390 trap.
|
|
259
|
+
function _isStdioProcess() {
|
|
260
|
+
return process.env.MCP_STDIO_MODE === 'true';
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* #419: decide whether `shutdownActualApi` keeps the api singleton alive (sync-only)
|
|
264
|
+
* or tears it fully down (which resets `isApiInitialized`). Kept alive when another
|
|
265
|
+
* HTTP session still owns the singleton, OR when this is a stdio process and no
|
|
266
|
+
* infrastructure-level error forced a teardown.
|
|
267
|
+
*
|
|
268
|
+
* `forceFullShutdown` defeats ONLY the stdio process keep-alive, never the
|
|
269
|
+
* active-HTTP-session keep-alive: with active sessions other sessions own the
|
|
270
|
+
* singleton, so tearing it down would break them, and a stdio process always has
|
|
271
|
+
* `activeSessions === 0`, so forcing there tears down exactly its own singleton.
|
|
272
|
+
*
|
|
273
|
+
* Exported for unit tests: the branch is observable behaviour, not an internal.
|
|
274
|
+
*/
|
|
275
|
+
export function _shouldKeepSingletonAlive(activeSessions, forceFullShutdown) {
|
|
276
|
+
if (activeSessions > 0)
|
|
277
|
+
return true;
|
|
278
|
+
return _isStdioProcess() && !forceFullShutdown;
|
|
279
|
+
}
|
|
251
280
|
/**
|
|
252
281
|
* Decide whether an error from the wrapped operation suggests the api
|
|
253
282
|
* singleton is in a corrupted state and the pool's session connection should
|
|
@@ -397,17 +426,32 @@ export async function withActualApi(rawOperation) {
|
|
|
397
426
|
}
|
|
398
427
|
}, { budget: _affinityBudget() });
|
|
399
428
|
}
|
|
400
|
-
|
|
429
|
+
// #419: only warn when a real re-init will actually happen. In a stdio process
|
|
430
|
+
// the singleton is kept alive between ops, so after the first login this is warm
|
|
431
|
+
// reuse, not a costly miss; gating on !isApiInitialized() makes the line appear
|
|
432
|
+
// at most once per process rather than per call, and keeps it honest for HTTP.
|
|
433
|
+
if (sessionId && !isApiInitialized()) {
|
|
401
434
|
logger.warn(`[ADAPTER] Pool miss for session ${sessionId}; falling back to per-op init`);
|
|
402
435
|
}
|
|
403
436
|
// Legacy mode: init+shutdown around every operation.
|
|
404
437
|
return withApiLock(async () => {
|
|
438
|
+
let forceFullShutdown = false;
|
|
405
439
|
try {
|
|
406
440
|
await initActualApiForOperation();
|
|
407
441
|
return await withOpTimeout(operation);
|
|
408
442
|
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
// #419: the stdio keep-alive branch leaves the singleton live; on an
|
|
445
|
+
// infrastructure-level error it may be corrupt, so force a full teardown
|
|
446
|
+
// (which resets isApiInitialized) so the next op re-inits fresh. Mirrors
|
|
447
|
+
// the pool branch's _shouldDropPoolOnError drop above. Domain/validation
|
|
448
|
+
// errors leave the singleton intact (no per-op login reintroduced).
|
|
449
|
+
if (_shouldDropPoolOnError(err))
|
|
450
|
+
forceFullShutdown = true;
|
|
451
|
+
throw err;
|
|
452
|
+
}
|
|
409
453
|
finally {
|
|
410
|
-
await shutdownActualApi();
|
|
454
|
+
await shutdownActualApi({ forceFullShutdown });
|
|
411
455
|
}
|
|
412
456
|
}, { budget: _affinityBudget() });
|
|
413
457
|
}
|
|
@@ -473,16 +517,25 @@ export async function withActualApiWrite(operation) {
|
|
|
473
517
|
}
|
|
474
518
|
}, { budget: _affinityBudget() });
|
|
475
519
|
}
|
|
476
|
-
|
|
520
|
+
// #419: see the read-path note above; warn only when a real re-init happens.
|
|
521
|
+
if (sessionId && !isApiInitialized()) {
|
|
477
522
|
logger.warn(`[ADAPTER] Pool miss for session ${sessionId}; falling back to per-op init (write)`);
|
|
478
523
|
}
|
|
479
524
|
return withApiLock(async () => {
|
|
525
|
+
let forceFullShutdown = false;
|
|
480
526
|
try {
|
|
481
527
|
await initActualApiForOperation();
|
|
482
528
|
return await withOpTimeout(operation);
|
|
483
529
|
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
// #419: force a full teardown on an infrastructure-level error so the next
|
|
532
|
+
// stdio op re-inits fresh (see the read-path catch for the rationale).
|
|
533
|
+
if (_shouldDropPoolOnError(err))
|
|
534
|
+
forceFullShutdown = true;
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
484
537
|
finally {
|
|
485
|
-
await shutdownActualApi();
|
|
538
|
+
await shutdownActualApi({ forceFullShutdown });
|
|
486
539
|
}
|
|
487
540
|
}, { budget: _affinityBudget() });
|
|
488
541
|
}
|
|
@@ -604,39 +657,56 @@ async function initActualApiForOperation() {
|
|
|
604
657
|
throw err;
|
|
605
658
|
}
|
|
606
659
|
}
|
|
607
|
-
async function shutdownActualApi() {
|
|
660
|
+
async function shutdownActualApi(opts) {
|
|
661
|
+
const forceFullShutdown = opts?.forceFullShutdown === true;
|
|
608
662
|
if (_skipApiInitForTests) {
|
|
609
|
-
|
|
663
|
+
// Honour the SAME keep-alive decision the real path makes, so unit tests can
|
|
664
|
+
// observe the stdio keep-alive and self-heal behaviour without driving a live
|
|
665
|
+
// api.init(). In every non-stdio context _shouldKeepSingletonAlive is false
|
|
666
|
+
// (activeSessions === 0, not stdio), so this stays the pre-#419 always-reset
|
|
667
|
+
// and existing tests are unaffected.
|
|
668
|
+
let activeSessions = 0;
|
|
669
|
+
try {
|
|
670
|
+
activeSessions = connectionPool.getStats().activeSessions;
|
|
671
|
+
}
|
|
672
|
+
catch { /* pool not up */ }
|
|
673
|
+
if (!_shouldKeepSingletonAlive(activeSessions, forceFullShutdown)) {
|
|
674
|
+
setApiInitialized(false);
|
|
675
|
+
}
|
|
610
676
|
return;
|
|
611
677
|
}
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
// tool call
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
678
|
+
// Keep the api singleton alive (sync only) when another session still owns it
|
|
679
|
+
// OR when this is a stdio process (#419): tearing it down would invalidate an
|
|
680
|
+
// active session's pool entry, or, for stdio, force a fresh upstream LOGIN on
|
|
681
|
+
// the next tool call (the #127 burst, per stdio call). Instead sync (the
|
|
682
|
+
// persistence guarantee shutdown provided implicitly) and leave it alive.
|
|
683
|
+
//
|
|
684
|
+
// #419 self-heal: forceFullShutdown, set by a legacy branch on an
|
|
685
|
+
// infrastructure-level error, defeats ONLY the stdio keep-alive so the next op
|
|
686
|
+
// re-inits fresh against a clean singleton, exactly as the always-full-shutdown
|
|
687
|
+
// legacy path used to give for free.
|
|
688
|
+
let activeSessions = 0;
|
|
619
689
|
try {
|
|
620
|
-
|
|
621
|
-
if (stats.activeSessions > 0) {
|
|
622
|
-
try {
|
|
623
|
-
const apiAny = api;
|
|
624
|
-
if (typeof apiAny.sync === 'function') {
|
|
625
|
-
await withOpTimeout(() => apiAny.sync(), 'sync');
|
|
626
|
-
logger.debug('[ADAPTER] api.sync() instead of shutdown (pool has active sessions)');
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
catch (syncErr) {
|
|
630
|
-
logger.error('[ADAPTER] sync-without-shutdown failed:', syncErr);
|
|
631
|
-
// Don't propagate — shutdown was best-effort anyway.
|
|
632
|
-
}
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
690
|
+
activeSessions = connectionPool.getStats().activeSessions;
|
|
635
691
|
}
|
|
636
692
|
catch (statsErr) {
|
|
637
|
-
// Pool not available (e.g. early startup)
|
|
693
|
+
// Pool not available (e.g. early startup): fall through to legacy shutdown.
|
|
638
694
|
logger.debug('[ADAPTER] could not read pool stats; defaulting to full shutdown:', statsErr);
|
|
639
695
|
}
|
|
696
|
+
if (_shouldKeepSingletonAlive(activeSessions, forceFullShutdown)) {
|
|
697
|
+
try {
|
|
698
|
+
const apiAny = api;
|
|
699
|
+
if (typeof apiAny.sync === 'function') {
|
|
700
|
+
await withOpTimeout(() => apiAny.sync(), 'sync');
|
|
701
|
+
logger.debug('[ADAPTER] api.sync() instead of shutdown (singleton kept alive)');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch (syncErr) {
|
|
705
|
+
logger.error('[ADAPTER] sync-without-shutdown failed:', syncErr);
|
|
706
|
+
// Don't propagate: shutdown was best-effort anyway.
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
640
710
|
try {
|
|
641
711
|
const maybeApi = api;
|
|
642
712
|
if (typeof maybeApi.shutdown === 'function') {
|
|
@@ -965,6 +1035,11 @@ async function processWriteQueue() {
|
|
|
965
1035
|
// Each op keeps its own withOpTimeout (#270) and still settles individually, so one
|
|
966
1036
|
// stalled or rejected operation cannot hold the api mutex or fail its siblings.
|
|
967
1037
|
let stuckLoadError = null;
|
|
1038
|
+
// #419: the drain inits once and shuts down once per batch. If any op hits
|
|
1039
|
+
// an infrastructure-level error, force the batch-end shutdownActualApi to
|
|
1040
|
+
// fully tear the singleton down (self-heal) so the next stdio batch re-inits
|
|
1041
|
+
// fresh, instead of the stdio keep-alive leaving a corrupt singleton live.
|
|
1042
|
+
let drainForceFullShutdown = false;
|
|
968
1043
|
// Counted so the event is logged ONCE with a total. Without this the fail-fast path sits
|
|
969
1044
|
// outside the per-operation try, so it never reaches the '[WRITE QUEUE] Operation failed'
|
|
970
1045
|
// line and N operations fail with no log entry anywhere. Given this whole ticket family
|
|
@@ -1064,6 +1139,10 @@ async function processWriteQueue() {
|
|
|
1064
1139
|
}
|
|
1065
1140
|
catch (error) {
|
|
1066
1141
|
logger.error('[WRITE QUEUE] Operation failed:', error);
|
|
1142
|
+
// #419: remember an infrastructure-level failure so the batch-end
|
|
1143
|
+
// shutdown self-heals (see drainForceFullShutdown above).
|
|
1144
|
+
if (_shouldDropPoolOnError(error))
|
|
1145
|
+
drainForceFullShutdown = true;
|
|
1067
1146
|
reject(error);
|
|
1068
1147
|
}
|
|
1069
1148
|
finally {
|
|
@@ -1122,6 +1201,11 @@ async function processWriteQueue() {
|
|
|
1122
1201
|
}
|
|
1123
1202
|
catch (syncError) {
|
|
1124
1203
|
logger.error('[WRITE QUEUE] Sync failed:', syncError);
|
|
1204
|
+
// #419: a legacy-branch infrastructure-level sync failure self-heals
|
|
1205
|
+
// too, so the next stdio batch re-inits fresh rather than reusing a
|
|
1206
|
+
// singleton whose sync just failed.
|
|
1207
|
+
if (!usePoolBranch && _shouldDropPoolOnError(syncError))
|
|
1208
|
+
drainForceFullShutdown = true;
|
|
1125
1209
|
// Pool branch: drop the connection on infrastructure-level sync
|
|
1126
1210
|
// failure so the next write re-initialises cleanly. Mirrors
|
|
1127
1211
|
// withActualApiWrite's policy.
|
|
@@ -1143,7 +1227,9 @@ async function processWriteQueue() {
|
|
|
1143
1227
|
// Legacy branch only: actually shut the singleton down.
|
|
1144
1228
|
// shutdownActualApi() itself short-circuits to sync-only if another
|
|
1145
1229
|
// path has active pool sessions, so this is safe under contention.
|
|
1146
|
-
|
|
1230
|
+
// #419: forceFullShutdown defeats the stdio keep-alive when the batch
|
|
1231
|
+
// saw an infrastructure-level error, so the next batch re-inits fresh.
|
|
1232
|
+
await shutdownActualApi({ forceFullShutdown: drainForceFullShutdown });
|
|
1147
1233
|
}
|
|
1148
1234
|
logger.debug(`[WRITE QUEUE] Batch completed successfully`);
|
|
1149
1235
|
}
|
|
@@ -1173,7 +1259,9 @@ async function processWriteQueue() {
|
|
|
1173
1259
|
}
|
|
1174
1260
|
}
|
|
1175
1261
|
else {
|
|
1176
|
-
|
|
1262
|
+
// #419: a fatal batch error is infrastructure-level by definition, so
|
|
1263
|
+
// force a full teardown (stdio self-heal) regardless of the per-op flag.
|
|
1264
|
+
await shutdownActualApi({ forceFullShutdown: true });
|
|
1177
1265
|
}
|
|
1178
1266
|
}
|
|
1179
1267
|
}, { budget: batchBudget });
|
|
@@ -2659,9 +2747,10 @@ export async function runQuery(queryString) {
|
|
|
2659
2747
|
});
|
|
2660
2748
|
query = query.select(fieldList);
|
|
2661
2749
|
}
|
|
2662
|
-
// Apply WHERE conditions
|
|
2750
|
+
// Apply WHERE conditions. tableName is passed so parseWhereClause can resolve column
|
|
2751
|
+
// types and coerce boolean literals to real booleans (#420).
|
|
2663
2752
|
if (whereClause) {
|
|
2664
|
-
query = parseWhereClause(query, whereClause);
|
|
2753
|
+
query = parseWhereClause(query, whereClause, tableName);
|
|
2665
2754
|
}
|
|
2666
2755
|
// Apply ORDER BY
|
|
2667
2756
|
if (orderField) {
|
|
@@ -211,3 +211,25 @@ export function isValidField(tableName, fieldName) {
|
|
|
211
211
|
export function isValidJoinPath(path) {
|
|
212
212
|
return path in JOIN_PATHS;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Resolve a WHERE field to its declared type, for the given base table.
|
|
216
|
+
*
|
|
217
|
+
* Handles both a plain column (`is_parent`, resolved against `tableName`) and a joined field
|
|
218
|
+
* (`category.hidden`, resolved through JOIN_PATHS to the target table's column). Returns the
|
|
219
|
+
* `type` string from the schema, or `undefined` when the field is not known.
|
|
220
|
+
*
|
|
221
|
+
* #420: used to decide whether a WHERE literal must be coerced to a real boolean. It is
|
|
222
|
+
* deliberately conservative: an unknown field returns `undefined` and the caller then leaves the
|
|
223
|
+
* value untouched, so this can never turn a valid string comparison into a boolean by accident.
|
|
224
|
+
* The query validator (`query-validator.ts`) is what rejects an unknown field; this only reports a
|
|
225
|
+
* type when it is certain of one.
|
|
226
|
+
*/
|
|
227
|
+
export function getFieldType(tableName, field) {
|
|
228
|
+
if (field.includes('.')) {
|
|
229
|
+
const join = JOIN_PATHS[field];
|
|
230
|
+
if (!join)
|
|
231
|
+
return undefined;
|
|
232
|
+
return ACTUAL_SCHEMA[join.table]?.[join.field]?.type;
|
|
233
|
+
}
|
|
234
|
+
return ACTUAL_SCHEMA[tableName]?.[field]?.type;
|
|
235
|
+
}
|
|
@@ -63,16 +63,19 @@ export async function startStdioServer(mcp, capabilities, implementedTools, serv
|
|
|
63
63
|
// - no `principal`, or getActiveBudgetConfig() would attempt the #189
|
|
64
64
|
// preferred-budget restore for a caller that has no authenticated
|
|
65
65
|
// identity, and the env default would stop being authoritative.
|
|
66
|
-
// - `transport: 'stdio'` is what keeps stdio OFF the pooled path
|
|
67
|
-
// switchBudget's slow path calls
|
|
68
|
-
// WITHOUT this marker the first switch
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
66
|
+
// - `transport: 'stdio'` is what keeps stdio OFF the pooled path, and it
|
|
67
|
+
// stays that way after #419. switchBudget's slow path calls
|
|
68
|
+
// connectionPool.getConnection(), so WITHOUT this marker the first switch
|
|
69
|
+
// would silently move stdio onto the pooled branch, and the entry would
|
|
70
|
+
// never be touched (touch() is called only from httpServer.ts), so it
|
|
71
|
+
// would expire after the idle timeout and be torn down by the cleanup
|
|
72
|
+
// sweep without the api lock, possibly mid-operation. That is a far larger
|
|
73
|
+
// change than we want by accident.
|
|
74
|
+
// #419 solved the per-call upstream login WITHOUT pooling stdio: a stdio
|
|
75
|
+
// process keeps the api singleton alive between ops in shutdownActualApi
|
|
76
|
+
// (see _shouldKeepSingletonAlive), so the legacy branch's init no-ops
|
|
77
|
+
// after the first login. No pool entry is created, so there is nothing for
|
|
78
|
+
// the sweep to evict and this comment's hazard never arises.
|
|
76
79
|
// - `allowedBudgets` is absent, so under AUTH_PROVIDER=oidc switchBudget
|
|
77
80
|
// still denies. Fail closed: a local pipe must not gain budget access an
|
|
78
81
|
// HTTP caller would need an ACL for.
|
|
@@ -24,6 +24,11 @@ Supported WHERE operators:
|
|
|
24
24
|
• LIKE / NOT LIKE for pattern search (case-insensitive, accent-insensitive; use % as wildcard)
|
|
25
25
|
e.g. "WHERE imported_payee LIKE '%amazon%'" to find raw bank-sync payee strings
|
|
26
26
|
• IS NULL / IS NOT NULL e.g. "WHERE imported_payee IS NULL" to find unmerged rows
|
|
27
|
+
• Boolean columns take true / false (case-insensitive; 1 / 0 also accepted)
|
|
28
|
+
e.g. "WHERE cleared = false" for uncleared, "WHERE reconciled = true",
|
|
29
|
+
"WHERE category.hidden = true", "WHERE account.closed = true"
|
|
30
|
+
Note: transactions are returned in split-INLINE mode, so "WHERE is_parent = true" returns
|
|
31
|
+
nothing (parent rows are hidden); use "is_parent = false" to exclude split children.
|
|
27
32
|
• Combine conditions with AND. OR, REGEXP, NOT IN, and parenthesised groups are not yet
|
|
28
33
|
supported and will return an error (the query is never silently run unfiltered).
|
|
29
34
|
|