db-mcp 4.0.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -25
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/database-adapter.d.ts +9 -0
- package/dist/adapters/database-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite/query-executor.d.ts.map +1 -1
- package/dist/adapters/sqlite/read-write-lock.d.ts +49 -0
- package/dist/adapters/sqlite/read-write-lock.d.ts.map +1 -0
- package/dist/adapters/sqlite/schemas/admin.d.ts +5 -5
- package/dist/adapters/sqlite/schemas/admin.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/core.d.ts +94 -0
- package/dist/adapters/sqlite/schemas/core.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/introspection.d.ts +8 -7
- package/dist/adapters/sqlite/schemas/introspection.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/migration.d.ts +2 -2
- package/dist/adapters/sqlite/schemas/migration.d.ts.map +1 -1
- package/dist/adapters/sqlite/sqlite-adapter.d.ts +6 -0
- package/dist/adapters/sqlite/sqlite-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/backup/create.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/backup/dump.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/backup/restore.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/pragma.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/verify.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/codemode.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/core/convenience.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/core/index.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/core/queries.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/core/versioning.d.ts +19 -0
- package/dist/adapters/sqlite/tools/core/versioning.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/introspection/graph/tools.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-operations/crud.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/migration/helpers.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/vector/metadata.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/vector/search.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/vector/storage.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/virtual/vtable/analyze-csv.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/virtual/vtable/csv.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/virtual/vtable/info.d.ts.map +1 -1
- package/dist/adapters/sqlite/types.d.ts +1 -1
- package/dist/adapters/sqlite/types.d.ts.map +1 -1
- package/dist/adapters/sqlite-native/index.d.ts +1 -1
- package/dist/adapters/sqlite-native/index.d.ts.map +1 -1
- package/dist/adapters/sqlite-native/tools/window/helpers.d.ts +52 -0
- package/dist/adapters/sqlite-native/tools/window/helpers.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/index.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/index.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/lag-lead.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/lag-lead.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/moving-average.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/moving-average.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/ntile.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/ntile.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/rank.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/rank.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/row-number.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/row-number.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/running-total.d.ts +7 -0
- package/dist/adapters/sqlite-native/tools/window/running-total.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window/schemas.d.ts +159 -0
- package/dist/adapters/sqlite-native/tools/window/schemas.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/transaction-methods.d.ts.map +1 -1
- package/dist/audit/logger.d.ts.map +1 -1
- package/dist/auth/authorization-server-discovery.d.ts.map +1 -1
- package/dist/auth/scopes/mapping.d.ts.map +1 -1
- package/dist/auth/token-validator.d.ts.map +1 -1
- package/dist/{chunk-EQF3WZHX.js → chunk-27C5EE6U.js} +15 -2
- package/dist/{chunk-R5M4WKDM.js → chunk-2TYICG7B.js} +381 -185
- package/dist/{chunk-TGE7LFAR.js → chunk-UE7GVOBV.js} +1069 -355
- package/dist/{chunk-NIC5Z2CA.js → chunk-WYBUO4FL.js} +394 -381
- package/dist/{chunk-Y57WFKAP.js → chunk-XLIYO3YA.js} +178 -72
- package/dist/cli.js +18 -6
- package/dist/codemode/api-constants.d.ts.map +1 -1
- package/dist/codemode/api.d.ts.map +1 -1
- package/dist/codemode/sandbox-factory.d.ts.map +1 -1
- package/dist/codemode/sandbox.d.ts +0 -3
- package/dist/codemode/sandbox.d.ts.map +1 -1
- package/dist/codemode/security.d.ts.map +1 -1
- package/dist/codemode/types.d.ts +2 -0
- package/dist/codemode/types.d.ts.map +1 -1
- package/dist/constants/server-instructions.d.ts.map +1 -1
- package/dist/filtering/tool-constants.d.ts +5 -5
- package/dist/filtering/tool-constants.d.ts.map +1 -1
- package/dist/{http-2WGR3E5K.js → http-2XDGZF4F.js} +177 -32
- package/dist/index.js +4 -4
- package/dist/observability/system-db.d.ts.map +1 -1
- package/dist/server/mcp-server.d.ts.map +1 -1
- package/dist/server/registration/admin-tools.d.ts.map +1 -1
- package/dist/server/registration/audit-tools/backup.d.ts +8 -0
- package/dist/server/registration/audit-tools/backup.d.ts.map +1 -0
- package/dist/server/registration/audit-tools/helpers.d.ts +11 -0
- package/dist/server/registration/audit-tools/helpers.d.ts.map +1 -0
- package/dist/server/registration/audit-tools/index.d.ts +4 -0
- package/dist/server/registration/audit-tools/index.d.ts.map +1 -0
- package/dist/server/registration/audit-tools/resource.d.ts +8 -0
- package/dist/server/registration/audit-tools/resource.d.ts.map +1 -0
- package/dist/server/registration/audit-tools/search.d.ts +7 -0
- package/dist/server/registration/audit-tools/search.d.ts.map +1 -0
- package/dist/server/registration/index.d.ts +1 -1
- package/dist/server/registration/index.d.ts.map +1 -1
- package/dist/{sqlite-D4WCNPU4.js → sqlite-XWZPEZJO.js} +192 -19
- package/dist/{sqlite-native-DLVS2T52.js → sqlite-native-5GQ3WKCZ.js} +156 -117
- package/dist/transports/http/middleware.d.ts.map +1 -1
- package/dist/transports/http/oauth.d.ts.map +1 -1
- package/dist/transports/http/sessions/legacy-sse.d.ts.map +1 -1
- package/dist/transports/http/sessions/stateful.d.ts +5 -1
- package/dist/transports/http/sessions/stateful.d.ts.map +1 -1
- package/dist/transports/http/transport.d.ts +2 -0
- package/dist/transports/http/transport.d.ts.map +1 -1
- package/dist/transports/http/types.d.ts +9 -0
- package/dist/transports/http/types.d.ts.map +1 -1
- package/dist/types/auth.d.ts +4 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/server.d.ts +3 -0
- package/dist/types/server.d.ts.map +1 -1
- package/dist/utils/annotations.d.ts +8 -0
- package/dist/utils/annotations.d.ts.map +1 -1
- package/dist/utils/errors/categories.d.ts +16 -11
- package/dist/utils/errors/categories.d.ts.map +1 -1
- package/dist/utils/errors/classes.d.ts +37 -0
- package/dist/utils/errors/classes.d.ts.map +1 -1
- package/dist/utils/errors/format.d.ts +2 -2
- package/dist/utils/errors/format.d.ts.map +1 -1
- package/dist/utils/errors/index.d.ts +1 -1
- package/dist/utils/errors/index.d.ts.map +1 -1
- package/dist/utils/errors/suggestions.d.ts +1 -0
- package/dist/utils/errors/suggestions.d.ts.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/security-utils.d.ts +29 -0
- package/dist/utils/security-utils.d.ts.map +1 -0
- package/dist/utils/stream-utils.d.ts +14 -0
- package/dist/utils/stream-utils.d.ts.map +1 -0
- package/dist/utils/where-clause.d.ts +0 -13
- package/dist/utils/where-clause.d.ts.map +1 -1
- package/package.json +36 -39
- package/dist/adapters/sqlite-native/tools/window.d.ts +0 -7
- package/dist/adapters/sqlite-native/tools/window.d.ts.map +0 -1
- package/dist/server/registration/audit-tools.d.ts +0 -17
- package/dist/server/registration/audit-tools.d.ts.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { validateQuery } from './chunk-
|
|
2
|
-
import { ErrorResponseFields, redactObject, SENSITIVE_KEY_PATTERN, HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY, ASSISTANT_FOCUSED, readOnly, sanitizeIdentifier, createColumnList, buildWhereClause, write, idempotent, destructive, sanitizeWhereClause, validateJsonPath, validateAggregateFunction, VacuumOutputSchema, admin, validateSameDirPath, adminFs, BackupOutputSchema, BackupSchema, AnalyzeOutputSchema, AnalyzeSchema, IntegrityCheckOutputSchema, IntegrityCheckSchema, OptimizeOutputSchema, OptimizeSchema, RestoreOutputSchema, RestoreSchema, VerifyBackupOutputSchema, VerifyBackupSchema, IndexStatsOutputSchema, IndexStatsSchema, PragmaCompileOptionsOutputSchema, PragmaCompileOptionsSchema, PragmaDatabaseListOutputSchema, PragmaDatabaseListSchema, PragmaOptimizeOutputSchema, PragmaOptimizeSchema, PragmaSettingsOutputSchema, PragmaSettingsSchema, PragmaTableInfoOutputSchema, PragmaTableInfoSchema, AttachDatabaseOutputSchema, AttachDatabaseSchema, DetachDatabaseOutputSchema, DetachDatabaseSchema, VacuumIntoCopyOutputSchema, VacuumIntoCopySchema, SqlDumpOutputSchema, SqlDumpSchema, ReindexOutputSchema, ReindexSchema, WalOutputSchema, WalSchema } from './chunk-
|
|
3
|
-
import { createModuleLogger, logger, ResourceNotFoundError, DbMcpError, formatHandlerError, findSuggestion, ERROR_CODES, QueryError, ValidationError, ExtensionNotAvailableError, getAuthContext, scopesGrantToolAccess } from './chunk-
|
|
1
|
+
import { validateQuery } from './chunk-27C5EE6U.js';
|
|
2
|
+
import { ErrorResponseFields, redactObject, SENSITIVE_KEY_PATTERN, HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY, ASSISTANT_FOCUSED, readOnly, sanitizeIdentifier, createColumnList, buildWhereClause, buildProgressContext, streamResultRows, write, idempotent, destructive, sanitizeWhereClause, validateJsonPath, validateAggregateFunction, VacuumOutputSchema, sendProgress, admin, assertSafeIoPath, validateSameDirPath, adminFs, BackupOutputSchema, BackupSchema, AnalyzeOutputSchema, AnalyzeSchema, IntegrityCheckOutputSchema, IntegrityCheckSchema, OptimizeOutputSchema, OptimizeSchema, RestoreOutputSchema, RestoreSchema, VerifyBackupOutputSchema, VerifyBackupSchema, IndexStatsOutputSchema, IndexStatsSchema, PragmaCompileOptionsOutputSchema, PragmaCompileOptionsSchema, PragmaDatabaseListOutputSchema, PragmaDatabaseListSchema, PragmaOptimizeOutputSchema, PragmaOptimizeSchema, PragmaSettingsOutputSchema, PragmaSettingsSchema, PragmaTableInfoOutputSchema, PragmaTableInfoSchema, AttachDatabaseOutputSchema, AttachDatabaseSchema, DetachDatabaseOutputSchema, DetachDatabaseSchema, VacuumIntoCopyOutputSchema, VacuumIntoCopySchema, SqlDumpOutputSchema, SqlDumpSchema, ReindexOutputSchema, ReindexSchema, WalOutputSchema, WalSchema } from './chunk-2TYICG7B.js';
|
|
3
|
+
import { createModuleLogger, logger, InternalError, ResourceNotFoundError, DbMcpError, ErrorCategory, formatHandlerError, RateLimitError, TimeoutError, findSuggestion, ERROR_CODES, QueryError, ValidationError, ConflictError, ExtensionNotAvailableError, getAuthContext, scopesGrantToolAccess, AuthorizationError } from './chunk-XLIYO3YA.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
5
|
+
import * as nodePath from 'path';
|
|
6
|
+
import nodePath__default from 'path';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import { createHash } from 'crypto';
|
|
9
9
|
import { createClient } from 'redis';
|
|
@@ -42,7 +42,9 @@ var ReadQueryOutputSchema = z.object({
|
|
|
42
42
|
rowCount: z.number().optional(),
|
|
43
43
|
rows: z.array(RowRecordSchema).optional(),
|
|
44
44
|
nextCursor: z.string().optional(),
|
|
45
|
-
executionTimeMs: z.number().optional()
|
|
45
|
+
executionTimeMs: z.number().optional(),
|
|
46
|
+
streamed: z.boolean().optional(),
|
|
47
|
+
chunksEmitted: z.number().optional()
|
|
46
48
|
}).extend(ErrorResponseFields.shape);
|
|
47
49
|
var WriteQueryOutputSchema = z.object({
|
|
48
50
|
success: z.boolean(),
|
|
@@ -124,12 +126,21 @@ var ReadQuerySchema = z.object({
|
|
|
124
126
|
query: z.string().max(1e5).default("").describe("SELECT query to execute"),
|
|
125
127
|
sql: z.string().max(1e5).optional().describe("Alias for query"),
|
|
126
128
|
params: z.array(z.unknown()).optional().describe("Query parameters for prepared statements"),
|
|
127
|
-
cursor: z.string().optional().describe("Opaque cursor for pagination")
|
|
129
|
+
cursor: z.string().optional().describe("Opaque cursor for pagination"),
|
|
130
|
+
stream: z.boolean().optional().default(false).describe(
|
|
131
|
+
"Stream results row-by-row via MCP progress notifications instead of returning all rows in the response. Requires a progressToken in the request _meta. Falls back to normal behavior if unavailable. The final response contains rowCount and metadata but not the rows themselves."
|
|
132
|
+
),
|
|
133
|
+
chunkSize: z.number().int().positive().optional().describe(
|
|
134
|
+
"When streaming, the number of rows to include in each progress notification chunk (default 10)"
|
|
135
|
+
)
|
|
128
136
|
});
|
|
129
137
|
var WriteQuerySchema = z.object({
|
|
130
138
|
query: z.string().max(1e5).default("").describe("INSERT/UPDATE/DELETE query to execute"),
|
|
131
139
|
sql: z.string().max(1e5).optional().describe("Alias for query"),
|
|
132
|
-
params: z.array(z.unknown()).optional().describe("Query parameters for prepared statements")
|
|
140
|
+
params: z.array(z.unknown()).optional().describe("Query parameters for prepared statements"),
|
|
141
|
+
expectedVersion: z.number().int().positive().optional().describe(
|
|
142
|
+
"If set and rowsAffected is 0, throws ConflictError instead of returning silently. Use with manual WHERE _version = ? guards."
|
|
143
|
+
)
|
|
133
144
|
});
|
|
134
145
|
var CreateTableSchema = z.object({
|
|
135
146
|
table: z.string().default("").describe("Name of the table to create"),
|
|
@@ -244,7 +255,10 @@ var UpsertSchema = z.object({
|
|
|
244
255
|
updateColumns: z.array(z.string()).optional().describe(
|
|
245
256
|
"Columns to update on conflict (default: all except conflict columns). Only used if conflictColumns is provided."
|
|
246
257
|
),
|
|
247
|
-
returning: z.union([z.boolean(), z.array(z.string())]).optional().describe("Columns to return, or true for all columns")
|
|
258
|
+
returning: z.union([z.boolean(), z.array(z.string())]).optional().describe("Columns to return, or true for all columns"),
|
|
259
|
+
expectedVersion: z.number().int().positive().optional().describe(
|
|
260
|
+
"Expected _version value for optimistic concurrency control. Requires conflictColumns and a versioned table."
|
|
261
|
+
)
|
|
248
262
|
});
|
|
249
263
|
var BatchInsertSchema = z.object({
|
|
250
264
|
table: z.string().default("").describe("Table name"),
|
|
@@ -350,9 +364,64 @@ var DropTriggerOutputSchema = z.object({
|
|
|
350
364
|
success: z.boolean(),
|
|
351
365
|
message: z.string().optional()
|
|
352
366
|
}).extend(ErrorResponseFields.shape);
|
|
353
|
-
|
|
367
|
+
var EnableVersioningSchema = z.object({
|
|
368
|
+
table: z.string().describe("Name of the table to enable versioning for"),
|
|
369
|
+
tableName: z.string().optional().describe("Alias for table")
|
|
370
|
+
});
|
|
371
|
+
var EnableVersioningOutputSchema = z.object({
|
|
372
|
+
success: z.boolean(),
|
|
373
|
+
message: z.string().optional(),
|
|
374
|
+
sql: z.string().optional(),
|
|
375
|
+
alreadyEnabled: z.boolean().optional()
|
|
376
|
+
}).extend(ErrorResponseFields.shape);
|
|
377
|
+
var DisableVersioningSchema = z.object({
|
|
378
|
+
table: z.string().describe("Name of the table to disable versioning for"),
|
|
379
|
+
tableName: z.string().optional().describe("Alias for table"),
|
|
380
|
+
ifExists: z.boolean().optional().default(false)
|
|
381
|
+
});
|
|
382
|
+
var DisableVersioningOutputSchema = z.object({
|
|
383
|
+
success: z.boolean(),
|
|
384
|
+
message: z.string().optional(),
|
|
385
|
+
sql: z.string().optional()
|
|
386
|
+
}).extend(ErrorResponseFields.shape);
|
|
387
|
+
var CheckVersionSchema = z.object({
|
|
388
|
+
table: z.string().describe("Table name"),
|
|
389
|
+
tableName: z.string().optional().describe("Alias for table"),
|
|
390
|
+
rowId: z.union([z.number(), z.string()]).describe("Primary key or rowid value of the row"),
|
|
391
|
+
idColumn: z.string().optional().describe("Name of the primary key column (default: 'rowid')")
|
|
392
|
+
});
|
|
393
|
+
var CheckVersionOutputSchema = z.object({
|
|
394
|
+
success: z.boolean(),
|
|
395
|
+
version: z.number().optional(),
|
|
396
|
+
row: RowRecordSchema.optional()
|
|
397
|
+
}).extend(ErrorResponseFields.shape);
|
|
398
|
+
var ConditionalUpdateSchema = z.object({
|
|
399
|
+
table: z.string().describe("Table name"),
|
|
400
|
+
tableName: z.string().optional().describe("Alias for table"),
|
|
401
|
+
data: z.record(z.string(), z.unknown()).describe("Column-value pairs to update"),
|
|
402
|
+
conditions: z.array(WhereConditionSchema).describe("WHERE conditions to locate the row (e.g. id = 1)"),
|
|
403
|
+
expectedVersion: z.number().int().positive().describe("The _version value expected to be currently in the database")
|
|
404
|
+
});
|
|
405
|
+
var ConditionalUpdateOutputSchema = z.object({
|
|
406
|
+
success: z.boolean(),
|
|
407
|
+
rowsAffected: z.number().optional(),
|
|
408
|
+
currentVersion: z.number().optional(),
|
|
409
|
+
rows: z.array(RowRecordSchema).optional()
|
|
410
|
+
}).extend(ErrorResponseFields.shape);
|
|
411
|
+
function resolveAliases(params, aliasMap = {}) {
|
|
354
412
|
if (typeof params !== "object" || params === null) return params;
|
|
355
|
-
const obj = params;
|
|
413
|
+
const obj = { ...params };
|
|
414
|
+
for (const key of Object.keys(obj)) {
|
|
415
|
+
if (key.includes("_")) {
|
|
416
|
+
const camelKey = key.replace(
|
|
417
|
+
/_([a-z])/g,
|
|
418
|
+
(_, letter) => letter.toUpperCase()
|
|
419
|
+
);
|
|
420
|
+
if (obj[camelKey] === void 0 && obj[key] !== void 0) {
|
|
421
|
+
obj[camelKey] = obj[key];
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
356
425
|
for (const [alias, canonical] of Object.entries(aliasMap)) {
|
|
357
426
|
if ((obj[canonical] === void 0 || obj[canonical] === "") && obj[alias] !== void 0) {
|
|
358
427
|
obj[canonical] = obj[alias];
|
|
@@ -591,6 +660,24 @@ function createReadQueryTool(adapter) {
|
|
|
591
660
|
JSON.stringify({ offset: nextOffset })
|
|
592
661
|
).toString("base64");
|
|
593
662
|
}
|
|
663
|
+
if (input.stream && !_context.isCodeMode) {
|
|
664
|
+
const progressCtx = buildProgressContext(_context);
|
|
665
|
+
if (progressCtx) {
|
|
666
|
+
const chunksEmitted = await streamResultRows(
|
|
667
|
+
progressCtx,
|
|
668
|
+
result.rows ?? [],
|
|
669
|
+
input.chunkSize
|
|
670
|
+
);
|
|
671
|
+
return {
|
|
672
|
+
success: true,
|
|
673
|
+
rowCount: result.rows?.length ?? 0,
|
|
674
|
+
nextCursor,
|
|
675
|
+
executionTimeMs: result.executionTimeMs,
|
|
676
|
+
streamed: true,
|
|
677
|
+
chunksEmitted
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
594
681
|
return {
|
|
595
682
|
success: true,
|
|
596
683
|
rowCount: result.rows?.length ?? 0,
|
|
@@ -695,11 +782,60 @@ function createWriteQueryTool(adapter) {
|
|
|
695
782
|
rowsAffected: 0
|
|
696
783
|
};
|
|
697
784
|
}
|
|
785
|
+
if (input.expectedVersion === void 0) {
|
|
786
|
+
const tables = [];
|
|
787
|
+
const tableRegex = /\b(?:UPDATE|INSERT\s+INTO|DELETE\s+FROM|REPLACE\s+INTO)\s+(?:["'`]?([a-zA-Z0-9_]+)["'`]?)\b/gi;
|
|
788
|
+
let match;
|
|
789
|
+
while ((match = tableRegex.exec(input.query)) !== null) {
|
|
790
|
+
if (match[1]) tables.push(match[1]);
|
|
791
|
+
}
|
|
792
|
+
for (const tableName of tables) {
|
|
793
|
+
try {
|
|
794
|
+
const pragmaCheck = await adapter.executeReadQuery(
|
|
795
|
+
`PRAGMA table_info(${sanitizeIdentifier(tableName)})`
|
|
796
|
+
);
|
|
797
|
+
const isVersioned = (pragmaCheck.rows ?? []).some(
|
|
798
|
+
(col) => col["name"] === "_version"
|
|
799
|
+
);
|
|
800
|
+
if (isVersioned) {
|
|
801
|
+
return {
|
|
802
|
+
...formatHandlerError(
|
|
803
|
+
new ConflictError(
|
|
804
|
+
`expectedVersion is required when updating versioned table '${tableName}'`,
|
|
805
|
+
"CONFLICT_ERROR",
|
|
806
|
+
{
|
|
807
|
+
conflictType: "missing_expected_version",
|
|
808
|
+
suggestion: "Use sqlite_check_version to get the current version, then include expectedVersion in your request."
|
|
809
|
+
}
|
|
810
|
+
)
|
|
811
|
+
),
|
|
812
|
+
rowsAffected: 0
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
698
819
|
try {
|
|
699
820
|
const result = await adapter.executeWriteQuery(
|
|
700
821
|
input.query,
|
|
701
822
|
input.params
|
|
702
823
|
);
|
|
824
|
+
if (result.rowsAffected === 0 && input.expectedVersion !== void 0) {
|
|
825
|
+
return {
|
|
826
|
+
...formatHandlerError(
|
|
827
|
+
new ConflictError(
|
|
828
|
+
`Version conflict or row not found. expectedVersion ${input.expectedVersion} was provided but 0 rows were affected.`,
|
|
829
|
+
"CONFLICT_ERROR",
|
|
830
|
+
{
|
|
831
|
+
conflictType: "version_mismatch_or_not_found",
|
|
832
|
+
suggestion: "Verify the row exists and the version matches."
|
|
833
|
+
}
|
|
834
|
+
)
|
|
835
|
+
),
|
|
836
|
+
rowsAffected: 0
|
|
837
|
+
};
|
|
838
|
+
}
|
|
703
839
|
return {
|
|
704
840
|
success: true,
|
|
705
841
|
rowsAffected: result.rowsAffected,
|
|
@@ -1416,7 +1552,7 @@ async function validateTableExists2(adapter, table) {
|
|
|
1416
1552
|
success: false,
|
|
1417
1553
|
error: `Table or view '${table}' not found. Use sqlite_list_tables to see available tables.`,
|
|
1418
1554
|
code: "TABLE_NOT_FOUND",
|
|
1419
|
-
category:
|
|
1555
|
+
category: ErrorCategory.RESOURCE,
|
|
1420
1556
|
suggestion: "Table or view does not exist. Run sqlite_list_tables to see available tables.",
|
|
1421
1557
|
details: void 0,
|
|
1422
1558
|
recoverable: false
|
|
@@ -1451,12 +1587,38 @@ function createUpsertTool(adapter) {
|
|
|
1451
1587
|
conflictColumns: parsed.conflictColumns !== void 0 ? Array.isArray(parsed.conflictColumns) ? parsed.conflictColumns : [parsed.conflictColumns] : []
|
|
1452
1588
|
};
|
|
1453
1589
|
if (Object.keys(input.data).length === 0)
|
|
1454
|
-
throw new
|
|
1590
|
+
throw new ValidationError("data (or values alias) is required");
|
|
1455
1591
|
} catch (error) {
|
|
1456
1592
|
return { ...formatHandlerError(error), rowsAffected: 0 };
|
|
1457
1593
|
}
|
|
1458
1594
|
const validationError = await validateTableExists2(adapter, input.table);
|
|
1459
1595
|
if (validationError) return { ...validationError, rowsAffected: 0 };
|
|
1596
|
+
if (input.expectedVersion === void 0) {
|
|
1597
|
+
try {
|
|
1598
|
+
const pragmaCheck = await adapter.executeReadQuery(
|
|
1599
|
+
`PRAGMA table_info(${sanitizeIdentifier(input.table)})`
|
|
1600
|
+
);
|
|
1601
|
+
const isVersioned = (pragmaCheck.rows ?? []).some(
|
|
1602
|
+
(col) => col["name"] === "_version"
|
|
1603
|
+
);
|
|
1604
|
+
if (isVersioned) {
|
|
1605
|
+
return {
|
|
1606
|
+
...formatHandlerError(
|
|
1607
|
+
new ConflictError(
|
|
1608
|
+
`expectedVersion is required when updating versioned table '${input.table}'`,
|
|
1609
|
+
"CONFLICT_ERROR",
|
|
1610
|
+
{
|
|
1611
|
+
conflictType: "missing_expected_version",
|
|
1612
|
+
suggestion: "Use sqlite_check_version to get the current version, then include expectedVersion in your request."
|
|
1613
|
+
}
|
|
1614
|
+
)
|
|
1615
|
+
),
|
|
1616
|
+
rowsAffected: 0
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1460
1622
|
const columns = Object.keys(input.data);
|
|
1461
1623
|
const values = Object.values(input.data);
|
|
1462
1624
|
const placeholders = columns.map(() => "?").join(", ");
|
|
@@ -1471,11 +1633,35 @@ function createUpsertTool(adapter) {
|
|
|
1471
1633
|
const updateSets = colsToUpdate.map(
|
|
1472
1634
|
(c) => `${sanitizeIdentifier(c)} = EXCLUDED.${sanitizeIdentifier(c)}`
|
|
1473
1635
|
).join(", ");
|
|
1474
|
-
|
|
1636
|
+
let updateClause = ` ON CONFLICT (${conflictCols}) DO UPDATE SET ${updateSets}`;
|
|
1637
|
+
if (input.expectedVersion !== void 0) {
|
|
1638
|
+
updateClause += ` WHERE _version = ${input.expectedVersion}`;
|
|
1639
|
+
}
|
|
1640
|
+
sql += updateClause;
|
|
1475
1641
|
} else {
|
|
1642
|
+
if (input.expectedVersion !== void 0) {
|
|
1643
|
+
return {
|
|
1644
|
+
...formatHandlerError(
|
|
1645
|
+
new ValidationError(
|
|
1646
|
+
"expectedVersion requires colsToUpdate (cannot use with DO NOTHING)"
|
|
1647
|
+
)
|
|
1648
|
+
),
|
|
1649
|
+
rowsAffected: 0
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1476
1652
|
sql += ` ON CONFLICT (${conflictCols}) DO NOTHING`;
|
|
1477
1653
|
}
|
|
1478
1654
|
} else {
|
|
1655
|
+
if (input.expectedVersion !== void 0) {
|
|
1656
|
+
return {
|
|
1657
|
+
...formatHandlerError(
|
|
1658
|
+
new ValidationError(
|
|
1659
|
+
"expectedVersion requires explicit conflictColumns for OCC"
|
|
1660
|
+
)
|
|
1661
|
+
),
|
|
1662
|
+
rowsAffected: 0
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1479
1665
|
sql = `INSERT OR REPLACE INTO ${safeTable} (${safeColumns.join(", ")}) VALUES (${placeholders})`;
|
|
1480
1666
|
}
|
|
1481
1667
|
if (input.returning !== void 0 && input.returning !== false) {
|
|
@@ -1488,6 +1674,21 @@ function createUpsertTool(adapter) {
|
|
|
1488
1674
|
}
|
|
1489
1675
|
try {
|
|
1490
1676
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
1677
|
+
if (result.rowsAffected === 0 && input.expectedVersion !== void 0) {
|
|
1678
|
+
return {
|
|
1679
|
+
...formatHandlerError(
|
|
1680
|
+
new ConflictError(
|
|
1681
|
+
`Version conflict during upsert: expected version ${input.expectedVersion}. Re-read the row and retry.`,
|
|
1682
|
+
"CONFLICT_ERROR",
|
|
1683
|
+
{
|
|
1684
|
+
conflictType: "version_mismatch",
|
|
1685
|
+
suggestion: "Re-read the row to get the current version, then retry the update."
|
|
1686
|
+
}
|
|
1687
|
+
)
|
|
1688
|
+
),
|
|
1689
|
+
rowsAffected: 0
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1491
1692
|
const response = {
|
|
1492
1693
|
success: true,
|
|
1493
1694
|
rowsAffected: result.rowsAffected,
|
|
@@ -2571,6 +2772,269 @@ function createAlterTableTool(adapter) {
|
|
|
2571
2772
|
};
|
|
2572
2773
|
}
|
|
2573
2774
|
|
|
2775
|
+
// src/adapters/sqlite/tools/core/versioning.ts
|
|
2776
|
+
function createEnableVersioningTool(adapter) {
|
|
2777
|
+
return {
|
|
2778
|
+
name: "sqlite_enable_versioning",
|
|
2779
|
+
description: "Enable optimistic concurrency control (OCC) on a table. Adds a _version column and an auto-increment trigger.",
|
|
2780
|
+
group: "core",
|
|
2781
|
+
inputSchema: EnableVersioningSchema,
|
|
2782
|
+
outputSchema: EnableVersioningOutputSchema,
|
|
2783
|
+
requiredScopes: ["admin"],
|
|
2784
|
+
annotations: write("Enable Versioning"),
|
|
2785
|
+
handler: async (params, _context) => {
|
|
2786
|
+
let input;
|
|
2787
|
+
try {
|
|
2788
|
+
input = EnableVersioningSchema.parse(params);
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
return { ...formatHandlerError(error), sql: "" };
|
|
2791
|
+
}
|
|
2792
|
+
const validationError = await validateTableExists2(adapter, input.table);
|
|
2793
|
+
if (validationError) return { ...validationError, sql: "" };
|
|
2794
|
+
const safeTable = sanitizeIdentifier(input.table);
|
|
2795
|
+
const triggerName = sanitizeIdentifier(`_mcp_version_${input.table}`);
|
|
2796
|
+
try {
|
|
2797
|
+
const pragmaCheck = await adapter.executeReadQuery(
|
|
2798
|
+
`PRAGMA table_info(${safeTable})`
|
|
2799
|
+
);
|
|
2800
|
+
const hasVersionColumn = (pragmaCheck.rows ?? []).some(
|
|
2801
|
+
(col) => col["name"] === "_version"
|
|
2802
|
+
);
|
|
2803
|
+
const statements = [];
|
|
2804
|
+
if (!hasVersionColumn) {
|
|
2805
|
+
statements.push(
|
|
2806
|
+
`ALTER TABLE ${safeTable} ADD COLUMN _version INTEGER NOT NULL DEFAULT 1;`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
const triggerSql = `
|
|
2810
|
+
CREATE TRIGGER IF NOT EXISTS ${triggerName}
|
|
2811
|
+
BEFORE UPDATE ON ${safeTable}
|
|
2812
|
+
FOR EACH ROW
|
|
2813
|
+
BEGIN
|
|
2814
|
+
UPDATE ${safeTable} SET _version = OLD._version + 1 WHERE rowid = OLD.rowid;
|
|
2815
|
+
END;`;
|
|
2816
|
+
statements.push(triggerSql.trim());
|
|
2817
|
+
for (const stmt of statements) {
|
|
2818
|
+
await adapter.executeWriteQuery(stmt);
|
|
2819
|
+
}
|
|
2820
|
+
const sql = statements.join("\n");
|
|
2821
|
+
return {
|
|
2822
|
+
success: true,
|
|
2823
|
+
message: hasVersionColumn ? `Versioning already active on '${input.table}', trigger ensured.` : `Versioning enabled on '${input.table}'. Added _version column and trigger.`,
|
|
2824
|
+
sql,
|
|
2825
|
+
alreadyEnabled: hasVersionColumn
|
|
2826
|
+
};
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
return { ...formatHandlerError(error), sql: "" };
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
function createDisableVersioningTool(adapter) {
|
|
2834
|
+
return {
|
|
2835
|
+
name: "sqlite_disable_versioning",
|
|
2836
|
+
description: "Disable optimistic concurrency control (OCC) on a table. Drops the _version column and its trigger.",
|
|
2837
|
+
group: "core",
|
|
2838
|
+
inputSchema: DisableVersioningSchema,
|
|
2839
|
+
outputSchema: DisableVersioningOutputSchema,
|
|
2840
|
+
requiredScopes: ["admin"],
|
|
2841
|
+
annotations: write("Disable Versioning"),
|
|
2842
|
+
handler: async (params, _context) => {
|
|
2843
|
+
let input;
|
|
2844
|
+
try {
|
|
2845
|
+
input = DisableVersioningSchema.parse(params);
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
return { ...formatHandlerError(error), sql: "" };
|
|
2848
|
+
}
|
|
2849
|
+
const safeTable = sanitizeIdentifier(input.table);
|
|
2850
|
+
const triggerName = sanitizeIdentifier(`_mcp_version_${input.table}`);
|
|
2851
|
+
const tableCheck = await adapter.executeReadQuery(
|
|
2852
|
+
`SELECT 1 FROM pragma_table_list(?) WHERE type IN ('table', 'view') LIMIT 1`,
|
|
2853
|
+
[input.table]
|
|
2854
|
+
);
|
|
2855
|
+
if (!tableCheck.rows || tableCheck.rows.length === 0) {
|
|
2856
|
+
if (input.ifExists) {
|
|
2857
|
+
return {
|
|
2858
|
+
success: true,
|
|
2859
|
+
message: `Table '${input.table}' does not exist (no changes made).`,
|
|
2860
|
+
sql: ""
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
return {
|
|
2864
|
+
...formatHandlerError(
|
|
2865
|
+
new ValidationError(`Table '${input.table}' does not exist.`)
|
|
2866
|
+
),
|
|
2867
|
+
sql: ""
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
try {
|
|
2871
|
+
const pragmaCheck = await adapter.executeReadQuery(
|
|
2872
|
+
`PRAGMA table_info(${safeTable})`
|
|
2873
|
+
);
|
|
2874
|
+
const hasVersionColumn = (pragmaCheck.rows ?? []).some(
|
|
2875
|
+
(col) => col["name"] === "_version"
|
|
2876
|
+
);
|
|
2877
|
+
const statements = [];
|
|
2878
|
+
statements.push(`DROP TRIGGER IF EXISTS ${triggerName};`);
|
|
2879
|
+
if (hasVersionColumn) {
|
|
2880
|
+
statements.push(`ALTER TABLE ${safeTable} DROP COLUMN _version;`);
|
|
2881
|
+
}
|
|
2882
|
+
for (const stmt of statements) {
|
|
2883
|
+
await adapter.executeWriteQuery(stmt);
|
|
2884
|
+
}
|
|
2885
|
+
const sql = statements.join("\n");
|
|
2886
|
+
return {
|
|
2887
|
+
success: true,
|
|
2888
|
+
message: hasVersionColumn ? `Versioning disabled on '${input.table}'. Dropped _version column and trigger.` : `Versioning already disabled on '${input.table}', trigger dropped if existed.`,
|
|
2889
|
+
sql
|
|
2890
|
+
};
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
return { ...formatHandlerError(error), sql: "" };
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
function createCheckVersionTool(adapter) {
|
|
2898
|
+
return {
|
|
2899
|
+
name: "sqlite_check_version",
|
|
2900
|
+
description: "Read the current _version of a specific row for optimistic concurrency control.",
|
|
2901
|
+
group: "core",
|
|
2902
|
+
inputSchema: CheckVersionSchema,
|
|
2903
|
+
outputSchema: CheckVersionOutputSchema,
|
|
2904
|
+
requiredScopes: ["read"],
|
|
2905
|
+
annotations: readOnly("Check Version"),
|
|
2906
|
+
handler: async (params, _context) => {
|
|
2907
|
+
let input;
|
|
2908
|
+
try {
|
|
2909
|
+
input = CheckVersionSchema.parse(params);
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
return formatHandlerError(error);
|
|
2912
|
+
}
|
|
2913
|
+
const validationError = await validateTableExists2(adapter, input.table);
|
|
2914
|
+
if (validationError) return validationError;
|
|
2915
|
+
const safeTable = sanitizeIdentifier(input.table);
|
|
2916
|
+
const safeIdCol = sanitizeIdentifier(input.idColumn ?? "rowid");
|
|
2917
|
+
try {
|
|
2918
|
+
const sql = `SELECT * FROM ${safeTable} WHERE ${safeIdCol} = ? LIMIT 1`;
|
|
2919
|
+
const result = await adapter.executeReadQuery(sql, [input.rowId]);
|
|
2920
|
+
if (!result.rows || result.rows.length === 0) {
|
|
2921
|
+
return formatHandlerError(
|
|
2922
|
+
new ResourceNotFoundError(
|
|
2923
|
+
`Row not found in table '${input.table}' with ${safeIdCol} = ${input.rowId}`
|
|
2924
|
+
)
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
const row = result.rows[0];
|
|
2928
|
+
if (row && typeof row["_version"] === "number") {
|
|
2929
|
+
return { success: true, version: row["_version"], row };
|
|
2930
|
+
} else {
|
|
2931
|
+
return formatHandlerError(
|
|
2932
|
+
new ValidationError(
|
|
2933
|
+
`Table '${input.table}' does not appear to have versioning enabled (missing _version column).`
|
|
2934
|
+
)
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
} catch (error) {
|
|
2938
|
+
return formatHandlerError(error);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
function createConditionalUpdateTool(adapter) {
|
|
2944
|
+
return {
|
|
2945
|
+
name: "sqlite_conditional_update",
|
|
2946
|
+
description: "Update a row only if its _version matches expectedVersion. Prevents lost updates in multi-agent environments.",
|
|
2947
|
+
group: "core",
|
|
2948
|
+
inputSchema: ConditionalUpdateSchema,
|
|
2949
|
+
outputSchema: ConditionalUpdateOutputSchema,
|
|
2950
|
+
requiredScopes: ["write"],
|
|
2951
|
+
annotations: write("Conditional Update"),
|
|
2952
|
+
handler: async (params, _context) => {
|
|
2953
|
+
let input;
|
|
2954
|
+
try {
|
|
2955
|
+
input = ConditionalUpdateSchema.parse(params);
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
return formatHandlerError(error);
|
|
2958
|
+
}
|
|
2959
|
+
const validationError = await validateTableExists2(adapter, input.table);
|
|
2960
|
+
if (validationError) return validationError;
|
|
2961
|
+
const safeTable = sanitizeIdentifier(input.table);
|
|
2962
|
+
const columns = Object.keys(input.data);
|
|
2963
|
+
if (columns.length === 0) {
|
|
2964
|
+
return formatHandlerError(
|
|
2965
|
+
new ValidationError("Update data cannot be empty")
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
if (input.conditions.length === 0) {
|
|
2969
|
+
return formatHandlerError(
|
|
2970
|
+
new ValidationError("Conditions are required to identify the row")
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
try {
|
|
2974
|
+
const queryParams = [];
|
|
2975
|
+
const safeColumns = columns.map(sanitizeIdentifier);
|
|
2976
|
+
const setClauses = safeColumns.map((c) => `${c} = ?`).join(", ");
|
|
2977
|
+
queryParams.push(...Object.values(input.data));
|
|
2978
|
+
const { sql: whereSql, params: whereParams } = buildWhereClause(
|
|
2979
|
+
input.conditions
|
|
2980
|
+
);
|
|
2981
|
+
queryParams.push(...whereParams);
|
|
2982
|
+
queryParams.push(input.expectedVersion);
|
|
2983
|
+
const updateSql = `UPDATE ${safeTable} SET ${setClauses} WHERE (${whereSql}) AND _version = ? RETURNING *`;
|
|
2984
|
+
const result = await adapter.executeWriteQuery(updateSql, queryParams);
|
|
2985
|
+
if (result.rowsAffected === 0) {
|
|
2986
|
+
const checkSql = `SELECT _version FROM ${safeTable} WHERE ${whereSql}`;
|
|
2987
|
+
const checkResult = await adapter.executeReadQuery(
|
|
2988
|
+
checkSql,
|
|
2989
|
+
whereParams
|
|
2990
|
+
);
|
|
2991
|
+
if (!checkResult.rows || checkResult.rows.length === 0) {
|
|
2992
|
+
return formatHandlerError(
|
|
2993
|
+
new ResourceNotFoundError(
|
|
2994
|
+
"Row not found matching the provided conditions"
|
|
2995
|
+
)
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
const currentVersionRaw = checkResult.rows[0]?.["_version"];
|
|
2999
|
+
if (currentVersionRaw === void 0 || currentVersionRaw === null) {
|
|
3000
|
+
return formatHandlerError(
|
|
3001
|
+
new ValidationError(
|
|
3002
|
+
`Table '${input.table}' does not appear to have versioning enabled (missing _version column).`
|
|
3003
|
+
)
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
const currentVersion2 = Number(currentVersionRaw);
|
|
3007
|
+
return formatHandlerError(
|
|
3008
|
+
new ConflictError(
|
|
3009
|
+
`Version conflict: expected version ${input.expectedVersion} but row has version ${currentVersion2}. Re-read the row and retry.`,
|
|
3010
|
+
"CONFLICT_ERROR",
|
|
3011
|
+
{
|
|
3012
|
+
conflictType: "version_mismatch",
|
|
3013
|
+
suggestion: "Re-read the row to get the current version, then retry the update.",
|
|
3014
|
+
details: {
|
|
3015
|
+
table: input.table,
|
|
3016
|
+
expectedVersion: input.expectedVersion,
|
|
3017
|
+
currentVersion: currentVersion2
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
)
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
const newRow = result.rows?.[0];
|
|
3024
|
+
const currentVersion = newRow ? newRow["_version"] : void 0;
|
|
3025
|
+
return {
|
|
3026
|
+
success: true,
|
|
3027
|
+
rowsAffected: result.rowsAffected,
|
|
3028
|
+
currentVersion,
|
|
3029
|
+
rows: result.rows
|
|
3030
|
+
};
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
return formatHandlerError(error);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
|
|
2574
3038
|
// src/adapters/sqlite/tools/core/index.ts
|
|
2575
3039
|
function getCoreTools(adapter) {
|
|
2576
3040
|
return [
|
|
@@ -2594,7 +3058,11 @@ function getCoreTools(adapter) {
|
|
|
2594
3058
|
createListConstraintsTool(adapter),
|
|
2595
3059
|
createDateAddTool(adapter),
|
|
2596
3060
|
createDateDiffTool(adapter),
|
|
2597
|
-
createAlterTableTool(adapter)
|
|
3061
|
+
createAlterTableTool(adapter),
|
|
3062
|
+
createEnableVersioningTool(adapter),
|
|
3063
|
+
createDisableVersioningTool(adapter),
|
|
3064
|
+
createCheckVersionTool(adapter),
|
|
3065
|
+
createConditionalUpdateTool(adapter)
|
|
2598
3066
|
];
|
|
2599
3067
|
}
|
|
2600
3068
|
|
|
@@ -2714,7 +3182,7 @@ var SchemaManager = class {
|
|
|
2714
3182
|
throw new DbMcpError(
|
|
2715
3183
|
"Invalid table name",
|
|
2716
3184
|
"INVALID_TABLE",
|
|
2717
|
-
|
|
3185
|
+
ErrorCategory.VALIDATION
|
|
2718
3186
|
);
|
|
2719
3187
|
}
|
|
2720
3188
|
const result = await this.executor.executeReadQuery(
|
|
@@ -2724,7 +3192,7 @@ var SchemaManager = class {
|
|
|
2724
3192
|
throw new DbMcpError(
|
|
2725
3193
|
`Table '${tableName}' does not exist`,
|
|
2726
3194
|
"TABLE_NOT_FOUND",
|
|
2727
|
-
|
|
3195
|
+
ErrorCategory.RESOURCE
|
|
2728
3196
|
);
|
|
2729
3197
|
}
|
|
2730
3198
|
const columns = (result.rows ?? []).map((row) => ({
|
|
@@ -2893,7 +3361,7 @@ function createTableSchemaResource(adapter) {
|
|
|
2893
3361
|
throw new DbMcpError(
|
|
2894
3362
|
"Invalid table URI format",
|
|
2895
3363
|
"RESOURCE_INVALID_URI",
|
|
2896
|
-
|
|
3364
|
+
ErrorCategory.VALIDATION
|
|
2897
3365
|
);
|
|
2898
3366
|
}
|
|
2899
3367
|
const tableName = decodeURIComponent(match[1]);
|
|
@@ -3864,7 +4332,7 @@ function parseJsonValue(value) {
|
|
|
3864
4332
|
throw new DbMcpError(
|
|
3865
4333
|
"JSONB blob cannot be parsed directly. Use json() in SQL query.",
|
|
3866
4334
|
"JSON_PARSE_ERROR",
|
|
3867
|
-
|
|
4335
|
+
ErrorCategory.VALIDATION
|
|
3868
4336
|
);
|
|
3869
4337
|
}
|
|
3870
4338
|
if (typeof value === "string") {
|
|
@@ -4401,6 +4869,7 @@ function createJsonSetTool(adapter) {
|
|
|
4401
4869
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
4402
4870
|
return {
|
|
4403
4871
|
success: true,
|
|
4872
|
+
message: `Set value at ${input.path} in ${table}.${column}`,
|
|
4404
4873
|
rowsAffected: result.rowsAffected,
|
|
4405
4874
|
...result.rowsAffected === 0 ? {
|
|
4406
4875
|
warning: "No rows matched the WHERE clause \u2014 no changes were made"
|
|
@@ -4442,6 +4911,7 @@ function createJsonRemoveTool(adapter) {
|
|
|
4442
4911
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
4443
4912
|
return {
|
|
4444
4913
|
success: true,
|
|
4914
|
+
message: `Removed value at ${input.path} from ${table}.${column}`,
|
|
4445
4915
|
rowsAffected: result.rowsAffected,
|
|
4446
4916
|
...result.rowsAffected === 0 ? {
|
|
4447
4917
|
warning: "No rows matched the WHERE clause \u2014 no changes were made"
|
|
@@ -4584,6 +5054,7 @@ function createJsonArrayAppendTool(adapter) {
|
|
|
4584
5054
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
4585
5055
|
return {
|
|
4586
5056
|
success: true,
|
|
5057
|
+
message: `Appended to ${input.path} in ${table}.${column}`,
|
|
4587
5058
|
rowsAffected: result.rowsAffected
|
|
4588
5059
|
};
|
|
4589
5060
|
} catch (error) {
|
|
@@ -6816,7 +7287,7 @@ function cosineSimilarity(a, b) {
|
|
|
6816
7287
|
throw new DbMcpError(
|
|
6817
7288
|
"Vector dimensions must match",
|
|
6818
7289
|
"VECTOR_MISMATCH",
|
|
6819
|
-
|
|
7290
|
+
ErrorCategory.VALIDATION
|
|
6820
7291
|
);
|
|
6821
7292
|
}
|
|
6822
7293
|
let dotProd = 0;
|
|
@@ -6837,7 +7308,7 @@ function euclideanDistance(a, b) {
|
|
|
6837
7308
|
throw new DbMcpError(
|
|
6838
7309
|
"Vector dimensions must match",
|
|
6839
7310
|
"VECTOR_MISMATCH",
|
|
6840
|
-
|
|
7311
|
+
ErrorCategory.VALIDATION
|
|
6841
7312
|
);
|
|
6842
7313
|
}
|
|
6843
7314
|
let sum = 0;
|
|
@@ -6854,7 +7325,7 @@ function dotProduct(a, b) {
|
|
|
6854
7325
|
throw new DbMcpError(
|
|
6855
7326
|
"Vector dimensions must match",
|
|
6856
7327
|
"VECTOR_MISMATCH",
|
|
6857
|
-
|
|
7328
|
+
ErrorCategory.VALIDATION
|
|
6858
7329
|
);
|
|
6859
7330
|
}
|
|
6860
7331
|
let sum = 0;
|
|
@@ -6884,7 +7355,7 @@ function parseVector(value) {
|
|
|
6884
7355
|
throw new DbMcpError(
|
|
6885
7356
|
"Invalid vector format",
|
|
6886
7357
|
"VECTOR_INVALID",
|
|
6887
|
-
|
|
7358
|
+
ErrorCategory.VALIDATION
|
|
6888
7359
|
);
|
|
6889
7360
|
}
|
|
6890
7361
|
|
|
@@ -8005,8 +8476,8 @@ var StatsSampleSchema = z.object({
|
|
|
8005
8476
|
table: z.string().describe("Table name"),
|
|
8006
8477
|
sampleSize: z.preprocess(
|
|
8007
8478
|
coerceNumber4,
|
|
8008
|
-
z.number().optional().default(
|
|
8009
|
-
"Number of random rows to return (default:
|
|
8479
|
+
z.number().optional().default(20).describe(
|
|
8480
|
+
"Number of random rows to return (default: 20, max: 50). WARNING: Uses ORDER BY RANDOM() which is O(N) \u2014 slow on very large tables."
|
|
8010
8481
|
)
|
|
8011
8482
|
),
|
|
8012
8483
|
conditions: z.array(WhereConditionSchema).optional().describe("Optional WHERE conditions"),
|
|
@@ -8429,7 +8900,7 @@ function createPercentileTool(adapter) {
|
|
|
8429
8900
|
}
|
|
8430
8901
|
};
|
|
8431
8902
|
}
|
|
8432
|
-
var MAX_SAMPLE_SIZE =
|
|
8903
|
+
var MAX_SAMPLE_SIZE = 50;
|
|
8433
8904
|
function createSampleTool(adapter) {
|
|
8434
8905
|
return {
|
|
8435
8906
|
name: "sqlite_stats_sample",
|
|
@@ -9119,7 +9590,7 @@ function matrixInverse(A) {
|
|
|
9119
9590
|
throw new DbMcpError(
|
|
9120
9591
|
"Matrix is singular, cannot compute inverse",
|
|
9121
9592
|
"MATH_SINGULAR",
|
|
9122
|
-
|
|
9593
|
+
ErrorCategory.VALIDATION
|
|
9123
9594
|
);
|
|
9124
9595
|
}
|
|
9125
9596
|
for (let j = 0; j < 2 * n; j++) {
|
|
@@ -9186,7 +9657,7 @@ function createRegressionTool(adapter) {
|
|
|
9186
9657
|
throw new DbMcpError(
|
|
9187
9658
|
`Insufficient data in table '${input.table}' for degree ${degree} regression (need at least ${degree + 1} points, got ${pairs.length})`,
|
|
9188
9659
|
"STATS_INSUFFICIENT_SAMPLE",
|
|
9189
|
-
|
|
9660
|
+
ErrorCategory.VALIDATION
|
|
9190
9661
|
);
|
|
9191
9662
|
}
|
|
9192
9663
|
const X = pairs.map(
|
|
@@ -9295,7 +9766,7 @@ function createHypothesisTool(adapter) {
|
|
|
9295
9766
|
throw new DbMcpError(
|
|
9296
9767
|
"Insufficient sample size for t-test",
|
|
9297
9768
|
"STATS_INSUFFICIENT_SAMPLE",
|
|
9298
|
-
|
|
9769
|
+
ErrorCategory.VALIDATION
|
|
9299
9770
|
);
|
|
9300
9771
|
}
|
|
9301
9772
|
const tStatistic = (mean - expectedMean) / (stdDev / Math.sqrt(n));
|
|
@@ -9303,7 +9774,7 @@ function createHypothesisTool(adapter) {
|
|
|
9303
9774
|
throw new DbMcpError(
|
|
9304
9775
|
`Cannot compute t-statistic: stdDev=${stdDev.toFixed(4)}, n=${n}. This may indicate zero variance, non-numeric data, or that column "${input.column}" does not exist.`,
|
|
9305
9776
|
"STATS_COMPUTATION_FAILED",
|
|
9306
|
-
|
|
9777
|
+
ErrorCategory.VALIDATION
|
|
9307
9778
|
);
|
|
9308
9779
|
}
|
|
9309
9780
|
const pValue = tDistPValue(tStatistic, df);
|
|
@@ -9326,7 +9797,7 @@ function createHypothesisTool(adapter) {
|
|
|
9326
9797
|
throw new DbMcpError(
|
|
9327
9798
|
"column2 is required for two-sample t-test",
|
|
9328
9799
|
"STATS_MISSING_COLUMN",
|
|
9329
|
-
|
|
9800
|
+
ErrorCategory.VALIDATION
|
|
9330
9801
|
);
|
|
9331
9802
|
}
|
|
9332
9803
|
await validateColumnExists(adapter, input.table, input.column2);
|
|
@@ -9353,7 +9824,7 @@ function createHypothesisTool(adapter) {
|
|
|
9353
9824
|
throw new DbMcpError(
|
|
9354
9825
|
"Insufficient sample size for t-test",
|
|
9355
9826
|
"STATS_INSUFFICIENT_SAMPLE",
|
|
9356
|
-
|
|
9827
|
+
ErrorCategory.VALIDATION
|
|
9357
9828
|
);
|
|
9358
9829
|
}
|
|
9359
9830
|
const tStatistic = (mean1 - mean2) / Math.sqrt(var1 / n1 + var2 / n2);
|
|
@@ -9361,7 +9832,7 @@ function createHypothesisTool(adapter) {
|
|
|
9361
9832
|
throw new DbMcpError(
|
|
9362
9833
|
`Cannot compute t-statistic: var1=${var1.toFixed(4)}, var2=${var2.toFixed(4)}. This may indicate zero variance or non-numeric data.`,
|
|
9363
9834
|
"STATS_COMPUTATION_FAILED",
|
|
9364
|
-
|
|
9835
|
+
ErrorCategory.VALIDATION
|
|
9365
9836
|
);
|
|
9366
9837
|
}
|
|
9367
9838
|
const dfNum = Math.pow(var1 / n1 + var2 / n2, 2);
|
|
@@ -9385,7 +9856,7 @@ function createHypothesisTool(adapter) {
|
|
|
9385
9856
|
throw new DbMcpError(
|
|
9386
9857
|
"groupColumn is required for chi-square test",
|
|
9387
9858
|
"STATS_MISSING_COLUMN",
|
|
9388
|
-
|
|
9859
|
+
ErrorCategory.VALIDATION
|
|
9389
9860
|
);
|
|
9390
9861
|
}
|
|
9391
9862
|
await validateColumnExists(adapter, input.table, input.groupColumn);
|
|
@@ -9422,7 +9893,7 @@ function createHypothesisTool(adapter) {
|
|
|
9422
9893
|
throw new DbMcpError(
|
|
9423
9894
|
`Insufficient categories for chi-square test: "${input.column}" has ${rowTotals.size} category(s), "${input.groupColumn}" has ${colTotals.size} category(s). Both columns must have at least 2 distinct values.`,
|
|
9424
9895
|
"STATS_INSUFFICIENT_CATEGORIES",
|
|
9425
|
-
|
|
9896
|
+
ErrorCategory.VALIDATION
|
|
9426
9897
|
);
|
|
9427
9898
|
}
|
|
9428
9899
|
const pValue = Math.exp(-chiSquare / 2);
|
|
@@ -10089,7 +10560,7 @@ var DropViewSchema = z.object({
|
|
|
10089
10560
|
});
|
|
10090
10561
|
var DbStatSchema = z.object({
|
|
10091
10562
|
table: z.string().optional().describe("Optional table name to filter"),
|
|
10092
|
-
summarize: z.boolean().optional().default(
|
|
10563
|
+
summarize: z.boolean().optional().default(true).describe(
|
|
10093
10564
|
"If true, return aggregated per-table stats instead of raw page-level data"
|
|
10094
10565
|
),
|
|
10095
10566
|
limit: z.preprocess(
|
|
@@ -10344,34 +10815,6 @@ function createDropViewTool(adapter) {
|
|
|
10344
10815
|
};
|
|
10345
10816
|
}
|
|
10346
10817
|
|
|
10347
|
-
// src/utils/progress-utils.ts
|
|
10348
|
-
function buildProgressContext(ctx) {
|
|
10349
|
-
if (ctx?.server === void 0 || ctx.progressToken === void 0) {
|
|
10350
|
-
return void 0;
|
|
10351
|
-
}
|
|
10352
|
-
return {
|
|
10353
|
-
server: ctx.server,
|
|
10354
|
-
progressToken: ctx.progressToken
|
|
10355
|
-
};
|
|
10356
|
-
}
|
|
10357
|
-
async function sendProgress(ctx, progress, total, message) {
|
|
10358
|
-
if (ctx === void 0) return;
|
|
10359
|
-
if (ctx.progressToken === void 0) return;
|
|
10360
|
-
try {
|
|
10361
|
-
const notification = {
|
|
10362
|
-
method: "notifications/progress",
|
|
10363
|
-
params: {
|
|
10364
|
-
progressToken: ctx.progressToken,
|
|
10365
|
-
progress,
|
|
10366
|
-
...total !== void 0 && { total },
|
|
10367
|
-
...message !== void 0 && message !== "" && { message }
|
|
10368
|
-
}
|
|
10369
|
-
};
|
|
10370
|
-
await ctx.server.notification(notification);
|
|
10371
|
-
} catch {
|
|
10372
|
-
}
|
|
10373
|
-
}
|
|
10374
|
-
|
|
10375
10818
|
// src/adapters/sqlite/tools/virtual/analysis.ts
|
|
10376
10819
|
function createDbStatTool(adapter) {
|
|
10377
10820
|
return {
|
|
@@ -10638,7 +11081,9 @@ function createVirtualTableInfoTool(adapter) {
|
|
|
10638
11081
|
module: "unknown",
|
|
10639
11082
|
sql: "",
|
|
10640
11083
|
error: `Virtual table '${input.tableName}' not found`,
|
|
10641
|
-
code: "TABLE_NOT_FOUND"
|
|
11084
|
+
code: "TABLE_NOT_FOUND",
|
|
11085
|
+
category: "resource",
|
|
11086
|
+
recoverable: false
|
|
10642
11087
|
};
|
|
10643
11088
|
}
|
|
10644
11089
|
const sqlStr = typeof sqlResult.rows[0]?.["sql"] === "string" ? sqlResult.rows[0]["sql"] : "";
|
|
@@ -10756,10 +11201,10 @@ function createCsvTableTool(adapter) {
|
|
|
10756
11201
|
try {
|
|
10757
11202
|
const input = CreateCsvTableSchema.parse(params);
|
|
10758
11203
|
const safeTableName = sanitizeIdentifier(input.tableName);
|
|
10759
|
-
if (!
|
|
11204
|
+
if (!nodePath.isAbsolute(input.filePath)) {
|
|
10760
11205
|
return {
|
|
10761
11206
|
success: false,
|
|
10762
|
-
error: `Relative path not supported. Please use an absolute path. Example: ${
|
|
11207
|
+
error: `Relative path not supported. Please use an absolute path. Example: ${nodePath.resolve(input.filePath)}`,
|
|
10763
11208
|
code: "VALIDATION_ERROR",
|
|
10764
11209
|
category: "validation",
|
|
10765
11210
|
message: "",
|
|
@@ -10767,20 +11212,37 @@ function createCsvTableTool(adapter) {
|
|
|
10767
11212
|
columns: []
|
|
10768
11213
|
};
|
|
10769
11214
|
}
|
|
10770
|
-
const
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
11215
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
11216
|
+
if (allowedIoRoots !== void 0) {
|
|
11217
|
+
try {
|
|
11218
|
+
assertSafeIoPath(input.filePath, allowedIoRoots, false);
|
|
11219
|
+
} catch (error) {
|
|
11220
|
+
return {
|
|
11221
|
+
success: false,
|
|
11222
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
11223
|
+
code: "SECURITY_ERROR",
|
|
11224
|
+
category: "security",
|
|
11225
|
+
message: "",
|
|
11226
|
+
sql: "",
|
|
11227
|
+
columns: []
|
|
11228
|
+
};
|
|
11229
|
+
}
|
|
11230
|
+
} else {
|
|
11231
|
+
const pathCheck = validateSameDirPath(
|
|
11232
|
+
input.filePath,
|
|
11233
|
+
adapter.getConfiguredPath()
|
|
11234
|
+
);
|
|
11235
|
+
if (!pathCheck.valid) {
|
|
11236
|
+
return {
|
|
11237
|
+
success: false,
|
|
11238
|
+
error: pathCheck.error,
|
|
11239
|
+
code: "SECURITY_ERROR",
|
|
11240
|
+
category: "security",
|
|
11241
|
+
message: "",
|
|
11242
|
+
sql: "",
|
|
11243
|
+
columns: []
|
|
11244
|
+
};
|
|
11245
|
+
}
|
|
10784
11246
|
}
|
|
10785
11247
|
const { available: csvAvailable } = await isCsvModuleAvailable(adapter);
|
|
10786
11248
|
if (!csvAvailable) {
|
|
@@ -10863,10 +11325,10 @@ function createAnalyzeCsvSchemaTool(adapter) {
|
|
|
10863
11325
|
columns: []
|
|
10864
11326
|
};
|
|
10865
11327
|
}
|
|
10866
|
-
if (!
|
|
11328
|
+
if (!nodePath.isAbsolute(input.filePath)) {
|
|
10867
11329
|
return {
|
|
10868
11330
|
success: false,
|
|
10869
|
-
error: `Relative path not supported. Please use an absolute path. Example: ${
|
|
11331
|
+
error: `Relative path not supported. Please use an absolute path. Example: ${nodePath.resolve(input.filePath)}`,
|
|
10870
11332
|
code: "VALIDATION_ERROR",
|
|
10871
11333
|
category: "validation",
|
|
10872
11334
|
hasHeader: false,
|
|
@@ -10874,20 +11336,37 @@ function createAnalyzeCsvSchemaTool(adapter) {
|
|
|
10874
11336
|
columns: []
|
|
10875
11337
|
};
|
|
10876
11338
|
}
|
|
10877
|
-
const
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
11339
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
11340
|
+
if (allowedIoRoots !== void 0) {
|
|
11341
|
+
try {
|
|
11342
|
+
assertSafeIoPath(input.filePath, allowedIoRoots, false);
|
|
11343
|
+
} catch (error) {
|
|
11344
|
+
return {
|
|
11345
|
+
success: false,
|
|
11346
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
11347
|
+
code: "SECURITY_ERROR",
|
|
11348
|
+
category: "security",
|
|
11349
|
+
hasHeader: false,
|
|
11350
|
+
rowCount: 0,
|
|
11351
|
+
columns: []
|
|
11352
|
+
};
|
|
11353
|
+
}
|
|
11354
|
+
} else {
|
|
11355
|
+
const validation = validateSameDirPath(
|
|
11356
|
+
input.filePath,
|
|
11357
|
+
adapter.getConfiguredPath()
|
|
11358
|
+
);
|
|
11359
|
+
if (!validation.valid) {
|
|
11360
|
+
return {
|
|
11361
|
+
success: false,
|
|
11362
|
+
error: validation.error || "Path validation failed",
|
|
11363
|
+
code: "SECURITY_ERROR",
|
|
11364
|
+
category: "security",
|
|
11365
|
+
hasHeader: false,
|
|
11366
|
+
rowCount: 0,
|
|
11367
|
+
columns: []
|
|
11368
|
+
};
|
|
11369
|
+
}
|
|
10891
11370
|
}
|
|
10892
11371
|
const tempName = `_csv_analyze_${Date.now()}`;
|
|
10893
11372
|
try {
|
|
@@ -11318,10 +11797,7 @@ function createVectorCreateTableTool(adapter) {
|
|
|
11318
11797
|
try {
|
|
11319
11798
|
const input = VectorCreateTableSchema.parse(params);
|
|
11320
11799
|
if (input.dimensions < 1) {
|
|
11321
|
-
|
|
11322
|
-
success: false,
|
|
11323
|
-
error: "Dimensions must be at least 1"
|
|
11324
|
-
};
|
|
11800
|
+
throw new ValidationError("Dimensions must be at least 1");
|
|
11325
11801
|
}
|
|
11326
11802
|
const tableName = sanitizeIdentifier(input.tableName);
|
|
11327
11803
|
const columns = [
|
|
@@ -11407,15 +11883,15 @@ function createVectorStoreTool(adapter) {
|
|
|
11407
11883
|
try {
|
|
11408
11884
|
const input = VectorStoreSchema.parse(params);
|
|
11409
11885
|
if (input.vector.length === 0) {
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
};
|
|
11886
|
+
throw new ValidationError(
|
|
11887
|
+
"vector is required and must be a non-empty array of numbers"
|
|
11888
|
+
);
|
|
11414
11889
|
}
|
|
11415
11890
|
const table = sanitizeIdentifier(input.table);
|
|
11416
11891
|
const idColumn = sanitizeIdentifier(input.idColumn);
|
|
11417
11892
|
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
11418
11893
|
let hasDimsColumn = false;
|
|
11894
|
+
let expectedDims;
|
|
11419
11895
|
try {
|
|
11420
11896
|
const dims = await validateDimensions(
|
|
11421
11897
|
adapter,
|
|
@@ -11424,15 +11900,15 @@ function createVectorStoreTool(adapter) {
|
|
|
11424
11900
|
input.vectorColumn
|
|
11425
11901
|
);
|
|
11426
11902
|
hasDimsColumn = dims.hasDimsColumn;
|
|
11427
|
-
|
|
11428
|
-
return {
|
|
11429
|
-
success: false,
|
|
11430
|
-
error: `Dimension mismatch: vector has ${input.vector.length} dimensions but table expects ${dims.expectedDims}`,
|
|
11431
|
-
code: "DIMENSION_MISMATCH"
|
|
11432
|
-
};
|
|
11433
|
-
}
|
|
11903
|
+
expectedDims = dims.expectedDims;
|
|
11434
11904
|
} catch {
|
|
11435
11905
|
}
|
|
11906
|
+
if (expectedDims !== void 0 && input.vector.length !== expectedDims) {
|
|
11907
|
+
throw new ValidationError(
|
|
11908
|
+
`Dimension mismatch: vector has ${input.vector.length} dimensions but table expects ${expectedDims}`,
|
|
11909
|
+
"DIMENSION_MISMATCH"
|
|
11910
|
+
);
|
|
11911
|
+
}
|
|
11436
11912
|
const vectorJson = JSON.stringify(input.vector);
|
|
11437
11913
|
const updateSql = `UPDATE ${table} SET ${vectorColumn} = ? WHERE ${idColumn} = ?`;
|
|
11438
11914
|
const updateResult = await adapter.executeWriteQuery(updateSql, [
|
|
@@ -11474,10 +11950,11 @@ function createVectorBatchStoreTool(adapter) {
|
|
|
11474
11950
|
input.table
|
|
11475
11951
|
]);
|
|
11476
11952
|
if (!checkResult.rows || checkResult.rows.length === 0) {
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11953
|
+
throw new ResourceNotFoundError(
|
|
11954
|
+
`Table '${input.table}' does not exist`,
|
|
11955
|
+
"TABLE_NOT_FOUND",
|
|
11956
|
+
{ resourceType: "table", resourceName: input.table }
|
|
11957
|
+
);
|
|
11481
11958
|
}
|
|
11482
11959
|
return {
|
|
11483
11960
|
success: true,
|
|
@@ -11488,6 +11965,7 @@ function createVectorBatchStoreTool(adapter) {
|
|
|
11488
11965
|
const idColumn = sanitizeIdentifier(input.idColumn);
|
|
11489
11966
|
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
11490
11967
|
let hasDimsColumn = false;
|
|
11968
|
+
let expectedDims;
|
|
11491
11969
|
try {
|
|
11492
11970
|
const dims = await validateDimensions(
|
|
11493
11971
|
adapter,
|
|
@@ -11496,19 +11974,19 @@ function createVectorBatchStoreTool(adapter) {
|
|
|
11496
11974
|
input.vectorColumn
|
|
11497
11975
|
);
|
|
11498
11976
|
hasDimsColumn = dims.hasDimsColumn;
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
}
|
|
11508
|
-
|
|
11977
|
+
expectedDims = dims.expectedDims;
|
|
11978
|
+
} catch {
|
|
11979
|
+
}
|
|
11980
|
+
if (expectedDims !== void 0) {
|
|
11981
|
+
for (let i = 0; i < input.items.length; i++) {
|
|
11982
|
+
const item = input.items[i];
|
|
11983
|
+
if (item && item.vector.length !== expectedDims) {
|
|
11984
|
+
throw new ValidationError(
|
|
11985
|
+
`Dimension mismatch at item[${i}]: vector has ${item.vector.length} dimensions but table expects ${expectedDims}`,
|
|
11986
|
+
"DIMENSION_MISMATCH"
|
|
11987
|
+
);
|
|
11509
11988
|
}
|
|
11510
11989
|
}
|
|
11511
|
-
} catch {
|
|
11512
11990
|
}
|
|
11513
11991
|
let stored = 0;
|
|
11514
11992
|
for (const item of input.items) {
|
|
@@ -11578,10 +12056,9 @@ function createVectorSearchTool(adapter) {
|
|
|
11578
12056
|
try {
|
|
11579
12057
|
const input = VectorSearchSchema.parse(params);
|
|
11580
12058
|
if (input.queryVector.length === 0) {
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
};
|
|
12059
|
+
throw new ValidationError(
|
|
12060
|
+
"queryVector is required and must be a non-empty array of numbers"
|
|
12061
|
+
);
|
|
11585
12062
|
}
|
|
11586
12063
|
const table = sanitizeIdentifier(input.table);
|
|
11587
12064
|
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
@@ -11629,10 +12106,9 @@ function createVectorSearchTool(adapter) {
|
|
|
11629
12106
|
score = cosineSimilarity(queryVector, storedVector);
|
|
11630
12107
|
break;
|
|
11631
12108
|
default:
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
};
|
|
12109
|
+
throw new ValidationError(
|
|
12110
|
+
`Invalid metric '${input.metric}'. Valid values: cosine, euclidean, dot`
|
|
12111
|
+
);
|
|
11636
12112
|
}
|
|
11637
12113
|
scored.push({
|
|
11638
12114
|
...row,
|
|
@@ -11700,26 +12176,17 @@ function createVectorGetTool(adapter) {
|
|
|
11700
12176
|
const sql = `SELECT * FROM ${table} WHERE ${idColumn} = ?`;
|
|
11701
12177
|
const result = await adapter.executeReadQuery(sql, [input.id]);
|
|
11702
12178
|
if (!result.rows || result.rows.length === 0) {
|
|
11703
|
-
|
|
11704
|
-
success: false,
|
|
11705
|
-
error: "Vector not found",
|
|
11706
|
-
code: "VECTOR_NOT_FOUND"
|
|
11707
|
-
};
|
|
12179
|
+
throw new ResourceNotFoundError("Vector not found", "VECTOR_NOT_FOUND", { resourceType: "vector", resourceName: String(input.id) });
|
|
11708
12180
|
}
|
|
11709
12181
|
const row = result.rows[0];
|
|
11710
12182
|
if (!row) {
|
|
11711
|
-
|
|
11712
|
-
success: false,
|
|
11713
|
-
error: "Vector not found",
|
|
11714
|
-
code: "VECTOR_NOT_FOUND"
|
|
11715
|
-
};
|
|
12183
|
+
throw new ResourceNotFoundError("Vector not found", "VECTOR_NOT_FOUND", { resourceType: "vector", resourceName: String(input.id) });
|
|
11716
12184
|
}
|
|
11717
12185
|
const rawVector = row[input.vectorColumn];
|
|
11718
12186
|
if (rawVector === void 0 || rawVector === null) {
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
};
|
|
12187
|
+
throw new ValidationError(
|
|
12188
|
+
`Column '${input.vectorColumn}' not found or contains NULL. Available columns: ${Object.keys(row).join(", ")}`
|
|
12189
|
+
);
|
|
11723
12190
|
}
|
|
11724
12191
|
const vectorData = parseVector(rawVector);
|
|
11725
12192
|
const metadata = Object.fromEntries(
|
|
@@ -11891,13 +12358,9 @@ function createVectorNormalizeTool() {
|
|
|
11891
12358
|
try {
|
|
11892
12359
|
const input = VectorNormalizeSchema.parse(params);
|
|
11893
12360
|
if (input.vector.length === 0) {
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
code: "VALIDATION_ERROR",
|
|
11898
|
-
category: "validation",
|
|
11899
|
-
recoverable: false
|
|
11900
|
-
});
|
|
12361
|
+
throw new ValidationError(
|
|
12362
|
+
"vector is required and must be a non-empty array of numbers"
|
|
12363
|
+
);
|
|
11901
12364
|
}
|
|
11902
12365
|
const normalized = normalizeVector(input.vector);
|
|
11903
12366
|
return Promise.resolve({
|
|
@@ -11925,22 +12388,14 @@ function createVectorDistanceTool() {
|
|
|
11925
12388
|
annotations: readOnly("Vector Distance"),
|
|
11926
12389
|
handler: (params, _context) => {
|
|
11927
12390
|
try {
|
|
11928
|
-
const input = VectorDistanceSchema.parse(params);
|
|
11929
|
-
if (input.vector1.length === 0 || input.vector2.length === 0) {
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
code: "VALIDATION_ERROR",
|
|
11934
|
-
category: "validation",
|
|
11935
|
-
recoverable: false
|
|
11936
|
-
});
|
|
12391
|
+
const input = VectorDistanceSchema.parse(params);
|
|
12392
|
+
if (input.vector1.length === 0 || input.vector2.length === 0) {
|
|
12393
|
+
throw new ValidationError(
|
|
12394
|
+
"vector1 and vector2 are required and must be non-empty arrays of numbers"
|
|
12395
|
+
);
|
|
11937
12396
|
}
|
|
11938
12397
|
if (input.vector1.length !== input.vector2.length) {
|
|
11939
|
-
|
|
11940
|
-
success: false,
|
|
11941
|
-
error: "Vector dimensions must match",
|
|
11942
|
-
code: "DIMENSION_MISMATCH"
|
|
11943
|
-
});
|
|
12398
|
+
throw new ValidationError("Vector dimensions must match", "DIMENSION_MISMATCH");
|
|
11944
12399
|
}
|
|
11945
12400
|
let result;
|
|
11946
12401
|
switch (input.metric) {
|
|
@@ -11954,13 +12409,9 @@ function createVectorDistanceTool() {
|
|
|
11954
12409
|
result = 1 - cosineSimilarity(input.vector1, input.vector2);
|
|
11955
12410
|
break;
|
|
11956
12411
|
default:
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
code: "VALIDATION_ERROR",
|
|
11961
|
-
category: "validation",
|
|
11962
|
-
recoverable: false
|
|
11963
|
-
});
|
|
12412
|
+
throw new ValidationError(
|
|
12413
|
+
`Invalid metric '${input.metric}'. Valid values: cosine, euclidean, dot`
|
|
12414
|
+
);
|
|
11964
12415
|
}
|
|
11965
12416
|
return Promise.resolve({
|
|
11966
12417
|
success: true,
|
|
@@ -12103,14 +12554,14 @@ function requireCoordinate(value, name, min, max) {
|
|
|
12103
12554
|
throw new DbMcpError(
|
|
12104
12555
|
`Invalid ${name}: value is not a valid number.`,
|
|
12105
12556
|
"GEO_INVALID_COORDINATES",
|
|
12106
|
-
|
|
12557
|
+
ErrorCategory.VALIDATION
|
|
12107
12558
|
);
|
|
12108
12559
|
}
|
|
12109
12560
|
if (value < min || value > max) {
|
|
12110
12561
|
throw new DbMcpError(
|
|
12111
12562
|
`Invalid ${name}: ${String(value)}. Must be between ${String(min)} and ${String(max)}.`,
|
|
12112
12563
|
"GEO_INVALID_COORDINATES",
|
|
12113
|
-
|
|
12564
|
+
ErrorCategory.VALIDATION
|
|
12114
12565
|
);
|
|
12115
12566
|
}
|
|
12116
12567
|
return value;
|
|
@@ -12120,7 +12571,7 @@ function requireNumber(value, name) {
|
|
|
12120
12571
|
throw new DbMcpError(
|
|
12121
12572
|
`Invalid ${name}: value is not a valid number.`,
|
|
12122
12573
|
"GEO_INVALID_COORDINATES",
|
|
12123
|
-
|
|
12574
|
+
ErrorCategory.VALIDATION
|
|
12124
12575
|
);
|
|
12125
12576
|
}
|
|
12126
12577
|
return value;
|
|
@@ -12396,19 +12847,35 @@ function createBackupTool(adapter) {
|
|
|
12396
12847
|
path: ""
|
|
12397
12848
|
};
|
|
12398
12849
|
}
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12850
|
+
let resolvedPath;
|
|
12851
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
12852
|
+
if (allowedIoRoots !== void 0) {
|
|
12853
|
+
try {
|
|
12854
|
+
assertSafeIoPath(input.targetPath, allowedIoRoots);
|
|
12855
|
+
resolvedPath = nodePath__default.resolve(input.targetPath);
|
|
12856
|
+
} catch (error) {
|
|
12857
|
+
return {
|
|
12858
|
+
success: false,
|
|
12859
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
12860
|
+
code: "SECURITY_ERROR",
|
|
12861
|
+
path: input.targetPath
|
|
12862
|
+
};
|
|
12863
|
+
}
|
|
12864
|
+
} else {
|
|
12865
|
+
const pathCheck = validateSameDirPath(
|
|
12866
|
+
input.targetPath,
|
|
12867
|
+
adapter.getConfiguredPath()
|
|
12868
|
+
);
|
|
12869
|
+
if (input.targetPath.includes("..") || !pathCheck.valid) {
|
|
12870
|
+
return {
|
|
12871
|
+
success: false,
|
|
12872
|
+
error: !pathCheck.valid ? pathCheck.error : "Invalid path: must not contain '..'",
|
|
12873
|
+
code: "SECURITY_ERROR",
|
|
12874
|
+
path: input.targetPath
|
|
12875
|
+
};
|
|
12876
|
+
}
|
|
12877
|
+
resolvedPath = pathCheck.resolvedPath;
|
|
12410
12878
|
}
|
|
12411
|
-
const resolvedPath = pathCheck.resolvedPath;
|
|
12412
12879
|
const escapedPath = resolvedPath.replace(/'/g, "''");
|
|
12413
12880
|
const sql = `VACUUM INTO '${escapedPath}'`;
|
|
12414
12881
|
const start = Date.now();
|
|
@@ -12418,7 +12885,7 @@ function createBackupTool(adapter) {
|
|
|
12418
12885
|
progress,
|
|
12419
12886
|
1,
|
|
12420
12887
|
2,
|
|
12421
|
-
`Creating backup at ${
|
|
12888
|
+
`Creating backup at ${nodePath__default.basename(resolvedPath)}...`
|
|
12422
12889
|
);
|
|
12423
12890
|
await adapter.executeQuery(sql);
|
|
12424
12891
|
await sendProgress(progress, 2, 2, "Backup complete");
|
|
@@ -12468,18 +12935,34 @@ function createVacuumIntoTool(adapter) {
|
|
|
12468
12935
|
...formatHandlerError(new ValidationError("outputPath is required"))
|
|
12469
12936
|
};
|
|
12470
12937
|
}
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12938
|
+
let resolvedPath;
|
|
12939
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
12940
|
+
if (allowedIoRoots !== void 0) {
|
|
12941
|
+
try {
|
|
12942
|
+
assertSafeIoPath(input.outputPath, allowedIoRoots);
|
|
12943
|
+
resolvedPath = nodePath__default.resolve(input.outputPath);
|
|
12944
|
+
} catch (error) {
|
|
12945
|
+
return {
|
|
12946
|
+
success: false,
|
|
12947
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
12948
|
+
code: "SECURITY_ERROR",
|
|
12949
|
+
outputPath: input.outputPath
|
|
12950
|
+
};
|
|
12951
|
+
}
|
|
12952
|
+
} else {
|
|
12953
|
+
const pathCheck = validateSameDirPath(
|
|
12954
|
+
input.outputPath,
|
|
12955
|
+
adapter.getConfiguredPath()
|
|
12956
|
+
);
|
|
12957
|
+
if (input.outputPath.includes("..") || !pathCheck.valid) {
|
|
12958
|
+
return {
|
|
12959
|
+
success: false,
|
|
12960
|
+
error: !pathCheck.valid ? pathCheck.error : "Invalid path: must not contain '..'",
|
|
12961
|
+
code: "SECURITY_ERROR"
|
|
12962
|
+
};
|
|
12963
|
+
}
|
|
12964
|
+
resolvedPath = pathCheck.resolvedPath;
|
|
12481
12965
|
}
|
|
12482
|
-
const resolvedPath = pathCheck.resolvedPath;
|
|
12483
12966
|
const escapedPath = resolvedPath.replace(/'/g, "''");
|
|
12484
12967
|
const sql = `VACUUM INTO '${escapedPath}'`;
|
|
12485
12968
|
const start = Date.now();
|
|
@@ -12706,26 +13189,53 @@ function createRestoreTool(adapter) {
|
|
|
12706
13189
|
sourcePath: ""
|
|
12707
13190
|
};
|
|
12708
13191
|
}
|
|
12709
|
-
|
|
12710
|
-
const
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
}
|
|
13192
|
+
let escapedPath;
|
|
13193
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
13194
|
+
if (allowedIoRoots !== void 0) {
|
|
13195
|
+
try {
|
|
13196
|
+
assertSafeIoPath(input.sourcePath, allowedIoRoots);
|
|
13197
|
+
const resolvedPath = nodePath__default.resolve(input.sourcePath);
|
|
13198
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
13199
|
+
return {
|
|
13200
|
+
...formatHandlerError(
|
|
13201
|
+
new ValidationError(
|
|
13202
|
+
`Source file not found: ${input.sourcePath}`
|
|
13203
|
+
)
|
|
13204
|
+
),
|
|
13205
|
+
sourcePath: input.sourcePath
|
|
13206
|
+
};
|
|
13207
|
+
}
|
|
13208
|
+
escapedPath = resolvedPath.replace(/'/g, "''");
|
|
13209
|
+
} catch (error) {
|
|
13210
|
+
return {
|
|
13211
|
+
success: false,
|
|
13212
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
13213
|
+
code: "SECURITY_ERROR",
|
|
13214
|
+
sourcePath: input.sourcePath
|
|
13215
|
+
};
|
|
13216
|
+
}
|
|
13217
|
+
} else {
|
|
13218
|
+
const resolvedPath = nodePath__default.resolve(input.sourcePath);
|
|
13219
|
+
const pathCheck = validateSameDirPath(
|
|
13220
|
+
input.sourcePath,
|
|
13221
|
+
adapter.getConfiguredPath()
|
|
13222
|
+
);
|
|
13223
|
+
if (!pathCheck.valid) {
|
|
13224
|
+
return {
|
|
13225
|
+
...formatHandlerError(new ValidationError(pathCheck.error)),
|
|
13226
|
+
sourcePath: input.sourcePath
|
|
13227
|
+
};
|
|
13228
|
+
}
|
|
13229
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
13230
|
+
return {
|
|
13231
|
+
...formatHandlerError(
|
|
13232
|
+
new ValidationError(`Source file not found: ${input.sourcePath}`)
|
|
13233
|
+
),
|
|
13234
|
+
sourcePath: input.sourcePath
|
|
13235
|
+
};
|
|
13236
|
+
}
|
|
13237
|
+
escapedPath = pathCheck.resolvedPath.replace(/'/g, "''");
|
|
12727
13238
|
}
|
|
12728
|
-
const escapedPath = pathCheck.resolvedPath.replace(/'/g, "''");
|
|
12729
13239
|
await adapter.executeReadQuery("PRAGMA integrity_check(1)");
|
|
12730
13240
|
await sendProgress(progress, 2, 5, "Attaching backup database...");
|
|
12731
13241
|
try {
|
|
@@ -12938,21 +13448,37 @@ function createDumpTool(adapter) {
|
|
|
12938
13448
|
...formatHandlerError(new ValidationError("outputPath is required"))
|
|
12939
13449
|
};
|
|
12940
13450
|
}
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
13451
|
+
let resolvedPath;
|
|
13452
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
13453
|
+
if (allowedIoRoots !== void 0) {
|
|
13454
|
+
try {
|
|
13455
|
+
assertSafeIoPath(input.outputPath, allowedIoRoots);
|
|
13456
|
+
resolvedPath = nodePath__default.resolve(input.outputPath);
|
|
13457
|
+
} catch (error) {
|
|
13458
|
+
return {
|
|
13459
|
+
success: false,
|
|
13460
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
13461
|
+
code: "SECURITY_ERROR",
|
|
13462
|
+
path: input.outputPath
|
|
13463
|
+
};
|
|
13464
|
+
}
|
|
13465
|
+
} else {
|
|
13466
|
+
const pathCheck = validateSameDirPath(
|
|
13467
|
+
input.outputPath,
|
|
13468
|
+
adapter.getConfiguredPath()
|
|
13469
|
+
);
|
|
13470
|
+
if (input.outputPath.includes("..") || !pathCheck.valid) {
|
|
13471
|
+
return {
|
|
13472
|
+
success: false,
|
|
13473
|
+
error: !pathCheck.valid ? pathCheck.error : "Invalid path: must not contain '..'",
|
|
13474
|
+
code: "SECURITY_ERROR",
|
|
13475
|
+
path: input.outputPath
|
|
13476
|
+
};
|
|
13477
|
+
}
|
|
13478
|
+
resolvedPath = pathCheck.resolvedPath;
|
|
12952
13479
|
}
|
|
12953
13480
|
const progress = buildProgressContext(context);
|
|
12954
13481
|
const start = Date.now();
|
|
12955
|
-
const resolvedPath = pathCheck.resolvedPath;
|
|
12956
13482
|
await sendProgress(progress, 1, 4, "Preparing dump file...");
|
|
12957
13483
|
const stream = fs.createWriteStream(resolvedPath, { encoding: "utf8" });
|
|
12958
13484
|
try {
|
|
@@ -13058,29 +13584,57 @@ function createVerifyBackupTool(adapter) {
|
|
|
13058
13584
|
backupPath: ""
|
|
13059
13585
|
};
|
|
13060
13586
|
}
|
|
13061
|
-
|
|
13062
|
-
const
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
|
|
13080
|
-
|
|
13081
|
-
|
|
13587
|
+
let escapedPath;
|
|
13588
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
13589
|
+
if (allowedIoRoots !== void 0) {
|
|
13590
|
+
try {
|
|
13591
|
+
assertSafeIoPath(input.backupPath, allowedIoRoots);
|
|
13592
|
+
const resolvedPath = nodePath__default.resolve(input.backupPath);
|
|
13593
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
13594
|
+
return {
|
|
13595
|
+
...formatHandlerError(
|
|
13596
|
+
new ValidationError(
|
|
13597
|
+
`Backup file not found: ${input.backupPath}`,
|
|
13598
|
+
"FILE_NOT_FOUND"
|
|
13599
|
+
)
|
|
13600
|
+
),
|
|
13601
|
+
backupPath: input.backupPath
|
|
13602
|
+
};
|
|
13603
|
+
}
|
|
13604
|
+
escapedPath = resolvedPath.replace(/'/g, "''");
|
|
13605
|
+
} catch (error) {
|
|
13606
|
+
return {
|
|
13607
|
+
success: false,
|
|
13608
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
13609
|
+
code: "SECURITY_ERROR",
|
|
13610
|
+
backupPath: input.backupPath
|
|
13611
|
+
};
|
|
13612
|
+
}
|
|
13613
|
+
} else {
|
|
13614
|
+
const resolvedPath = nodePath__default.resolve(input.backupPath);
|
|
13615
|
+
const pathCheck = validateSameDirPath(
|
|
13616
|
+
input.backupPath,
|
|
13617
|
+
adapter.getConfiguredPath()
|
|
13618
|
+
);
|
|
13619
|
+
if (!pathCheck.valid) {
|
|
13620
|
+
return {
|
|
13621
|
+
...formatHandlerError(new ValidationError(pathCheck.error)),
|
|
13622
|
+
backupPath: input.backupPath
|
|
13623
|
+
};
|
|
13624
|
+
}
|
|
13625
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
13626
|
+
return {
|
|
13627
|
+
...formatHandlerError(
|
|
13628
|
+
new ValidationError(
|
|
13629
|
+
`Backup file not found: ${input.backupPath}`,
|
|
13630
|
+
"FILE_NOT_FOUND"
|
|
13631
|
+
)
|
|
13632
|
+
),
|
|
13633
|
+
backupPath: input.backupPath
|
|
13634
|
+
};
|
|
13635
|
+
}
|
|
13636
|
+
escapedPath = resolvedPath.replace(/'/g, "''");
|
|
13082
13637
|
}
|
|
13083
|
-
const escapedPath = resolvedPath.replace(/'/g, "''");
|
|
13084
13638
|
try {
|
|
13085
13639
|
await adapter.rawQuery(
|
|
13086
13640
|
`ATTACH DATABASE '${escapedPath}' AS backup_verify`
|
|
@@ -13453,18 +14007,37 @@ function createAttachDatabaseTool(adapter) {
|
|
|
13453
14007
|
code: "VALIDATION_ERROR"
|
|
13454
14008
|
};
|
|
13455
14009
|
}
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
)
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
}
|
|
14010
|
+
let escapedPath;
|
|
14011
|
+
let finalResolvedPath;
|
|
14012
|
+
const allowedIoRoots = adapter.getAllowedIoRoots();
|
|
14013
|
+
if (allowedIoRoots !== void 0) {
|
|
14014
|
+
try {
|
|
14015
|
+
assertSafeIoPath(input.filepath, allowedIoRoots);
|
|
14016
|
+
const resolvedPath = input.filepath;
|
|
14017
|
+
escapedPath = resolvedPath.replace(/'/g, "''");
|
|
14018
|
+
finalResolvedPath = resolvedPath;
|
|
14019
|
+
} catch (error) {
|
|
14020
|
+
return {
|
|
14021
|
+
success: false,
|
|
14022
|
+
error: error instanceof Error ? error.message : "Security error",
|
|
14023
|
+
code: "SECURITY_ERROR"
|
|
14024
|
+
};
|
|
14025
|
+
}
|
|
14026
|
+
} else {
|
|
14027
|
+
const pathCheck = validateSameDirPath(
|
|
14028
|
+
input.filepath,
|
|
14029
|
+
adapter.getConfiguredPath()
|
|
14030
|
+
);
|
|
14031
|
+
if (!pathCheck.valid) {
|
|
14032
|
+
return {
|
|
14033
|
+
success: false,
|
|
14034
|
+
error: pathCheck.error,
|
|
14035
|
+
code: "SECURITY_ERROR"
|
|
14036
|
+
};
|
|
14037
|
+
}
|
|
14038
|
+
escapedPath = pathCheck.resolvedPath.replace(/'/g, "''");
|
|
14039
|
+
finalResolvedPath = pathCheck.resolvedPath;
|
|
13466
14040
|
}
|
|
13467
|
-
const escapedPath = pathCheck.resolvedPath.replace(/'/g, "''");
|
|
13468
14041
|
await adapter.rawQuery(
|
|
13469
14042
|
`ATTACH DATABASE '${escapedPath}' AS "${input.alias.replace(/"/g, '""')}"`
|
|
13470
14043
|
);
|
|
@@ -13472,7 +14045,7 @@ function createAttachDatabaseTool(adapter) {
|
|
|
13472
14045
|
success: true,
|
|
13473
14046
|
message: `Database attached as '${input.alias}'`,
|
|
13474
14047
|
alias: input.alias,
|
|
13475
|
-
filepath:
|
|
14048
|
+
filepath: finalResolvedPath.split(/[\\/]/).pop() ?? ""
|
|
13476
14049
|
};
|
|
13477
14050
|
} catch (error) {
|
|
13478
14051
|
return formatHandlerError(error);
|
|
@@ -14034,13 +14607,13 @@ var IndexAuditSchema = z.object({
|
|
|
14034
14607
|
}).default({ recommendComposite: false });
|
|
14035
14608
|
var SchemaSnapshotSchema = z.object({
|
|
14036
14609
|
sections: z.array(z.enum(["tables", "views", "indexes", "triggers"])).optional().describe("Specific sections to include (default: all)"),
|
|
14037
|
-
compact: z.boolean().optional().describe(
|
|
14038
|
-
"Omit column details from tables section for reduced payload (default:
|
|
14610
|
+
compact: z.boolean().optional().default(true).describe(
|
|
14611
|
+
"Omit column details from tables section for reduced payload (default: true)"
|
|
14039
14612
|
),
|
|
14040
14613
|
excludeSystemTables: z.boolean().optional().describe(
|
|
14041
14614
|
"Exclude SpatiaLite system tables, views, indexes, and triggers (default: true)"
|
|
14042
14615
|
)
|
|
14043
|
-
}).default({});
|
|
14616
|
+
}).default({ compact: true });
|
|
14044
14617
|
var MigrationRisksSchema = z.object({
|
|
14045
14618
|
statements: z.array(z.string()).optional().default([]).describe("Array of DDL statements to analyze for risks")
|
|
14046
14619
|
}).default({ statements: [] });
|
|
@@ -14059,6 +14632,7 @@ var ConstraintAnalysisSchema = z.object({
|
|
|
14059
14632
|
)
|
|
14060
14633
|
}).default({});
|
|
14061
14634
|
var DependencyGraphSchema = z.object({
|
|
14635
|
+
table: z.string().optional().describe("Optional table name to filter the graph"),
|
|
14062
14636
|
includeRowCounts: z.boolean().optional().describe("Include row counts per table (default: true)"),
|
|
14063
14637
|
nodesOnly: z.boolean().optional().describe(
|
|
14064
14638
|
"Return only nodes without edges for a lightweight response (default: false)"
|
|
@@ -14077,7 +14651,10 @@ var TopologicalSortSchema = z.object({
|
|
|
14077
14651
|
}).default({});
|
|
14078
14652
|
var CascadeSimulatorSchema = z.object({
|
|
14079
14653
|
table: z.string().optional().default("").describe("Table name to simulate deletion from"),
|
|
14080
|
-
operation: z.
|
|
14654
|
+
operation: z.preprocess(
|
|
14655
|
+
(val) => typeof val === "string" ? val.toUpperCase() : val,
|
|
14656
|
+
z.enum(["DELETE", "DROP", "TRUNCATE"])
|
|
14657
|
+
).optional().describe("Operation to simulate (default: DELETE)"),
|
|
14081
14658
|
compact: z.boolean().optional().describe(
|
|
14082
14659
|
"Omit path arrays from affected entries to reduce payload (default: false)"
|
|
14083
14660
|
)
|
|
@@ -14144,10 +14721,16 @@ var SchemaDiffOutputSchema = z.object({
|
|
|
14144
14721
|
comparedAt: z.string().optional()
|
|
14145
14722
|
}).extend(ErrorResponseFields.shape);
|
|
14146
14723
|
var SchemaDiffSchema = z.object({
|
|
14147
|
-
baseline: z.
|
|
14724
|
+
baseline: z.preprocess(
|
|
14725
|
+
(val) => Array.isArray(val) ? val.length === 0 ? {} : { tables: val } : val,
|
|
14726
|
+
z.union([z.literal("current"), SchemaSnapshotShape])
|
|
14727
|
+
).optional().describe(
|
|
14148
14728
|
"Baseline schema \u2014 'current' to snapshot live DB, or an inline snapshot object from a previous sqlite_schema_snapshot call"
|
|
14149
14729
|
),
|
|
14150
|
-
target: z.
|
|
14730
|
+
target: z.preprocess(
|
|
14731
|
+
(val) => Array.isArray(val) ? val.length === 0 ? {} : { tables: val } : val,
|
|
14732
|
+
z.union([z.literal("current"), SchemaSnapshotShape])
|
|
14733
|
+
).optional().describe(
|
|
14151
14734
|
"Target schema to compare against baseline \u2014 'current' to snapshot live DB, or an inline snapshot object"
|
|
14152
14735
|
),
|
|
14153
14736
|
sections: z.array(z.enum(["tables", "views", "indexes", "triggers"])).optional().describe("Sections to compare (default: all)"),
|
|
@@ -14169,42 +14752,80 @@ function createDependencyGraphTool(adapter) {
|
|
|
14169
14752
|
handler: async (params, _context) => {
|
|
14170
14753
|
try {
|
|
14171
14754
|
const input = DependencyGraphSchema.parse(params);
|
|
14755
|
+
if (input.table) {
|
|
14756
|
+
const tableCheck = await adapter.executeReadQuery(
|
|
14757
|
+
`SELECT 1 FROM sqlite_master WHERE type='table' AND name=?`,
|
|
14758
|
+
[input.table]
|
|
14759
|
+
);
|
|
14760
|
+
if ((tableCheck.rows?.length ?? 0) === 0) {
|
|
14761
|
+
return {
|
|
14762
|
+
success: false,
|
|
14763
|
+
error: `Table '${input.table}' does not exist`,
|
|
14764
|
+
code: "TABLE_NOT_FOUND",
|
|
14765
|
+
category: "resource",
|
|
14766
|
+
suggestion: "Table not found. Run sqlite_list_tables to see available tables.",
|
|
14767
|
+
recoverable: false
|
|
14768
|
+
};
|
|
14769
|
+
}
|
|
14770
|
+
}
|
|
14172
14771
|
const includeRowCounts = input.includeRowCounts !== false;
|
|
14173
14772
|
const { nodes, edges } = await buildForeignKeyGraph(adapter, {
|
|
14174
14773
|
excludeSystemTables: input.excludeSystemTables,
|
|
14175
14774
|
includeRowCounts
|
|
14176
14775
|
});
|
|
14776
|
+
let finalNodes = nodes;
|
|
14777
|
+
let finalEdges = edges;
|
|
14778
|
+
if (input.table) {
|
|
14779
|
+
const connected = /* @__PURE__ */ new Set([input.table]);
|
|
14780
|
+
let added = true;
|
|
14781
|
+
while (added) {
|
|
14782
|
+
added = false;
|
|
14783
|
+
for (const e of edges) {
|
|
14784
|
+
if (connected.has(e.from) && !connected.has(e.to)) {
|
|
14785
|
+
connected.add(e.to);
|
|
14786
|
+
added = true;
|
|
14787
|
+
} else if (connected.has(e.to) && !connected.has(e.from)) {
|
|
14788
|
+
connected.add(e.from);
|
|
14789
|
+
added = true;
|
|
14790
|
+
}
|
|
14791
|
+
}
|
|
14792
|
+
}
|
|
14793
|
+
finalNodes = nodes.filter((n) => connected.has(n.table));
|
|
14794
|
+
finalEdges = edges.filter(
|
|
14795
|
+
(e) => connected.has(e.from) && connected.has(e.to)
|
|
14796
|
+
);
|
|
14797
|
+
}
|
|
14177
14798
|
const adjacency = /* @__PURE__ */ new Map();
|
|
14178
|
-
for (const node of
|
|
14799
|
+
for (const node of finalNodes) {
|
|
14179
14800
|
adjacency.set(node.table, []);
|
|
14180
14801
|
}
|
|
14181
|
-
for (const edge of
|
|
14802
|
+
for (const edge of finalEdges) {
|
|
14182
14803
|
adjacency.get(edge.from)?.push(edge.to);
|
|
14183
14804
|
}
|
|
14184
14805
|
const cycles = detectCycles(
|
|
14185
14806
|
adjacency,
|
|
14186
|
-
|
|
14807
|
+
finalNodes.map((n) => n.table)
|
|
14187
14808
|
);
|
|
14188
|
-
const referencedTables = new Set(
|
|
14189
|
-
const referencingTables = new Set(
|
|
14190
|
-
const rootTables =
|
|
14809
|
+
const referencedTables = new Set(finalEdges.map((e) => e.to));
|
|
14810
|
+
const referencingTables = new Set(finalEdges.map((e) => e.from));
|
|
14811
|
+
const rootTables = finalNodes.filter(
|
|
14191
14812
|
(n) => referencedTables.has(n.table) && !referencingTables.has(n.table)
|
|
14192
14813
|
).map((n) => n.table);
|
|
14193
|
-
const leafTables =
|
|
14814
|
+
const leafTables = finalNodes.filter(
|
|
14194
14815
|
(n) => referencingTables.has(n.table) && !referencedTables.has(n.table)
|
|
14195
14816
|
).map((n) => n.table);
|
|
14196
14817
|
const nodesOnly = input.nodesOnly === true;
|
|
14197
14818
|
return {
|
|
14198
14819
|
success: true,
|
|
14199
|
-
nodes:
|
|
14820
|
+
nodes: finalNodes.map((n) => ({
|
|
14200
14821
|
table: n.table,
|
|
14201
14822
|
...includeRowCounts ? { rowCount: n.rowCount } : {}
|
|
14202
14823
|
})),
|
|
14203
|
-
...!nodesOnly ? { edges } : {},
|
|
14824
|
+
...!nodesOnly ? { edges: finalEdges } : {},
|
|
14204
14825
|
circularDependencies: cycles.length > 0 ? cycles : void 0,
|
|
14205
14826
|
stats: {
|
|
14206
|
-
totalTables:
|
|
14207
|
-
totalRelationships:
|
|
14827
|
+
totalTables: finalNodes.length,
|
|
14828
|
+
totalRelationships: finalEdges.length,
|
|
14208
14829
|
rootTables,
|
|
14209
14830
|
leafTables
|
|
14210
14831
|
}
|
|
@@ -15781,7 +16402,7 @@ var MigrationStatusOutputSchema = z.object({
|
|
|
15781
16402
|
}).optional(),
|
|
15782
16403
|
sourceSystems: z.array(z.string()).optional()
|
|
15783
16404
|
}).extend(ErrorResponseFields.shape);
|
|
15784
|
-
var MigrationInitSchema = z.object({}).default({});
|
|
16405
|
+
var MigrationInitSchema = z.object({}).strict().default({});
|
|
15785
16406
|
var MigrationRecordSchema = z.object({
|
|
15786
16407
|
version: z.string().describe("Version identifier (e.g., '1.0.0', '2024-01-15-add-users')"),
|
|
15787
16408
|
description: z.string().optional().describe("Human-readable description of the migration"),
|
|
@@ -15856,7 +16477,7 @@ var MigrationHistorySchema = z.object({
|
|
|
15856
16477
|
"Omit migrationHash and sourceSystem from records to reduce payload (default: false)"
|
|
15857
16478
|
)
|
|
15858
16479
|
}).default({});
|
|
15859
|
-
var MigrationStatusSchema = z.object({}).default({});
|
|
16480
|
+
var MigrationStatusSchema = z.object({}).strict().default({});
|
|
15860
16481
|
var MIGRATIONS_TABLE = "_mcp_migrations";
|
|
15861
16482
|
function toMigrationRecord(row) {
|
|
15862
16483
|
return {
|
|
@@ -15884,7 +16505,7 @@ function validateMigrationSql(sql) {
|
|
|
15884
16505
|
const cleanSql = sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--.*$/gm, "");
|
|
15885
16506
|
const upperSql = cleanSql.toUpperCase();
|
|
15886
16507
|
if (upperSql.includes("ATTACH ") || upperSql.includes("DETACH ") || upperSql.includes("PRAGMA ") || upperSql.includes("LOAD_EXTENSION(")) {
|
|
15887
|
-
throw new
|
|
16508
|
+
throw new ValidationError(
|
|
15888
16509
|
`Migration validation failed: unauthorized command or function call`
|
|
15889
16510
|
);
|
|
15890
16511
|
}
|
|
@@ -16577,6 +17198,10 @@ var POSITIONAL_PARAM_MAP = {
|
|
|
16577
17198
|
listConstraints: "table",
|
|
16578
17199
|
dateAdd: ["table", "column", "amount", "unit", "whereClause"],
|
|
16579
17200
|
dateDiff: ["table", "column1", "column2", "unit", "whereClause"],
|
|
17201
|
+
enableVersioning: "table",
|
|
17202
|
+
disableVersioning: "table",
|
|
17203
|
+
checkVersion: ["table", "rowId"],
|
|
17204
|
+
conditionalUpdate: ["table", "data", "conditions", "expectedVersion"],
|
|
16580
17205
|
// JSON
|
|
16581
17206
|
extract: ["table", "column", "path", "whereClause"],
|
|
16582
17207
|
set: ["table", "column", "path", "value", "whereClause"],
|
|
@@ -16731,7 +17356,7 @@ function normalizeParams(methodName, args) {
|
|
|
16731
17356
|
if (Array.isArray(paramMapping2) && paramMapping2[0] !== void 0) {
|
|
16732
17357
|
return { [paramMapping2[0]]: arg };
|
|
16733
17358
|
}
|
|
16734
|
-
throw new
|
|
17359
|
+
throw new ValidationError(
|
|
16735
17360
|
`Positional arguments are not supported for method: ${methodName}. Please use an options object.`
|
|
16736
17361
|
);
|
|
16737
17362
|
}
|
|
@@ -16739,7 +17364,7 @@ function normalizeParams(methodName, args) {
|
|
|
16739
17364
|
}
|
|
16740
17365
|
const paramMapping = POSITIONAL_PARAM_MAP[methodName];
|
|
16741
17366
|
if (paramMapping === void 0) {
|
|
16742
|
-
throw new
|
|
17367
|
+
throw new ValidationError(
|
|
16743
17368
|
`Positional arguments are not supported for method: ${methodName}. Please use an options object.`
|
|
16744
17369
|
);
|
|
16745
17370
|
}
|
|
@@ -16789,7 +17414,7 @@ function createGroupApi(groupName, tools, baseContext) {
|
|
|
16789
17414
|
api[methodName] = async (...args) => {
|
|
16790
17415
|
if (baseContext?.auth !== void 0) {
|
|
16791
17416
|
if (!scopesGrantToolAccess(baseContext.auth.scopes ?? [], tool.name)) {
|
|
16792
|
-
throw new
|
|
17417
|
+
throw new AuthorizationError(
|
|
16793
17418
|
`Forbidden: Required scope for tool '${tool.name}' not granted.`
|
|
16794
17419
|
);
|
|
16795
17420
|
}
|
|
@@ -16800,7 +17425,8 @@ function createGroupApi(groupName, tools, baseContext) {
|
|
|
16800
17425
|
requestId: crypto.randomUUID(),
|
|
16801
17426
|
...baseContext?.server !== void 0 ? { server: baseContext.server } : {},
|
|
16802
17427
|
...baseContext?.progressToken !== void 0 ? { progressToken: baseContext.progressToken } : {},
|
|
16803
|
-
...baseContext?.auth !== void 0 ? { auth: baseContext.auth } : {}
|
|
17428
|
+
...baseContext?.auth !== void 0 ? { auth: baseContext.auth } : {},
|
|
17429
|
+
isCodeMode: true
|
|
16804
17430
|
};
|
|
16805
17431
|
const result = await tool.handler(normalizedParams, context);
|
|
16806
17432
|
return result;
|
|
@@ -16962,7 +17588,8 @@ function createSqliteApi(tools, baseContext) {
|
|
|
16962
17588
|
var DEFAULT_SANDBOX_OPTIONS = {
|
|
16963
17589
|
memoryLimitMb: 128,
|
|
16964
17590
|
timeoutMs: 5e3,
|
|
16965
|
-
cpuLimitMs: 1e4
|
|
17591
|
+
cpuLimitMs: 1e4,
|
|
17592
|
+
strictIsolation: true
|
|
16966
17593
|
};
|
|
16967
17594
|
var DEFAULT_POOL_OPTIONS = {
|
|
16968
17595
|
minInstances: 2,
|
|
@@ -17158,7 +17785,7 @@ var CodeModeSecurityManager = class {
|
|
|
17158
17785
|
const redactedResult = redactObject(result, 0, 15);
|
|
17159
17786
|
const serialized = JSON.stringify(redactedResult);
|
|
17160
17787
|
if (serialized === void 0) {
|
|
17161
|
-
throw new
|
|
17788
|
+
throw new InternalError("Not serializable");
|
|
17162
17789
|
}
|
|
17163
17790
|
if (serialized.length > this.config.maxResultSize) {
|
|
17164
17791
|
return {
|
|
@@ -17258,6 +17885,9 @@ return ${lastStmt}`;
|
|
|
17258
17885
|
}
|
|
17259
17886
|
|
|
17260
17887
|
// src/codemode/sandbox.ts
|
|
17888
|
+
var GROUP_NAME_REGEX = /^[a-zA-Z0-9_]+$/;
|
|
17889
|
+
var astCache = /* @__PURE__ */ new Map();
|
|
17890
|
+
var MAX_AST_CACHE_SIZE = 500;
|
|
17261
17891
|
var CodeModeSandbox = class _CodeModeSandbox {
|
|
17262
17892
|
options;
|
|
17263
17893
|
disposed = false;
|
|
@@ -17286,9 +17916,8 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17286
17916
|
metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 }
|
|
17287
17917
|
};
|
|
17288
17918
|
}
|
|
17289
|
-
const groupNameRegex = /^[a-zA-Z0-9_]+$/;
|
|
17290
17919
|
for (const groupName of Object.keys(apiBindings)) {
|
|
17291
|
-
if (!
|
|
17920
|
+
if (!GROUP_NAME_REGEX.test(groupName) || groupName === "__proto__" || groupName === "constructor" || groupName === "prototype") {
|
|
17292
17921
|
return {
|
|
17293
17922
|
success: false,
|
|
17294
17923
|
error: `Security Error: Invalid tool group name '${groupName}'`,
|
|
@@ -17298,30 +17927,39 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17298
17927
|
}
|
|
17299
17928
|
try {
|
|
17300
17929
|
const wrappedCode = `async function __wrapper() { ${code} }`;
|
|
17301
|
-
|
|
17302
|
-
|
|
17303
|
-
|
|
17304
|
-
|
|
17305
|
-
|
|
17306
|
-
|
|
17307
|
-
|
|
17308
|
-
|
|
17309
|
-
|
|
17310
|
-
|
|
17311
|
-
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
if (["process", "require", "global", "globalThis"].includes(objName)) {
|
|
17315
|
-
throw new Error(`Access to '${objName}' is forbidden.`);
|
|
17930
|
+
if (!astCache.has(wrappedCode)) {
|
|
17931
|
+
const ast = acorn.parse(wrappedCode, {
|
|
17932
|
+
ecmaVersion: "latest",
|
|
17933
|
+
sourceType: "script"
|
|
17934
|
+
});
|
|
17935
|
+
const validateAst = (node) => {
|
|
17936
|
+
if (node === null || node === void 0 || typeof node !== "object")
|
|
17937
|
+
return;
|
|
17938
|
+
const n = node;
|
|
17939
|
+
if (n["type"] === "WithStatement") {
|
|
17940
|
+
throw new ValidationError(
|
|
17941
|
+
"'with' statements are forbidden in sandbox code."
|
|
17942
|
+
);
|
|
17316
17943
|
}
|
|
17317
|
-
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17944
|
+
if (n["type"] === "MemberExpression" && n["object"] !== null && n["object"] !== void 0 && typeof n["object"] === "object" && n["object"]["type"] === "Identifier") {
|
|
17945
|
+
const objName = n["object"]["name"];
|
|
17946
|
+
if (["process", "require", "global", "globalThis"].includes(objName)) {
|
|
17947
|
+
throw new ValidationError(`Access to '${objName}' is forbidden.`);
|
|
17948
|
+
}
|
|
17949
|
+
}
|
|
17950
|
+
for (const key in n) {
|
|
17951
|
+
if (key !== "loc" && key !== "start" && key !== "end") {
|
|
17952
|
+
validateAst(n[key]);
|
|
17953
|
+
}
|
|
17321
17954
|
}
|
|
17955
|
+
};
|
|
17956
|
+
validateAst(ast);
|
|
17957
|
+
if (astCache.size >= MAX_AST_CACHE_SIZE) {
|
|
17958
|
+
const firstKey = astCache.keys().next().value;
|
|
17959
|
+
if (firstKey) astCache.delete(firstKey);
|
|
17322
17960
|
}
|
|
17323
|
-
|
|
17324
|
-
|
|
17961
|
+
astCache.set(wrappedCode, ast);
|
|
17962
|
+
}
|
|
17325
17963
|
} catch (e) {
|
|
17326
17964
|
return {
|
|
17327
17965
|
success: false,
|
|
@@ -17337,6 +17975,13 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17337
17975
|
} catch {
|
|
17338
17976
|
}
|
|
17339
17977
|
if (!ivmLib) {
|
|
17978
|
+
if (this.options.strictIsolation) {
|
|
17979
|
+
return {
|
|
17980
|
+
success: false,
|
|
17981
|
+
error: "Security Error: isolated-vm native bindings failed to load. Code Mode strict isolation is enabled and node:vm fallback is prohibited.",
|
|
17982
|
+
metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 }
|
|
17983
|
+
};
|
|
17984
|
+
}
|
|
17340
17985
|
const vm = await import('vm');
|
|
17341
17986
|
const logs2 = [];
|
|
17342
17987
|
const sandboxEnv = {
|
|
@@ -17363,6 +18008,28 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17363
18008
|
}
|
|
17364
18009
|
}
|
|
17365
18010
|
const context2 = vm.createContext(sandboxEnv);
|
|
18011
|
+
vm.runInContext(
|
|
18012
|
+
`
|
|
18013
|
+
const proxyHandler = {
|
|
18014
|
+
get(target, prop, receiver) {
|
|
18015
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
18016
|
+
const camelProp = prop.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
18017
|
+
if (camelProp in target) {
|
|
18018
|
+
return Reflect.get(target, camelProp, receiver);
|
|
18019
|
+
}
|
|
18020
|
+
}
|
|
18021
|
+
return Reflect.get(target, prop, receiver);
|
|
18022
|
+
}
|
|
18023
|
+
};
|
|
18024
|
+
sqlite = new Proxy(sqlite, proxyHandler);
|
|
18025
|
+
for (const key of Object.keys(sqlite)) {
|
|
18026
|
+
if (typeof sqlite[key] === "object" && sqlite[key] !== null) {
|
|
18027
|
+
sqlite[key] = new Proxy(sqlite[key], proxyHandler);
|
|
18028
|
+
}
|
|
18029
|
+
}
|
|
18030
|
+
`,
|
|
18031
|
+
context2
|
|
18032
|
+
);
|
|
17366
18033
|
const startTime2 = performance.now();
|
|
17367
18034
|
let result2;
|
|
17368
18035
|
let success2 = true;
|
|
@@ -17425,62 +18092,90 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17425
18092
|
context.evalSync(setupScript);
|
|
17426
18093
|
let rpcCount = 0;
|
|
17427
18094
|
const MAX_RPC_CALLS = 100;
|
|
18095
|
+
let batchedScript = "";
|
|
17428
18096
|
for (const [groupName, groupValue] of Object.entries(apiBindings)) {
|
|
17429
18097
|
if (typeof groupValue === "object" && groupValue !== null) {
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
);
|
|
18098
|
+
batchedScript += `globalThis.sqlite[${JSON.stringify(groupName)}] = {};
|
|
18099
|
+
`;
|
|
17433
18100
|
for (const [methodName, methodFn] of Object.entries(groupValue)) {
|
|
17434
18101
|
if (typeof methodFn === "function") {
|
|
17435
18102
|
const fnRef = new ivmLib.Reference(async (...args) => {
|
|
17436
18103
|
if (++rpcCount > MAX_RPC_CALLS) {
|
|
17437
|
-
throw new
|
|
18104
|
+
throw new RateLimitError(
|
|
17438
18105
|
`QuotaExceededError: Maximum number of host tool calls (${MAX_RPC_CALLS}) exceeded (attempted call ${rpcCount}).`
|
|
17439
18106
|
);
|
|
17440
18107
|
}
|
|
17441
18108
|
try {
|
|
17442
18109
|
return await methodFn(...args);
|
|
17443
18110
|
} catch (e) {
|
|
17444
|
-
throw new
|
|
17445
|
-
|
|
17446
|
-
|
|
18111
|
+
throw new InternalError(
|
|
18112
|
+
e instanceof Error ? e.message : String(e),
|
|
18113
|
+
"RPC_EXECUTION_ERROR",
|
|
18114
|
+
{
|
|
18115
|
+
cause: e instanceof Error ? e : void 0
|
|
18116
|
+
}
|
|
18117
|
+
);
|
|
17447
18118
|
}
|
|
17448
18119
|
});
|
|
17449
18120
|
refCleanup.push(fnRef);
|
|
17450
18121
|
const refName = `fnRef_${groupName}_${methodName}`;
|
|
17451
18122
|
context.global.setSync(refName, fnRef);
|
|
17452
|
-
|
|
17453
|
-
globalThis.sqlite[${JSON.stringify(groupName)}][${JSON.stringify(methodName)}] = async (...args) => {
|
|
18123
|
+
batchedScript += `globalThis.sqlite[${JSON.stringify(groupName)}][${JSON.stringify(methodName)}] = async (...args) => {
|
|
17454
18124
|
return await globalThis[${JSON.stringify(refName)}].apply(undefined, args, { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
17455
18125
|
};
|
|
17456
|
-
|
|
18126
|
+
`;
|
|
17457
18127
|
}
|
|
17458
18128
|
}
|
|
17459
18129
|
} else if (typeof groupValue === "function") {
|
|
17460
18130
|
const fnRef = new ivmLib.Reference(async (...args) => {
|
|
17461
18131
|
if (++rpcCount > MAX_RPC_CALLS) {
|
|
17462
|
-
throw new
|
|
18132
|
+
throw new RateLimitError(
|
|
17463
18133
|
`QuotaExceededError: Maximum number of host tool calls (${MAX_RPC_CALLS}) exceeded (attempted call ${rpcCount}).`
|
|
17464
18134
|
);
|
|
17465
18135
|
}
|
|
17466
18136
|
try {
|
|
17467
18137
|
return await groupValue(...args);
|
|
17468
18138
|
} catch (e) {
|
|
17469
|
-
throw new
|
|
17470
|
-
|
|
17471
|
-
|
|
18139
|
+
throw new InternalError(
|
|
18140
|
+
e instanceof Error ? e.message : String(e),
|
|
18141
|
+
"RPC_EXECUTION_ERROR",
|
|
18142
|
+
{
|
|
18143
|
+
cause: e instanceof Error ? e : void 0
|
|
18144
|
+
}
|
|
18145
|
+
);
|
|
17472
18146
|
}
|
|
17473
18147
|
});
|
|
17474
18148
|
refCleanup.push(fnRef);
|
|
17475
18149
|
const refName = `fnRef_${groupName}`;
|
|
17476
18150
|
context.global.setSync(refName, fnRef);
|
|
17477
|
-
|
|
17478
|
-
globalThis.sqlite[${JSON.stringify(groupName)}] = async (...args) => {
|
|
18151
|
+
batchedScript += `globalThis.sqlite[${JSON.stringify(groupName)}] = async (...args) => {
|
|
17479
18152
|
return await globalThis[${JSON.stringify(refName)}].apply(undefined, args, { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
17480
18153
|
};
|
|
17481
|
-
|
|
18154
|
+
`;
|
|
17482
18155
|
}
|
|
17483
18156
|
}
|
|
18157
|
+
if (batchedScript.length > 0) {
|
|
18158
|
+
context.evalSync(batchedScript);
|
|
18159
|
+
}
|
|
18160
|
+
context.evalSync(`
|
|
18161
|
+
const proxyHandler = {
|
|
18162
|
+
get(target, prop, receiver) {
|
|
18163
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
18164
|
+
const camelProp = prop.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
18165
|
+
if (camelProp in target) {
|
|
18166
|
+
return Reflect.get(target, camelProp, receiver);
|
|
18167
|
+
}
|
|
18168
|
+
}
|
|
18169
|
+
return Reflect.get(target, prop, receiver);
|
|
18170
|
+
}
|
|
18171
|
+
};
|
|
18172
|
+
globalThis.sqlite = new Proxy(globalThis.sqlite, proxyHandler);
|
|
18173
|
+
for (const key of Object.keys(globalThis.sqlite)) {
|
|
18174
|
+
if (typeof globalThis.sqlite[key] === "object" && globalThis.sqlite[key] !== null) {
|
|
18175
|
+
globalThis.sqlite[key] = new Proxy(globalThis.sqlite[key], proxyHandler);
|
|
18176
|
+
}
|
|
18177
|
+
}
|
|
18178
|
+
`);
|
|
17484
18179
|
const wrappedCode = `(async () => { ${transformAutoReturn(code)} })()`;
|
|
17485
18180
|
script = isolate.compileScriptSync(wrappedCode, {
|
|
17486
18181
|
filename: `code-mode.js`
|
|
@@ -17559,7 +18254,10 @@ var SandboxPool = class _SandboxPool {
|
|
|
17559
18254
|
}
|
|
17560
18255
|
static getIvmLib() {
|
|
17561
18256
|
if (!_SandboxPool.cachedIvmLib) {
|
|
17562
|
-
throw new
|
|
18257
|
+
throw new InternalError(
|
|
18258
|
+
"ivmLib not initialized",
|
|
18259
|
+
"IVMLIB_NOT_INITIALIZED"
|
|
18260
|
+
);
|
|
17563
18261
|
}
|
|
17564
18262
|
return _SandboxPool.cachedIvmLib;
|
|
17565
18263
|
}
|
|
@@ -17578,8 +18276,9 @@ var SandboxPool = class _SandboxPool {
|
|
|
17578
18276
|
await _SandboxPool.initialize();
|
|
17579
18277
|
}
|
|
17580
18278
|
if (this.inUseCount >= this.options.maxInstances) {
|
|
17581
|
-
throw new
|
|
17582
|
-
`Sandbox pool exhausted (max ${this.options.maxInstances})
|
|
18279
|
+
throw new InternalError(
|
|
18280
|
+
`Sandbox pool exhausted (max ${this.options.maxInstances})`,
|
|
18281
|
+
"POOL_EXHAUSTED"
|
|
17583
18282
|
);
|
|
17584
18283
|
}
|
|
17585
18284
|
this.inUseCount++;
|
|
@@ -17716,15 +18415,14 @@ function createExecuteCodeTool(adapter) {
|
|
|
17716
18415
|
}
|
|
17717
18416
|
const clientId = _context.auth?.sub ?? _context.clientIp ?? processId;
|
|
17718
18417
|
if (!await security.checkRateLimit(clientId)) {
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
};
|
|
18418
|
+
throw new RateLimitError(
|
|
18419
|
+
`Rate limit exceeded. Maximum ${String(codemodeRateLimit)} executions per minute.`,
|
|
18420
|
+
"CODEMODE_RATE_LIMITED",
|
|
18421
|
+
{
|
|
18422
|
+
suggestion: "Wait before retrying. Combine multiple operations into fewer execute_code calls.",
|
|
18423
|
+
limit: codemodeRateLimit
|
|
18424
|
+
}
|
|
18425
|
+
);
|
|
17728
18426
|
}
|
|
17729
18427
|
if (!pool) {
|
|
17730
18428
|
initializePool();
|
|
@@ -17739,7 +18437,7 @@ function createExecuteCodeTool(adapter) {
|
|
|
17739
18437
|
throw new DbMcpError(
|
|
17740
18438
|
"Sandbox pool not initialized",
|
|
17741
18439
|
"CODEMODE_POOL_UNINITIALIZED",
|
|
17742
|
-
|
|
18440
|
+
ErrorCategory.INTERNAL
|
|
17743
18441
|
);
|
|
17744
18442
|
}
|
|
17745
18443
|
const result = await pool.execute(code, bindings, timeoutMs);
|
|
@@ -17759,6 +18457,13 @@ function createExecuteCodeTool(adapter) {
|
|
|
17759
18457
|
} catch {
|
|
17760
18458
|
}
|
|
17761
18459
|
}
|
|
18460
|
+
if (!result.success && typeof result.error === "string" && /script execution timed out/i.test(result.error)) {
|
|
18461
|
+
return formatHandlerError(
|
|
18462
|
+
new TimeoutError("Code execution timed out", "CODEMODE_TIMEOUT", {
|
|
18463
|
+
suggestion: "Reduce code complexity or increase the timeout parameter (max 30000ms). Break complex logic into smaller execute_code calls."
|
|
18464
|
+
})
|
|
18465
|
+
);
|
|
18466
|
+
}
|
|
17762
18467
|
return {
|
|
17763
18468
|
success: result.success,
|
|
17764
18469
|
result: sanitizedResult,
|
|
@@ -17776,6 +18481,14 @@ function createExecuteCodeTool(adapter) {
|
|
|
17776
18481
|
operation: "executeCode",
|
|
17777
18482
|
error: error instanceof Error ? error : void 0
|
|
17778
18483
|
});
|
|
18484
|
+
if (error instanceof Error && /script execution timed out/i.test(error.message)) {
|
|
18485
|
+
return formatHandlerError(
|
|
18486
|
+
new TimeoutError("Code execution timed out", "CODEMODE_TIMEOUT", {
|
|
18487
|
+
suggestion: "Reduce code complexity or increase the timeout parameter (max 30000ms). Break complex logic into smaller execute_code calls.",
|
|
18488
|
+
cause: error
|
|
18489
|
+
})
|
|
18490
|
+
);
|
|
18491
|
+
}
|
|
17779
18492
|
return formatHandlerError(error);
|
|
17780
18493
|
}
|
|
17781
18494
|
}
|
|
@@ -17825,7 +18538,8 @@ function initializePool() {
|
|
|
17825
18538
|
operation: "initialize"
|
|
17826
18539
|
});
|
|
17827
18540
|
setDefaultSandboxMode(mode);
|
|
17828
|
-
|
|
18541
|
+
const strictIsolation = process.env["CODE_MODE_STRICT_ISOLATION"] !== "false";
|
|
18542
|
+
pool = createSandboxPool(mode, void 0, { timeoutMs: 3e4, strictIsolation });
|
|
17829
18543
|
pool.initialize();
|
|
17830
18544
|
logger.info(`Code Mode initialized with ${mode} sandbox`, {
|
|
17831
18545
|
module: "CODEMODE",
|
|
@@ -17885,6 +18599,8 @@ function detectAndSetJsonbSupport(getVersion, log2) {
|
|
|
17885
18599
|
}
|
|
17886
18600
|
|
|
17887
18601
|
// src/adapters/sqlite/query-executor.ts
|
|
18602
|
+
var RETURNING_REGEX = /\bRETURNING\b/i;
|
|
18603
|
+
var INSERT_REGEX = /^\s*INSERT\s/i;
|
|
17888
18604
|
var log = createModuleLogger("SQLITE");
|
|
17889
18605
|
function translateSqliteError(error, sql, operation, overrideLog) {
|
|
17890
18606
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -17892,8 +18608,7 @@ function translateSqliteError(error, sql, operation, overrideLog) {
|
|
|
17892
18608
|
const improvedMessage = `${operation} failed: ${message}`;
|
|
17893
18609
|
const details = { sql };
|
|
17894
18610
|
if (match?.code === "TABLE_NOT_FOUND") {
|
|
17895
|
-
const
|
|
17896
|
-
const tableName = tableMatch ? tableMatch[2] : "unknown";
|
|
18611
|
+
const tableName = match.match?.[2] ?? "unknown";
|
|
17897
18612
|
throw new ResourceNotFoundError(
|
|
17898
18613
|
`Table '${tableName}' not found`,
|
|
17899
18614
|
"TABLE_NOT_FOUND",
|
|
@@ -17907,8 +18622,7 @@ function translateSqliteError(error, sql, operation, overrideLog) {
|
|
|
17907
18622
|
);
|
|
17908
18623
|
}
|
|
17909
18624
|
if (match?.code === "COLUMN_NOT_FOUND") {
|
|
17910
|
-
const
|
|
17911
|
-
const colName = colMatch ? colMatch[2] : "unknown";
|
|
18625
|
+
const colName = match.match?.[2] ?? "unknown";
|
|
17912
18626
|
throw new ResourceNotFoundError(
|
|
17913
18627
|
`Column '${colName}' not found`,
|
|
17914
18628
|
"COLUMN_NOT_FOUND",
|
|
@@ -17988,7 +18702,7 @@ function executeWrite(db, sql, params) {
|
|
|
17988
18702
|
try {
|
|
17989
18703
|
const normalizedParams = normalizeSqliteParams(params);
|
|
17990
18704
|
let rows = [];
|
|
17991
|
-
if (
|
|
18705
|
+
if (RETURNING_REGEX.test(sql)) {
|
|
17992
18706
|
const results = normalizedParams ? db.exec(sql, normalizedParams) : db.exec(sql);
|
|
17993
18707
|
if (results.length > 0 && results[0]) {
|
|
17994
18708
|
rows = rowsFromSqlJsResult(results[0]);
|
|
@@ -18003,7 +18717,7 @@ function executeWrite(db, sql, params) {
|
|
|
18003
18717
|
const changes = db.getRowsModified();
|
|
18004
18718
|
let lastInsertId;
|
|
18005
18719
|
try {
|
|
18006
|
-
if (
|
|
18720
|
+
if (INSERT_REGEX.test(sql) && !RETURNING_REGEX.test(sql)) {
|
|
18007
18721
|
const rowidResult = db.exec("SELECT last_insert_rowid()");
|
|
18008
18722
|
if (rowidResult[0]?.values[0]) {
|
|
18009
18723
|
lastInsertId = Number(rowidResult[0].values[0][0]);
|