db-mcp 3.0.2 → 5.0.0
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 +112 -75
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/database-adapter.d.ts +11 -1
- package/dist/adapters/database-adapter.d.ts.map +1 -1
- package/dist/adapters/registration/resources.d.ts.map +1 -1
- package/dist/adapters/registration/tools.d.ts.map +1 -1
- package/dist/adapters/sqlite/prompts/analysis.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/resources.d.ts +1 -1
- package/dist/adapters/sqlite/resources.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/admin.d.ts +33 -19
- package/dist/adapters/sqlite/schemas/admin.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/core.d.ts +96 -0
- package/dist/adapters/sqlite/schemas/core.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/fts.d.ts +4 -0
- package/dist/adapters/sqlite/schemas/fts.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/introspection.d.ts +25 -15
- 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/schemas/spatialite.d.ts +1 -0
- package/dist/adapters/sqlite/schemas/spatialite.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/text.d.ts +36 -0
- package/dist/adapters/sqlite/schemas/text.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/index.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/pragma.d.ts +0 -4
- 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/tables.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/fts.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/introspection/diagnostics/indexes.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/introspection/graph/tools.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-helpers/read.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-operations/crud.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-operations/query.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/migration/helpers.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/text/helpers.d.ts +9 -0
- package/dist/adapters/sqlite/tools/text/helpers.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/text/index.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/text/search.d.ts +4 -0
- package/dist/adapters/sqlite/tools/text/search.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 +5 -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/native-sqlite-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite-native/tools/spatialite/tools.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/interceptor.d.ts.map +1 -1
- package/dist/audit/logger.d.ts +26 -25
- 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-W5WQVNVX.js → chunk-27C5EE6U.js} +23 -17
- package/dist/chunk-2TYICG7B.js +1183 -0
- package/dist/{chunk-37G4DLL6.js → chunk-LLIAIJPO.js} +1146 -539
- package/dist/{chunk-NV34JSQM.js → chunk-UE7GVOBV.js} +1757 -1331
- package/dist/{chunk-L552U3QS.js → chunk-XLIYO3YA.js} +401 -77
- package/dist/cli.js +116 -11
- 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 +1 -1
- package/dist/constants/server-instructions.d.ts.map +1 -1
- package/dist/filtering/tool-constants.d.ts +11 -11
- package/dist/filtering/tool-constants.d.ts.map +1 -1
- package/dist/{http-HWTUVFIA.js → http-ZDMK46QU.js} +195 -32
- package/dist/index.js +4 -4
- package/dist/observability/metrics.d.ts +36 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/system-db.d.ts +13 -0
- package/dist/observability/system-db.d.ts.map +1 -0
- package/dist/server/mcp-server.d.ts +3 -0
- package/dist/server/mcp-server.d.ts.map +1 -1
- package/dist/server/registration/admin-tools.d.ts +6 -0
- package/dist/server/registration/admin-tools.d.ts.map +1 -0
- 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/help-resources.d.ts.map +1 -1
- package/dist/server/registration/index.d.ts +3 -1
- package/dist/server/registration/index.d.ts.map +1 -1
- package/dist/server/registration/observability-resources.d.ts +11 -0
- package/dist/server/registration/observability-resources.d.ts.map +1 -0
- package/dist/server/subscription-manager.d.ts +35 -0
- package/dist/server/subscription-manager.d.ts.map +1 -0
- package/dist/{sqlite-TB56Z7G3.js → sqlite-XWZPEZJO.js} +195 -19
- package/dist/{sqlite-native-IF75BB3O.js → sqlite-native-5GQ3WKCZ.js} +311 -147
- 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 +11 -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 +5 -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 +3 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/logger/types.d.ts +1 -1
- package/dist/utils/logger/types.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/tokens.d.ts +15 -0
- package/dist/utils/tokens.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 +43 -44
- 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/chunk-E5IESRTK.js +0 -489
- package/dist/server/registration/audit-tools.d.ts +0 -13
- package/dist/server/registration/audit-tools.d.ts.map +0 -1
- package/dist/utils/insights-manager.d.ts +0 -39
- package/dist/utils/insights-manager.d.ts.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { validateQuery } from './chunk-
|
|
2
|
-
import { ErrorResponseFields, redactObject, SENSITIVE_KEY_PATTERN, HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY, ASSISTANT_FOCUSED, readOnly, write, idempotent, destructive, VacuumOutputSchema, admin, 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,
|
|
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
|
|
7
|
-
import fs
|
|
5
|
+
import * as nodePath from 'path';
|
|
6
|
+
import nodePath__default from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
8
|
import { createHash } from 'crypto';
|
|
9
9
|
import { createClient } from 'redis';
|
|
10
10
|
import * as acorn from 'acorn';
|
|
@@ -41,7 +41,10 @@ var ReadQueryOutputSchema = z.object({
|
|
|
41
41
|
success: z.boolean(),
|
|
42
42
|
rowCount: z.number().optional(),
|
|
43
43
|
rows: z.array(RowRecordSchema).optional(),
|
|
44
|
-
|
|
44
|
+
nextCursor: z.string().optional(),
|
|
45
|
+
executionTimeMs: z.number().optional(),
|
|
46
|
+
streamed: z.boolean().optional(),
|
|
47
|
+
chunksEmitted: z.number().optional()
|
|
45
48
|
}).extend(ErrorResponseFields.shape);
|
|
46
49
|
var WriteQueryOutputSchema = z.object({
|
|
47
50
|
success: z.boolean(),
|
|
@@ -122,12 +125,22 @@ var DateMathOutputSchema = z.object({
|
|
|
122
125
|
var ReadQuerySchema = z.object({
|
|
123
126
|
query: z.string().max(1e5).default("").describe("SELECT query to execute"),
|
|
124
127
|
sql: z.string().max(1e5).optional().describe("Alias for query"),
|
|
125
|
-
params: z.array(z.unknown()).optional().describe("Query parameters for prepared statements")
|
|
128
|
+
params: z.array(z.unknown()).optional().describe("Query parameters for prepared statements"),
|
|
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
|
+
)
|
|
126
136
|
});
|
|
127
137
|
var WriteQuerySchema = z.object({
|
|
128
138
|
query: z.string().max(1e5).default("").describe("INSERT/UPDATE/DELETE query to execute"),
|
|
129
139
|
sql: z.string().max(1e5).optional().describe("Alias for query"),
|
|
130
|
-
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
|
+
)
|
|
131
144
|
});
|
|
132
145
|
var CreateTableSchema = z.object({
|
|
133
146
|
table: z.string().default("").describe("Name of the table to create"),
|
|
@@ -242,7 +255,10 @@ var UpsertSchema = z.object({
|
|
|
242
255
|
updateColumns: z.array(z.string()).optional().describe(
|
|
243
256
|
"Columns to update on conflict (default: all except conflict columns). Only used if conflictColumns is provided."
|
|
244
257
|
),
|
|
245
|
-
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
|
+
)
|
|
246
262
|
});
|
|
247
263
|
var BatchInsertSchema = z.object({
|
|
248
264
|
table: z.string().default("").describe("Table name"),
|
|
@@ -348,9 +364,64 @@ var DropTriggerOutputSchema = z.object({
|
|
|
348
364
|
success: z.boolean(),
|
|
349
365
|
message: z.string().optional()
|
|
350
366
|
}).extend(ErrorResponseFields.shape);
|
|
351
|
-
|
|
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 = {}) {
|
|
352
412
|
if (typeof params !== "object" || params === null) return params;
|
|
353
|
-
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
|
+
}
|
|
354
425
|
for (const [alias, canonical] of Object.entries(aliasMap)) {
|
|
355
426
|
if ((obj[canonical] === void 0 || obj[canonical] === "") && obj[alias] !== void 0) {
|
|
356
427
|
obj[canonical] = obj[alias];
|
|
@@ -541,16 +612,77 @@ function createReadQueryTool(adapter) {
|
|
|
541
612
|
}
|
|
542
613
|
try {
|
|
543
614
|
let finalQuery = input.query.replace(/[\s;]+$/g, "");
|
|
615
|
+
let offset = 0;
|
|
616
|
+
if (input.cursor) {
|
|
617
|
+
try {
|
|
618
|
+
const cursorData = JSON.parse(
|
|
619
|
+
Buffer.from(input.cursor, "base64").toString("utf8")
|
|
620
|
+
);
|
|
621
|
+
if (typeof cursorData["offset"] === "number") {
|
|
622
|
+
offset = cursorData["offset"];
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
return {
|
|
626
|
+
...formatHandlerError(
|
|
627
|
+
new ValidationError(
|
|
628
|
+
"Invalid cursor format",
|
|
629
|
+
"VALIDATION_ERROR",
|
|
630
|
+
{
|
|
631
|
+
suggestion: "Use the nextCursor value returned from a previous query.",
|
|
632
|
+
details: {}
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
),
|
|
636
|
+
rowCount: 0,
|
|
637
|
+
rows: []
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
544
641
|
const upperForLimit = finalQuery.toUpperCase();
|
|
545
642
|
const isLimitable = upperForLimit.startsWith("SELECT") || upperForLimit.startsWith("WITH");
|
|
546
|
-
|
|
547
|
-
|
|
643
|
+
const limit = 50;
|
|
644
|
+
const hasLimit = /\bLIMIT\b/i.test(finalQuery);
|
|
645
|
+
if (isLimitable && !hasLimit) {
|
|
646
|
+
finalQuery = `${finalQuery} LIMIT ${limit}`;
|
|
647
|
+
if (offset > 0) {
|
|
648
|
+
finalQuery = `${finalQuery} OFFSET ${offset}`;
|
|
649
|
+
}
|
|
650
|
+
} else if (isLimitable && hasLimit && offset > 0) {
|
|
651
|
+
if (!/\bOFFSET\b/i.test(finalQuery)) {
|
|
652
|
+
finalQuery = `${finalQuery} OFFSET ${offset}`;
|
|
653
|
+
}
|
|
548
654
|
}
|
|
549
655
|
const result = await adapter.executeReadQuery(finalQuery, input.params);
|
|
656
|
+
let nextCursor;
|
|
657
|
+
if (isLimitable && !hasLimit && result.rows?.length === limit) {
|
|
658
|
+
const nextOffset = offset + limit;
|
|
659
|
+
nextCursor = Buffer.from(
|
|
660
|
+
JSON.stringify({ offset: nextOffset })
|
|
661
|
+
).toString("base64");
|
|
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
|
+
}
|
|
550
681
|
return {
|
|
551
682
|
success: true,
|
|
552
683
|
rowCount: result.rows?.length ?? 0,
|
|
553
684
|
rows: result.rows,
|
|
685
|
+
nextCursor,
|
|
554
686
|
executionTimeMs: result.executionTimeMs
|
|
555
687
|
};
|
|
556
688
|
} catch (error) {
|
|
@@ -650,11 +782,60 @@ function createWriteQueryTool(adapter) {
|
|
|
650
782
|
rowsAffected: 0
|
|
651
783
|
};
|
|
652
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
|
+
}
|
|
653
819
|
try {
|
|
654
820
|
const result = await adapter.executeWriteQuery(
|
|
655
821
|
input.query,
|
|
656
822
|
input.params
|
|
657
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
|
+
}
|
|
658
839
|
return {
|
|
659
840
|
success: true,
|
|
660
841
|
rowsAffected: result.rowsAffected,
|
|
@@ -671,477 +852,6 @@ function createWriteQueryTool(adapter) {
|
|
|
671
852
|
};
|
|
672
853
|
}
|
|
673
854
|
|
|
674
|
-
// src/utils/where-clause.ts
|
|
675
|
-
var UnsafeWhereClauseError = class extends Error {
|
|
676
|
-
constructor(reason) {
|
|
677
|
-
super(`Unsafe WHERE clause: ${reason}`);
|
|
678
|
-
this.name = "UnsafeWhereClauseError";
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
var DANGEROUS_PATTERNS = [
|
|
682
|
-
// Statement terminators and new statements
|
|
683
|
-
// Comprehensive keyword list including statements missed in earlier versions:
|
|
684
|
-
// REPLACE, VACUUM, ANALYZE, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE
|
|
685
|
-
{
|
|
686
|
-
pattern: /;\s*(DROP|DELETE|TRUNCATE|INSERT|UPDATE|CREATE|ALTER|ATTACH|DETACH|SELECT|REPLACE|VACUUM|ANALYZE|BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE|REINDEX|EXPLAIN)\b/i,
|
|
687
|
-
reason: "contains statement terminator followed by SQL keyword"
|
|
688
|
-
},
|
|
689
|
-
// Trailing semicolons (potential statement injection)
|
|
690
|
-
{
|
|
691
|
-
pattern: /;\s*$/,
|
|
692
|
-
reason: "contains trailing semicolon"
|
|
693
|
-
},
|
|
694
|
-
// SQL comments (can be used to comment out security checks)
|
|
695
|
-
{
|
|
696
|
-
pattern: /--/,
|
|
697
|
-
reason: "contains SQL line comment"
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
pattern: /\/\*/,
|
|
701
|
-
reason: "contains SQL block comment"
|
|
702
|
-
},
|
|
703
|
-
// REGEXP and MATCH injection (blind extraction oracles)
|
|
704
|
-
{
|
|
705
|
-
pattern: /\bREGEXP\s+/i,
|
|
706
|
-
reason: "contains REGEXP (potential blind extraction oracle)"
|
|
707
|
-
},
|
|
708
|
-
{
|
|
709
|
-
pattern: /\bMATCH\s+/i,
|
|
710
|
-
reason: "contains MATCH (potential blind extraction oracle)"
|
|
711
|
-
},
|
|
712
|
-
// UNION injection (data exfiltration)
|
|
713
|
-
{
|
|
714
|
-
pattern: /\bUNION\s+(ALL\s+)?SELECT\b/i,
|
|
715
|
-
reason: "contains UNION SELECT"
|
|
716
|
-
},
|
|
717
|
-
// SQLite-specific: Extension loading (code execution)
|
|
718
|
-
{
|
|
719
|
-
pattern: /\bload_extension\s*\(/i,
|
|
720
|
-
reason: "contains load_extension (code execution)"
|
|
721
|
-
},
|
|
722
|
-
// SQLite-specific: ATTACH database (file system access)
|
|
723
|
-
{
|
|
724
|
-
pattern: /\bATTACH\s+(DATABASE\s+)?['"`]/i,
|
|
725
|
-
reason: "contains ATTACH DATABASE (file system access)"
|
|
726
|
-
},
|
|
727
|
-
// SQLite-specific: Pragma manipulation
|
|
728
|
-
{
|
|
729
|
-
pattern: /\bPRAGMA\s+/i,
|
|
730
|
-
reason: "contains PRAGMA statement"
|
|
731
|
-
},
|
|
732
|
-
// SQLite-specific: writefile/readfile (if using fileio extension)
|
|
733
|
-
{
|
|
734
|
-
pattern: /\b(writefile|readfile)\s*\(/i,
|
|
735
|
-
reason: "contains file I/O function"
|
|
736
|
-
},
|
|
737
|
-
// SQLite-specific: fts3_tokenizer (potential code execution)
|
|
738
|
-
{
|
|
739
|
-
pattern: /\bfts3_tokenizer\s*\(/i,
|
|
740
|
-
reason: "contains FTS tokenizer function"
|
|
741
|
-
},
|
|
742
|
-
// Subquery injection (data exfiltration via boolean-based blind)
|
|
743
|
-
{
|
|
744
|
-
pattern: /\(\s*SELECT\b/i,
|
|
745
|
-
reason: "contains subquery (potential data exfiltration)"
|
|
746
|
-
},
|
|
747
|
-
// Generic: Hexadecimal string injection
|
|
748
|
-
{
|
|
749
|
-
pattern: /\bX'[0-9A-Fa-f]+'/,
|
|
750
|
-
reason: "contains hex string literal (potential binary injection)"
|
|
751
|
-
},
|
|
752
|
-
// CASE WHEN (potential blind injection without subqueries)
|
|
753
|
-
{
|
|
754
|
-
pattern: /\bCASE\s+WHEN\b/i,
|
|
755
|
-
reason: "contains CASE WHEN (potential blind injection)"
|
|
756
|
-
},
|
|
757
|
-
// IIF() — conditional bypass alternative to CASE WHEN
|
|
758
|
-
{
|
|
759
|
-
pattern: /\bIIF\s*\(/i,
|
|
760
|
-
reason: "contains IIF() (potential blind injection)"
|
|
761
|
-
},
|
|
762
|
-
// GROUP_CONCAT() — data aggregation for exfiltration
|
|
763
|
-
{
|
|
764
|
-
pattern: /\bGROUP_CONCAT\s*\(/i,
|
|
765
|
-
reason: "contains GROUP_CONCAT() (potential data exfiltration)"
|
|
766
|
-
},
|
|
767
|
-
// GLOB — case-sensitive wildcard operator usable for character extraction oracles
|
|
768
|
-
{
|
|
769
|
-
pattern: /\bGLOB\b/i,
|
|
770
|
-
reason: "contains GLOB (potential case-sensitive oracle)"
|
|
771
|
-
},
|
|
772
|
-
// Conditional functions — blind injection oracles (bypass for CASE WHEN / IIF)
|
|
773
|
-
{
|
|
774
|
-
pattern: /\bCOALESCE\s*\(/i,
|
|
775
|
-
reason: "contains COALESCE() (potential blind injection oracle)"
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
pattern: /\bNULLIF\s*\(/i,
|
|
779
|
-
reason: "contains NULLIF() (potential blind injection oracle)"
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
pattern: /\bTYPEOF\s*\(/i,
|
|
783
|
-
reason: "contains TYPEOF() (potential blind injection oracle)"
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
pattern: /\bIFNULL\s*\(/i,
|
|
787
|
-
reason: "contains IFNULL() (potential blind injection oracle)"
|
|
788
|
-
},
|
|
789
|
-
// Memory allocation DoS — RANDOMBLOB(N) / ZEROBLOB(N) can allocate large blocks (CWE-400)
|
|
790
|
-
{
|
|
791
|
-
pattern: /\bRANDOMBLOB\s*\(/i,
|
|
792
|
-
reason: "contains RANDOMBLOB() (potential memory allocation DoS)"
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
pattern: /\bZEROBLOB\s*\(/i,
|
|
796
|
-
reason: "contains ZEROBLOB() (potential memory allocation DoS)"
|
|
797
|
-
},
|
|
798
|
-
// Blind extraction oracle functions — can be composed for character-by-character
|
|
799
|
-
// data extraction without subqueries or CASE WHEN (CWE-89, H-1 audit finding).
|
|
800
|
-
// Example: INSTR('abc...', SUBSTR(sqlite_version(), 1, 1)) = N
|
|
801
|
-
{
|
|
802
|
-
pattern: /\bINSTR\s*\(/i,
|
|
803
|
-
reason: "contains INSTR() (potential blind extraction oracle)"
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
pattern: /\bSUBSTR\s*\(/i,
|
|
807
|
-
reason: "contains SUBSTR() (potential blind extraction oracle)"
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
pattern: /\bSUBSTRING\s*\(/i,
|
|
811
|
-
reason: "contains SUBSTRING() (potential blind extraction oracle)"
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
pattern: /\bLENGTH\s*\(/i,
|
|
815
|
-
reason: "contains LENGTH() (potential blind extraction oracle)"
|
|
816
|
-
},
|
|
817
|
-
// TRIM functions — blind extraction oracle (bypass for LIKE / SUBSTR)
|
|
818
|
-
{
|
|
819
|
-
pattern: /\b(?:L|R)?TRIM\s*\(/i,
|
|
820
|
-
reason: "contains TRIM() (potential blind extraction oracle)"
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
pattern: /\bUNICODE\s*\(/i,
|
|
824
|
-
reason: "contains UNICODE() (potential blind extraction oracle)"
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
pattern: /\bCHAR\s*\(/i,
|
|
828
|
-
reason: "contains CHAR() (potential blind extraction oracle)"
|
|
829
|
-
},
|
|
830
|
-
{
|
|
831
|
-
pattern: /\bREPLACE\s*\(/i,
|
|
832
|
-
reason: "contains REPLACE() (potential blind extraction oracle)"
|
|
833
|
-
},
|
|
834
|
-
// ABS() — can be combined with subtraction to create numeric oracles (CWE-89)
|
|
835
|
-
{
|
|
836
|
-
pattern: /\bABS\s*\(/i,
|
|
837
|
-
reason: "contains ABS() (potential blind extraction oracle)"
|
|
838
|
-
},
|
|
839
|
-
// HEX() — encodes values for character-by-character extraction (CWE-89)
|
|
840
|
-
{
|
|
841
|
-
pattern: /\bHEX\s*\(/i,
|
|
842
|
-
reason: "contains HEX() (potential blind extraction oracle)"
|
|
843
|
-
},
|
|
844
|
-
// QUOTE() — returns SQL-literal representation, can leak type/value info (CWE-89)
|
|
845
|
-
{
|
|
846
|
-
pattern: /\bQUOTE\s*\(/i,
|
|
847
|
-
reason: "contains QUOTE() (potential blind extraction oracle)"
|
|
848
|
-
},
|
|
849
|
-
// PRINTF() — format-string based extraction of internal values (CWE-89)
|
|
850
|
-
{
|
|
851
|
-
pattern: /\bPRINTF\s*\(/i,
|
|
852
|
-
reason: "contains PRINTF() (potential blind extraction oracle)"
|
|
853
|
-
},
|
|
854
|
-
// FORMAT() — SQLite 3.38.0+ alias for printf() (CWE-89)
|
|
855
|
-
{
|
|
856
|
-
pattern: /\bFORMAT\s*\(/i,
|
|
857
|
-
reason: "contains FORMAT() (potential blind extraction oracle)"
|
|
858
|
-
},
|
|
859
|
-
// CAST() — can be used to convert text to numeric oracles (CWE-89)
|
|
860
|
-
{
|
|
861
|
-
pattern: /\bCAST\s*\(/i,
|
|
862
|
-
reason: "contains CAST() (potential blind extraction oracle)"
|
|
863
|
-
}
|
|
864
|
-
];
|
|
865
|
-
var FULLWIDTH_START = 65281;
|
|
866
|
-
var FULLWIDTH_END = 65374;
|
|
867
|
-
var FULLWIDTH_ASCII_OFFSET = 65248;
|
|
868
|
-
function normalizeForPatternMatching(input) {
|
|
869
|
-
let normalized = input.normalize("NFC");
|
|
870
|
-
let hasFullwidth = false;
|
|
871
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
872
|
-
const code = normalized.charCodeAt(i);
|
|
873
|
-
if (code >= FULLWIDTH_START && code <= FULLWIDTH_END) {
|
|
874
|
-
hasFullwidth = true;
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
if (hasFullwidth) {
|
|
879
|
-
const chars = [];
|
|
880
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
881
|
-
const code = normalized.charCodeAt(i);
|
|
882
|
-
if (code >= FULLWIDTH_START && code <= FULLWIDTH_END) {
|
|
883
|
-
chars.push(String.fromCharCode(code - FULLWIDTH_ASCII_OFFSET));
|
|
884
|
-
} else {
|
|
885
|
-
chars.push(normalized.charAt(i));
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
normalized = chars.join("");
|
|
889
|
-
}
|
|
890
|
-
return normalized;
|
|
891
|
-
}
|
|
892
|
-
function sanitizeWhereClause(where) {
|
|
893
|
-
if (!where || typeof where !== "string") {
|
|
894
|
-
throw new UnsafeWhereClauseError("WHERE clause must be a non-empty string");
|
|
895
|
-
}
|
|
896
|
-
const MAX_WHERE_LENGTH = 10240;
|
|
897
|
-
if (where.length > MAX_WHERE_LENGTH) {
|
|
898
|
-
throw new UnsafeWhereClauseError(
|
|
899
|
-
`WHERE clause exceeds maximum length of ${String(MAX_WHERE_LENGTH)} characters`
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
const normalized = normalizeForPatternMatching(where);
|
|
903
|
-
for (const { pattern, reason } of DANGEROUS_PATTERNS) {
|
|
904
|
-
if (pattern.test(normalized)) {
|
|
905
|
-
throw new UnsafeWhereClauseError(reason);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return where;
|
|
909
|
-
}
|
|
910
|
-
function buildWhereClause(conditions, whereClause) {
|
|
911
|
-
let finalSql = "";
|
|
912
|
-
const params = [];
|
|
913
|
-
const clauses = [];
|
|
914
|
-
if (conditions !== void 0 && conditions !== null && conditions.length > 0) {
|
|
915
|
-
const allowedOperators = /* @__PURE__ */ new Set([
|
|
916
|
-
"=",
|
|
917
|
-
"!=",
|
|
918
|
-
">",
|
|
919
|
-
">=",
|
|
920
|
-
"<",
|
|
921
|
-
"<=",
|
|
922
|
-
"LIKE",
|
|
923
|
-
"IN",
|
|
924
|
-
"IS",
|
|
925
|
-
"IS NOT",
|
|
926
|
-
"NOT LIKE"
|
|
927
|
-
]);
|
|
928
|
-
for (const cond of conditions) {
|
|
929
|
-
if (!allowedOperators.has(cond.operator.toUpperCase())) {
|
|
930
|
-
throw new Error(`Invalid operator: ${cond.operator}`);
|
|
931
|
-
}
|
|
932
|
-
const col = `"${cond.column.replace(/"/g, '""')}"`;
|
|
933
|
-
if (cond.operator.toUpperCase() === "IN" && Array.isArray(cond.value)) {
|
|
934
|
-
const placeholders = cond.value.map(() => "?").join(", ");
|
|
935
|
-
clauses.push(`${col} IN (${placeholders})`);
|
|
936
|
-
params.push(...cond.value);
|
|
937
|
-
} else {
|
|
938
|
-
clauses.push(`${col} ${cond.operator} ?`);
|
|
939
|
-
params.push(cond.value);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
finalSql = clauses.join(" AND ");
|
|
943
|
-
}
|
|
944
|
-
if (whereClause) {
|
|
945
|
-
const safeWhere = sanitizeWhereClause(whereClause);
|
|
946
|
-
if (finalSql) {
|
|
947
|
-
finalSql = `${finalSql} AND ${safeWhere}`;
|
|
948
|
-
} else {
|
|
949
|
-
finalSql = safeWhere;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return { sql: finalSql, params };
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// src/utils/identifiers.ts
|
|
956
|
-
var IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
957
|
-
var MAX_IDENTIFIER_LENGTH = 255;
|
|
958
|
-
var InvalidIdentifierError = class extends Error {
|
|
959
|
-
constructor(identifier, reason) {
|
|
960
|
-
super(`Invalid identifier "${identifier}": ${reason}`);
|
|
961
|
-
this.identifier = identifier;
|
|
962
|
-
this.reason = reason;
|
|
963
|
-
this.name = "InvalidIdentifierError";
|
|
964
|
-
}
|
|
965
|
-
identifier;
|
|
966
|
-
reason;
|
|
967
|
-
};
|
|
968
|
-
function validateIdentifier(name) {
|
|
969
|
-
if (!name || typeof name !== "string") {
|
|
970
|
-
throw new InvalidIdentifierError(
|
|
971
|
-
name,
|
|
972
|
-
"Identifier must be a non-empty string"
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
if (name.length > MAX_IDENTIFIER_LENGTH) {
|
|
976
|
-
throw new InvalidIdentifierError(
|
|
977
|
-
name,
|
|
978
|
-
`Identifier exceeds maximum length of ${String(MAX_IDENTIFIER_LENGTH)} characters`
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
if (!IDENTIFIER_PATTERN.test(name)) {
|
|
982
|
-
throw new InvalidIdentifierError(
|
|
983
|
-
name,
|
|
984
|
-
"Identifier contains invalid characters. Must start with a letter or underscore and contain only letters, digits, or underscores"
|
|
985
|
-
);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
function sanitizeIdentifier(name) {
|
|
989
|
-
validateIdentifier(name);
|
|
990
|
-
const escaped = name.replace(/"/g, '""');
|
|
991
|
-
return `"${escaped}"`;
|
|
992
|
-
}
|
|
993
|
-
function sanitizeIdentifiers(names) {
|
|
994
|
-
return names.map(sanitizeIdentifier);
|
|
995
|
-
}
|
|
996
|
-
function createColumnList(columns) {
|
|
997
|
-
return sanitizeIdentifiers(columns).join(", ");
|
|
998
|
-
}
|
|
999
|
-
function validateSameDirPath(targetPath, dbPath) {
|
|
1000
|
-
if (targetPath.includes("\0")) {
|
|
1001
|
-
return {
|
|
1002
|
-
valid: false,
|
|
1003
|
-
error: "Security: path must not contain null bytes.",
|
|
1004
|
-
dbDir: dbPath === ":memory:" ? "" : dirname(resolve(dbPath))
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
if (targetPath.startsWith("file:") || targetPath.includes("?")) {
|
|
1008
|
-
return {
|
|
1009
|
-
valid: false,
|
|
1010
|
-
error: "Security: SQLite URIs and query parameters are not allowed.",
|
|
1011
|
-
dbDir: dbPath === ":memory:" ? "" : dirname(resolve(dbPath))
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
const base = basename(targetPath);
|
|
1015
|
-
if (base.startsWith(".")) {
|
|
1016
|
-
return {
|
|
1017
|
-
valid: false,
|
|
1018
|
-
error: "Security: Cannot access hidden files or dotfiles.",
|
|
1019
|
-
dbDir: dbPath === ":memory:" ? "" : dirname(resolve(dbPath))
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
const validExtensions = [
|
|
1023
|
-
".db",
|
|
1024
|
-
".sqlite",
|
|
1025
|
-
".sqlite3",
|
|
1026
|
-
".sql",
|
|
1027
|
-
".csv",
|
|
1028
|
-
".tsv"
|
|
1029
|
-
];
|
|
1030
|
-
const hasValidExt = validExtensions.some(
|
|
1031
|
-
(ext) => base.toLowerCase().endsWith(ext)
|
|
1032
|
-
);
|
|
1033
|
-
if (!hasValidExt) {
|
|
1034
|
-
return {
|
|
1035
|
-
valid: false,
|
|
1036
|
-
error: `Security: Invalid file extension. Allowed extensions are: ${validExtensions.join(", ")}`,
|
|
1037
|
-
dbDir: dbPath === ":memory:" ? "" : dirname(resolve(dbPath))
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
if (dbPath === ":memory:") {
|
|
1041
|
-
return {
|
|
1042
|
-
valid: false,
|
|
1043
|
-
error: "Security: Filesystem operations are not permitted for :memory: databases.",
|
|
1044
|
-
dbDir: ""
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
const dbDir = dirname(resolve(dbPath));
|
|
1048
|
-
let resolvedTarget = resolve(targetPath);
|
|
1049
|
-
try {
|
|
1050
|
-
resolvedTarget = realpathSync(resolvedTarget);
|
|
1051
|
-
} catch {
|
|
1052
|
-
try {
|
|
1053
|
-
const parentDir = realpathSync(dirname(resolvedTarget));
|
|
1054
|
-
resolvedTarget = resolve(parentDir, basename(resolvedTarget));
|
|
1055
|
-
} catch {
|
|
1056
|
-
return {
|
|
1057
|
-
valid: false,
|
|
1058
|
-
error: "Security: Target parent directory must exist.",
|
|
1059
|
-
dbDir
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
let resolvedDbDir;
|
|
1064
|
-
try {
|
|
1065
|
-
resolvedDbDir = realpathSync(dbDir);
|
|
1066
|
-
} catch {
|
|
1067
|
-
return {
|
|
1068
|
-
valid: false,
|
|
1069
|
-
error: "Security: Database directory must exist.",
|
|
1070
|
-
dbDir
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
const normalizedTarget = normalize(resolvedTarget);
|
|
1074
|
-
const normalizedDir = normalize(resolvedDbDir);
|
|
1075
|
-
const targetDir = normalize(dirname(resolvedTarget));
|
|
1076
|
-
if (relative(normalizedDir, targetDir) !== "") {
|
|
1077
|
-
return {
|
|
1078
|
-
valid: false,
|
|
1079
|
-
error: `Security: path must be within the database directory. Path traversal is not allowed.`,
|
|
1080
|
-
dbDir
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
return { valid: true, resolvedPath: normalizedTarget };
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// src/utils/validate-json-path.ts
|
|
1087
|
-
var VALID_JSON_PATH_PATTERN = /^\$(\.[a-zA-Z_][a-zA-Z0-9_ -]*|\[\d+\]|\[#\]|\[\*\])*$/;
|
|
1088
|
-
var MAX_JSON_PATH_LENGTH = 500;
|
|
1089
|
-
function validateJsonPath(path3, param = "path") {
|
|
1090
|
-
if (!path3.startsWith("$")) {
|
|
1091
|
-
throw new ValidationError(
|
|
1092
|
-
`JSON ${param} must start with $`,
|
|
1093
|
-
"INVALID_JSON_PATH",
|
|
1094
|
-
{
|
|
1095
|
-
suggestion: "Use a valid JSON path starting with $. For example: $.key or $[0]"
|
|
1096
|
-
}
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
if (path3.length > MAX_JSON_PATH_LENGTH) {
|
|
1100
|
-
throw new ValidationError(
|
|
1101
|
-
`JSON ${param} exceeds maximum length of ${MAX_JSON_PATH_LENGTH} characters`,
|
|
1102
|
-
"INVALID_JSON_PATH"
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
if (!VALID_JSON_PATH_PATTERN.test(path3)) {
|
|
1106
|
-
throw new ValidationError(
|
|
1107
|
-
`Invalid JSON ${param} syntax: ${path3}`,
|
|
1108
|
-
"INVALID_JSON_PATH",
|
|
1109
|
-
{
|
|
1110
|
-
suggestion: "Use $.key, $[0], or $[*] patterns. Member names must start with a letter or underscore."
|
|
1111
|
-
}
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
var ALLOWED_AGGREGATE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1116
|
-
"count",
|
|
1117
|
-
"sum",
|
|
1118
|
-
"avg",
|
|
1119
|
-
"min",
|
|
1120
|
-
"max",
|
|
1121
|
-
"group_concat",
|
|
1122
|
-
"total"
|
|
1123
|
-
]);
|
|
1124
|
-
var AGGREGATE_FUNCTION_PATTERN = /^([A-Za-z_]+)\(\s*(\*|"[a-zA-Z_][a-zA-Z0-9_]*"|[a-zA-Z_][a-zA-Z0-9_]*)(?:\s*,\s*'[^']*')?\s*\)$/;
|
|
1125
|
-
function validateAggregateFunction(fn) {
|
|
1126
|
-
const match = AGGREGATE_FUNCTION_PATTERN.exec(fn.trim());
|
|
1127
|
-
if (!match) {
|
|
1128
|
-
throw new ValidationError(
|
|
1129
|
-
`Invalid aggregate function syntax: ${fn}`,
|
|
1130
|
-
"INVALID_AGGREGATE_FUNCTION",
|
|
1131
|
-
{
|
|
1132
|
-
suggestion: "Use a standard aggregate function like COUNT(*), SUM(column), AVG(column), MIN(column), MAX(column), GROUP_CONCAT(column), or TOTAL(column)."
|
|
1133
|
-
}
|
|
1134
|
-
);
|
|
1135
|
-
}
|
|
1136
|
-
const funcName = match[1]?.toLowerCase();
|
|
1137
|
-
if (!funcName || !ALLOWED_AGGREGATE_FUNCTIONS.has(funcName)) {
|
|
1138
|
-
throw new ValidationError(
|
|
1139
|
-
`Disallowed aggregate function: ${match[1]}. Allowed: ${[...ALLOWED_AGGREGATE_FUNCTIONS].join(", ").toUpperCase()}`,
|
|
1140
|
-
"INVALID_AGGREGATE_FUNCTION"
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
855
|
// src/adapters/sqlite/tools/core/tables.ts
|
|
1146
856
|
var SPATIALITE_SYSTEM_PREFIXES = [
|
|
1147
857
|
"geometry_columns",
|
|
@@ -1396,7 +1106,7 @@ function createDescribeTableTool(adapter) {
|
|
|
1396
1106
|
try {
|
|
1397
1107
|
const tableInfo = await adapter.describeTable(input.table);
|
|
1398
1108
|
const rawColumns = tableInfo.columns ?? [];
|
|
1399
|
-
|
|
1109
|
+
const enrichedColumns = rawColumns.map((c) => ({
|
|
1400
1110
|
name: c.name,
|
|
1401
1111
|
type: c.type,
|
|
1402
1112
|
nullable: c.nullable,
|
|
@@ -1422,28 +1132,39 @@ function createDescribeTableTool(adapter) {
|
|
|
1422
1132
|
[input.table]
|
|
1423
1133
|
);
|
|
1424
1134
|
const ddl = ddlResult.rows?.[0]?.["sql"] ?? "";
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
enriched.
|
|
1135
|
+
for (const row of xinfoResult.rows) {
|
|
1136
|
+
const colName = row["name"];
|
|
1137
|
+
const hidden = row["hidden"];
|
|
1138
|
+
if (hidden === 2 || hidden === 3) {
|
|
1139
|
+
let enriched = enrichedColumns.find(
|
|
1140
|
+
(c) => c.name === colName
|
|
1141
|
+
);
|
|
1142
|
+
if (!enriched) {
|
|
1143
|
+
enriched = {
|
|
1144
|
+
name: colName,
|
|
1145
|
+
type: row["type"],
|
|
1146
|
+
nullable: row["notnull"] === 0,
|
|
1147
|
+
primaryKey: row["pk"] === 1,
|
|
1148
|
+
defaultValue: row["dflt_value"]
|
|
1149
|
+
};
|
|
1150
|
+
enrichedColumns.push(enriched);
|
|
1151
|
+
}
|
|
1152
|
+
enriched.isGenerated = true;
|
|
1153
|
+
enriched.generatedType = hidden === 3 ? "STORED" : "VIRTUAL";
|
|
1154
|
+
const escapedName = colName.replace(
|
|
1155
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
1156
|
+
"\\$&"
|
|
1157
|
+
);
|
|
1158
|
+
const exprPattern = new RegExp(
|
|
1159
|
+
`"?${escapedName}"?\\s+\\w+[^,]*GENERATED\\s+ALWAYS\\s+AS\\s*\\(([^)]+)\\)`,
|
|
1160
|
+
"i"
|
|
1161
|
+
);
|
|
1162
|
+
const match = exprPattern.exec(ddl);
|
|
1163
|
+
if (match?.[1]) {
|
|
1164
|
+
enriched.generatedExpression = match[1].trim();
|
|
1165
|
+
}
|
|
1444
1166
|
}
|
|
1445
|
-
|
|
1446
|
-
});
|
|
1167
|
+
}
|
|
1447
1168
|
}
|
|
1448
1169
|
}
|
|
1449
1170
|
} catch {
|
|
@@ -1831,7 +1552,7 @@ async function validateTableExists2(adapter, table) {
|
|
|
1831
1552
|
success: false,
|
|
1832
1553
|
error: `Table or view '${table}' not found. Use sqlite_list_tables to see available tables.`,
|
|
1833
1554
|
code: "TABLE_NOT_FOUND",
|
|
1834
|
-
category:
|
|
1555
|
+
category: ErrorCategory.RESOURCE,
|
|
1835
1556
|
suggestion: "Table or view does not exist. Run sqlite_list_tables to see available tables.",
|
|
1836
1557
|
details: void 0,
|
|
1837
1558
|
recoverable: false
|
|
@@ -1866,12 +1587,38 @@ function createUpsertTool(adapter) {
|
|
|
1866
1587
|
conflictColumns: parsed.conflictColumns !== void 0 ? Array.isArray(parsed.conflictColumns) ? parsed.conflictColumns : [parsed.conflictColumns] : []
|
|
1867
1588
|
};
|
|
1868
1589
|
if (Object.keys(input.data).length === 0)
|
|
1869
|
-
throw new
|
|
1590
|
+
throw new ValidationError("data (or values alias) is required");
|
|
1870
1591
|
} catch (error) {
|
|
1871
1592
|
return { ...formatHandlerError(error), rowsAffected: 0 };
|
|
1872
1593
|
}
|
|
1873
1594
|
const validationError = await validateTableExists2(adapter, input.table);
|
|
1874
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
|
+
}
|
|
1875
1622
|
const columns = Object.keys(input.data);
|
|
1876
1623
|
const values = Object.values(input.data);
|
|
1877
1624
|
const placeholders = columns.map(() => "?").join(", ");
|
|
@@ -1886,11 +1633,35 @@ function createUpsertTool(adapter) {
|
|
|
1886
1633
|
const updateSets = colsToUpdate.map(
|
|
1887
1634
|
(c) => `${sanitizeIdentifier(c)} = EXCLUDED.${sanitizeIdentifier(c)}`
|
|
1888
1635
|
).join(", ");
|
|
1889
|
-
|
|
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;
|
|
1890
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
|
+
}
|
|
1891
1652
|
sql += ` ON CONFLICT (${conflictCols}) DO NOTHING`;
|
|
1892
1653
|
}
|
|
1893
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
|
+
}
|
|
1894
1665
|
sql = `INSERT OR REPLACE INTO ${safeTable} (${safeColumns.join(", ")}) VALUES (${placeholders})`;
|
|
1895
1666
|
}
|
|
1896
1667
|
if (input.returning !== void 0 && input.returning !== false) {
|
|
@@ -1903,6 +1674,21 @@ function createUpsertTool(adapter) {
|
|
|
1903
1674
|
}
|
|
1904
1675
|
try {
|
|
1905
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
|
+
}
|
|
1906
1692
|
const response = {
|
|
1907
1693
|
success: true,
|
|
1908
1694
|
rowsAffected: result.rowsAffected,
|
|
@@ -2986,6 +2772,269 @@ function createAlterTableTool(adapter) {
|
|
|
2986
2772
|
};
|
|
2987
2773
|
}
|
|
2988
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
|
+
|
|
2989
3038
|
// src/adapters/sqlite/tools/core/index.ts
|
|
2990
3039
|
function getCoreTools(adapter) {
|
|
2991
3040
|
return [
|
|
@@ -3009,7 +3058,11 @@ function getCoreTools(adapter) {
|
|
|
3009
3058
|
createListConstraintsTool(adapter),
|
|
3010
3059
|
createDateAddTool(adapter),
|
|
3011
3060
|
createDateDiffTool(adapter),
|
|
3012
|
-
createAlterTableTool(adapter)
|
|
3061
|
+
createAlterTableTool(adapter),
|
|
3062
|
+
createEnableVersioningTool(adapter),
|
|
3063
|
+
createDisableVersioningTool(adapter),
|
|
3064
|
+
createCheckVersionTool(adapter),
|
|
3065
|
+
createConditionalUpdateTool(adapter)
|
|
3013
3066
|
];
|
|
3014
3067
|
}
|
|
3015
3068
|
|
|
@@ -3129,7 +3182,7 @@ var SchemaManager = class {
|
|
|
3129
3182
|
throw new DbMcpError(
|
|
3130
3183
|
"Invalid table name",
|
|
3131
3184
|
"INVALID_TABLE",
|
|
3132
|
-
|
|
3185
|
+
ErrorCategory.VALIDATION
|
|
3133
3186
|
);
|
|
3134
3187
|
}
|
|
3135
3188
|
const result = await this.executor.executeReadQuery(
|
|
@@ -3139,7 +3192,7 @@ var SchemaManager = class {
|
|
|
3139
3192
|
throw new DbMcpError(
|
|
3140
3193
|
`Table '${tableName}' does not exist`,
|
|
3141
3194
|
"TABLE_NOT_FOUND",
|
|
3142
|
-
|
|
3195
|
+
ErrorCategory.RESOURCE
|
|
3143
3196
|
);
|
|
3144
3197
|
}
|
|
3145
3198
|
const columns = (result.rows ?? []).map((row) => ({
|
|
@@ -3227,55 +3280,6 @@ var SchemaManager = class {
|
|
|
3227
3280
|
}
|
|
3228
3281
|
};
|
|
3229
3282
|
|
|
3230
|
-
// src/utils/insights-manager.ts
|
|
3231
|
-
var InsightsManager = class {
|
|
3232
|
-
insights = [];
|
|
3233
|
-
/**
|
|
3234
|
-
* Append a new business insight
|
|
3235
|
-
*/
|
|
3236
|
-
append(insight) {
|
|
3237
|
-
if (insight.trim()) {
|
|
3238
|
-
this.insights.push(insight.trim());
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
/**
|
|
3242
|
-
* Get all insights
|
|
3243
|
-
*/
|
|
3244
|
-
getAll() {
|
|
3245
|
-
return [...this.insights];
|
|
3246
|
-
}
|
|
3247
|
-
/**
|
|
3248
|
-
* Get insight count
|
|
3249
|
-
*/
|
|
3250
|
-
count() {
|
|
3251
|
-
return this.insights.length;
|
|
3252
|
-
}
|
|
3253
|
-
/**
|
|
3254
|
-
* Clear all insights
|
|
3255
|
-
*/
|
|
3256
|
-
clear() {
|
|
3257
|
-
this.insights = [];
|
|
3258
|
-
}
|
|
3259
|
-
/**
|
|
3260
|
-
* Synthesize insights into a formatted memo
|
|
3261
|
-
*/
|
|
3262
|
-
synthesizeMemo() {
|
|
3263
|
-
if (this.insights.length === 0) {
|
|
3264
|
-
return "No business insights have been discovered yet.";
|
|
3265
|
-
}
|
|
3266
|
-
const insightsList = this.insights.map((insight) => `- ${insight}`).join("\n");
|
|
3267
|
-
let memo = "\u{1F4CA} Business Intelligence Memo \u{1F4CA}\n\n";
|
|
3268
|
-
memo += "Key Insights Discovered:\n\n";
|
|
3269
|
-
memo += insightsList;
|
|
3270
|
-
if (this.insights.length > 1) {
|
|
3271
|
-
memo += "\n\nSummary:\n";
|
|
3272
|
-
memo += `Analysis has revealed ${this.insights.length} key business insights that suggest opportunities for strategic optimization and growth.`;
|
|
3273
|
-
}
|
|
3274
|
-
return memo;
|
|
3275
|
-
}
|
|
3276
|
-
};
|
|
3277
|
-
var insightsManager = new InsightsManager();
|
|
3278
|
-
|
|
3279
3283
|
// src/adapters/sqlite/resources.ts
|
|
3280
3284
|
function getResourceDefinitions(adapter) {
|
|
3281
3285
|
return [
|
|
@@ -3287,8 +3291,7 @@ function getResourceDefinitions(adapter) {
|
|
|
3287
3291
|
createHealthResource(adapter),
|
|
3288
3292
|
createMetaResource(adapter),
|
|
3289
3293
|
createCompileOptionsResource(adapter),
|
|
3290
|
-
createPragmaResource(adapter)
|
|
3291
|
-
createInsightsResource()
|
|
3294
|
+
createPragmaResource(adapter)
|
|
3292
3295
|
];
|
|
3293
3296
|
}
|
|
3294
3297
|
function createSchemaResource(adapter) {
|
|
@@ -3358,7 +3361,7 @@ function createTableSchemaResource(adapter) {
|
|
|
3358
3361
|
throw new DbMcpError(
|
|
3359
3362
|
"Invalid table URI format",
|
|
3360
3363
|
"RESOURCE_INVALID_URI",
|
|
3361
|
-
|
|
3364
|
+
ErrorCategory.VALIDATION
|
|
3362
3365
|
);
|
|
3363
3366
|
}
|
|
3364
3367
|
const tableName = decodeURIComponent(match[1]);
|
|
@@ -3499,36 +3502,16 @@ function createMetaResource(adapter) {
|
|
|
3499
3502
|
} catch {
|
|
3500
3503
|
meta[pragma] = null;
|
|
3501
3504
|
}
|
|
3502
|
-
}
|
|
3503
|
-
return {
|
|
3504
|
-
contents: [
|
|
3505
|
-
{
|
|
3506
|
-
uri: "sqlite://meta",
|
|
3507
|
-
mimeType: "application/json",
|
|
3508
|
-
text: JSON.stringify(meta, null, 2)
|
|
3509
|
-
}
|
|
3510
|
-
]
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
};
|
|
3514
|
-
}
|
|
3515
|
-
function createInsightsResource() {
|
|
3516
|
-
return {
|
|
3517
|
-
name: "sqlite_insights",
|
|
3518
|
-
uri: "memo://insights",
|
|
3519
|
-
description: "Business insights memo that gets updated during database analysis",
|
|
3520
|
-
mimeType: "text/plain",
|
|
3521
|
-
annotations: HIGH_PRIORITY,
|
|
3522
|
-
handler: () => {
|
|
3523
|
-
return Promise.resolve({
|
|
3505
|
+
}
|
|
3506
|
+
return {
|
|
3524
3507
|
contents: [
|
|
3525
3508
|
{
|
|
3526
|
-
uri: "
|
|
3527
|
-
mimeType: "
|
|
3528
|
-
text:
|
|
3509
|
+
uri: "sqlite://meta",
|
|
3510
|
+
mimeType: "application/json",
|
|
3511
|
+
text: JSON.stringify(meta, null, 2)
|
|
3529
3512
|
}
|
|
3530
3513
|
]
|
|
3531
|
-
}
|
|
3514
|
+
};
|
|
3532
3515
|
}
|
|
3533
3516
|
};
|
|
3534
3517
|
}
|
|
@@ -4161,7 +4144,6 @@ You are guiding an interactive demo of the SQLite MCP Server capabilities.
|
|
|
4161
4144
|
|
|
4162
4145
|
**Prompts**: Pre-written templates that structure AI conversations (like this one)
|
|
4163
4146
|
**Tools**: SQL operations (read_query, write_query, create_table, etc.)
|
|
4164
|
-
**Resources**: Living documents like \`memo://insights\` that update during analysis
|
|
4165
4147
|
|
|
4166
4148
|
## Demo Workflow
|
|
4167
4149
|
|
|
@@ -4187,17 +4169,10 @@ After setup, present 3-4 natural language query choices:
|
|
|
4187
4169
|
For each user selection:
|
|
4188
4170
|
- Run the appropriate SQL query
|
|
4189
4171
|
- Explain the results
|
|
4190
|
-
- Use \`sqlite_append_insight\` to capture findings
|
|
4191
|
-
|
|
4192
|
-
### 5. Capture Insights
|
|
4193
|
-
Remind the user about \`memo://insights\`:
|
|
4194
|
-
- Each insight is automatically added to this resource
|
|
4195
|
-
- They can attach it to see all discoveries
|
|
4196
4172
|
|
|
4197
|
-
###
|
|
4173
|
+
### 5. Wrap Up
|
|
4198
4174
|
Summarize what was demonstrated:
|
|
4199
4175
|
- Data creation and querying
|
|
4200
|
-
- Insight capture via resources
|
|
4201
4176
|
- The power of MCP for database workflows
|
|
4202
4177
|
|
|
4203
4178
|
---
|
|
@@ -4228,6 +4203,150 @@ function getPromptDefinitions(adapter) {
|
|
|
4228
4203
|
createDemoPrompt()
|
|
4229
4204
|
];
|
|
4230
4205
|
}
|
|
4206
|
+
|
|
4207
|
+
// src/adapters/sqlite/json-utils.ts
|
|
4208
|
+
var DEFAULT_NORMALIZATION_OPTIONS = {
|
|
4209
|
+
sortKeys: true,
|
|
4210
|
+
compact: true,
|
|
4211
|
+
unicodeNormalize: true,
|
|
4212
|
+
typeCoercion: false,
|
|
4213
|
+
duplicateKeys: "last"
|
|
4214
|
+
};
|
|
4215
|
+
function normalizeJson(value, options = {}) {
|
|
4216
|
+
const opts = { ...DEFAULT_NORMALIZATION_OPTIONS, ...options };
|
|
4217
|
+
const changes = [];
|
|
4218
|
+
let parsed;
|
|
4219
|
+
if (typeof value === "string") {
|
|
4220
|
+
try {
|
|
4221
|
+
parsed = JSON.parse(value);
|
|
4222
|
+
} catch {
|
|
4223
|
+
return {
|
|
4224
|
+
normalized: value,
|
|
4225
|
+
wasModified: false,
|
|
4226
|
+
changes: ["Input is not valid JSON"]
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
4229
|
+
} else {
|
|
4230
|
+
parsed = value;
|
|
4231
|
+
}
|
|
4232
|
+
const normalized = normalizeValue(parsed, opts, changes, "");
|
|
4233
|
+
const normalizedStr = opts.compact ? JSON.stringify(normalized) : JSON.stringify(normalized, null, 2);
|
|
4234
|
+
const originalStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
4235
|
+
const wasModified = normalizedStr !== originalStr;
|
|
4236
|
+
return {
|
|
4237
|
+
normalized: normalizedStr,
|
|
4238
|
+
wasModified,
|
|
4239
|
+
changes
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
function normalizeValue(value, options, changes, path3) {
|
|
4243
|
+
if (value === null || value === void 0) {
|
|
4244
|
+
return null;
|
|
4245
|
+
}
|
|
4246
|
+
if (typeof value === "string") {
|
|
4247
|
+
let result = value;
|
|
4248
|
+
if (options.unicodeNormalize && value !== value.normalize("NFC")) {
|
|
4249
|
+
result = result.normalize("NFC");
|
|
4250
|
+
changes.push(`${path3 || "$"}: Unicode normalized`);
|
|
4251
|
+
}
|
|
4252
|
+
if (options.typeCoercion) {
|
|
4253
|
+
if (result.toLowerCase() === "true") {
|
|
4254
|
+
changes.push(`${path3 || "$"}: Coerced "true" to boolean`);
|
|
4255
|
+
return true;
|
|
4256
|
+
}
|
|
4257
|
+
if (result.toLowerCase() === "false") {
|
|
4258
|
+
changes.push(`${path3 || "$"}: Coerced "false" to boolean`);
|
|
4259
|
+
return false;
|
|
4260
|
+
}
|
|
4261
|
+
const num = Number(result);
|
|
4262
|
+
if (!isNaN(num) && result.trim() !== "") {
|
|
4263
|
+
changes.push(`${path3 || "$"}: Coerced "${result}" to number`);
|
|
4264
|
+
return num;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
return result;
|
|
4268
|
+
}
|
|
4269
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
4270
|
+
return value;
|
|
4271
|
+
}
|
|
4272
|
+
if (Array.isArray(value)) {
|
|
4273
|
+
return value.map(
|
|
4274
|
+
(item, index) => normalizeValue(item, options, changes, `${path3 || "$"}[${index}]`)
|
|
4275
|
+
);
|
|
4276
|
+
}
|
|
4277
|
+
if (typeof value === "object") {
|
|
4278
|
+
const obj = value;
|
|
4279
|
+
let keys = Object.keys(obj);
|
|
4280
|
+
if (options.sortKeys) {
|
|
4281
|
+
const originalOrder = keys.join(",");
|
|
4282
|
+
keys = keys.sort();
|
|
4283
|
+
if (keys.join(",") !== originalOrder) {
|
|
4284
|
+
changes.push(`${path3 || "$"}: Keys sorted`);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
const result = {};
|
|
4288
|
+
for (const key of keys) {
|
|
4289
|
+
result[key] = normalizeValue(
|
|
4290
|
+
obj[key],
|
|
4291
|
+
options,
|
|
4292
|
+
changes,
|
|
4293
|
+
`${path3 || "$"}.${key}`
|
|
4294
|
+
);
|
|
4295
|
+
}
|
|
4296
|
+
return result;
|
|
4297
|
+
}
|
|
4298
|
+
return value;
|
|
4299
|
+
}
|
|
4300
|
+
var jsonbSupportedCache = null;
|
|
4301
|
+
function isJsonbSupportedVersion(versionString) {
|
|
4302
|
+
const versionRegex = /^(\d+)\.(\d+)\.?(\d+)?/;
|
|
4303
|
+
const match = versionRegex.exec(versionString);
|
|
4304
|
+
if (!match) return false;
|
|
4305
|
+
const major = parseInt(match[1] ?? "0", 10);
|
|
4306
|
+
const minor = parseInt(match[2] ?? "0", 10);
|
|
4307
|
+
return major > 3 || major === 3 && minor >= 45;
|
|
4308
|
+
}
|
|
4309
|
+
function isJsonbSupported() {
|
|
4310
|
+
return jsonbSupportedCache ?? false;
|
|
4311
|
+
}
|
|
4312
|
+
function setJsonbSupported(supported) {
|
|
4313
|
+
jsonbSupportedCache = supported;
|
|
4314
|
+
}
|
|
4315
|
+
function detectJsonStorageFormat(value) {
|
|
4316
|
+
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
4317
|
+
return "jsonb";
|
|
4318
|
+
}
|
|
4319
|
+
if (typeof value === "string") {
|
|
4320
|
+
return "text";
|
|
4321
|
+
}
|
|
4322
|
+
return "unknown";
|
|
4323
|
+
}
|
|
4324
|
+
function parseJsonValue(value) {
|
|
4325
|
+
if (value === null || value === void 0) {
|
|
4326
|
+
return null;
|
|
4327
|
+
}
|
|
4328
|
+
if (typeof value === "object" && !(value instanceof Buffer)) {
|
|
4329
|
+
return value;
|
|
4330
|
+
}
|
|
4331
|
+
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
4332
|
+
throw new DbMcpError(
|
|
4333
|
+
"JSONB blob cannot be parsed directly. Use json() in SQL query.",
|
|
4334
|
+
"JSON_PARSE_ERROR",
|
|
4335
|
+
ErrorCategory.VALIDATION
|
|
4336
|
+
);
|
|
4337
|
+
}
|
|
4338
|
+
if (typeof value === "string") {
|
|
4339
|
+
try {
|
|
4340
|
+
return JSON.parse(value);
|
|
4341
|
+
} catch {
|
|
4342
|
+
return value;
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
4346
|
+
return value;
|
|
4347
|
+
}
|
|
4348
|
+
return null;
|
|
4349
|
+
}
|
|
4231
4350
|
var coerceNumber2 = (val) => {
|
|
4232
4351
|
if (typeof val === "string") {
|
|
4233
4352
|
if (val.trim() === "") return void 0;
|
|
@@ -4702,7 +4821,7 @@ function createJsonExtractTool(adapter) {
|
|
|
4702
4821
|
return {
|
|
4703
4822
|
success: true,
|
|
4704
4823
|
rowCount: result.rows?.length ?? 0,
|
|
4705
|
-
values: result.rows?.map((r) => r["value"])
|
|
4824
|
+
values: result.rows?.map((r) => parseJsonValue(r["value"]))
|
|
4706
4825
|
};
|
|
4707
4826
|
} catch (error) {
|
|
4708
4827
|
return formatHandlerError(error);
|
|
@@ -4750,6 +4869,7 @@ function createJsonSetTool(adapter) {
|
|
|
4750
4869
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
4751
4870
|
return {
|
|
4752
4871
|
success: true,
|
|
4872
|
+
message: `Set value at ${input.path} in ${table}.${column}`,
|
|
4753
4873
|
rowsAffected: result.rowsAffected,
|
|
4754
4874
|
...result.rowsAffected === 0 ? {
|
|
4755
4875
|
warning: "No rows matched the WHERE clause \u2014 no changes were made"
|
|
@@ -4791,6 +4911,7 @@ function createJsonRemoveTool(adapter) {
|
|
|
4791
4911
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
4792
4912
|
return {
|
|
4793
4913
|
success: true,
|
|
4914
|
+
message: `Removed value at ${input.path} from ${table}.${column}`,
|
|
4794
4915
|
rowsAffected: result.rowsAffected,
|
|
4795
4916
|
...result.rowsAffected === 0 ? {
|
|
4796
4917
|
warning: "No rows matched the WHERE clause \u2014 no changes were made"
|
|
@@ -4933,6 +5054,7 @@ function createJsonArrayAppendTool(adapter) {
|
|
|
4933
5054
|
const result = await adapter.executeWriteQuery(sql, queryParams);
|
|
4934
5055
|
return {
|
|
4935
5056
|
success: true,
|
|
5057
|
+
message: `Appended to ${input.path} in ${table}.${column}`,
|
|
4936
5058
|
rowsAffected: result.rowsAffected
|
|
4937
5059
|
};
|
|
4938
5060
|
} catch (error) {
|
|
@@ -5032,10 +5154,14 @@ function createJsonEachTool(adapter) {
|
|
|
5032
5154
|
}
|
|
5033
5155
|
sql += ` LIMIT ${input.limit}`;
|
|
5034
5156
|
const result = await adapter.executeReadQuery(sql, queryParams);
|
|
5157
|
+
const parsedElements = result.rows?.map((row) => ({
|
|
5158
|
+
...row,
|
|
5159
|
+
value: parseJsonValue(row["value"])
|
|
5160
|
+
})) ?? [];
|
|
5035
5161
|
return {
|
|
5036
5162
|
success: true,
|
|
5037
|
-
rowCount:
|
|
5038
|
-
elements:
|
|
5163
|
+
rowCount: parsedElements.length,
|
|
5164
|
+
elements: parsedElements
|
|
5039
5165
|
};
|
|
5040
5166
|
} catch (error) {
|
|
5041
5167
|
return formatHandlerError(error);
|
|
@@ -5089,10 +5215,17 @@ function createJsonGroupArrayTool(adapter) {
|
|
|
5089
5215
|
}
|
|
5090
5216
|
sql += groupByClause;
|
|
5091
5217
|
const result = await adapter.executeReadQuery(sql, queryParams);
|
|
5218
|
+
const parsedRows = result.rows?.map((row) => {
|
|
5219
|
+
const parsedRow = {};
|
|
5220
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5221
|
+
parsedRow[key] = parseJsonValue(value);
|
|
5222
|
+
}
|
|
5223
|
+
return parsedRow;
|
|
5224
|
+
}) ?? [];
|
|
5092
5225
|
return {
|
|
5093
5226
|
success: true,
|
|
5094
|
-
rowCount:
|
|
5095
|
-
rows:
|
|
5227
|
+
rowCount: parsedRows.length,
|
|
5228
|
+
rows: parsedRows
|
|
5096
5229
|
};
|
|
5097
5230
|
} catch (error) {
|
|
5098
5231
|
return formatHandlerError(error);
|
|
@@ -5144,10 +5277,17 @@ function createJsonGroupObjectTool(adapter) {
|
|
|
5144
5277
|
}
|
|
5145
5278
|
const sql2 = `SELECT ${outerSelect} FROM (${subquery})${outerGroupBy}`;
|
|
5146
5279
|
const result2 = await adapter.executeReadQuery(sql2, queryParams);
|
|
5280
|
+
const parsedRows2 = result2.rows?.map((row) => {
|
|
5281
|
+
const parsedRow = {};
|
|
5282
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5283
|
+
parsedRow[key] = parseJsonValue(value);
|
|
5284
|
+
}
|
|
5285
|
+
return parsedRow;
|
|
5286
|
+
}) ?? [];
|
|
5147
5287
|
return {
|
|
5148
5288
|
success: true,
|
|
5149
|
-
rowCount:
|
|
5150
|
-
rows:
|
|
5289
|
+
rowCount: parsedRows2.length,
|
|
5290
|
+
rows: parsedRows2
|
|
5151
5291
|
};
|
|
5152
5292
|
}
|
|
5153
5293
|
if (!input.valueColumn) {
|
|
@@ -5171,151 +5311,40 @@ function createJsonGroupObjectTool(adapter) {
|
|
|
5171
5311
|
if (input.groupByColumn) {
|
|
5172
5312
|
const groupByCol = input.allowExpressions ? input.groupByColumn : sanitizeIdentifier(input.groupByColumn);
|
|
5173
5313
|
const groupAlias = input.allowExpressions ? "group_key" : input.groupByColumn;
|
|
5174
|
-
selectClause = `${groupByCol} AS ${groupAlias}, ${selectClause}`;
|
|
5175
|
-
groupByClause = ` GROUP BY ${groupByCol}`;
|
|
5176
|
-
}
|
|
5177
|
-
let sql = `SELECT ${selectClause} FROM ${table}`;
|
|
5178
|
-
if (input.conditions || input.whereClause) {
|
|
5179
|
-
const { sql: whereSql, params: whereParams } = buildWhereClause(
|
|
5180
|
-
input.conditions,
|
|
5181
|
-
input.whereClause
|
|
5182
|
-
);
|
|
5183
|
-
if (whereSql !== "") {
|
|
5184
|
-
sql += ` WHERE ${whereSql}`;
|
|
5185
|
-
queryParams.push(...whereParams);
|
|
5186
|
-
}
|
|
5187
|
-
}
|
|
5188
|
-
sql += groupByClause;
|
|
5189
|
-
const result = await adapter.executeReadQuery(sql, queryParams);
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
return
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
}
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
sortKeys: true,
|
|
5206
|
-
compact: true,
|
|
5207
|
-
unicodeNormalize: true,
|
|
5208
|
-
typeCoercion: false,
|
|
5209
|
-
duplicateKeys: "last"
|
|
5210
|
-
};
|
|
5211
|
-
function normalizeJson(value, options = {}) {
|
|
5212
|
-
const opts = { ...DEFAULT_NORMALIZATION_OPTIONS, ...options };
|
|
5213
|
-
const changes = [];
|
|
5214
|
-
let parsed;
|
|
5215
|
-
if (typeof value === "string") {
|
|
5216
|
-
try {
|
|
5217
|
-
parsed = JSON.parse(value);
|
|
5218
|
-
} catch {
|
|
5219
|
-
return {
|
|
5220
|
-
normalized: value,
|
|
5221
|
-
wasModified: false,
|
|
5222
|
-
changes: ["Input is not valid JSON"]
|
|
5223
|
-
};
|
|
5224
|
-
}
|
|
5225
|
-
} else {
|
|
5226
|
-
parsed = value;
|
|
5227
|
-
}
|
|
5228
|
-
const normalized = normalizeValue(parsed, opts, changes, "");
|
|
5229
|
-
const normalizedStr = opts.compact ? JSON.stringify(normalized) : JSON.stringify(normalized, null, 2);
|
|
5230
|
-
const originalStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
5231
|
-
const wasModified = normalizedStr !== originalStr;
|
|
5232
|
-
return {
|
|
5233
|
-
normalized: normalizedStr,
|
|
5234
|
-
wasModified,
|
|
5235
|
-
changes
|
|
5236
|
-
};
|
|
5237
|
-
}
|
|
5238
|
-
function normalizeValue(value, options, changes, path3) {
|
|
5239
|
-
if (value === null || value === void 0) {
|
|
5240
|
-
return null;
|
|
5241
|
-
}
|
|
5242
|
-
if (typeof value === "string") {
|
|
5243
|
-
let result = value;
|
|
5244
|
-
if (options.unicodeNormalize && value !== value.normalize("NFC")) {
|
|
5245
|
-
result = result.normalize("NFC");
|
|
5246
|
-
changes.push(`${path3 || "$"}: Unicode normalized`);
|
|
5247
|
-
}
|
|
5248
|
-
if (options.typeCoercion) {
|
|
5249
|
-
if (result.toLowerCase() === "true") {
|
|
5250
|
-
changes.push(`${path3 || "$"}: Coerced "true" to boolean`);
|
|
5251
|
-
return true;
|
|
5252
|
-
}
|
|
5253
|
-
if (result.toLowerCase() === "false") {
|
|
5254
|
-
changes.push(`${path3 || "$"}: Coerced "false" to boolean`);
|
|
5255
|
-
return false;
|
|
5256
|
-
}
|
|
5257
|
-
const num = Number(result);
|
|
5258
|
-
if (!isNaN(num) && result.trim() !== "") {
|
|
5259
|
-
changes.push(`${path3 || "$"}: Coerced "${result}" to number`);
|
|
5260
|
-
return num;
|
|
5261
|
-
}
|
|
5262
|
-
}
|
|
5263
|
-
return result;
|
|
5264
|
-
}
|
|
5265
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
5266
|
-
return value;
|
|
5267
|
-
}
|
|
5268
|
-
if (Array.isArray(value)) {
|
|
5269
|
-
return value.map(
|
|
5270
|
-
(item, index) => normalizeValue(item, options, changes, `${path3 || "$"}[${index}]`)
|
|
5271
|
-
);
|
|
5272
|
-
}
|
|
5273
|
-
if (typeof value === "object") {
|
|
5274
|
-
const obj = value;
|
|
5275
|
-
let keys = Object.keys(obj);
|
|
5276
|
-
if (options.sortKeys) {
|
|
5277
|
-
const originalOrder = keys.join(",");
|
|
5278
|
-
keys = keys.sort();
|
|
5279
|
-
if (keys.join(",") !== originalOrder) {
|
|
5280
|
-
changes.push(`${path3 || "$"}: Keys sorted`);
|
|
5314
|
+
selectClause = `${groupByCol} AS ${groupAlias}, ${selectClause}`;
|
|
5315
|
+
groupByClause = ` GROUP BY ${groupByCol}`;
|
|
5316
|
+
}
|
|
5317
|
+
let sql = `SELECT ${selectClause} FROM ${table}`;
|
|
5318
|
+
if (input.conditions || input.whereClause) {
|
|
5319
|
+
const { sql: whereSql, params: whereParams } = buildWhereClause(
|
|
5320
|
+
input.conditions,
|
|
5321
|
+
input.whereClause
|
|
5322
|
+
);
|
|
5323
|
+
if (whereSql !== "") {
|
|
5324
|
+
sql += ` WHERE ${whereSql}`;
|
|
5325
|
+
queryParams.push(...whereParams);
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
sql += groupByClause;
|
|
5329
|
+
const result = await adapter.executeReadQuery(sql, queryParams);
|
|
5330
|
+
const parsedRows = result.rows?.map((row) => {
|
|
5331
|
+
const parsedRow = {};
|
|
5332
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5333
|
+
parsedRow[key] = parseJsonValue(value);
|
|
5334
|
+
}
|
|
5335
|
+
return parsedRow;
|
|
5336
|
+
}) ?? [];
|
|
5337
|
+
return {
|
|
5338
|
+
success: true,
|
|
5339
|
+
rowCount: parsedRows.length,
|
|
5340
|
+
rows: parsedRows,
|
|
5341
|
+
...duplicateKeyWarning && { hint: duplicateKeyWarning }
|
|
5342
|
+
};
|
|
5343
|
+
} catch (error) {
|
|
5344
|
+
return formatHandlerError(error);
|
|
5281
5345
|
}
|
|
5282
5346
|
}
|
|
5283
|
-
|
|
5284
|
-
for (const key of keys) {
|
|
5285
|
-
result[key] = normalizeValue(
|
|
5286
|
-
obj[key],
|
|
5287
|
-
options,
|
|
5288
|
-
changes,
|
|
5289
|
-
`${path3 || "$"}.${key}`
|
|
5290
|
-
);
|
|
5291
|
-
}
|
|
5292
|
-
return result;
|
|
5293
|
-
}
|
|
5294
|
-
return value;
|
|
5295
|
-
}
|
|
5296
|
-
var jsonbSupportedCache = null;
|
|
5297
|
-
function isJsonbSupportedVersion(versionString) {
|
|
5298
|
-
const versionRegex = /^(\d+)\.(\d+)\.?(\d+)?/;
|
|
5299
|
-
const match = versionRegex.exec(versionString);
|
|
5300
|
-
if (!match) return false;
|
|
5301
|
-
const major = parseInt(match[1] ?? "0", 10);
|
|
5302
|
-
const minor = parseInt(match[2] ?? "0", 10);
|
|
5303
|
-
return major > 3 || major === 3 && minor >= 45;
|
|
5304
|
-
}
|
|
5305
|
-
function isJsonbSupported() {
|
|
5306
|
-
return jsonbSupportedCache ?? false;
|
|
5307
|
-
}
|
|
5308
|
-
function setJsonbSupported(supported) {
|
|
5309
|
-
jsonbSupportedCache = supported;
|
|
5310
|
-
}
|
|
5311
|
-
function detectJsonStorageFormat(value) {
|
|
5312
|
-
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
5313
|
-
return "jsonb";
|
|
5314
|
-
}
|
|
5315
|
-
if (typeof value === "string") {
|
|
5316
|
-
return "text";
|
|
5317
|
-
}
|
|
5318
|
-
return "unknown";
|
|
5347
|
+
};
|
|
5319
5348
|
}
|
|
5320
5349
|
|
|
5321
5350
|
// src/adapters/sqlite/tools/json-operations/transform.ts
|
|
@@ -5616,7 +5645,7 @@ function createJsonSecurityScanTool(adapter) {
|
|
|
5616
5645
|
const issueMap = /* @__PURE__ */ new Map();
|
|
5617
5646
|
for (let i = 0; i < rows.length; i++) {
|
|
5618
5647
|
if (i > 0 && i % 500 === 0) {
|
|
5619
|
-
await new Promise((
|
|
5648
|
+
await new Promise((resolve3) => setTimeout(resolve3, 0));
|
|
5620
5649
|
}
|
|
5621
5650
|
const row = rows[i];
|
|
5622
5651
|
if (!row) continue;
|
|
@@ -5877,10 +5906,17 @@ function createJsonSelectTool(adapter) {
|
|
|
5877
5906
|
}
|
|
5878
5907
|
sql += ` LIMIT ${input.limit ?? 100}`;
|
|
5879
5908
|
const result = await adapter.executeReadQuery(sql, queryParams);
|
|
5909
|
+
const parsedRows = result.rows?.map((row) => {
|
|
5910
|
+
const parsedRow = {};
|
|
5911
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5912
|
+
parsedRow[key] = parseJsonValue(value);
|
|
5913
|
+
}
|
|
5914
|
+
return parsedRow;
|
|
5915
|
+
}) ?? [];
|
|
5880
5916
|
return {
|
|
5881
5917
|
success: true,
|
|
5882
|
-
rowCount:
|
|
5883
|
-
rows:
|
|
5918
|
+
rowCount: parsedRows.length,
|
|
5919
|
+
rows: parsedRows
|
|
5884
5920
|
};
|
|
5885
5921
|
} catch (error) {
|
|
5886
5922
|
return formatHandlerError(error);
|
|
@@ -5937,10 +5973,17 @@ function createJsonQueryTool(adapter) {
|
|
|
5937
5973
|
}
|
|
5938
5974
|
sql += ` LIMIT ${input.limit ?? 100}`;
|
|
5939
5975
|
const result = await adapter.executeReadQuery(sql, queryParams);
|
|
5976
|
+
const parsedRows = result.rows?.map((row) => {
|
|
5977
|
+
const parsedRow = {};
|
|
5978
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5979
|
+
parsedRow[key] = parseJsonValue(value);
|
|
5980
|
+
}
|
|
5981
|
+
return parsedRow;
|
|
5982
|
+
}) ?? [];
|
|
5940
5983
|
return {
|
|
5941
5984
|
success: true,
|
|
5942
|
-
rowCount:
|
|
5943
|
-
rows:
|
|
5985
|
+
rowCount: parsedRows.length,
|
|
5986
|
+
rows: parsedRows
|
|
5944
5987
|
};
|
|
5945
5988
|
} catch (error) {
|
|
5946
5989
|
return formatHandlerError(error);
|
|
@@ -6010,7 +6053,7 @@ function createAnalyzeJsonSchemaTool(adapter) {
|
|
|
6010
6053
|
const rows = result.rows ?? [];
|
|
6011
6054
|
for (let i = 0; i < rows.length; i++) {
|
|
6012
6055
|
if (i > 0 && i % 500 === 0) {
|
|
6013
|
-
await new Promise((
|
|
6056
|
+
await new Promise((resolve3) => setTimeout(resolve3, 0));
|
|
6014
6057
|
}
|
|
6015
6058
|
const row = rows[i];
|
|
6016
6059
|
if (!row) continue;
|
|
@@ -6354,6 +6397,18 @@ var VALID_VALIDATE_PATTERNS = [
|
|
|
6354
6397
|
var VALID_PHONETIC_ALGORITHMS = ["soundex", "metaphone"];
|
|
6355
6398
|
var VALID_TRIM_MODES = ["both", "left", "right"];
|
|
6356
6399
|
var VALID_SEARCH_TECHNIQUES = ["exact", "fuzzy", "phonetic"];
|
|
6400
|
+
function sanitizeFtsQuery(query) {
|
|
6401
|
+
if (!query) return "";
|
|
6402
|
+
let clean = query.replace(/\s+/g, " ").trim();
|
|
6403
|
+
const quoteCount = (clean.match(/"/g) ?? []).length;
|
|
6404
|
+
if (quoteCount % 2 !== 0) {
|
|
6405
|
+
clean = clean.replace(/"/g, "");
|
|
6406
|
+
}
|
|
6407
|
+
clean = clean.replace(/^(?:\s*(?:AND|OR|NOT))+(?=\s|$)/i, "");
|
|
6408
|
+
clean = clean.replace(/(?:^|\s)(?:AND|OR|NOT)(?:\s+(?:AND|OR|NOT))*$/i, "");
|
|
6409
|
+
clean = clean.replace(/(?:\s+(?:AND|OR|NOT)){2,}\s+/gi, " AND ");
|
|
6410
|
+
return clean.trim();
|
|
6411
|
+
}
|
|
6357
6412
|
var coerceNumber3 = (val) => {
|
|
6358
6413
|
if (typeof val === "string") {
|
|
6359
6414
|
if (val.trim() === "") return void 0;
|
|
@@ -6460,6 +6515,20 @@ var AdvancedSearchOutputSchema = z.object({
|
|
|
6460
6515
|
bestScore: z.number(),
|
|
6461
6516
|
bestType: z.string()
|
|
6462
6517
|
})
|
|
6518
|
+
).optional(),
|
|
6519
|
+
facets: z.record(z.string(), z.number()).optional()
|
|
6520
|
+
}).extend(ErrorResponseFields.shape);
|
|
6521
|
+
var HybridSearchOutputSchema = z.object({
|
|
6522
|
+
success: z.boolean(),
|
|
6523
|
+
query: z.string().optional(),
|
|
6524
|
+
matchCount: z.number().optional(),
|
|
6525
|
+
results: z.array(
|
|
6526
|
+
z.object({
|
|
6527
|
+
rowid: z.number().optional(),
|
|
6528
|
+
rrfScore: z.number().optional(),
|
|
6529
|
+
ftsRank: z.number().nullable().optional(),
|
|
6530
|
+
vectorSimilarity: z.number().nullable().optional()
|
|
6531
|
+
}).loose()
|
|
6463
6532
|
).optional()
|
|
6464
6533
|
}).extend(ErrorResponseFields.shape);
|
|
6465
6534
|
var SentimentResultItemSchema = z.object({
|
|
@@ -6630,7 +6699,20 @@ var AdvancedSearchSchema = z.object({
|
|
|
6630
6699
|
),
|
|
6631
6700
|
conditions: z.array(WhereConditionSchema).optional().describe("Optional WHERE conditions"),
|
|
6632
6701
|
whereClause: z.string().optional().describe("Deprecated: Use conditions instead"),
|
|
6633
|
-
limit: z.preprocess(coerceNumber3, z.number().optional().default(100))
|
|
6702
|
+
limit: z.preprocess(coerceNumber3, z.number().optional().default(100)),
|
|
6703
|
+
includeFacets: z.boolean().optional().default(false).describe("Return match counts per type")
|
|
6704
|
+
});
|
|
6705
|
+
var HybridSearchSchema = z.object({
|
|
6706
|
+
table: z.string().describe("Table name"),
|
|
6707
|
+
query: z.string().describe("Text search query (FTS5)"),
|
|
6708
|
+
vectorColumn: z.string().describe("Vector column to search"),
|
|
6709
|
+
queryVector: z.array(z.number()).describe("Vector embedding of the query"),
|
|
6710
|
+
limit: z.preprocess(coerceNumber3, z.number().optional().default(100)),
|
|
6711
|
+
rrfK: z.preprocess(
|
|
6712
|
+
coerceNumber3,
|
|
6713
|
+
z.number().optional().default(60).describe("RRF k constant")
|
|
6714
|
+
),
|
|
6715
|
+
metric: z.enum(["cosine", "euclidean", "dot"]).optional().default("cosine").describe("Vector similarity metric")
|
|
6634
6716
|
});
|
|
6635
6717
|
var TextSentimentSchema = z.object({
|
|
6636
6718
|
table: z.string().optional().describe("Table name"),
|
|
@@ -7199,6 +7281,84 @@ var VALIDATION_PATTERNS = {
|
|
|
7199
7281
|
ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
7200
7282
|
};
|
|
7201
7283
|
|
|
7284
|
+
// src/adapters/sqlite/tools/vector/helpers.ts
|
|
7285
|
+
function cosineSimilarity(a, b) {
|
|
7286
|
+
if (a.length !== b.length) {
|
|
7287
|
+
throw new DbMcpError(
|
|
7288
|
+
"Vector dimensions must match",
|
|
7289
|
+
"VECTOR_MISMATCH",
|
|
7290
|
+
ErrorCategory.VALIDATION
|
|
7291
|
+
);
|
|
7292
|
+
}
|
|
7293
|
+
let dotProd = 0;
|
|
7294
|
+
let normA = 0;
|
|
7295
|
+
let normB = 0;
|
|
7296
|
+
for (let i = 0; i < a.length; i++) {
|
|
7297
|
+
const aVal = a[i] ?? 0;
|
|
7298
|
+
const bVal = b[i] ?? 0;
|
|
7299
|
+
dotProd += aVal * bVal;
|
|
7300
|
+
normA += aVal * aVal;
|
|
7301
|
+
normB += bVal * bVal;
|
|
7302
|
+
}
|
|
7303
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
7304
|
+
return magnitude === 0 ? 0 : dotProd / magnitude;
|
|
7305
|
+
}
|
|
7306
|
+
function euclideanDistance(a, b) {
|
|
7307
|
+
if (a.length !== b.length) {
|
|
7308
|
+
throw new DbMcpError(
|
|
7309
|
+
"Vector dimensions must match",
|
|
7310
|
+
"VECTOR_MISMATCH",
|
|
7311
|
+
ErrorCategory.VALIDATION
|
|
7312
|
+
);
|
|
7313
|
+
}
|
|
7314
|
+
let sum = 0;
|
|
7315
|
+
for (let i = 0; i < a.length; i++) {
|
|
7316
|
+
const aVal = a[i] ?? 0;
|
|
7317
|
+
const bVal = b[i] ?? 0;
|
|
7318
|
+
const diff = aVal - bVal;
|
|
7319
|
+
sum += diff * diff;
|
|
7320
|
+
}
|
|
7321
|
+
return Math.sqrt(sum);
|
|
7322
|
+
}
|
|
7323
|
+
function dotProduct(a, b) {
|
|
7324
|
+
if (a.length !== b.length) {
|
|
7325
|
+
throw new DbMcpError(
|
|
7326
|
+
"Vector dimensions must match",
|
|
7327
|
+
"VECTOR_MISMATCH",
|
|
7328
|
+
ErrorCategory.VALIDATION
|
|
7329
|
+
);
|
|
7330
|
+
}
|
|
7331
|
+
let sum = 0;
|
|
7332
|
+
for (let i = 0; i < a.length; i++) {
|
|
7333
|
+
sum += (a[i] ?? 0) * (b[i] ?? 0);
|
|
7334
|
+
}
|
|
7335
|
+
return sum;
|
|
7336
|
+
}
|
|
7337
|
+
function normalizeVector(v) {
|
|
7338
|
+
const norm = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0));
|
|
7339
|
+
if (norm === 0) return v;
|
|
7340
|
+
return v.map((x) => x / norm);
|
|
7341
|
+
}
|
|
7342
|
+
function parseVector(value) {
|
|
7343
|
+
if (Array.isArray(value)) {
|
|
7344
|
+
return value.map(Number);
|
|
7345
|
+
}
|
|
7346
|
+
if (typeof value === "string") {
|
|
7347
|
+
try {
|
|
7348
|
+
const parsed = JSON.parse(value);
|
|
7349
|
+
if (Array.isArray(parsed)) {
|
|
7350
|
+
return parsed.map(Number);
|
|
7351
|
+
}
|
|
7352
|
+
} catch {
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
throw new DbMcpError(
|
|
7356
|
+
"Invalid vector format",
|
|
7357
|
+
"VECTOR_INVALID",
|
|
7358
|
+
ErrorCategory.VALIDATION
|
|
7359
|
+
);
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7202
7362
|
// src/adapters/sqlite/tools/text/search.ts
|
|
7203
7363
|
function createFuzzyMatchTool(adapter) {
|
|
7204
7364
|
return {
|
|
@@ -7429,12 +7589,131 @@ function createAdvancedSearchTool(adapter) {
|
|
|
7429
7589
|
}
|
|
7430
7590
|
allMatches.sort((a, b) => b.bestScore - a.bestScore);
|
|
7431
7591
|
const limited = allMatches.slice(0, input.limit);
|
|
7592
|
+
const facets = {};
|
|
7593
|
+
if (input.includeFacets) {
|
|
7594
|
+
for (const technique of input.techniques) {
|
|
7595
|
+
facets[technique] = 0;
|
|
7596
|
+
}
|
|
7597
|
+
for (const match of allMatches) {
|
|
7598
|
+
facets[match.bestType] = (facets[match.bestType] ?? 0) + 1;
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7432
7601
|
return {
|
|
7433
7602
|
success: true,
|
|
7434
7603
|
searchTerm: input.searchTerm,
|
|
7435
7604
|
techniques: input.techniques,
|
|
7436
7605
|
matchCount: limited.length,
|
|
7437
|
-
matches: limited
|
|
7606
|
+
matches: limited,
|
|
7607
|
+
...input.includeFacets ? { facets } : {}
|
|
7608
|
+
};
|
|
7609
|
+
} catch (error) {
|
|
7610
|
+
return formatHandlerError(error);
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7613
|
+
};
|
|
7614
|
+
}
|
|
7615
|
+
function createHybridSearchTool(adapter) {
|
|
7616
|
+
return {
|
|
7617
|
+
name: "sqlite_hybrid_search",
|
|
7618
|
+
description: "Hybrid search combining full-text search and vector similarity using Reciprocal Rank Fusion (RRF).",
|
|
7619
|
+
group: "text",
|
|
7620
|
+
inputSchema: HybridSearchSchema,
|
|
7621
|
+
outputSchema: HybridSearchOutputSchema,
|
|
7622
|
+
requiredScopes: ["read"],
|
|
7623
|
+
annotations: readOnly("Hybrid Search"),
|
|
7624
|
+
handler: async (params, _context) => {
|
|
7625
|
+
try {
|
|
7626
|
+
const input = HybridSearchSchema.parse(params);
|
|
7627
|
+
const table = sanitizeIdentifier(input.table);
|
|
7628
|
+
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
7629
|
+
await validateColumnExists(adapter, input.table, input.vectorColumn);
|
|
7630
|
+
let textResults = [];
|
|
7631
|
+
try {
|
|
7632
|
+
const sanitizedQuery = sanitizeFtsQuery(input.query);
|
|
7633
|
+
if (sanitizedQuery) {
|
|
7634
|
+
const queryEscaped = sanitizedQuery.replace(/'/g, "''");
|
|
7635
|
+
const ftsSql = `SELECT rowid as id, rank as score FROM ${table} WHERE ${table} MATCH '${queryEscaped}' ORDER BY rank LIMIT 1000`;
|
|
7636
|
+
const result = await adapter.executeReadQuery(ftsSql, []);
|
|
7637
|
+
textResults = (result.rows ?? []).map((r, i) => ({
|
|
7638
|
+
id: Number(r["id"] ?? r["rowid"]),
|
|
7639
|
+
rank: i + 1
|
|
7640
|
+
}));
|
|
7641
|
+
}
|
|
7642
|
+
} catch {
|
|
7643
|
+
}
|
|
7644
|
+
const vecSql = `SELECT rowid as id, ${vectorColumn} as vec FROM ${table} WHERE ${vectorColumn} IS NOT NULL LIMIT 10000`;
|
|
7645
|
+
const vecResult = await adapter.executeReadQuery(vecSql, []);
|
|
7646
|
+
const queryVector = input.queryVector;
|
|
7647
|
+
const vecScored = [];
|
|
7648
|
+
for (const row of vecResult.rows ?? []) {
|
|
7649
|
+
try {
|
|
7650
|
+
const storedVector = parseVector(row["vec"]);
|
|
7651
|
+
let score;
|
|
7652
|
+
switch (input.metric) {
|
|
7653
|
+
case "euclidean":
|
|
7654
|
+
score = 1 / (1 + euclideanDistance(queryVector, storedVector));
|
|
7655
|
+
break;
|
|
7656
|
+
case "dot":
|
|
7657
|
+
score = dotProduct(queryVector, storedVector);
|
|
7658
|
+
break;
|
|
7659
|
+
case "cosine":
|
|
7660
|
+
default:
|
|
7661
|
+
score = cosineSimilarity(queryVector, storedVector);
|
|
7662
|
+
break;
|
|
7663
|
+
}
|
|
7664
|
+
vecScored.push({ id: Number(row["id"] ?? row["rowid"]), score });
|
|
7665
|
+
} catch {
|
|
7666
|
+
}
|
|
7667
|
+
}
|
|
7668
|
+
vecScored.sort((a, b) => b.score - a.score);
|
|
7669
|
+
const vectorResults = vecScored.map((r, i) => ({
|
|
7670
|
+
id: r.id,
|
|
7671
|
+
rank: i + 1,
|
|
7672
|
+
score: r.score
|
|
7673
|
+
}));
|
|
7674
|
+
const combined = /* @__PURE__ */ new Map();
|
|
7675
|
+
const k = input.rrfK;
|
|
7676
|
+
for (const f of textResults) {
|
|
7677
|
+
if (!combined.has(f.id)) {
|
|
7678
|
+
combined.set(f.id, {
|
|
7679
|
+
id: f.id,
|
|
7680
|
+
rrfScore: 0,
|
|
7681
|
+
ftsRank: null,
|
|
7682
|
+
vectorSimilarity: null
|
|
7683
|
+
});
|
|
7684
|
+
}
|
|
7685
|
+
const entry = combined.get(f.id);
|
|
7686
|
+
if (entry) {
|
|
7687
|
+
entry.ftsRank = f.rank;
|
|
7688
|
+
entry.rrfScore += 1 / (k + f.rank);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
for (const v of vectorResults) {
|
|
7692
|
+
if (!combined.has(v.id)) {
|
|
7693
|
+
combined.set(v.id, {
|
|
7694
|
+
id: v.id,
|
|
7695
|
+
rrfScore: 0,
|
|
7696
|
+
ftsRank: null,
|
|
7697
|
+
vectorSimilarity: null
|
|
7698
|
+
});
|
|
7699
|
+
}
|
|
7700
|
+
const entry = combined.get(v.id);
|
|
7701
|
+
if (entry) {
|
|
7702
|
+
entry.vectorSimilarity = v.score;
|
|
7703
|
+
entry.rrfScore += 1 / (k + v.rank);
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
const sortedCombined = Array.from(combined.values()).sort((a, b) => b.rrfScore - a.rrfScore).slice(0, input.limit).map((r) => ({
|
|
7707
|
+
rowid: r.id,
|
|
7708
|
+
rrfScore: Math.round(r.rrfScore * 1e4) / 1e4,
|
|
7709
|
+
ftsRank: r.ftsRank,
|
|
7710
|
+
vectorSimilarity: r.vectorSimilarity !== null ? Math.round(r.vectorSimilarity * 1e4) / 1e4 : null
|
|
7711
|
+
}));
|
|
7712
|
+
return {
|
|
7713
|
+
success: true,
|
|
7714
|
+
query: input.query,
|
|
7715
|
+
matchCount: sortedCombined.length,
|
|
7716
|
+
results: sortedCombined
|
|
7438
7717
|
};
|
|
7439
7718
|
} catch (error) {
|
|
7440
7719
|
return formatHandlerError(error);
|
|
@@ -7768,6 +8047,7 @@ function getTextTools(adapter) {
|
|
|
7768
8047
|
createTextNormalizeTool(adapter),
|
|
7769
8048
|
createTextValidateTool(adapter),
|
|
7770
8049
|
createAdvancedSearchTool(adapter),
|
|
8050
|
+
createHybridSearchTool(adapter),
|
|
7771
8051
|
createTextSentimentTool(adapter)
|
|
7772
8052
|
];
|
|
7773
8053
|
}
|
|
@@ -8196,8 +8476,8 @@ var StatsSampleSchema = z.object({
|
|
|
8196
8476
|
table: z.string().describe("Table name"),
|
|
8197
8477
|
sampleSize: z.preprocess(
|
|
8198
8478
|
coerceNumber4,
|
|
8199
|
-
z.number().optional().default(
|
|
8200
|
-
"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."
|
|
8201
8481
|
)
|
|
8202
8482
|
),
|
|
8203
8483
|
conditions: z.array(WhereConditionSchema).optional().describe("Optional WHERE conditions"),
|
|
@@ -8620,7 +8900,7 @@ function createPercentileTool(adapter) {
|
|
|
8620
8900
|
}
|
|
8621
8901
|
};
|
|
8622
8902
|
}
|
|
8623
|
-
var MAX_SAMPLE_SIZE =
|
|
8903
|
+
var MAX_SAMPLE_SIZE = 50;
|
|
8624
8904
|
function createSampleTool(adapter) {
|
|
8625
8905
|
return {
|
|
8626
8906
|
name: "sqlite_stats_sample",
|
|
@@ -9310,7 +9590,7 @@ function matrixInverse(A) {
|
|
|
9310
9590
|
throw new DbMcpError(
|
|
9311
9591
|
"Matrix is singular, cannot compute inverse",
|
|
9312
9592
|
"MATH_SINGULAR",
|
|
9313
|
-
|
|
9593
|
+
ErrorCategory.VALIDATION
|
|
9314
9594
|
);
|
|
9315
9595
|
}
|
|
9316
9596
|
for (let j = 0; j < 2 * n; j++) {
|
|
@@ -9377,7 +9657,7 @@ function createRegressionTool(adapter) {
|
|
|
9377
9657
|
throw new DbMcpError(
|
|
9378
9658
|
`Insufficient data in table '${input.table}' for degree ${degree} regression (need at least ${degree + 1} points, got ${pairs.length})`,
|
|
9379
9659
|
"STATS_INSUFFICIENT_SAMPLE",
|
|
9380
|
-
|
|
9660
|
+
ErrorCategory.VALIDATION
|
|
9381
9661
|
);
|
|
9382
9662
|
}
|
|
9383
9663
|
const X = pairs.map(
|
|
@@ -9486,7 +9766,7 @@ function createHypothesisTool(adapter) {
|
|
|
9486
9766
|
throw new DbMcpError(
|
|
9487
9767
|
"Insufficient sample size for t-test",
|
|
9488
9768
|
"STATS_INSUFFICIENT_SAMPLE",
|
|
9489
|
-
|
|
9769
|
+
ErrorCategory.VALIDATION
|
|
9490
9770
|
);
|
|
9491
9771
|
}
|
|
9492
9772
|
const tStatistic = (mean - expectedMean) / (stdDev / Math.sqrt(n));
|
|
@@ -9494,7 +9774,7 @@ function createHypothesisTool(adapter) {
|
|
|
9494
9774
|
throw new DbMcpError(
|
|
9495
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.`,
|
|
9496
9776
|
"STATS_COMPUTATION_FAILED",
|
|
9497
|
-
|
|
9777
|
+
ErrorCategory.VALIDATION
|
|
9498
9778
|
);
|
|
9499
9779
|
}
|
|
9500
9780
|
const pValue = tDistPValue(tStatistic, df);
|
|
@@ -9517,7 +9797,7 @@ function createHypothesisTool(adapter) {
|
|
|
9517
9797
|
throw new DbMcpError(
|
|
9518
9798
|
"column2 is required for two-sample t-test",
|
|
9519
9799
|
"STATS_MISSING_COLUMN",
|
|
9520
|
-
|
|
9800
|
+
ErrorCategory.VALIDATION
|
|
9521
9801
|
);
|
|
9522
9802
|
}
|
|
9523
9803
|
await validateColumnExists(adapter, input.table, input.column2);
|
|
@@ -9544,7 +9824,7 @@ function createHypothesisTool(adapter) {
|
|
|
9544
9824
|
throw new DbMcpError(
|
|
9545
9825
|
"Insufficient sample size for t-test",
|
|
9546
9826
|
"STATS_INSUFFICIENT_SAMPLE",
|
|
9547
|
-
|
|
9827
|
+
ErrorCategory.VALIDATION
|
|
9548
9828
|
);
|
|
9549
9829
|
}
|
|
9550
9830
|
const tStatistic = (mean1 - mean2) / Math.sqrt(var1 / n1 + var2 / n2);
|
|
@@ -9552,7 +9832,7 @@ function createHypothesisTool(adapter) {
|
|
|
9552
9832
|
throw new DbMcpError(
|
|
9553
9833
|
`Cannot compute t-statistic: var1=${var1.toFixed(4)}, var2=${var2.toFixed(4)}. This may indicate zero variance or non-numeric data.`,
|
|
9554
9834
|
"STATS_COMPUTATION_FAILED",
|
|
9555
|
-
|
|
9835
|
+
ErrorCategory.VALIDATION
|
|
9556
9836
|
);
|
|
9557
9837
|
}
|
|
9558
9838
|
const dfNum = Math.pow(var1 / n1 + var2 / n2, 2);
|
|
@@ -9576,7 +9856,7 @@ function createHypothesisTool(adapter) {
|
|
|
9576
9856
|
throw new DbMcpError(
|
|
9577
9857
|
"groupColumn is required for chi-square test",
|
|
9578
9858
|
"STATS_MISSING_COLUMN",
|
|
9579
|
-
|
|
9859
|
+
ErrorCategory.VALIDATION
|
|
9580
9860
|
);
|
|
9581
9861
|
}
|
|
9582
9862
|
await validateColumnExists(adapter, input.table, input.groupColumn);
|
|
@@ -9613,7 +9893,7 @@ function createHypothesisTool(adapter) {
|
|
|
9613
9893
|
throw new DbMcpError(
|
|
9614
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.`,
|
|
9615
9895
|
"STATS_INSUFFICIENT_CATEGORIES",
|
|
9616
|
-
|
|
9896
|
+
ErrorCategory.VALIDATION
|
|
9617
9897
|
);
|
|
9618
9898
|
}
|
|
9619
9899
|
const pValue = Math.exp(-chiSquare / 2);
|
|
@@ -10280,7 +10560,7 @@ var DropViewSchema = z.object({
|
|
|
10280
10560
|
});
|
|
10281
10561
|
var DbStatSchema = z.object({
|
|
10282
10562
|
table: z.string().optional().describe("Optional table name to filter"),
|
|
10283
|
-
summarize: z.boolean().optional().default(
|
|
10563
|
+
summarize: z.boolean().optional().default(true).describe(
|
|
10284
10564
|
"If true, return aggregated per-table stats instead of raw page-level data"
|
|
10285
10565
|
),
|
|
10286
10566
|
limit: z.preprocess(
|
|
@@ -10535,34 +10815,6 @@ function createDropViewTool(adapter) {
|
|
|
10535
10815
|
};
|
|
10536
10816
|
}
|
|
10537
10817
|
|
|
10538
|
-
// src/utils/progress-utils.ts
|
|
10539
|
-
function buildProgressContext(ctx) {
|
|
10540
|
-
if (ctx?.server === void 0 || ctx.progressToken === void 0) {
|
|
10541
|
-
return void 0;
|
|
10542
|
-
}
|
|
10543
|
-
return {
|
|
10544
|
-
server: ctx.server,
|
|
10545
|
-
progressToken: ctx.progressToken
|
|
10546
|
-
};
|
|
10547
|
-
}
|
|
10548
|
-
async function sendProgress(ctx, progress, total, message) {
|
|
10549
|
-
if (ctx === void 0) return;
|
|
10550
|
-
if (ctx.progressToken === void 0) return;
|
|
10551
|
-
try {
|
|
10552
|
-
const notification = {
|
|
10553
|
-
method: "notifications/progress",
|
|
10554
|
-
params: {
|
|
10555
|
-
progressToken: ctx.progressToken,
|
|
10556
|
-
progress,
|
|
10557
|
-
...total !== void 0 && { total },
|
|
10558
|
-
...message !== void 0 && message !== "" && { message }
|
|
10559
|
-
}
|
|
10560
|
-
};
|
|
10561
|
-
await ctx.server.notification(notification);
|
|
10562
|
-
} catch {
|
|
10563
|
-
}
|
|
10564
|
-
}
|
|
10565
|
-
|
|
10566
10818
|
// src/adapters/sqlite/tools/virtual/analysis.ts
|
|
10567
10819
|
function createDbStatTool(adapter) {
|
|
10568
10820
|
return {
|
|
@@ -10829,7 +11081,9 @@ function createVirtualTableInfoTool(adapter) {
|
|
|
10829
11081
|
module: "unknown",
|
|
10830
11082
|
sql: "",
|
|
10831
11083
|
error: `Virtual table '${input.tableName}' not found`,
|
|
10832
|
-
code: "TABLE_NOT_FOUND"
|
|
11084
|
+
code: "TABLE_NOT_FOUND",
|
|
11085
|
+
category: "resource",
|
|
11086
|
+
recoverable: false
|
|
10833
11087
|
};
|
|
10834
11088
|
}
|
|
10835
11089
|
const sqlStr = typeof sqlResult.rows[0]?.["sql"] === "string" ? sqlResult.rows[0]["sql"] : "";
|
|
@@ -10947,10 +11201,10 @@ function createCsvTableTool(adapter) {
|
|
|
10947
11201
|
try {
|
|
10948
11202
|
const input = CreateCsvTableSchema.parse(params);
|
|
10949
11203
|
const safeTableName = sanitizeIdentifier(input.tableName);
|
|
10950
|
-
if (!
|
|
11204
|
+
if (!nodePath.isAbsolute(input.filePath)) {
|
|
10951
11205
|
return {
|
|
10952
11206
|
success: false,
|
|
10953
|
-
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)}`,
|
|
10954
11208
|
code: "VALIDATION_ERROR",
|
|
10955
11209
|
category: "validation",
|
|
10956
11210
|
message: "",
|
|
@@ -10958,20 +11212,37 @@ function createCsvTableTool(adapter) {
|
|
|
10958
11212
|
columns: []
|
|
10959
11213
|
};
|
|
10960
11214
|
}
|
|
10961
|
-
const
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
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
|
+
}
|
|
10975
11246
|
}
|
|
10976
11247
|
const { available: csvAvailable } = await isCsvModuleAvailable(adapter);
|
|
10977
11248
|
if (!csvAvailable) {
|
|
@@ -11054,10 +11325,10 @@ function createAnalyzeCsvSchemaTool(adapter) {
|
|
|
11054
11325
|
columns: []
|
|
11055
11326
|
};
|
|
11056
11327
|
}
|
|
11057
|
-
if (!
|
|
11328
|
+
if (!nodePath.isAbsolute(input.filePath)) {
|
|
11058
11329
|
return {
|
|
11059
11330
|
success: false,
|
|
11060
|
-
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)}`,
|
|
11061
11332
|
code: "VALIDATION_ERROR",
|
|
11062
11333
|
category: "validation",
|
|
11063
11334
|
hasHeader: false,
|
|
@@ -11065,20 +11336,37 @@ function createAnalyzeCsvSchemaTool(adapter) {
|
|
|
11065
11336
|
columns: []
|
|
11066
11337
|
};
|
|
11067
11338
|
}
|
|
11068
|
-
const
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
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
|
+
}
|
|
11082
11370
|
}
|
|
11083
11371
|
const tempName = `_csv_analyze_${Date.now()}`;
|
|
11084
11372
|
try {
|
|
@@ -11509,10 +11797,7 @@ function createVectorCreateTableTool(adapter) {
|
|
|
11509
11797
|
try {
|
|
11510
11798
|
const input = VectorCreateTableSchema.parse(params);
|
|
11511
11799
|
if (input.dimensions < 1) {
|
|
11512
|
-
|
|
11513
|
-
success: false,
|
|
11514
|
-
error: "Dimensions must be at least 1"
|
|
11515
|
-
};
|
|
11800
|
+
throw new ValidationError("Dimensions must be at least 1");
|
|
11516
11801
|
}
|
|
11517
11802
|
const tableName = sanitizeIdentifier(input.tableName);
|
|
11518
11803
|
const columns = [
|
|
@@ -11598,15 +11883,15 @@ function createVectorStoreTool(adapter) {
|
|
|
11598
11883
|
try {
|
|
11599
11884
|
const input = VectorStoreSchema.parse(params);
|
|
11600
11885
|
if (input.vector.length === 0) {
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
};
|
|
11886
|
+
throw new ValidationError(
|
|
11887
|
+
"vector is required and must be a non-empty array of numbers"
|
|
11888
|
+
);
|
|
11605
11889
|
}
|
|
11606
11890
|
const table = sanitizeIdentifier(input.table);
|
|
11607
11891
|
const idColumn = sanitizeIdentifier(input.idColumn);
|
|
11608
11892
|
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
11609
11893
|
let hasDimsColumn = false;
|
|
11894
|
+
let expectedDims;
|
|
11610
11895
|
try {
|
|
11611
11896
|
const dims = await validateDimensions(
|
|
11612
11897
|
adapter,
|
|
@@ -11615,15 +11900,15 @@ function createVectorStoreTool(adapter) {
|
|
|
11615
11900
|
input.vectorColumn
|
|
11616
11901
|
);
|
|
11617
11902
|
hasDimsColumn = dims.hasDimsColumn;
|
|
11618
|
-
|
|
11619
|
-
return {
|
|
11620
|
-
success: false,
|
|
11621
|
-
error: `Dimension mismatch: vector has ${input.vector.length} dimensions but table expects ${dims.expectedDims}`,
|
|
11622
|
-
code: "DIMENSION_MISMATCH"
|
|
11623
|
-
};
|
|
11624
|
-
}
|
|
11903
|
+
expectedDims = dims.expectedDims;
|
|
11625
11904
|
} catch {
|
|
11626
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
|
+
}
|
|
11627
11912
|
const vectorJson = JSON.stringify(input.vector);
|
|
11628
11913
|
const updateSql = `UPDATE ${table} SET ${vectorColumn} = ? WHERE ${idColumn} = ?`;
|
|
11629
11914
|
const updateResult = await adapter.executeWriteQuery(updateSql, [
|
|
@@ -11665,10 +11950,11 @@ function createVectorBatchStoreTool(adapter) {
|
|
|
11665
11950
|
input.table
|
|
11666
11951
|
]);
|
|
11667
11952
|
if (!checkResult.rows || checkResult.rows.length === 0) {
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11953
|
+
throw new ResourceNotFoundError(
|
|
11954
|
+
`Table '${input.table}' does not exist`,
|
|
11955
|
+
"TABLE_NOT_FOUND",
|
|
11956
|
+
{ resourceType: "table", resourceName: input.table }
|
|
11957
|
+
);
|
|
11672
11958
|
}
|
|
11673
11959
|
return {
|
|
11674
11960
|
success: true,
|
|
@@ -11679,6 +11965,7 @@ function createVectorBatchStoreTool(adapter) {
|
|
|
11679
11965
|
const idColumn = sanitizeIdentifier(input.idColumn);
|
|
11680
11966
|
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
11681
11967
|
let hasDimsColumn = false;
|
|
11968
|
+
let expectedDims;
|
|
11682
11969
|
try {
|
|
11683
11970
|
const dims = await validateDimensions(
|
|
11684
11971
|
adapter,
|
|
@@ -11687,19 +11974,19 @@ function createVectorBatchStoreTool(adapter) {
|
|
|
11687
11974
|
input.vectorColumn
|
|
11688
11975
|
);
|
|
11689
11976
|
hasDimsColumn = dims.hasDimsColumn;
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
}
|
|
11699
|
-
|
|
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
|
+
);
|
|
11700
11988
|
}
|
|
11701
11989
|
}
|
|
11702
|
-
} catch {
|
|
11703
11990
|
}
|
|
11704
11991
|
let stored = 0;
|
|
11705
11992
|
for (const item of input.items) {
|
|
@@ -11754,84 +12041,6 @@ function createVectorDeleteTool(adapter) {
|
|
|
11754
12041
|
};
|
|
11755
12042
|
}
|
|
11756
12043
|
|
|
11757
|
-
// src/adapters/sqlite/tools/vector/helpers.ts
|
|
11758
|
-
function cosineSimilarity(a, b) {
|
|
11759
|
-
if (a.length !== b.length) {
|
|
11760
|
-
throw new DbMcpError(
|
|
11761
|
-
"Vector dimensions must match",
|
|
11762
|
-
"VECTOR_MISMATCH",
|
|
11763
|
-
"validation" /* VALIDATION */
|
|
11764
|
-
);
|
|
11765
|
-
}
|
|
11766
|
-
let dotProd = 0;
|
|
11767
|
-
let normA = 0;
|
|
11768
|
-
let normB = 0;
|
|
11769
|
-
for (let i = 0; i < a.length; i++) {
|
|
11770
|
-
const aVal = a[i] ?? 0;
|
|
11771
|
-
const bVal = b[i] ?? 0;
|
|
11772
|
-
dotProd += aVal * bVal;
|
|
11773
|
-
normA += aVal * aVal;
|
|
11774
|
-
normB += bVal * bVal;
|
|
11775
|
-
}
|
|
11776
|
-
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
11777
|
-
return magnitude === 0 ? 0 : dotProd / magnitude;
|
|
11778
|
-
}
|
|
11779
|
-
function euclideanDistance(a, b) {
|
|
11780
|
-
if (a.length !== b.length) {
|
|
11781
|
-
throw new DbMcpError(
|
|
11782
|
-
"Vector dimensions must match",
|
|
11783
|
-
"VECTOR_MISMATCH",
|
|
11784
|
-
"validation" /* VALIDATION */
|
|
11785
|
-
);
|
|
11786
|
-
}
|
|
11787
|
-
let sum = 0;
|
|
11788
|
-
for (let i = 0; i < a.length; i++) {
|
|
11789
|
-
const aVal = a[i] ?? 0;
|
|
11790
|
-
const bVal = b[i] ?? 0;
|
|
11791
|
-
const diff = aVal - bVal;
|
|
11792
|
-
sum += diff * diff;
|
|
11793
|
-
}
|
|
11794
|
-
return Math.sqrt(sum);
|
|
11795
|
-
}
|
|
11796
|
-
function dotProduct(a, b) {
|
|
11797
|
-
if (a.length !== b.length) {
|
|
11798
|
-
throw new DbMcpError(
|
|
11799
|
-
"Vector dimensions must match",
|
|
11800
|
-
"VECTOR_MISMATCH",
|
|
11801
|
-
"validation" /* VALIDATION */
|
|
11802
|
-
);
|
|
11803
|
-
}
|
|
11804
|
-
let sum = 0;
|
|
11805
|
-
for (let i = 0; i < a.length; i++) {
|
|
11806
|
-
sum += (a[i] ?? 0) * (b[i] ?? 0);
|
|
11807
|
-
}
|
|
11808
|
-
return sum;
|
|
11809
|
-
}
|
|
11810
|
-
function normalizeVector(v) {
|
|
11811
|
-
const norm = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0));
|
|
11812
|
-
if (norm === 0) return v;
|
|
11813
|
-
return v.map((x) => x / norm);
|
|
11814
|
-
}
|
|
11815
|
-
function parseVector(value) {
|
|
11816
|
-
if (Array.isArray(value)) {
|
|
11817
|
-
return value.map(Number);
|
|
11818
|
-
}
|
|
11819
|
-
if (typeof value === "string") {
|
|
11820
|
-
try {
|
|
11821
|
-
const parsed = JSON.parse(value);
|
|
11822
|
-
if (Array.isArray(parsed)) {
|
|
11823
|
-
return parsed.map(Number);
|
|
11824
|
-
}
|
|
11825
|
-
} catch {
|
|
11826
|
-
}
|
|
11827
|
-
}
|
|
11828
|
-
throw new DbMcpError(
|
|
11829
|
-
"Invalid vector format",
|
|
11830
|
-
"VECTOR_INVALID",
|
|
11831
|
-
"validation" /* VALIDATION */
|
|
11832
|
-
);
|
|
11833
|
-
}
|
|
11834
|
-
|
|
11835
12044
|
// src/adapters/sqlite/tools/vector/search.ts
|
|
11836
12045
|
function createVectorSearchTool(adapter) {
|
|
11837
12046
|
return {
|
|
@@ -11847,10 +12056,9 @@ function createVectorSearchTool(adapter) {
|
|
|
11847
12056
|
try {
|
|
11848
12057
|
const input = VectorSearchSchema.parse(params);
|
|
11849
12058
|
if (input.queryVector.length === 0) {
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
};
|
|
12059
|
+
throw new ValidationError(
|
|
12060
|
+
"queryVector is required and must be a non-empty array of numbers"
|
|
12061
|
+
);
|
|
11854
12062
|
}
|
|
11855
12063
|
const table = sanitizeIdentifier(input.table);
|
|
11856
12064
|
const vectorColumn = sanitizeIdentifier(input.vectorColumn);
|
|
@@ -11880,7 +12088,7 @@ function createVectorSearchTool(adapter) {
|
|
|
11880
12088
|
const rows = result.rows ?? [];
|
|
11881
12089
|
for (let i = 0; i < rows.length; i++) {
|
|
11882
12090
|
if (i > 0 && i % 500 === 0) {
|
|
11883
|
-
await new Promise((
|
|
12091
|
+
await new Promise((resolve3) => setTimeout(resolve3, 0));
|
|
11884
12092
|
}
|
|
11885
12093
|
const row = rows[i];
|
|
11886
12094
|
if (!row) continue;
|
|
@@ -11898,10 +12106,9 @@ function createVectorSearchTool(adapter) {
|
|
|
11898
12106
|
score = cosineSimilarity(queryVector, storedVector);
|
|
11899
12107
|
break;
|
|
11900
12108
|
default:
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
};
|
|
12109
|
+
throw new ValidationError(
|
|
12110
|
+
`Invalid metric '${input.metric}'. Valid values: cosine, euclidean, dot`
|
|
12111
|
+
);
|
|
11905
12112
|
}
|
|
11906
12113
|
scored.push({
|
|
11907
12114
|
...row,
|
|
@@ -11969,26 +12176,17 @@ function createVectorGetTool(adapter) {
|
|
|
11969
12176
|
const sql = `SELECT * FROM ${table} WHERE ${idColumn} = ?`;
|
|
11970
12177
|
const result = await adapter.executeReadQuery(sql, [input.id]);
|
|
11971
12178
|
if (!result.rows || result.rows.length === 0) {
|
|
11972
|
-
|
|
11973
|
-
success: false,
|
|
11974
|
-
error: "Vector not found",
|
|
11975
|
-
code: "VECTOR_NOT_FOUND"
|
|
11976
|
-
};
|
|
12179
|
+
throw new ResourceNotFoundError("Vector not found", "VECTOR_NOT_FOUND", { resourceType: "vector", resourceName: String(input.id) });
|
|
11977
12180
|
}
|
|
11978
12181
|
const row = result.rows[0];
|
|
11979
12182
|
if (!row) {
|
|
11980
|
-
|
|
11981
|
-
success: false,
|
|
11982
|
-
error: "Vector not found",
|
|
11983
|
-
code: "VECTOR_NOT_FOUND"
|
|
11984
|
-
};
|
|
12183
|
+
throw new ResourceNotFoundError("Vector not found", "VECTOR_NOT_FOUND", { resourceType: "vector", resourceName: String(input.id) });
|
|
11985
12184
|
}
|
|
11986
12185
|
const rawVector = row[input.vectorColumn];
|
|
11987
12186
|
if (rawVector === void 0 || rawVector === null) {
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
};
|
|
12187
|
+
throw new ValidationError(
|
|
12188
|
+
`Column '${input.vectorColumn}' not found or contains NULL. Available columns: ${Object.keys(row).join(", ")}`
|
|
12189
|
+
);
|
|
11992
12190
|
}
|
|
11993
12191
|
const vectorData = parseVector(rawVector);
|
|
11994
12192
|
const metadata = Object.fromEntries(
|
|
@@ -12059,7 +12257,7 @@ function createVectorStatsTool(adapter) {
|
|
|
12059
12257
|
const rows = result.rows ?? [];
|
|
12060
12258
|
for (let i = 0; i < rows.length; i++) {
|
|
12061
12259
|
if (i > 0 && i % 500 === 0) {
|
|
12062
|
-
await new Promise((
|
|
12260
|
+
await new Promise((resolve3) => setTimeout(resolve3, 0));
|
|
12063
12261
|
}
|
|
12064
12262
|
try {
|
|
12065
12263
|
const row = rows[i];
|
|
@@ -12160,13 +12358,9 @@ function createVectorNormalizeTool() {
|
|
|
12160
12358
|
try {
|
|
12161
12359
|
const input = VectorNormalizeSchema.parse(params);
|
|
12162
12360
|
if (input.vector.length === 0) {
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
code: "VALIDATION_ERROR",
|
|
12167
|
-
category: "validation",
|
|
12168
|
-
recoverable: false
|
|
12169
|
-
});
|
|
12361
|
+
throw new ValidationError(
|
|
12362
|
+
"vector is required and must be a non-empty array of numbers"
|
|
12363
|
+
);
|
|
12170
12364
|
}
|
|
12171
12365
|
const normalized = normalizeVector(input.vector);
|
|
12172
12366
|
return Promise.resolve({
|
|
@@ -12196,20 +12390,12 @@ function createVectorDistanceTool() {
|
|
|
12196
12390
|
try {
|
|
12197
12391
|
const input = VectorDistanceSchema.parse(params);
|
|
12198
12392
|
if (input.vector1.length === 0 || input.vector2.length === 0) {
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
code: "VALIDATION_ERROR",
|
|
12203
|
-
category: "validation",
|
|
12204
|
-
recoverable: false
|
|
12205
|
-
});
|
|
12393
|
+
throw new ValidationError(
|
|
12394
|
+
"vector1 and vector2 are required and must be non-empty arrays of numbers"
|
|
12395
|
+
);
|
|
12206
12396
|
}
|
|
12207
12397
|
if (input.vector1.length !== input.vector2.length) {
|
|
12208
|
-
|
|
12209
|
-
success: false,
|
|
12210
|
-
error: "Vector dimensions must match",
|
|
12211
|
-
code: "DIMENSION_MISMATCH"
|
|
12212
|
-
});
|
|
12398
|
+
throw new ValidationError("Vector dimensions must match", "DIMENSION_MISMATCH");
|
|
12213
12399
|
}
|
|
12214
12400
|
let result;
|
|
12215
12401
|
switch (input.metric) {
|
|
@@ -12223,13 +12409,9 @@ function createVectorDistanceTool() {
|
|
|
12223
12409
|
result = 1 - cosineSimilarity(input.vector1, input.vector2);
|
|
12224
12410
|
break;
|
|
12225
12411
|
default:
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
code: "VALIDATION_ERROR",
|
|
12230
|
-
category: "validation",
|
|
12231
|
-
recoverable: false
|
|
12232
|
-
});
|
|
12412
|
+
throw new ValidationError(
|
|
12413
|
+
`Invalid metric '${input.metric}'. Valid values: cosine, euclidean, dot`
|
|
12414
|
+
);
|
|
12233
12415
|
}
|
|
12234
12416
|
return Promise.resolve({
|
|
12235
12417
|
success: true,
|
|
@@ -12372,14 +12554,14 @@ function requireCoordinate(value, name, min, max) {
|
|
|
12372
12554
|
throw new DbMcpError(
|
|
12373
12555
|
`Invalid ${name}: value is not a valid number.`,
|
|
12374
12556
|
"GEO_INVALID_COORDINATES",
|
|
12375
|
-
|
|
12557
|
+
ErrorCategory.VALIDATION
|
|
12376
12558
|
);
|
|
12377
12559
|
}
|
|
12378
12560
|
if (value < min || value > max) {
|
|
12379
12561
|
throw new DbMcpError(
|
|
12380
12562
|
`Invalid ${name}: ${String(value)}. Must be between ${String(min)} and ${String(max)}.`,
|
|
12381
12563
|
"GEO_INVALID_COORDINATES",
|
|
12382
|
-
|
|
12564
|
+
ErrorCategory.VALIDATION
|
|
12383
12565
|
);
|
|
12384
12566
|
}
|
|
12385
12567
|
return value;
|
|
@@ -12389,7 +12571,7 @@ function requireNumber(value, name) {
|
|
|
12389
12571
|
throw new DbMcpError(
|
|
12390
12572
|
`Invalid ${name}: value is not a valid number.`,
|
|
12391
12573
|
"GEO_INVALID_COORDINATES",
|
|
12392
|
-
|
|
12574
|
+
ErrorCategory.VALIDATION
|
|
12393
12575
|
);
|
|
12394
12576
|
}
|
|
12395
12577
|
return value;
|
|
@@ -12665,19 +12847,35 @@ function createBackupTool(adapter) {
|
|
|
12665
12847
|
path: ""
|
|
12666
12848
|
};
|
|
12667
12849
|
}
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
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;
|
|
12679
12878
|
}
|
|
12680
|
-
const resolvedPath = pathCheck.resolvedPath;
|
|
12681
12879
|
const escapedPath = resolvedPath.replace(/'/g, "''");
|
|
12682
12880
|
const sql = `VACUUM INTO '${escapedPath}'`;
|
|
12683
12881
|
const start = Date.now();
|
|
@@ -12687,7 +12885,7 @@ function createBackupTool(adapter) {
|
|
|
12687
12885
|
progress,
|
|
12688
12886
|
1,
|
|
12689
12887
|
2,
|
|
12690
|
-
`Creating backup at ${
|
|
12888
|
+
`Creating backup at ${nodePath__default.basename(resolvedPath)}...`
|
|
12691
12889
|
);
|
|
12692
12890
|
await adapter.executeQuery(sql);
|
|
12693
12891
|
await sendProgress(progress, 2, 2, "Backup complete");
|
|
@@ -12737,18 +12935,34 @@ function createVacuumIntoTool(adapter) {
|
|
|
12737
12935
|
...formatHandlerError(new ValidationError("outputPath is required"))
|
|
12738
12936
|
};
|
|
12739
12937
|
}
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
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;
|
|
12750
12965
|
}
|
|
12751
|
-
const resolvedPath = pathCheck.resolvedPath;
|
|
12752
12966
|
const escapedPath = resolvedPath.replace(/'/g, "''");
|
|
12753
12967
|
const sql = `VACUUM INTO '${escapedPath}'`;
|
|
12754
12968
|
const start = Date.now();
|
|
@@ -12975,26 +13189,53 @@ function createRestoreTool(adapter) {
|
|
|
12975
13189
|
sourcePath: ""
|
|
12976
13190
|
};
|
|
12977
13191
|
}
|
|
12978
|
-
|
|
12979
|
-
const
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
}
|
|
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, "''");
|
|
12996
13238
|
}
|
|
12997
|
-
const escapedPath = pathCheck.resolvedPath.replace(/'/g, "''");
|
|
12998
13239
|
await adapter.executeReadQuery("PRAGMA integrity_check(1)");
|
|
12999
13240
|
await sendProgress(progress, 2, 5, "Attaching backup database...");
|
|
13000
13241
|
try {
|
|
@@ -13203,25 +13444,41 @@ function createDumpTool(adapter) {
|
|
|
13203
13444
|
};
|
|
13204
13445
|
}
|
|
13205
13446
|
if (!input.outputPath?.trim()) {
|
|
13206
|
-
return {
|
|
13207
|
-
...formatHandlerError(new ValidationError("outputPath is required"))
|
|
13208
|
-
};
|
|
13209
|
-
}
|
|
13210
|
-
const pathCheck = validateSameDirPath(
|
|
13211
|
-
input.outputPath,
|
|
13212
|
-
adapter.getConfiguredPath()
|
|
13213
|
-
);
|
|
13214
|
-
if (input.outputPath.includes("..") || !pathCheck.valid) {
|
|
13215
|
-
return {
|
|
13216
|
-
success: false,
|
|
13217
|
-
error: !pathCheck.valid ? pathCheck.error : "Invalid path: must not contain '..'",
|
|
13218
|
-
code: "SECURITY_ERROR",
|
|
13219
|
-
path: input.outputPath
|
|
13447
|
+
return {
|
|
13448
|
+
...formatHandlerError(new ValidationError("outputPath is required"))
|
|
13220
13449
|
};
|
|
13221
13450
|
}
|
|
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;
|
|
13479
|
+
}
|
|
13222
13480
|
const progress = buildProgressContext(context);
|
|
13223
13481
|
const start = Date.now();
|
|
13224
|
-
const resolvedPath = pathCheck.resolvedPath;
|
|
13225
13482
|
await sendProgress(progress, 1, 4, "Preparing dump file...");
|
|
13226
13483
|
const stream = fs.createWriteStream(resolvedPath, { encoding: "utf8" });
|
|
13227
13484
|
try {
|
|
@@ -13273,9 +13530,9 @@ function createDumpTool(adapter) {
|
|
|
13273
13530
|
}
|
|
13274
13531
|
}
|
|
13275
13532
|
stream.write("COMMIT;\n");
|
|
13276
|
-
await new Promise((
|
|
13533
|
+
await new Promise((resolve3, reject) => {
|
|
13277
13534
|
stream.end(
|
|
13278
|
-
(err) => err ? reject(err) :
|
|
13535
|
+
(err) => err ? reject(err) : resolve3()
|
|
13279
13536
|
);
|
|
13280
13537
|
});
|
|
13281
13538
|
} catch (err) {
|
|
@@ -13327,29 +13584,57 @@ function createVerifyBackupTool(adapter) {
|
|
|
13327
13584
|
backupPath: ""
|
|
13328
13585
|
};
|
|
13329
13586
|
}
|
|
13330
|
-
|
|
13331
|
-
const
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
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, "''");
|
|
13351
13637
|
}
|
|
13352
|
-
const escapedPath = resolvedPath.replace(/'/g, "''");
|
|
13353
13638
|
try {
|
|
13354
13639
|
await adapter.rawQuery(
|
|
13355
13640
|
`ATTACH DATABASE '${escapedPath}' AS backup_verify`
|
|
@@ -13521,10 +13806,10 @@ function createPragmaDatabaseListTool(adapter) {
|
|
|
13521
13806
|
};
|
|
13522
13807
|
});
|
|
13523
13808
|
const configuredPath = adapter.getConfiguredPath();
|
|
13524
|
-
const
|
|
13809
|
+
const normalize = (p) => p.replace(/\\/g, "/");
|
|
13525
13810
|
const mainDb = databases.find((db) => db.name === "main");
|
|
13526
13811
|
const internalPathDiffers = Boolean(
|
|
13527
|
-
mainDb?.file &&
|
|
13812
|
+
mainDb?.file && normalize(mainDb.file) !== normalize(configuredPath.split(/[\\/]/).pop() ?? "")
|
|
13528
13813
|
);
|
|
13529
13814
|
return {
|
|
13530
13815
|
success: true,
|
|
@@ -13702,46 +13987,6 @@ function createPragmaTableInfoTool(adapter) {
|
|
|
13702
13987
|
}
|
|
13703
13988
|
};
|
|
13704
13989
|
}
|
|
13705
|
-
function createAppendInsightTool() {
|
|
13706
|
-
return {
|
|
13707
|
-
name: "sqlite_append_insight",
|
|
13708
|
-
description: "Add a business insight to the memo://insights resource. Use this to capture key findings during data analysis.",
|
|
13709
|
-
group: "admin",
|
|
13710
|
-
inputSchema: AppendInsightSchema,
|
|
13711
|
-
outputSchema: AppendInsightOutputSchema,
|
|
13712
|
-
requiredScopes: ["write"],
|
|
13713
|
-
annotations: write("Append Insight"),
|
|
13714
|
-
handler: (_params, _context) => {
|
|
13715
|
-
try {
|
|
13716
|
-
const input = AppendInsightSchema.parse(_params);
|
|
13717
|
-
let sanitizedInsight = input.insight.replace(/[<>]/g, "");
|
|
13718
|
-
if (sanitizedInsight.length > 500) {
|
|
13719
|
-
sanitizedInsight = sanitizedInsight.substring(0, 500) + "...";
|
|
13720
|
-
}
|
|
13721
|
-
if (!sanitizedInsight || sanitizedInsight.trim().length === 0) {
|
|
13722
|
-
return Promise.resolve({
|
|
13723
|
-
success: false,
|
|
13724
|
-
error: "Insight must be a non-empty string",
|
|
13725
|
-
message: "",
|
|
13726
|
-
insightCount: insightsManager.count()
|
|
13727
|
-
});
|
|
13728
|
-
}
|
|
13729
|
-
insightsManager.append(sanitizedInsight);
|
|
13730
|
-
return Promise.resolve({
|
|
13731
|
-
success: true,
|
|
13732
|
-
message: "Insight added to memo",
|
|
13733
|
-
insightCount: insightsManager.count()
|
|
13734
|
-
});
|
|
13735
|
-
} catch (error) {
|
|
13736
|
-
return Promise.resolve({
|
|
13737
|
-
...formatHandlerError(error),
|
|
13738
|
-
message: "",
|
|
13739
|
-
insightCount: insightsManager.count()
|
|
13740
|
-
});
|
|
13741
|
-
}
|
|
13742
|
-
}
|
|
13743
|
-
};
|
|
13744
|
-
}
|
|
13745
13990
|
function createAttachDatabaseTool(adapter) {
|
|
13746
13991
|
return {
|
|
13747
13992
|
name: "sqlite_attach_database",
|
|
@@ -13762,18 +14007,37 @@ function createAttachDatabaseTool(adapter) {
|
|
|
13762
14007
|
code: "VALIDATION_ERROR"
|
|
13763
14008
|
};
|
|
13764
14009
|
}
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
)
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
}
|
|
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;
|
|
13775
14040
|
}
|
|
13776
|
-
const escapedPath = pathCheck.resolvedPath.replace(/'/g, "''");
|
|
13777
14041
|
await adapter.rawQuery(
|
|
13778
14042
|
`ATTACH DATABASE '${escapedPath}' AS "${input.alias.replace(/"/g, '""')}"`
|
|
13779
14043
|
);
|
|
@@ -13781,7 +14045,7 @@ function createAttachDatabaseTool(adapter) {
|
|
|
13781
14045
|
success: true,
|
|
13782
14046
|
message: `Database attached as '${input.alias}'`,
|
|
13783
14047
|
alias: input.alias,
|
|
13784
|
-
filepath:
|
|
14048
|
+
filepath: finalResolvedPath.split(/[\\/]/).pop() ?? ""
|
|
13785
14049
|
};
|
|
13786
14050
|
} catch (error) {
|
|
13787
14051
|
return formatHandlerError(error);
|
|
@@ -13990,7 +14254,6 @@ function getAdminTools(adapter) {
|
|
|
13990
14254
|
createPragmaOptimizeTool(adapter),
|
|
13991
14255
|
createPragmaSettingsTool(adapter),
|
|
13992
14256
|
createPragmaTableInfoTool(adapter),
|
|
13993
|
-
createAppendInsightTool(),
|
|
13994
14257
|
createAttachDatabaseTool(adapter),
|
|
13995
14258
|
createDetachDatabaseTool(adapter),
|
|
13996
14259
|
createVacuumIntoTool(adapter),
|
|
@@ -14262,7 +14525,8 @@ var IndexAuditOutputSchema = z.object({
|
|
|
14262
14525
|
type: z.enum([
|
|
14263
14526
|
"redundant",
|
|
14264
14527
|
"missing_fk_index",
|
|
14265
|
-
"unindexed_large_table"
|
|
14528
|
+
"unindexed_large_table",
|
|
14529
|
+
"missing_composite_index"
|
|
14266
14530
|
]),
|
|
14267
14531
|
severity: z.enum(["info", "warning", "error"]),
|
|
14268
14532
|
table: z.string(),
|
|
@@ -14315,23 +14579,6 @@ var coerceNumber8 = (val) => {
|
|
|
14315
14579
|
}
|
|
14316
14580
|
return val;
|
|
14317
14581
|
};
|
|
14318
|
-
var VALID_SECTIONS = ["tables", "views", "indexes", "triggers"];
|
|
14319
|
-
var coerceSections = (val) => Array.isArray(val) ? val.filter(
|
|
14320
|
-
(v) => typeof v === "string" && VALID_SECTIONS.includes(v)
|
|
14321
|
-
) : val;
|
|
14322
|
-
var VALID_CHECKS = [
|
|
14323
|
-
"missing_pk",
|
|
14324
|
-
"missing_not_null",
|
|
14325
|
-
"unindexed_fk",
|
|
14326
|
-
"missing_fk"
|
|
14327
|
-
];
|
|
14328
|
-
var coerceChecks = (val) => Array.isArray(val) ? val.filter(
|
|
14329
|
-
(v) => typeof v === "string" && VALID_CHECKS.includes(v)
|
|
14330
|
-
) : val;
|
|
14331
|
-
var VALID_DIRECTIONS = ["create", "drop"];
|
|
14332
|
-
var coerceDirection = (val) => typeof val === "string" && VALID_DIRECTIONS.includes(val) ? val : typeof val === "string" ? void 0 : val;
|
|
14333
|
-
var VALID_OPERATIONS = ["DELETE", "DROP", "TRUNCATE"];
|
|
14334
|
-
var coerceOperation = (val) => typeof val === "string" && VALID_OPERATIONS.includes(val) ? val : typeof val === "string" ? void 0 : val;
|
|
14335
14582
|
var StorageAnalysisSchema = z.object({
|
|
14336
14583
|
includeTableDetails: z.boolean().optional().describe("Include per-table size breakdown (default: true)"),
|
|
14337
14584
|
excludeSystemTables: z.boolean().optional().describe(
|
|
@@ -14350,41 +14597,42 @@ var IndexAuditSchema = z.object({
|
|
|
14350
14597
|
),
|
|
14351
14598
|
minSeverity: z.enum(["info", "warning", "error"]).optional().describe(
|
|
14352
14599
|
"Minimum severity to include in findings (default: all). Reduces payload for large databases."
|
|
14600
|
+
),
|
|
14601
|
+
recommendComposite: z.boolean().optional().default(false).describe(
|
|
14602
|
+
"Analyze queriesToAnalyze to recommend composite/partial indexes"
|
|
14603
|
+
),
|
|
14604
|
+
queriesToAnalyze: z.array(z.string()).optional().describe(
|
|
14605
|
+
"Queries to analyze using EXPLAIN QUERY PLAN (used if recommendComposite is true)"
|
|
14353
14606
|
)
|
|
14354
|
-
}).default({});
|
|
14607
|
+
}).default({ recommendComposite: false });
|
|
14355
14608
|
var SchemaSnapshotSchema = z.object({
|
|
14356
|
-
sections: z.
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
).describe("Specific sections to include (default: all)"),
|
|
14360
|
-
compact: z.boolean().optional().describe(
|
|
14361
|
-
"Omit column details from tables section for reduced payload (default: false)"
|
|
14609
|
+
sections: z.array(z.enum(["tables", "views", "indexes", "triggers"])).optional().describe("Specific sections to include (default: all)"),
|
|
14610
|
+
compact: z.boolean().optional().default(true).describe(
|
|
14611
|
+
"Omit column details from tables section for reduced payload (default: true)"
|
|
14362
14612
|
),
|
|
14363
14613
|
excludeSystemTables: z.boolean().optional().describe(
|
|
14364
14614
|
"Exclude SpatiaLite system tables, views, indexes, and triggers (default: true)"
|
|
14365
14615
|
)
|
|
14366
|
-
}).default({});
|
|
14616
|
+
}).default({ compact: true });
|
|
14367
14617
|
var MigrationRisksSchema = z.object({
|
|
14368
14618
|
statements: z.array(z.string()).optional().default([]).describe("Array of DDL statements to analyze for risks")
|
|
14369
14619
|
}).default({ statements: [] });
|
|
14370
14620
|
var ConstraintAnalysisSchema = z.object({
|
|
14371
14621
|
table: z.string().optional().describe("Analyze constraints for a specific table only"),
|
|
14372
|
-
checks: z.
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
])
|
|
14381
|
-
).optional()
|
|
14382
|
-
).describe("Specific checks to run (default: all)"),
|
|
14622
|
+
checks: z.array(
|
|
14623
|
+
z.enum([
|
|
14624
|
+
"missing_pk",
|
|
14625
|
+
"missing_not_null",
|
|
14626
|
+
"unindexed_fk",
|
|
14627
|
+
"missing_fk"
|
|
14628
|
+
])
|
|
14629
|
+
).optional().describe("Specific checks to run (default: all)"),
|
|
14383
14630
|
excludeSystemTables: z.boolean().optional().describe(
|
|
14384
14631
|
"Exclude SpatiaLite system tables from constraint analysis (default: true)"
|
|
14385
14632
|
)
|
|
14386
14633
|
}).default({});
|
|
14387
14634
|
var DependencyGraphSchema = z.object({
|
|
14635
|
+
table: z.string().optional().describe("Optional table name to filter the graph"),
|
|
14388
14636
|
includeRowCounts: z.boolean().optional().describe("Include row counts per table (default: true)"),
|
|
14389
14637
|
nodesOnly: z.boolean().optional().describe(
|
|
14390
14638
|
"Return only nodes without edges for a lightweight response (default: false)"
|
|
@@ -14394,7 +14642,7 @@ var DependencyGraphSchema = z.object({
|
|
|
14394
14642
|
)
|
|
14395
14643
|
}).default({});
|
|
14396
14644
|
var TopologicalSortSchema = z.object({
|
|
14397
|
-
direction: z.
|
|
14645
|
+
direction: z.enum(["create", "drop"]).optional().describe(
|
|
14398
14646
|
"Sort direction: 'create' = dependencies first, 'drop' = dependents first (default: create)"
|
|
14399
14647
|
),
|
|
14400
14648
|
excludeSystemTables: z.boolean().optional().describe(
|
|
@@ -14404,9 +14652,9 @@ var TopologicalSortSchema = z.object({
|
|
|
14404
14652
|
var CascadeSimulatorSchema = z.object({
|
|
14405
14653
|
table: z.string().optional().default("").describe("Table name to simulate deletion from"),
|
|
14406
14654
|
operation: z.preprocess(
|
|
14407
|
-
|
|
14408
|
-
z.enum(["DELETE", "DROP", "TRUNCATE"])
|
|
14409
|
-
).describe("Operation to simulate (default: DELETE)"),
|
|
14655
|
+
(val) => typeof val === "string" ? val.toUpperCase() : val,
|
|
14656
|
+
z.enum(["DELETE", "DROP", "TRUNCATE"])
|
|
14657
|
+
).optional().describe("Operation to simulate (default: DELETE)"),
|
|
14410
14658
|
compact: z.boolean().optional().describe(
|
|
14411
14659
|
"Omit path arrays from affected entries to reduce payload (default: false)"
|
|
14412
14660
|
)
|
|
@@ -14472,21 +14720,20 @@ var SchemaDiffOutputSchema = z.object({
|
|
|
14472
14720
|
}).optional(),
|
|
14473
14721
|
comparedAt: z.string().optional()
|
|
14474
14722
|
}).extend(ErrorResponseFields.shape);
|
|
14475
|
-
var VALID_DIFF_SECTIONS = ["tables", "views", "indexes", "triggers"];
|
|
14476
|
-
var coerceDiffSections = (val) => Array.isArray(val) ? val.filter(
|
|
14477
|
-
(v) => typeof v === "string" && VALID_DIFF_SECTIONS.includes(v)
|
|
14478
|
-
) : val;
|
|
14479
14723
|
var SchemaDiffSchema = z.object({
|
|
14480
|
-
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(
|
|
14481
14728
|
"Baseline schema \u2014 'current' to snapshot live DB, or an inline snapshot object from a previous sqlite_schema_snapshot call"
|
|
14482
14729
|
),
|
|
14483
|
-
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(
|
|
14484
14734
|
"Target schema to compare against baseline \u2014 'current' to snapshot live DB, or an inline snapshot object"
|
|
14485
14735
|
),
|
|
14486
|
-
sections: z.
|
|
14487
|
-
coerceDiffSections,
|
|
14488
|
-
z.array(z.enum(["tables", "views", "indexes", "triggers"])).optional()
|
|
14489
|
-
).describe("Sections to compare (default: all)"),
|
|
14736
|
+
sections: z.array(z.enum(["tables", "views", "indexes", "triggers"])).optional().describe("Sections to compare (default: all)"),
|
|
14490
14737
|
excludeSystemTables: z.boolean().optional().describe(
|
|
14491
14738
|
"Exclude SpatiaLite system tables when capturing live schema (default: true)"
|
|
14492
14739
|
)
|
|
@@ -14505,42 +14752,80 @@ function createDependencyGraphTool(adapter) {
|
|
|
14505
14752
|
handler: async (params, _context) => {
|
|
14506
14753
|
try {
|
|
14507
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
|
+
}
|
|
14508
14771
|
const includeRowCounts = input.includeRowCounts !== false;
|
|
14509
14772
|
const { nodes, edges } = await buildForeignKeyGraph(adapter, {
|
|
14510
14773
|
excludeSystemTables: input.excludeSystemTables,
|
|
14511
14774
|
includeRowCounts
|
|
14512
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
|
+
}
|
|
14513
14798
|
const adjacency = /* @__PURE__ */ new Map();
|
|
14514
|
-
for (const node of
|
|
14799
|
+
for (const node of finalNodes) {
|
|
14515
14800
|
adjacency.set(node.table, []);
|
|
14516
14801
|
}
|
|
14517
|
-
for (const edge of
|
|
14802
|
+
for (const edge of finalEdges) {
|
|
14518
14803
|
adjacency.get(edge.from)?.push(edge.to);
|
|
14519
14804
|
}
|
|
14520
14805
|
const cycles = detectCycles(
|
|
14521
14806
|
adjacency,
|
|
14522
|
-
|
|
14807
|
+
finalNodes.map((n) => n.table)
|
|
14523
14808
|
);
|
|
14524
|
-
const referencedTables = new Set(
|
|
14525
|
-
const referencingTables = new Set(
|
|
14526
|
-
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(
|
|
14527
14812
|
(n) => referencedTables.has(n.table) && !referencingTables.has(n.table)
|
|
14528
14813
|
).map((n) => n.table);
|
|
14529
|
-
const leafTables =
|
|
14814
|
+
const leafTables = finalNodes.filter(
|
|
14530
14815
|
(n) => referencingTables.has(n.table) && !referencedTables.has(n.table)
|
|
14531
14816
|
).map((n) => n.table);
|
|
14532
14817
|
const nodesOnly = input.nodesOnly === true;
|
|
14533
14818
|
return {
|
|
14534
14819
|
success: true,
|
|
14535
|
-
nodes:
|
|
14820
|
+
nodes: finalNodes.map((n) => ({
|
|
14536
14821
|
table: n.table,
|
|
14537
14822
|
...includeRowCounts ? { rowCount: n.rowCount } : {}
|
|
14538
14823
|
})),
|
|
14539
|
-
...!nodesOnly ? { edges } : {},
|
|
14824
|
+
...!nodesOnly ? { edges: finalEdges } : {},
|
|
14540
14825
|
circularDependencies: cycles.length > 0 ? cycles : void 0,
|
|
14541
14826
|
stats: {
|
|
14542
|
-
totalTables:
|
|
14543
|
-
totalRelationships:
|
|
14827
|
+
totalTables: finalNodes.length,
|
|
14828
|
+
totalRelationships: finalEdges.length,
|
|
14544
14829
|
rootTables,
|
|
14545
14830
|
leafTables
|
|
14546
14831
|
}
|
|
@@ -15845,6 +16130,35 @@ function createIndexAuditTool(adapter) {
|
|
|
15845
16130
|
} catch {
|
|
15846
16131
|
}
|
|
15847
16132
|
}
|
|
16133
|
+
if (input.recommendComposite && input.queriesToAnalyze && input.queriesToAnalyze.length > 0) {
|
|
16134
|
+
for (const query of input.queriesToAnalyze) {
|
|
16135
|
+
try {
|
|
16136
|
+
if (/^\s*SELECT/i.test(query)) {
|
|
16137
|
+
const planResult = await adapter.executeReadQuery(
|
|
16138
|
+
`EXPLAIN QUERY PLAN ${query}`
|
|
16139
|
+
);
|
|
16140
|
+
const planRows = planResult.rows ?? [];
|
|
16141
|
+
for (const row of planRows) {
|
|
16142
|
+
const detailVal = row["detail"];
|
|
16143
|
+
const detail = typeof detailVal === "string" ? detailVal : "";
|
|
16144
|
+
if (detail.startsWith("SCAN TABLE") || detail.startsWith("SCAN ")) {
|
|
16145
|
+
const match = /SCAN (?:TABLE )?([\w_]+)/i.exec(detail);
|
|
16146
|
+
const tableName = match ? match[1] : "unknown";
|
|
16147
|
+
if (tableName && tableName !== "unknown") {
|
|
16148
|
+
findings.push({
|
|
16149
|
+
type: "missing_composite_index",
|
|
16150
|
+
severity: "warning",
|
|
16151
|
+
table: tableName,
|
|
16152
|
+
suggestion: `Query resulted in SCAN TABLE "${tableName}". Consider analyzing WHERE/JOIN clauses to add a composite index.`
|
|
16153
|
+
});
|
|
16154
|
+
}
|
|
16155
|
+
}
|
|
16156
|
+
}
|
|
16157
|
+
}
|
|
16158
|
+
} catch {
|
|
16159
|
+
}
|
|
16160
|
+
}
|
|
16161
|
+
}
|
|
15848
16162
|
const redundantCount = findings.filter(
|
|
15849
16163
|
(f) => f.type === "redundant"
|
|
15850
16164
|
).length;
|
|
@@ -16088,7 +16402,7 @@ var MigrationStatusOutputSchema = z.object({
|
|
|
16088
16402
|
}).optional(),
|
|
16089
16403
|
sourceSystems: z.array(z.string()).optional()
|
|
16090
16404
|
}).extend(ErrorResponseFields.shape);
|
|
16091
|
-
var MigrationInitSchema = z.object({}).default({});
|
|
16405
|
+
var MigrationInitSchema = z.object({}).strict().default({});
|
|
16092
16406
|
var MigrationRecordSchema = z.object({
|
|
16093
16407
|
version: z.string().describe("Version identifier (e.g., '1.0.0', '2024-01-15-add-users')"),
|
|
16094
16408
|
description: z.string().optional().describe("Human-readable description of the migration"),
|
|
@@ -16163,7 +16477,7 @@ var MigrationHistorySchema = z.object({
|
|
|
16163
16477
|
"Omit migrationHash and sourceSystem from records to reduce payload (default: false)"
|
|
16164
16478
|
)
|
|
16165
16479
|
}).default({});
|
|
16166
|
-
var MigrationStatusSchema = z.object({}).default({});
|
|
16480
|
+
var MigrationStatusSchema = z.object({}).strict().default({});
|
|
16167
16481
|
var MIGRATIONS_TABLE = "_mcp_migrations";
|
|
16168
16482
|
function toMigrationRecord(row) {
|
|
16169
16483
|
return {
|
|
@@ -16191,7 +16505,7 @@ function validateMigrationSql(sql) {
|
|
|
16191
16505
|
const cleanSql = sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--.*$/gm, "");
|
|
16192
16506
|
const upperSql = cleanSql.toUpperCase();
|
|
16193
16507
|
if (upperSql.includes("ATTACH ") || upperSql.includes("DETACH ") || upperSql.includes("PRAGMA ") || upperSql.includes("LOAD_EXTENSION(")) {
|
|
16194
|
-
throw new
|
|
16508
|
+
throw new ValidationError(
|
|
16195
16509
|
`Migration validation failed: unauthorized command or function call`
|
|
16196
16510
|
);
|
|
16197
16511
|
}
|
|
@@ -16884,6 +17198,10 @@ var POSITIONAL_PARAM_MAP = {
|
|
|
16884
17198
|
listConstraints: "table",
|
|
16885
17199
|
dateAdd: ["table", "column", "amount", "unit", "whereClause"],
|
|
16886
17200
|
dateDiff: ["table", "column1", "column2", "unit", "whereClause"],
|
|
17201
|
+
enableVersioning: "table",
|
|
17202
|
+
disableVersioning: "table",
|
|
17203
|
+
checkVersion: ["table", "rowId"],
|
|
17204
|
+
conditionalUpdate: ["table", "data", "conditions", "expectedVersion"],
|
|
16887
17205
|
// JSON
|
|
16888
17206
|
extract: ["table", "column", "path", "whereClause"],
|
|
16889
17207
|
set: ["table", "column", "path", "value", "whereClause"],
|
|
@@ -16909,6 +17227,7 @@ var POSITIONAL_PARAM_MAP = {
|
|
|
16909
17227
|
jsonbConvert: ["table", "column"],
|
|
16910
17228
|
createJsonCollection: "tableName",
|
|
16911
17229
|
// Text
|
|
17230
|
+
sentiment: "text",
|
|
16912
17231
|
regexExtract: ["table", "column", "pattern"],
|
|
16913
17232
|
regexMatch: ["table", "column", "pattern"],
|
|
16914
17233
|
split: ["table", "column", "delimiter"],
|
|
@@ -16966,7 +17285,6 @@ var POSITIONAL_PARAM_MAP = {
|
|
|
16966
17285
|
pragmaSettings: "pragma",
|
|
16967
17286
|
pragmaTableInfo: "table",
|
|
16968
17287
|
pragmaCompileOptions: "filter",
|
|
16969
|
-
appendInsight: "insight",
|
|
16970
17288
|
generateSeries: ["start", "stop", "step"],
|
|
16971
17289
|
createView: ["viewName", "selectQuery"],
|
|
16972
17290
|
dropView: "viewName",
|
|
@@ -17038,7 +17356,7 @@ function normalizeParams(methodName, args) {
|
|
|
17038
17356
|
if (Array.isArray(paramMapping2) && paramMapping2[0] !== void 0) {
|
|
17039
17357
|
return { [paramMapping2[0]]: arg };
|
|
17040
17358
|
}
|
|
17041
|
-
throw new
|
|
17359
|
+
throw new ValidationError(
|
|
17042
17360
|
`Positional arguments are not supported for method: ${methodName}. Please use an options object.`
|
|
17043
17361
|
);
|
|
17044
17362
|
}
|
|
@@ -17046,7 +17364,7 @@ function normalizeParams(methodName, args) {
|
|
|
17046
17364
|
}
|
|
17047
17365
|
const paramMapping = POSITIONAL_PARAM_MAP[methodName];
|
|
17048
17366
|
if (paramMapping === void 0) {
|
|
17049
|
-
throw new
|
|
17367
|
+
throw new ValidationError(
|
|
17050
17368
|
`Positional arguments are not supported for method: ${methodName}. Please use an options object.`
|
|
17051
17369
|
);
|
|
17052
17370
|
}
|
|
@@ -17096,7 +17414,7 @@ function createGroupApi(groupName, tools, baseContext) {
|
|
|
17096
17414
|
api[methodName] = async (...args) => {
|
|
17097
17415
|
if (baseContext?.auth !== void 0) {
|
|
17098
17416
|
if (!scopesGrantToolAccess(baseContext.auth.scopes ?? [], tool.name)) {
|
|
17099
|
-
throw new
|
|
17417
|
+
throw new AuthorizationError(
|
|
17100
17418
|
`Forbidden: Required scope for tool '${tool.name}' not granted.`
|
|
17101
17419
|
);
|
|
17102
17420
|
}
|
|
@@ -17107,7 +17425,8 @@ function createGroupApi(groupName, tools, baseContext) {
|
|
|
17107
17425
|
requestId: crypto.randomUUID(),
|
|
17108
17426
|
...baseContext?.server !== void 0 ? { server: baseContext.server } : {},
|
|
17109
17427
|
...baseContext?.progressToken !== void 0 ? { progressToken: baseContext.progressToken } : {},
|
|
17110
|
-
...baseContext?.auth !== void 0 ? { auth: baseContext.auth } : {}
|
|
17428
|
+
...baseContext?.auth !== void 0 ? { auth: baseContext.auth } : {},
|
|
17429
|
+
isCodeMode: true
|
|
17111
17430
|
};
|
|
17112
17431
|
const result = await tool.handler(normalizedParams, context);
|
|
17113
17432
|
return result;
|
|
@@ -17269,7 +17588,8 @@ function createSqliteApi(tools, baseContext) {
|
|
|
17269
17588
|
var DEFAULT_SANDBOX_OPTIONS = {
|
|
17270
17589
|
memoryLimitMb: 128,
|
|
17271
17590
|
timeoutMs: 5e3,
|
|
17272
|
-
cpuLimitMs: 1e4
|
|
17591
|
+
cpuLimitMs: 1e4,
|
|
17592
|
+
strictIsolation: true
|
|
17273
17593
|
};
|
|
17274
17594
|
var DEFAULT_POOL_OPTIONS = {
|
|
17275
17595
|
minInstances: 2,
|
|
@@ -17465,7 +17785,7 @@ var CodeModeSecurityManager = class {
|
|
|
17465
17785
|
const redactedResult = redactObject(result, 0, 15);
|
|
17466
17786
|
const serialized = JSON.stringify(redactedResult);
|
|
17467
17787
|
if (serialized === void 0) {
|
|
17468
|
-
throw new
|
|
17788
|
+
throw new InternalError("Not serializable");
|
|
17469
17789
|
}
|
|
17470
17790
|
if (serialized.length > this.config.maxResultSize) {
|
|
17471
17791
|
return {
|
|
@@ -17565,6 +17885,9 @@ return ${lastStmt}`;
|
|
|
17565
17885
|
}
|
|
17566
17886
|
|
|
17567
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;
|
|
17568
17891
|
var CodeModeSandbox = class _CodeModeSandbox {
|
|
17569
17892
|
options;
|
|
17570
17893
|
disposed = false;
|
|
@@ -17593,9 +17916,8 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17593
17916
|
metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 }
|
|
17594
17917
|
};
|
|
17595
17918
|
}
|
|
17596
|
-
const groupNameRegex = /^[a-zA-Z0-9_]+$/;
|
|
17597
17919
|
for (const groupName of Object.keys(apiBindings)) {
|
|
17598
|
-
if (!
|
|
17920
|
+
if (!GROUP_NAME_REGEX.test(groupName) || groupName === "__proto__" || groupName === "constructor" || groupName === "prototype") {
|
|
17599
17921
|
return {
|
|
17600
17922
|
success: false,
|
|
17601
17923
|
error: `Security Error: Invalid tool group name '${groupName}'`,
|
|
@@ -17605,30 +17927,39 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17605
17927
|
}
|
|
17606
17928
|
try {
|
|
17607
17929
|
const wrappedCode = `async function __wrapper() { ${code} }`;
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
if (["process", "require", "global", "globalThis"].includes(objName)) {
|
|
17622
|
-
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
|
+
);
|
|
17623
17943
|
}
|
|
17624
|
-
|
|
17625
|
-
|
|
17626
|
-
|
|
17627
|
-
|
|
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
|
+
}
|
|
17628
17949
|
}
|
|
17950
|
+
for (const key in n) {
|
|
17951
|
+
if (key !== "loc" && key !== "start" && key !== "end") {
|
|
17952
|
+
validateAst(n[key]);
|
|
17953
|
+
}
|
|
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);
|
|
17629
17960
|
}
|
|
17630
|
-
|
|
17631
|
-
|
|
17961
|
+
astCache.set(wrappedCode, ast);
|
|
17962
|
+
}
|
|
17632
17963
|
} catch (e) {
|
|
17633
17964
|
return {
|
|
17634
17965
|
success: false,
|
|
@@ -17644,6 +17975,13 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17644
17975
|
} catch {
|
|
17645
17976
|
}
|
|
17646
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
|
+
}
|
|
17647
17985
|
const vm = await import('vm');
|
|
17648
17986
|
const logs2 = [];
|
|
17649
17987
|
const sandboxEnv = {
|
|
@@ -17670,6 +18008,28 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17670
18008
|
}
|
|
17671
18009
|
}
|
|
17672
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
|
+
);
|
|
17673
18033
|
const startTime2 = performance.now();
|
|
17674
18034
|
let result2;
|
|
17675
18035
|
let success2 = true;
|
|
@@ -17699,94 +18059,125 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17699
18059
|
const isolate = new ivmLib.Isolate({
|
|
17700
18060
|
memoryLimit: this.options.memoryLimitMb
|
|
17701
18061
|
});
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
const logs = [];
|
|
17706
|
-
const logRef = new ivmLib.Reference((level, ...args) => {
|
|
17707
|
-
const msg = args.map(
|
|
17708
|
-
(a) => typeof a === "object" && a !== null ? JSON.stringify(a) : String(a)
|
|
17709
|
-
).join(" ");
|
|
17710
|
-
logs.push(level === "LOG" ? msg : `[${level}] ${msg}`);
|
|
17711
|
-
});
|
|
17712
|
-
context.global.setSync("logRef", logRef);
|
|
17713
|
-
const setupScript = `
|
|
17714
|
-
globalThis.console = {
|
|
17715
|
-
log: (...args) => logRef.applyIgnored(undefined, ['LOG', ...args], { arguments: { copy: true } }),
|
|
17716
|
-
error: (...args) => logRef.applyIgnored(undefined, ['ERROR', ...args], { arguments: { copy: true } }),
|
|
17717
|
-
warn: (...args) => logRef.applyIgnored(undefined, ['WARN', ...args], { arguments: { copy: true } }),
|
|
17718
|
-
info: (...args) => logRef.applyIgnored(undefined, ['INFO', ...args], { arguments: { copy: true } }),
|
|
17719
|
-
debug: (...args) => logRef.applyIgnored(undefined, ['DEBUG', ...args], { arguments: { copy: true } })
|
|
17720
|
-
};
|
|
17721
|
-
globalThis.sqlite = {};
|
|
17722
|
-
`;
|
|
17723
|
-
context.evalSync(setupScript);
|
|
17724
|
-
let rpcCount = 0;
|
|
17725
|
-
const MAX_RPC_CALLS = 100;
|
|
18062
|
+
let context;
|
|
18063
|
+
let logRef;
|
|
18064
|
+
let script;
|
|
17726
18065
|
const refCleanup = [];
|
|
17727
|
-
|
|
17728
|
-
if (typeof groupValue === "object" && groupValue !== null) {
|
|
17729
|
-
context.evalSync(
|
|
17730
|
-
`globalThis.sqlite[${JSON.stringify(groupName)}] = {};`
|
|
17731
|
-
);
|
|
17732
|
-
for (const [methodName, methodFn] of Object.entries(groupValue)) {
|
|
17733
|
-
if (typeof methodFn === "function") {
|
|
17734
|
-
const fnRef = new ivmLib.Reference(async (...args) => {
|
|
17735
|
-
if (++rpcCount > MAX_RPC_CALLS) {
|
|
17736
|
-
throw new Error(
|
|
17737
|
-
`QuotaExceededError: Maximum number of host tool calls (${MAX_RPC_CALLS}) exceeded (attempted call ${rpcCount}).`
|
|
17738
|
-
);
|
|
17739
|
-
}
|
|
17740
|
-
try {
|
|
17741
|
-
return await methodFn(...args);
|
|
17742
|
-
} catch (e) {
|
|
17743
|
-
throw new Error(e instanceof Error ? e.message : String(e), {
|
|
17744
|
-
cause: e
|
|
17745
|
-
});
|
|
17746
|
-
}
|
|
17747
|
-
});
|
|
17748
|
-
refCleanup.push(fnRef);
|
|
17749
|
-
const refName = `fnRef_${groupName}_${methodName}`;
|
|
17750
|
-
context.global.setSync(refName, fnRef);
|
|
17751
|
-
context.evalSync(`
|
|
17752
|
-
globalThis.sqlite[${JSON.stringify(groupName)}][${JSON.stringify(methodName)}] = async (...args) => {
|
|
17753
|
-
return await globalThis[${JSON.stringify(refName)}].apply(undefined, args, { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
17754
|
-
};
|
|
17755
|
-
`);
|
|
17756
|
-
}
|
|
17757
|
-
}
|
|
17758
|
-
} else if (typeof groupValue === "function") {
|
|
17759
|
-
const fnRef = new ivmLib.Reference(async (...args) => {
|
|
17760
|
-
if (++rpcCount > MAX_RPC_CALLS) {
|
|
17761
|
-
throw new Error(
|
|
17762
|
-
`QuotaExceededError: Maximum number of host tool calls (${MAX_RPC_CALLS}) exceeded (attempted call ${rpcCount}).`
|
|
17763
|
-
);
|
|
17764
|
-
}
|
|
17765
|
-
try {
|
|
17766
|
-
return await groupValue(...args);
|
|
17767
|
-
} catch (e) {
|
|
17768
|
-
throw new Error(e instanceof Error ? e.message : String(e), {
|
|
17769
|
-
cause: e
|
|
17770
|
-
});
|
|
17771
|
-
}
|
|
17772
|
-
});
|
|
17773
|
-
refCleanup.push(fnRef);
|
|
17774
|
-
const refName = `fnRef_${groupName}`;
|
|
17775
|
-
context.global.setSync(refName, fnRef);
|
|
17776
|
-
context.evalSync(`
|
|
17777
|
-
globalThis.sqlite[${JSON.stringify(groupName)}] = async (...args) => {
|
|
17778
|
-
return await globalThis[${JSON.stringify(refName)}].apply(undefined, args, { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
17779
|
-
};
|
|
17780
|
-
`);
|
|
17781
|
-
}
|
|
17782
|
-
}
|
|
18066
|
+
const logs = [];
|
|
17783
18067
|
const startTime = performance.now();
|
|
17784
18068
|
let result;
|
|
17785
18069
|
let success = true;
|
|
17786
18070
|
let errorMsg;
|
|
17787
18071
|
try {
|
|
18072
|
+
context = isolate.createContextSync();
|
|
18073
|
+
const jail = context.global;
|
|
18074
|
+
jail.setSync("global", jail.derefInto());
|
|
18075
|
+
logRef = new ivmLib.Reference((level, ...args) => {
|
|
18076
|
+
const msg = args.map(
|
|
18077
|
+
(a) => typeof a === "object" && a !== null ? JSON.stringify(a) : String(a)
|
|
18078
|
+
).join(" ");
|
|
18079
|
+
logs.push(level === "LOG" ? msg : `[${level}] ${msg}`);
|
|
18080
|
+
});
|
|
18081
|
+
context.global.setSync("logRef", logRef);
|
|
18082
|
+
const setupScript = `
|
|
18083
|
+
globalThis.console = {
|
|
18084
|
+
log: (...args) => logRef.applyIgnored(undefined, ['LOG', ...args], { arguments: { copy: true } }),
|
|
18085
|
+
error: (...args) => logRef.applyIgnored(undefined, ['ERROR', ...args], { arguments: { copy: true } }),
|
|
18086
|
+
warn: (...args) => logRef.applyIgnored(undefined, ['WARN', ...args], { arguments: { copy: true } }),
|
|
18087
|
+
info: (...args) => logRef.applyIgnored(undefined, ['INFO', ...args], { arguments: { copy: true } }),
|
|
18088
|
+
debug: (...args) => logRef.applyIgnored(undefined, ['DEBUG', ...args], { arguments: { copy: true } })
|
|
18089
|
+
};
|
|
18090
|
+
globalThis.sqlite = {};
|
|
18091
|
+
`;
|
|
18092
|
+
context.evalSync(setupScript);
|
|
18093
|
+
let rpcCount = 0;
|
|
18094
|
+
const MAX_RPC_CALLS = 100;
|
|
18095
|
+
let batchedScript = "";
|
|
18096
|
+
for (const [groupName, groupValue] of Object.entries(apiBindings)) {
|
|
18097
|
+
if (typeof groupValue === "object" && groupValue !== null) {
|
|
18098
|
+
batchedScript += `globalThis.sqlite[${JSON.stringify(groupName)}] = {};
|
|
18099
|
+
`;
|
|
18100
|
+
for (const [methodName, methodFn] of Object.entries(groupValue)) {
|
|
18101
|
+
if (typeof methodFn === "function") {
|
|
18102
|
+
const fnRef = new ivmLib.Reference(async (...args) => {
|
|
18103
|
+
if (++rpcCount > MAX_RPC_CALLS) {
|
|
18104
|
+
throw new RateLimitError(
|
|
18105
|
+
`QuotaExceededError: Maximum number of host tool calls (${MAX_RPC_CALLS}) exceeded (attempted call ${rpcCount}).`
|
|
18106
|
+
);
|
|
18107
|
+
}
|
|
18108
|
+
try {
|
|
18109
|
+
return await methodFn(...args);
|
|
18110
|
+
} catch (e) {
|
|
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
|
+
);
|
|
18118
|
+
}
|
|
18119
|
+
});
|
|
18120
|
+
refCleanup.push(fnRef);
|
|
18121
|
+
const refName = `fnRef_${groupName}_${methodName}`;
|
|
18122
|
+
context.global.setSync(refName, fnRef);
|
|
18123
|
+
batchedScript += `globalThis.sqlite[${JSON.stringify(groupName)}][${JSON.stringify(methodName)}] = async (...args) => {
|
|
18124
|
+
return await globalThis[${JSON.stringify(refName)}].apply(undefined, args, { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
18125
|
+
};
|
|
18126
|
+
`;
|
|
18127
|
+
}
|
|
18128
|
+
}
|
|
18129
|
+
} else if (typeof groupValue === "function") {
|
|
18130
|
+
const fnRef = new ivmLib.Reference(async (...args) => {
|
|
18131
|
+
if (++rpcCount > MAX_RPC_CALLS) {
|
|
18132
|
+
throw new RateLimitError(
|
|
18133
|
+
`QuotaExceededError: Maximum number of host tool calls (${MAX_RPC_CALLS}) exceeded (attempted call ${rpcCount}).`
|
|
18134
|
+
);
|
|
18135
|
+
}
|
|
18136
|
+
try {
|
|
18137
|
+
return await groupValue(...args);
|
|
18138
|
+
} catch (e) {
|
|
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
|
+
);
|
|
18146
|
+
}
|
|
18147
|
+
});
|
|
18148
|
+
refCleanup.push(fnRef);
|
|
18149
|
+
const refName = `fnRef_${groupName}`;
|
|
18150
|
+
context.global.setSync(refName, fnRef);
|
|
18151
|
+
batchedScript += `globalThis.sqlite[${JSON.stringify(groupName)}] = async (...args) => {
|
|
18152
|
+
return await globalThis[${JSON.stringify(refName)}].apply(undefined, args, { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
18153
|
+
};
|
|
18154
|
+
`;
|
|
18155
|
+
}
|
|
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
|
+
`);
|
|
17788
18179
|
const wrappedCode = `(async () => { ${transformAutoReturn(code)} })()`;
|
|
17789
|
-
|
|
18180
|
+
script = isolate.compileScriptSync(wrappedCode, {
|
|
17790
18181
|
filename: `code-mode.js`
|
|
17791
18182
|
});
|
|
17792
18183
|
result = await script.run(context, {
|
|
@@ -17799,11 +18190,27 @@ var CodeModeSandbox = class _CodeModeSandbox {
|
|
|
17799
18190
|
errorMsg = error instanceof Error ? error.message : String(error);
|
|
17800
18191
|
} finally {
|
|
17801
18192
|
for (const ref of refCleanup) {
|
|
17802
|
-
|
|
18193
|
+
try {
|
|
18194
|
+
ref.release();
|
|
18195
|
+
} catch {
|
|
18196
|
+
}
|
|
18197
|
+
}
|
|
18198
|
+
try {
|
|
18199
|
+
logRef?.release();
|
|
18200
|
+
} catch {
|
|
18201
|
+
}
|
|
18202
|
+
try {
|
|
18203
|
+
script?.release();
|
|
18204
|
+
} catch {
|
|
18205
|
+
}
|
|
18206
|
+
try {
|
|
18207
|
+
context?.release();
|
|
18208
|
+
} catch {
|
|
18209
|
+
}
|
|
18210
|
+
try {
|
|
18211
|
+
isolate.dispose();
|
|
18212
|
+
} catch {
|
|
17803
18213
|
}
|
|
17804
|
-
logRef.release();
|
|
17805
|
-
context.release();
|
|
17806
|
-
isolate.dispose();
|
|
17807
18214
|
}
|
|
17808
18215
|
const endTime = performance.now();
|
|
17809
18216
|
this.accumulatedLogs.push(...logs);
|
|
@@ -17847,7 +18254,10 @@ var SandboxPool = class _SandboxPool {
|
|
|
17847
18254
|
}
|
|
17848
18255
|
static getIvmLib() {
|
|
17849
18256
|
if (!_SandboxPool.cachedIvmLib) {
|
|
17850
|
-
throw new
|
|
18257
|
+
throw new InternalError(
|
|
18258
|
+
"ivmLib not initialized",
|
|
18259
|
+
"IVMLIB_NOT_INITIALIZED"
|
|
18260
|
+
);
|
|
17851
18261
|
}
|
|
17852
18262
|
return _SandboxPool.cachedIvmLib;
|
|
17853
18263
|
}
|
|
@@ -17866,8 +18276,9 @@ var SandboxPool = class _SandboxPool {
|
|
|
17866
18276
|
await _SandboxPool.initialize();
|
|
17867
18277
|
}
|
|
17868
18278
|
if (this.inUseCount >= this.options.maxInstances) {
|
|
17869
|
-
throw new
|
|
17870
|
-
`Sandbox pool exhausted (max ${this.options.maxInstances})
|
|
18279
|
+
throw new InternalError(
|
|
18280
|
+
`Sandbox pool exhausted (max ${this.options.maxInstances})`,
|
|
18281
|
+
"POOL_EXHAUSTED"
|
|
17871
18282
|
);
|
|
17872
18283
|
}
|
|
17873
18284
|
this.inUseCount++;
|
|
@@ -18004,15 +18415,14 @@ function createExecuteCodeTool(adapter) {
|
|
|
18004
18415
|
}
|
|
18005
18416
|
const clientId = _context.auth?.sub ?? _context.clientIp ?? processId;
|
|
18006
18417
|
if (!await security.checkRateLimit(clientId)) {
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
};
|
|
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
|
+
);
|
|
18016
18426
|
}
|
|
18017
18427
|
if (!pool) {
|
|
18018
18428
|
initializePool();
|
|
@@ -18027,7 +18437,7 @@ function createExecuteCodeTool(adapter) {
|
|
|
18027
18437
|
throw new DbMcpError(
|
|
18028
18438
|
"Sandbox pool not initialized",
|
|
18029
18439
|
"CODEMODE_POOL_UNINITIALIZED",
|
|
18030
|
-
|
|
18440
|
+
ErrorCategory.INTERNAL
|
|
18031
18441
|
);
|
|
18032
18442
|
}
|
|
18033
18443
|
const result = await pool.execute(code, bindings, timeoutMs);
|
|
@@ -18047,6 +18457,13 @@ function createExecuteCodeTool(adapter) {
|
|
|
18047
18457
|
} catch {
|
|
18048
18458
|
}
|
|
18049
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
|
+
}
|
|
18050
18467
|
return {
|
|
18051
18468
|
success: result.success,
|
|
18052
18469
|
result: sanitizedResult,
|
|
@@ -18064,6 +18481,14 @@ function createExecuteCodeTool(adapter) {
|
|
|
18064
18481
|
operation: "executeCode",
|
|
18065
18482
|
error: error instanceof Error ? error : void 0
|
|
18066
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
|
+
}
|
|
18067
18492
|
return formatHandlerError(error);
|
|
18068
18493
|
}
|
|
18069
18494
|
}
|
|
@@ -18113,7 +18538,8 @@ function initializePool() {
|
|
|
18113
18538
|
operation: "initialize"
|
|
18114
18539
|
});
|
|
18115
18540
|
setDefaultSandboxMode(mode);
|
|
18116
|
-
|
|
18541
|
+
const strictIsolation = process.env["CODE_MODE_STRICT_ISOLATION"] !== "false";
|
|
18542
|
+
pool = createSandboxPool(mode, void 0, { timeoutMs: 3e4, strictIsolation });
|
|
18117
18543
|
pool.initialize();
|
|
18118
18544
|
logger.info(`Code Mode initialized with ${mode} sandbox`, {
|
|
18119
18545
|
module: "CODEMODE",
|
|
@@ -18173,6 +18599,8 @@ function detectAndSetJsonbSupport(getVersion, log2) {
|
|
|
18173
18599
|
}
|
|
18174
18600
|
|
|
18175
18601
|
// src/adapters/sqlite/query-executor.ts
|
|
18602
|
+
var RETURNING_REGEX = /\bRETURNING\b/i;
|
|
18603
|
+
var INSERT_REGEX = /^\s*INSERT\s/i;
|
|
18176
18604
|
var log = createModuleLogger("SQLITE");
|
|
18177
18605
|
function translateSqliteError(error, sql, operation, overrideLog) {
|
|
18178
18606
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -18180,8 +18608,7 @@ function translateSqliteError(error, sql, operation, overrideLog) {
|
|
|
18180
18608
|
const improvedMessage = `${operation} failed: ${message}`;
|
|
18181
18609
|
const details = { sql };
|
|
18182
18610
|
if (match?.code === "TABLE_NOT_FOUND") {
|
|
18183
|
-
const
|
|
18184
|
-
const tableName = tableMatch ? tableMatch[2] : "unknown";
|
|
18611
|
+
const tableName = match.match?.[2] ?? "unknown";
|
|
18185
18612
|
throw new ResourceNotFoundError(
|
|
18186
18613
|
`Table '${tableName}' not found`,
|
|
18187
18614
|
"TABLE_NOT_FOUND",
|
|
@@ -18195,8 +18622,7 @@ function translateSqliteError(error, sql, operation, overrideLog) {
|
|
|
18195
18622
|
);
|
|
18196
18623
|
}
|
|
18197
18624
|
if (match?.code === "COLUMN_NOT_FOUND") {
|
|
18198
|
-
const
|
|
18199
|
-
const colName = colMatch ? colMatch[2] : "unknown";
|
|
18625
|
+
const colName = match.match?.[2] ?? "unknown";
|
|
18200
18626
|
throw new ResourceNotFoundError(
|
|
18201
18627
|
`Column '${colName}' not found`,
|
|
18202
18628
|
"COLUMN_NOT_FOUND",
|
|
@@ -18276,7 +18702,7 @@ function executeWrite(db, sql, params) {
|
|
|
18276
18702
|
try {
|
|
18277
18703
|
const normalizedParams = normalizeSqliteParams(params);
|
|
18278
18704
|
let rows = [];
|
|
18279
|
-
if (
|
|
18705
|
+
if (RETURNING_REGEX.test(sql)) {
|
|
18280
18706
|
const results = normalizedParams ? db.exec(sql, normalizedParams) : db.exec(sql);
|
|
18281
18707
|
if (results.length > 0 && results[0]) {
|
|
18282
18708
|
rows = rowsFromSqlJsResult(results[0]);
|
|
@@ -18291,7 +18717,7 @@ function executeWrite(db, sql, params) {
|
|
|
18291
18717
|
const changes = db.getRowsModified();
|
|
18292
18718
|
let lastInsertId;
|
|
18293
18719
|
try {
|
|
18294
|
-
if (
|
|
18720
|
+
if (INSERT_REGEX.test(sql) && !RETURNING_REGEX.test(sql)) {
|
|
18295
18721
|
const rowidResult = db.exec("SELECT last_insert_rowid()");
|
|
18296
18722
|
if (rowidResult[0]?.values[0]) {
|
|
18297
18723
|
lastInsertId = Number(rowidResult[0].values[0][0]);
|
|
@@ -18340,4 +18766,4 @@ function executeGeneral(db, sql, params) {
|
|
|
18340
18766
|
}
|
|
18341
18767
|
}
|
|
18342
18768
|
|
|
18343
|
-
export { FtsHeadlineSchema, RowRecordSchema, SchemaManager, WhereConditionSchema, applyCommonPragmas, autoEnableWal,
|
|
18769
|
+
export { FtsHeadlineSchema, RowRecordSchema, SchemaManager, WhereConditionSchema, applyCommonPragmas, autoEnableWal, detectAndSetJsonbSupport, executeGeneral, executeRead, executeWrite, getAdminTools, getCodeModeTools, getCoreTools, getGeoTools, getIntrospectionTools, getJsonHelperTools, getJsonOperationTools, getMigrationTools, getPromptDefinitions, getResourceDefinitions, getStatsTools, getTextTools, getVectorTools, getVirtualTools, isDDL, normalizeSqliteParams, resolveAliases, sanitizeFtsQuery, translateSqliteError, validateTableExists };
|