db-mcp 1.1.1 → 2.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.
Files changed (84) hide show
  1. package/.gitleaks.toml +9 -0
  2. package/.trivyignore +8 -0
  3. package/README.md +193 -120
  4. package/dist/{chunk-TVIZ3XJH.js → chunk-5Y42NPBP.js} +4168 -2411
  5. package/dist/{chunk-AOUL5SHS.js → chunk-645ZEFLA.js} +70 -20
  6. package/dist/chunk-OKOVZ5QE.js +28 -0
  7. package/dist/chunk-SFJQCNG7.js +131 -0
  8. package/dist/{chunk-FW7UCRLN.js → chunk-VIDSICEL.js} +3 -0
  9. package/dist/chunk-WBER5YY4.js +2053 -0
  10. package/dist/{chunk-4IA3DB5C.js → chunk-X3MUUOWM.js} +19 -2
  11. package/dist/{chunk-Z2GFQU3G.js → chunk-Z7C2TM4L.js} +114 -21
  12. package/dist/cli.js +79 -5
  13. package/dist/{http-VSB7DBJR.js → http-6KF4ULDI.js} +199 -77
  14. package/dist/index.d.ts +231 -8
  15. package/dist/index.js +6 -5
  16. package/dist/{sqlite-26V3Y4MK.js → sqlite-U5KSYQXK.js} +62 -138
  17. package/dist/{sqlite-native-5O7FZJGB.js → sqlite-native-JXMCFQBA.js} +575 -113
  18. package/dist/worker-script.js +34 -10
  19. package/logs/.gitkeep +1 -0
  20. package/mcp-config-example.json +83 -0
  21. package/package.json +9 -8
  22. package/playwright.config.ts +1 -1
  23. package/scripts/update-badges.ts +99 -0
  24. package/server.json +7 -5
  25. package/test-server/README.md +20 -23
  26. package/test-server/code-map.md +45 -33
  27. package/test-server/reset-database.ps1 +59 -17
  28. package/test-server/scripts/README.md +27 -0
  29. package/test-server/{test-help-resources.mjs → scripts/test-help-resources.mjs} +12 -5
  30. package/test-server/scripts/test-prompts.mjs +251 -0
  31. package/test-server/{test-tool-annotations.mjs → scripts/test-tool-annotations.mjs} +9 -4
  32. package/test-server/test-advanced/README.md +70 -0
  33. package/test-server/test-advanced/test-codemode-advanced-admin.md +174 -0
  34. package/test-server/test-advanced/test-codemode-advanced-core.md +193 -0
  35. package/test-server/test-advanced/test-codemode-advanced-geo.md +157 -0
  36. package/test-server/test-advanced/test-codemode-advanced-introspection.md +171 -0
  37. package/test-server/test-advanced/test-codemode-advanced-json.md +169 -0
  38. package/test-server/test-advanced/test-codemode-advanced-migration.md +160 -0
  39. package/test-server/test-advanced/test-codemode-advanced-stats.md +185 -0
  40. package/test-server/test-advanced/test-codemode-advanced-text.md +183 -0
  41. package/test-server/test-advanced/test-codemode-advanced-transactions.md +136 -0
  42. package/test-server/test-advanced/test-codemode-advanced-vector.md +141 -0
  43. package/test-server/test-codemode/README.md +121 -0
  44. package/test-server/test-codemode/test-codemode-admin.md +223 -0
  45. package/test-server/test-codemode/test-codemode-core.md +286 -0
  46. package/test-server/test-codemode/test-codemode-geo.md +177 -0
  47. package/test-server/test-codemode/test-codemode-introspection.md +190 -0
  48. package/test-server/test-codemode/test-codemode-json.md +237 -0
  49. package/test-server/test-codemode/test-codemode-migration.md +278 -0
  50. package/test-server/test-codemode/test-codemode-sandbox.md +413 -0
  51. package/test-server/test-codemode/test-codemode-stats.md +232 -0
  52. package/test-server/test-codemode/test-codemode-text.md +237 -0
  53. package/test-server/test-codemode/test-codemode-transactions.md +236 -0
  54. package/test-server/test-codemode/test-codemode-vector.md +244 -0
  55. package/test-server/test-codemode/test-codemode-wasm-degradation.md +394 -0
  56. package/test-server/test-database.sql +37 -1
  57. package/test-server/test-resources.md +43 -16
  58. package/test-server/test-tool-groups/README.md +100 -0
  59. package/test-server/test-tool-groups/test-admin-core.md +165 -0
  60. package/test-server/test-tool-groups/test-admin-extensions.md +133 -0
  61. package/test-server/{test-tools.md → test-tool-groups/test-core-data.md} +103 -17
  62. package/test-server/test-tool-groups/test-core-schema.md +240 -0
  63. package/test-server/test-tool-groups/test-geo-haversine.md +130 -0
  64. package/test-server/test-tool-groups/test-geo-spatialite.md +110 -0
  65. package/test-server/test-tool-groups/test-introspection-diagnostics.md +123 -0
  66. package/test-server/test-tool-groups/test-introspection-schema.md +133 -0
  67. package/test-server/test-tool-groups/test-json-read.md +219 -0
  68. package/test-server/test-tool-groups/test-json-write.md +157 -0
  69. package/test-server/test-tool-groups/test-migration.md +193 -0
  70. package/test-server/test-tool-groups/test-stats-advanced.md +118 -0
  71. package/test-server/test-tool-groups/test-stats-basic.md +156 -0
  72. package/test-server/test-tool-groups/test-text-advanced.md +169 -0
  73. package/test-server/test-tool-groups/test-text-basic.md +177 -0
  74. package/test-server/test-tool-groups/test-transactions.md +179 -0
  75. package/test-server/test-tool-groups/test-vector-read.md +130 -0
  76. package/test-server/test-tool-groups/test-vector-write.md +115 -0
  77. package/test-server/tool-reference.md +79 -58
  78. package/tsconfig.build.json +6 -0
  79. package/dist/chunk-RHVEZ42P.js +0 -873
  80. package/test-server/test-agent-experience.md +0 -243
  81. package/test-server/test-group-tools.md +0 -861
  82. package/test-server/test-tools-advanced-1.md +0 -517
  83. package/test-server/test-tools-advanced-2.md +0 -487
  84. package/test-server/test-tools-codemode.md +0 -629
@@ -0,0 +1,2053 @@
1
+ import { TOOL_GROUP_SCOPES, getAuthContext, SCOPES } from './chunk-SFJQCNG7.js';
2
+ import { VERSION, NAME, READ_ONLY, SERVER_ICONS } from './chunk-VIDSICEL.js';
3
+ import { TOOL_GROUPS, parseToolFilter, getToolFilterFromEnv, logger, getFilterSummary, DbMcpError } from './chunk-645ZEFLA.js';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { open, stat, mkdir, rename, readdir, unlink, mkdtemp, writeFile, rmdir, readFile, appendFile } from 'fs/promises';
7
+ import { dirname, join, basename, sep } from 'path';
8
+ import { gzip, gunzipSync } from 'zlib';
9
+ import { promisify } from 'util';
10
+ import { performance } from 'perf_hooks';
11
+ import { z } from 'zod';
12
+
13
+ // src/constants/server-instructions.ts
14
+ var INSTRUCTIONS = `# db-mcp (SQLite MCP Server)
15
+
16
+ ## Quick Access
17
+
18
+ | Purpose | Action |
19
+ | --------------- | -------------------------- |
20
+ | Health check | \`server_health\` tool |
21
+ | Server info | \`server_info\` tool |
22
+ | Database schema | \`sqlite://schema\` resource |
23
+ | Tool help | \`sqlite://help\` resource |
24
+
25
+ ## Built-in Tools
26
+
27
+ \`server_info\`, \`server_health\`, \`list_adapters\` \u2014 always available.
28
+
29
+ ## Help Resources
30
+
31
+ Read \`sqlite://help\` for gotchas and critical usage patterns.
32
+ Read \`sqlite://help/{group}\` for group-specific tool reference (json, text, stats, vector, geo, admin, transactions, introspection, migration).
33
+ Only help resources for your enabled tool groups are registered.`;
34
+ var HELP_CONTENT = /* @__PURE__ */ new Map([
35
+ [
36
+ "admin",
37
+ `# db-mcp Help \u2014 Database Administration (29 Native / 29 WASM)
38
+
39
+ ## Maintenance
40
+
41
+ \`\`\`javascript
42
+ sqlite_integrity_check({ maxErrors: 10 }); // check for corruption
43
+ sqlite_optimize({ analyze: true, reindex: true }); // optimize performance
44
+ sqlite_vacuum(); // reclaim space
45
+ sqlite_analyze({ table: "orders" }); // update statistics for query planner
46
+ sqlite_dbstat({ summarize: true }); // storage stats (\u26A0\uFE0F summarize native-only; WASM returns counts only)
47
+ \`\`\`
48
+
49
+ ## Backup/Restore (Native only)
50
+
51
+ \`\`\`javascript
52
+ sqlite_backup({ targetPath: "/path/to/backup.db" });
53
+ sqlite_verify_backup({ backupPath: "/path/to/backup.db" }); // check integrity without restoring
54
+ sqlite_restore({ sourcePath: "/path/to/backup.db" }); // \u26A0\uFE0F WARNING: Replaces current database
55
+ \`\`\`
56
+
57
+ ## Audit Backups (Requires --audit-backup)
58
+
59
+ \`\`\`javascript
60
+ sqlite_audit_list_backups(); // list pre-mutation DDL snapshots
61
+ sqlite_audit_get_backup({ filename: "snapshot_123.json" }); // retrieve specific snapshot
62
+ sqlite_audit_cleanup(); // apply retention policy and delete old snapshots
63
+ \`\`\`
64
+
65
+ ## PRAGMA
66
+
67
+ \`\`\`javascript
68
+ sqlite_pragma_settings({ pragma: "journal_mode" }); // get value
69
+ sqlite_pragma_settings({ pragma: "cache_size", value: 10000 }); // set value
70
+ sqlite_pragma_table_info({ table: "users" }); // column details
71
+ sqlite_pragma_compile_options({ filter: "FTS" }); // \u26A0\uFE0F WASM may show FTS3, not FTS5
72
+ sqlite_pragma_database_list(); // list attached databases
73
+ sqlite_pragma_optimize(); // run PRAGMA optimize
74
+ \`\`\`
75
+
76
+ ## Index & Stats
77
+
78
+ - \`sqlite_index_stats({ table: "orders" })\` \u2014 stats for explicit indexes
79
+
80
+ ## Views
81
+
82
+ \`\`\`javascript
83
+ sqlite_create_view({
84
+ viewName: "active_orders",
85
+ selectQuery: "SELECT * FROM orders WHERE status = 'active'",
86
+ });
87
+ sqlite_create_view({ viewName: "v", selectQuery: "...", replace: true }); // CREATE OR REPLACE
88
+ sqlite_list_views(); // list all views
89
+ sqlite_drop_view({ viewName: "active_orders" });
90
+ \`\`\`
91
+
92
+ ## Virtual Tables
93
+
94
+ \`\`\`javascript
95
+ sqlite_list_virtual_tables(); // list FTS5, R-Tree, CSV tables
96
+ sqlite_virtual_table_info({ tableName: "articles_fts" }); // module and column info
97
+ sqlite_drop_virtual_table({ tableName: "old_fts", ifExists: true });
98
+ \`\`\`
99
+
100
+ ## Generate Series (pure JS)
101
+
102
+ \`\`\`javascript
103
+ sqlite_generate_series({ start: 1, stop: 100, step: 5 }); // returns array of values
104
+ // \u26A0\uFE0F Creates a REGULAR table (not virtual) \u2014 use sqlite_drop_table to remove
105
+ sqlite_create_series_table({ tableName: "numbers", start: 1, stop: 1000 });
106
+ \`\`\`
107
+
108
+ ## R-Tree (Native only)
109
+
110
+ \`\`\`javascript
111
+ sqlite_create_rtree_table({ tableName: "locations_idx", dimensions: 2 }); // 2D: minX, maxX, minY, maxY
112
+ // Returns graceful error with wasmLimitation: true in WASM
113
+ \`\`\`
114
+
115
+ ## CSV Virtual Tables (Native only)
116
+
117
+ \u26A0\uFE0F Requires ABSOLUTE file paths
118
+
119
+ \`\`\`javascript
120
+ sqlite_analyze_csv_schema({ filePath: "/absolute/path/to/data.csv" }); // analyze CSV structure
121
+ sqlite_create_csv_table({
122
+ tableName: "csv_data",
123
+ filePath: "/absolute/path/to/data.csv",
124
+ });
125
+ \`\`\`
126
+
127
+ ## Business Insights
128
+
129
+ \`\`\`javascript
130
+ sqlite_append_insight({ insight: "Q4 revenue increased 23% YoY" }); // add to memo://insights
131
+ \`\`\``
132
+ ],
133
+ [
134
+ "core",
135
+ `# db-mcp Help \u2014 Core Operations (14 tools)
136
+
137
+ ## Basic Queries
138
+
139
+ - \`sqlite_read_query({ query: "SELECT * FROM users LIMIT 10" })\` \u2014 execute SELECT, PRAGMA, EXPLAIN, or WITH statements
140
+ - \`sqlite_write_query({ query: "INSERT INTO users (name) VALUES ('Alice')" })\` \u2014 execute INSERT, UPDATE, DELETE, or DDL statements
141
+
142
+ ## Tables & Schema
143
+
144
+ - \`sqlite_list_tables({ excludeSystemTables?: boolean })\` \u2014 list all tables in the database (system tables excluded by default)
145
+ - \`sqlite_describe_table({ table: "users" })\` \u2014 get detailed schema, columns, and foreign keys for a specific table
146
+ - \`sqlite_create_table({ table: "users", columns: [{ name: "id", type: "INTEGER PRIMARY KEY" }, { name: "email", type: "TEXT UNIQUE" }] })\` \u2014 create a new table
147
+ - \`sqlite_drop_table({ table: "users", ifExists?: true })\` \u2014 drop an existing table
148
+
149
+ ## Indexes
150
+
151
+ - \`sqlite_get_indexes({ table?: "users", excludeSystemIndexes?: true })\` \u2014 list indexes (optionally filtered by table)
152
+ - \`sqlite_create_index({ indexName: "idx_users_email", table: "users", columns: ["email"], unique?: true })\` \u2014 create a new index
153
+ - \`sqlite_drop_index({ indexName: "idx_users_email", ifExists?: true })\` \u2014 drop an existing index
154
+
155
+ ## Convenience Tools (High-Level Data Operations)
156
+
157
+ - \`sqlite_upsert({ table: "users", data: [{ id: 1, name: "Alice" }], conflictColumns: ["id"], updateColumns: ["name"], returning: true })\` \u2014 insert or update rows using \`ON CONFLICT\` (or \`REPLACE\` fallback). Supports \`returning: true\` or array of columns.
158
+ - \`sqlite_batch_insert({ table: "users", data: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }], returning: true })\` \u2014 insert multiple rows in a single batch. Supports \`returning: true\` or array of columns.
159
+ - \`sqlite_count({ table: "users", whereClause?: "status = 'active'" })\` \u2014 count rows in a table (faster than a full query)
160
+ - \`sqlite_exists({ table: "users", whereClause: "email = 'test@example.com'" })\` \u2014 check if a row exists (stops at first match)
161
+ - \`sqlite_truncate({ table: "users" })\` \u2014 quickly delete all rows from a table (executes \`DELETE FROM table\`)`
162
+ ],
163
+ [
164
+ "geo",
165
+ `# db-mcp Help \u2014 Geospatial Operations (4 basic + 7 SpatiaLite)
166
+
167
+ ## Basic Geo (always available \u2014 Haversine formula)
168
+
169
+ \`\`\`javascript
170
+ sqlite_geo_distance({
171
+ lat1: 40.7128,
172
+ lon1: -74.006,
173
+ lat2: 34.0522,
174
+ lon2: -118.2437,
175
+ }); // returns km
176
+ sqlite_geo_bounding_box({
177
+ table: "stores",
178
+ latColumn: "lat",
179
+ lonColumn: "lon",
180
+ minLat: 40,
181
+ maxLat: 41,
182
+ minLon: -75,
183
+ maxLon: -73,
184
+ });
185
+ sqlite_geo_nearby({
186
+ table: "stores",
187
+ latColumn: "lat",
188
+ lonColumn: "lon",
189
+ centerLat: 40.7,
190
+ centerLon: -74,
191
+ radius: 10,
192
+ unit: "km",
193
+ });
194
+ sqlite_geo_cluster({
195
+ table: "customers",
196
+ latColumn: "lat",
197
+ lonColumn: "lon",
198
+ gridSize: 0.1,
199
+ });
200
+ \`\`\`
201
+
202
+ ## SpatiaLite (7 tools, Native only)
203
+
204
+ \`\`\`javascript
205
+ // Load extension first
206
+ sqlite_spatialite_load(); // \u26A0\uFE0F Required before using other spatial tools
207
+
208
+ // Create spatial table with geometry column
209
+ sqlite_spatialite_create_table({
210
+ tableName: "places",
211
+ geometryColumn: "geom",
212
+ geometryType: "POINT",
213
+ srid: 4326,
214
+ });
215
+
216
+ // Import data (WKT or GeoJSON)
217
+ sqlite_spatialite_import({
218
+ tableName: "places",
219
+ format: "wkt",
220
+ data: "POINT(-73.99 40.75)",
221
+ additionalData: { name: "NYC" },
222
+ });
223
+ sqlite_spatialite_import({
224
+ tableName: "places",
225
+ format: "geojson",
226
+ data: '{"type":"Point","coordinates":[-73.99,40.75]}',
227
+ });
228
+
229
+ // Spatial queries (SELECT only)
230
+ sqlite_spatialite_query({
231
+ query: "SELECT name, AsText(geom) FROM places WHERE ST_Within(geom, ...)",
232
+ });
233
+
234
+ // Spatial analysis
235
+ // analysisType: "spatial_extent" | "point_in_polygon" | "nearest_neighbor" | "distance_matrix"
236
+ // \u26A0\uFE0F nearest_neighbor/distance_matrix return CARTESIAN distance (degrees), not geodetic (km/miles)
237
+ // For same source/target table, use excludeSelf: true to avoid self-matches
238
+ sqlite_spatialite_analyze({
239
+ analysisType: "spatial_extent",
240
+ sourceTable: "places",
241
+ geometryColumn: "geom",
242
+ });
243
+ sqlite_spatialite_analyze({
244
+ analysisType: "nearest_neighbor",
245
+ sourceTable: "pts",
246
+ targetTable: "pts",
247
+ excludeSelf: true,
248
+ });
249
+
250
+ // Geometry transforms
251
+ // buffer: 'distance' = radius; simplify: 'distance' = tolerance (0.0001 for lat/lon)
252
+ // \u26A0\uFE0F Buffer auto-simplifies output by default (tolerance=0.0001). Use simplifyTolerance: 0 to disable
253
+ sqlite_spatialite_transform({
254
+ operation: "buffer",
255
+ geometry1: "POINT(-73.99 40.75)",
256
+ distance: 0.01,
257
+ });
258
+ sqlite_spatialite_transform({
259
+ operation: "simplify",
260
+ geometry1: "...",
261
+ distance: 0.001,
262
+ });
263
+
264
+ // Spatial index (R-Tree)
265
+ sqlite_spatialite_index({
266
+ tableName: "places",
267
+ geometryColumn: "geom",
268
+ action: "create",
269
+ }); // create, drop, or check
270
+ \`\`\``
271
+ ],
272
+ [
273
+ "gotchas",
274
+ `# db-mcp Help \u2014 Gotchas & Code Mode
275
+
276
+ ## \u26A0\uFE0F Critical Gotchas
277
+
278
+ 1. **sqlite_write_query**: \u26D4 Only INSERT/UPDATE/DELETE \u2014 use \`sqlite_read_query\` for SELECT
279
+ 2. **Regex patterns**: Double-escape backslashes (\`\\\\\\\\\`) when passing through JSON/MCP
280
+ 3. **FTS5 virtual tables**: \`*_fts\` and shadow tables \`*_fts_*\` are hidden from \`sqlite_list_tables\` for cleaner output
281
+ 4. **FTS5 boolean logic**: Uses AND by default \u2014 \`"machine learning"\` = rows with BOTH words. Use OR explicitly: \`"machine OR learning"\`
282
+ 5. **json_each row multiplication**: Expands arrays to rows \u2014 use \`limit\` param for large arrays
283
+ 6. **json_group_object without groupByColumn**: Each row creates a key-value pair; duplicate keys result if key values aren't unique
284
+ 7. **allowExpressions**: For column extraction ONLY (e.g., \`json_extract\`), NOT aggregate functions \u2014 use \`aggregateFunction\` param instead
285
+ 8. **sqlite_json_normalize_column**: Defaults to \`preserve\` (maintains original format); use \`outputFormat: 'text'\` to force text
286
+ 9. **Fuzzy matching tokenization**: Matches WORD TOKENS by default \u2014 \`"laptop"\` matches \`"Laptop Pro 15"\` (distance 0 on first token). Use \`tokenize: false\` for full-string matching
287
+ 10. **SpatiaLite distances**: \`nearest_neighbor\`/\`distance_matrix\` return CARTESIAN distance (degrees), not geodetic (km/miles)
288
+ 11. **SpatiaLite buffer**: Auto-simplifies output by default (tolerance=0.0001). Use \`simplifyTolerance: 0\` to disable
289
+ 12. **sqlite_stats_top_n**: Auto-excludes long-content columns (description, body, notes, etc.) when \`selectColumns\` is omitted. Use \`selectColumns\` to override
290
+ 13. **CSV virtual tables**: Require ABSOLUTE file paths
291
+ 14. **sqlite_create_series_table**: Creates a REGULAR table (not virtual) \u2014 use \`sqlite_drop_table\` to remove
292
+ 15. **sqlite_dbstat**: \`summarize\` only works in native; WASM returns counts only
293
+ 16. **PRAGMA compile options**: WASM may show FTS3, not FTS5
294
+ 17. **Vector tool schemas**: Vector tools use distinct schemas for specific operations. E.g., \`sqlite.vector.dimensions\` requires \`vectorColumn\`. Additionally, \`sqlite.vector.get\` wraps metadata inside a \`metadata\` object (e.g., \`metadata.content\`), and \`sqlite.vector.stats\` returns \`sampleSize\` and \`magnitudeStats\` (not \`count\` and \`stats\`).
295
+ 18. **FTS5 trigger cleanup**: Dropping an FTS5 table with \`sqlite_drop_table\` automatically finds and removes the associated \`_ai\`, \`_ad\`, and \`_au\` sync triggers from the source table.
296
+
297
+ ## WASM vs Native
298
+
299
+ | Feature | Native | WASM | Fallback |
300
+ | ------------------------------------------------- | --------------------- | ----------- | ---------------- |
301
+ | FTS5 full-text search | \u2705 | \u274C | None |
302
+ | Transactions (8 tools) | \u2705 | \u274C | None |
303
+ | Window functions (6 tools in stats group) | \u2705 | \u274C | None |
304
+ | SpatiaLite GIS (7 tools; 4 basic geo always work) | \u2705 | \u274C | None |
305
+ | Backup/Restore (3 tools) | \u2705 | \u274C | Graceful error |
306
+ | R-Tree spatial indexing | \u2705 | \u274C | Graceful error |
307
+ | CSV virtual tables | \u2705 | \u274C | Graceful error |
308
+ | generate_series | JS fallback | JS fallback | \u2014 |
309
+ | dbstat | \u2705 native (per-table) | \u274C | JS (counts only) |
310
+ | soundex() | \u2705 native | \u274C | JS |
311
+
312
+ ## Code Mode (1 tool)
313
+
314
+ | Tool | Description |
315
+ | --------------------- | ----------------------------------------------------------------- |
316
+ | \`sqlite_execute_code\` | Execute JavaScript in a sandboxed environment with \`sqlite.*\` API |
317
+
318
+ **Usage**: \`sqlite_execute_code({ code: "const tables = await sqlite.core.listTables(); return tables;" })\`
319
+ **Discover**: \`sqlite.help()\` for all groups, \`sqlite.<group>.help()\` for methods.
320
+ **Groups**: \`sqlite.core\`, \`sqlite.json\`, \`sqlite.text\`, \`sqlite.stats\`, \`sqlite.vector\`, \`sqlite.admin\`, \`sqlite.transactions\` (Native-only), \`sqlite.geo\`, \`sqlite.introspection\`, \`sqlite.migration\`
321
+
322
+ > **Note**: Code Mode dynamically filters capabilities. In WASM environments, \`sqlite.help()\` will omit unsupported groups (e.g., \`transactions\`) and tools (e.g., FTS5) to accurately reflect the active runtime.
323
+
324
+ ## Code Mode API Mapping
325
+
326
+ \`sqlite_group_action\` \u2192 \`sqlite.group.action()\` (group prefixes dropped: \`sqlite_json_insert\` \u2192 \`sqlite.json.insert()\`)
327
+
328
+ **Positional args work**: \`sqlite.core.readQuery("SELECT...")\`, \`sqlite.json.insert("docs", "data", {...})\`
329
+
330
+ **Discovery**: \`sqlite.help()\` returns all groups and methods. \`sqlite.core.help()\`, \`sqlite.json.help()\` for group-specific methods.`
331
+ ],
332
+ [
333
+ "introspection",
334
+ `# db-mcp Help \u2014 Schema Introspection (9 tools)
335
+
336
+ All introspection tools are **read-only** \u2014 they query PRAGMAs and sqlite_master, never modify data.
337
+
338
+ ## Graph Analysis (3 tools)
339
+
340
+ \`\`\`javascript
341
+ // Build FK dependency graph \u2014 nodesOnly: true for lightweight response (nodes without edges)
342
+ sqlite_dependency_graph({ includeRowCounts: true, excludeSystemTables: true });
343
+ sqlite_dependency_graph({ nodesOnly: true }); // lighter payload
344
+
345
+ // Safe DDL execution order \u2014 "create" = parents first (default), "drop" = children first
346
+ sqlite_topological_sort({ direction: "create", excludeSystemTables: true });
347
+ sqlite_topological_sort({ direction: "drop" }); // safe DROP order
348
+
349
+ // Simulate DELETE/DROP/TRUNCATE impact \u2014 shows affected tables, cascade paths, severity scoring
350
+ sqlite_cascade_simulator({ table: "users", operation: "delete" });
351
+ sqlite_cascade_simulator({ table: "users", operation: "drop", compact: true }); // compact omits path arrays
352
+ \`\`\`
353
+
354
+ ## Schema Analysis (3 tools)
355
+
356
+ \`\`\`javascript
357
+ // Full schema in one call \u2014 compact: true omits column details, sections limits scope
358
+ sqlite_schema_snapshot({
359
+ sections: ["tables", "indexes"],
360
+ excludeSystemTables: true,
361
+ });
362
+ sqlite_schema_snapshot({ compact: true }); // lighter payload
363
+
364
+ // Find constraint health issues \u2014 missing PKs, nullable FKs, unindexed FKs
365
+ sqlite_constraint_analysis({ excludeSystemTables: true });
366
+ sqlite_constraint_analysis({
367
+ table: "orders",
368
+ checks: ["missing_pk", "unindexed_fk"],
369
+ }); // limit scope
370
+
371
+ // Analyze DDL for SQLite-specific risks \u2014 does NOT execute the statements
372
+ sqlite_migration_risks({
373
+ statements: [
374
+ "ALTER TABLE users ADD COLUMN email TEXT",
375
+ "DROP TABLE old_logs",
376
+ ],
377
+ });
378
+ \`\`\`
379
+
380
+ ## Diagnostics (3 tools)
381
+
382
+ \`\`\`javascript
383
+ // Fragmentation, per-table size breakdown, optimization recommendations
384
+ sqlite_storage_analysis({
385
+ includeTableDetails: true,
386
+ excludeSystemTables: true,
387
+ });
388
+ sqlite_storage_analysis({ limit: 10 }); // top 10 tables only
389
+
390
+ // Audit index effectiveness \u2014 find redundant, missing FK, unindexed large tables
391
+ sqlite_index_audit({ excludeSystemTables: true });
392
+ sqlite_index_audit({ table: "orders", minSeverity: "warning" }); // reduce payload
393
+
394
+ // EXPLAIN QUERY PLAN with scan-type classification and optimization suggestions (SELECT/WITH only)
395
+ sqlite_query_plan({ sql: "SELECT * FROM orders WHERE status = 'active'" });
396
+ \`\`\`
397
+
398
+ ## \u26A0\uFE0F Gotchas
399
+
400
+ - \`excludeSystemTables\` defaults to \`true\` \u2014 SpatiaLite system tables are hidden for cleaner output. Pass \`false\` to include them
401
+ - \`sqlite_migration_risks\` analyzes DDL text statically \u2014 it does NOT execute the statements`
402
+ ],
403
+ [
404
+ "json",
405
+ `# db-mcp Help \u2014 JSON Operations (24 tools)
406
+
407
+ ## Collection & CRUD
408
+
409
+ - \`sqlite_create_json_collection({ tableName, indexes: [{ path: "$.type" }, { path: "$.author" }] })\` \u2014 creates table with JSON indexes
410
+ - \`sqlite_json_insert({ table, column, data: { type: "article", title: "Hello", tags: ["news"] } })\` \u2014 insert JSON document
411
+ - \`sqlite_json_select({ table, column, extractPaths? })\` \u2014 select rows, optionally extract specific JSON paths
412
+ - \`sqlite_json_update({ table, column, path, value, whereClause })\` \u2014 update value at JSON path
413
+ - \`sqlite_json_query({ table, column, filterPaths: { "$.type": "article" }, selectPaths: ["$.title"] })\` \u2014 query with path-based filters and projections
414
+
415
+ ## Path Operations
416
+
417
+ - \`sqlite_json_extract({ table, column, path: "$.title" })\` \u2014 extract value at path. Returns null if path doesn't exist
418
+ - \`sqlite_json_set({ table, column, path: "$.views", value: 100, whereClause: "id = 1" })\` \u2014 set value at path (creates if missing)
419
+ - \`sqlite_json_merge({ table, column, mergeData: { featured: true }, whereClause: "id = 1" })\` \u2014 merge object into existing JSON
420
+ - \`sqlite_json_remove({ table, column, path, whereClause })\` \u2014 remove key at path
421
+ - \`sqlite_json_validate_path({ path: "$.store.books[0].title" })\` \u2014 validate JSON path syntax without executing a query
422
+ - \`sqlite_json_type({ table, column, path })\` \u2014 get JSON type (null, true, false, integer, real, text, array, object)
423
+
424
+ ## Array Operations
425
+
426
+ \u26A0\uFE0F \`json_each\` multiplies output rows \u2014 use \`limit\` param for large arrays
427
+
428
+ - \`sqlite_json_array_append({ table, column, path: "$.tags", value: "featured", whereClause: "id = 1" })\` \u2014 append to array
429
+ - \`sqlite_json_array_length({ table, column, path })\` \u2014 get array length
430
+ - \`sqlite_json_each({ table, column, path: "$.tags", limit: 50 })\` \u2014 expand array to rows
431
+
432
+ ## Aggregation & Analysis
433
+
434
+ \`\`\`javascript
435
+ // Regular tables: use column names directly
436
+ sqlite_json_group_array({
437
+ table: "events",
438
+ valueColumn: "user_id",
439
+ groupByColumn: "event_type",
440
+ });
441
+
442
+ // JSON collections: use allowExpressions with json_extract for both columns
443
+ // \u26A0\uFE0F allowExpressions is for column extraction ONLY, NOT aggregate functions
444
+ // \u26A0\uFE0F Without groupByColumn, each row creates a key-value pair; duplicate keys if values aren't unique
445
+ sqlite_json_group_array({
446
+ table: "docs",
447
+ valueColumn: "json_extract(data, '$.author')",
448
+ groupByColumn: "json_extract(data, '$.type')",
449
+ allowExpressions: true,
450
+ });
451
+
452
+ // For aggregate values (COUNT, SUM, AVG), use aggregateFunction parameter instead
453
+ sqlite_json_group_object({
454
+ table: "events",
455
+ keyColumn: "event_type",
456
+ aggregateFunction: "COUNT(*)",
457
+ });
458
+ sqlite_json_group_object({
459
+ table: "orders",
460
+ keyColumn: "status",
461
+ aggregateFunction: "SUM(total)",
462
+ });
463
+ \`\`\`
464
+
465
+ - \`sqlite_json_keys({ table, column, path? })\` \u2014 get distinct keys of JSON objects
466
+ - \`sqlite_json_pretty({ table, column, whereClause? })\` \u2014 format JSON with indentation
467
+ - \`sqlite_json_valid({ table, column })\` \u2014 check if values are valid JSON
468
+ - \`sqlite_json_analyze_schema({ table, column })\` \u2014 infer schema types
469
+
470
+ ## Security
471
+
472
+ - \`sqlite_json_security_scan({ table, column, sampleSize?, whereClause? })\` \u2014 scan JSON column for sensitive keys (password, token, ssn, etc.), SQL injection patterns, and XSS patterns. Returns \`riskLevel\` (low/medium/high) and issue details. Use larger \`sampleSize\` for thorough scans
473
+
474
+ ## JSONB Optimization (SQLite 3.45+)
475
+
476
+ - \`sqlite_json_storage_info({ table, column })\` \u2014 check text vs JSONB format
477
+ - \`sqlite_jsonb_convert({ table, column })\` \u2014 convert to JSONB for faster queries
478
+ - \`sqlite_json_normalize_column({ table, column, outputFormat? })\` \u2014 normalize JSON (sort keys, compact). \u26A0\uFE0F Defaults to \`"preserve"\` (maintains original format); use \`outputFormat: "text"\` to force text output`
479
+ ],
480
+ [
481
+ "migration",
482
+ `# db-mcp Help \u2014 Migration Tracking (6 tools)
483
+
484
+ \u26A0\uFE0F Must call \`sqlite_migration_init()\` before using any other migration tool \u2014 it creates the tracking table.
485
+
486
+ \`\`\`javascript
487
+ // Initialize tracking table (idempotent \u2014 safe to call multiple times)
488
+ sqlite_migration_init();
489
+
490
+ // Apply migration: executes SQL and records atomically. If SQL fails, no record is created
491
+ // SHA-256 dedup \u2014 submitting the same SQL twice is rejected as a duplicate
492
+ sqlite_migration_apply({
493
+ version: "2024-01-15-add-users",
494
+ description: "Create users table",
495
+ sql:
496
+ "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE)",
497
+ rollbackSql: "DROP TABLE users",
498
+ sourceSystem: "agent",
499
+ appliedBy: "claude",
500
+ });
501
+
502
+ // Record externally-applied migration (does NOT execute the SQL)
503
+ sqlite_migration_record({
504
+ version: "2024-01-10-initial",
505
+ description: "Initial schema (pre-existing)",
506
+ sql: "CREATE TABLE orders (...)",
507
+ sourceSystem: "manual",
508
+ });
509
+
510
+ // Rollback by ID or version \u2014 requires rollbackSql to have been recorded
511
+ sqlite_migration_rollback({ version: "2024-01-15-add-users" });
512
+ sqlite_migration_rollback({ id: 3, dryRun: true }); // preview SQL without executing
513
+
514
+ // Query history with filters and pagination
515
+ sqlite_migration_history({ status: "applied", limit: 10 });
516
+ sqlite_migration_history({ sourceSystem: "agent", compact: true }); // compact omits hash and source system
517
+
518
+ // Summary: latest version, counts by status, unique source systems
519
+ sqlite_migration_status();
520
+ \`\`\`
521
+
522
+ ## \u26A0\uFE0F Gotchas
523
+
524
+ - Rollback requires \`rollbackSql\` to have been provided when the migration was recorded/applied
525
+ - Migration group is **opt-in** \u2014 not included in any shortcut except \`dev-schema\` and \`full\``
526
+ ],
527
+ [
528
+ "stats",
529
+ `# db-mcp Help \u2014 Statistical Analysis (16 core + 6 window)
530
+
531
+ ## Core Statistics (always available)
532
+
533
+ \`\`\`javascript
534
+ sqlite_stats_basic({ table: "employees", column: "salary" }); // count, sum, avg, min, max
535
+ sqlite_stats_count({ table: "orders", column: "status", distinct: true }); // count rows, optionally distinct
536
+ sqlite_stats_group_by({ table: "orders", column: "total", groupBy: "status" }); // aggregate by group
537
+ sqlite_stats_percentile({
538
+ table: "sales",
539
+ column: "revenue",
540
+ percentiles: [25, 50, 75, 90],
541
+ });
542
+ sqlite_stats_histogram({ table: "products", column: "price", buckets: 10 });
543
+ sqlite_stats_correlation({ table: "data", column1: "x", column2: "y" }); // Pearson coefficient
544
+ sqlite_stats_distinct({ table: "orders", column: "status" }); // distinct values
545
+ sqlite_stats_summary({
546
+ table: "data",
547
+ columns: ["price", "quantity", "discount"],
548
+ }); // multi-column summary
549
+ sqlite_stats_frequency({ table: "orders", column: "status" }); // frequency distribution
550
+ sqlite_stats_regression({ table: "data", xColumn: "year", yColumn: "revenue" }); // linear
551
+ sqlite_stats_regression({
552
+ table: "data",
553
+ xColumn: "year",
554
+ yColumn: "revenue",
555
+ degree: 2,
556
+ }); // quadratic
557
+ sqlite_stats_outliers({ table: "sales", column: "amount", method: "iqr" }); // or "zscore". Use maxOutliers to limit payload
558
+ sqlite_stats_hypothesis({
559
+ table: "samples",
560
+ column: "value",
561
+ testType: "ttest_one",
562
+ expectedMean: 100,
563
+ });
564
+ \`\`\`
565
+
566
+ \u26A0\uFE0F \`sqlite_stats_top_n\` \u2014 always use \`selectColumns\` to avoid returning all columns (large payloads with text fields):
567
+
568
+ \`\`\`javascript
569
+ sqlite_stats_top_n({
570
+ table: "products",
571
+ column: "price",
572
+ n: 10,
573
+ selectColumns: ["id", "name", "price"],
574
+ });
575
+ \`\`\`
576
+
577
+ ## Anomaly Detection (3 tools)
578
+
579
+ \`\`\`javascript
580
+ // Data distribution anomaly detection \u2014 z-score analysis on numeric columns
581
+ sqlite_stats_detect_anomalies({ table: "sales", threshold: 2.0 }); // auto-detects numeric columns
582
+ sqlite_stats_detect_anomalies({
583
+ table: "metrics",
584
+ columns: ["latency_ms", "error_rate"],
585
+ threshold: 3.0,
586
+ limit: 20,
587
+ });
588
+
589
+ // Fragmentation/bloat risk scoring \u2014 PRAGMA + dbstat analysis
590
+ sqlite_stats_detect_bloat(); // riskiest tables (>0 score), default limit 50
591
+ sqlite_stats_detect_bloat({ includeZeroRisk: true, limit: 10 }); // include zero risk, top 10
592
+
593
+ // Schema health risk scoring \u2014 FK indexes, PKs, wide tables
594
+ sqlite_stats_detect_schema_risks(); // riskiest tables (>0 score)
595
+ sqlite_stats_detect_schema_risks({ excludeSystemTables: false }); // include SpatiaLite tables
596
+ \`\`\`
597
+
598
+ ## Window Functions (6 tools, Native only)
599
+
600
+ \`\`\`javascript
601
+ sqlite_window_row_number({
602
+ table: "employees",
603
+ orderBy: "hire_date",
604
+ partitionBy: "department",
605
+ });
606
+ sqlite_window_rank({
607
+ table: "sales",
608
+ orderBy: "revenue DESC",
609
+ partitionBy: "region",
610
+ rankType: "dense_rank",
611
+ });
612
+ sqlite_window_lag_lead({
613
+ table: "sales",
614
+ orderBy: "date",
615
+ column: "revenue",
616
+ partitionBy: "region",
617
+ }); // access previous/next row values
618
+ sqlite_window_running_total({
619
+ table: "transactions",
620
+ column: "amount",
621
+ orderBy: "date",
622
+ });
623
+ sqlite_window_moving_avg({
624
+ table: "stock_prices",
625
+ column: "close_price",
626
+ orderBy: "date",
627
+ windowSize: 7,
628
+ });
629
+ sqlite_window_ntile({
630
+ table: "employees",
631
+ orderBy: "salary DESC",
632
+ buckets: 4,
633
+ partitionBy: "department",
634
+ }); // quartiles
635
+ \`\`\`
636
+
637
+ \u26A0\uFE0F **Payload Sizes**: Window functions retrieve all standard columns by default (up to the \`limit\`). When querying wide tables, this can result in large payloads. It is highly recommended to pass \`selectColumns\` to reduce token usage:
638
+
639
+ \`\`\`javascript
640
+ sqlite_window_moving_avg({
641
+ table: "stock_prices",
642
+ column: "close_price",
643
+ orderBy: "date",
644
+ windowSize: 7,
645
+ selectColumns: ["date"]
646
+ });
647
+ \`\`\``
648
+ ],
649
+ [
650
+ "text",
651
+ `# db-mcp Help \u2014 Text Processing & FTS5
652
+
653
+ ## Full-Text Search / FTS5 (5 tools, Native only)
654
+
655
+ \`\`\`javascript
656
+ // Create FTS5 table with triggers for auto-sync on future changes
657
+ // (Dropping this FTS table via sqlite_drop_table will automatically clean up these sync triggers)
658
+ sqlite_fts_create({
659
+ tableName: "articles_fts",
660
+ sourceTable: "articles",
661
+ columns: ["title", "content"],
662
+ });
663
+ sqlite_fts_rebuild({ table: "articles_fts" }); // \u26A0\uFE0F Required: populate index with existing data
664
+
665
+ // FTS5 uses AND by default: "machine learning" = rows containing BOTH words
666
+ // Use OR explicitly: "machine OR learning" for rows containing EITHER word
667
+ sqlite_fts_search({
668
+ table: "articles_fts",
669
+ query: "machine learning",
670
+ limit: 10,
671
+ });
672
+ sqlite_fts_match_info({ table: "articles_fts", query: "machine learning" }); // bm25 ranking info
673
+
674
+ // Highlighted snippets from FTS5 search matches (headline/snippet)
675
+ sqlite_fts_headline({ table: "articles_fts", query: "machine learning" });
676
+ // \u2192 results with <b>machine</b> <b>learning</b> highlighted in context
677
+
678
+ // Custom markers and snippet window
679
+ sqlite_fts_headline({
680
+ table: "articles_fts",
681
+ query: "database",
682
+ startSel: "**",
683
+ stopSel: "**",
684
+ snippetWords: 5,
685
+ });
686
+ \`\`\`
687
+
688
+ \u26A0\uFE0F FTS5 virtual tables (\`*_fts\`) and shadow tables (\`*_fts_*\`) are hidden from \`sqlite_list_tables\` for cleaner output
689
+
690
+ ## Text Processing (14 tools)
691
+
692
+ \`\`\`javascript
693
+ // Regex patterns: \u26A0\uFE0F Double-escape backslashes (\\\\) when passing through JSON/MCP
694
+ sqlite_regex_match({
695
+ table: "logs",
696
+ column: "message",
697
+ pattern: "ERROR:\\\\\\\\s+(\\\\\\\\w+)",
698
+ });
699
+ sqlite_regex_extract({
700
+ table: "users",
701
+ column: "email",
702
+ pattern: "@([a-zA-Z0-9.-]+)",
703
+ groupIndex: 1,
704
+ });
705
+
706
+ // Text manipulation
707
+ sqlite_text_split({ table: "users", column: "email", delimiter: "@" }); // split into parts array
708
+ sqlite_text_concat({
709
+ table: "users",
710
+ columns: ["first_name", "last_name"],
711
+ separator: " ",
712
+ });
713
+ sqlite_text_replace({
714
+ table: "docs",
715
+ column: "content",
716
+ searchPattern: "old",
717
+ replaceWith: "new",
718
+ });
719
+ sqlite_text_trim({ table: "users", column: "name" }); // trim whitespace
720
+ sqlite_text_case({ table: "products", column: "name", mode: "upper" }); // or "lower"
721
+ sqlite_text_substring({ table: "codes", column: "value", start: 1, length: 3 }); // extract substring
722
+ sqlite_text_normalize({
723
+ table: "docs",
724
+ column: "content",
725
+ mode: "strip_accents",
726
+ }); // or nfc, nfd, nfkc, nfkd
727
+
728
+ // Validation patterns: email, phone, url, uuid, ipv4, custom
729
+ sqlite_text_validate({ table: "users", column: "email", pattern: "email" });
730
+ sqlite_text_validate({
731
+ table: "data",
732
+ column: "field",
733
+ pattern: "custom",
734
+ customPattern: "^[A-Z]{2}[0-9]{4}$",
735
+ });
736
+
737
+ // Fuzzy matching \u2014 matches WORD TOKENS by default (not entire value)
738
+ // "laptop" matches "Laptop Pro 15" (distance 0 on first token). Use tokenize: false for full-string matching.
739
+ sqlite_fuzzy_match({
740
+ table: "products",
741
+ column: "name",
742
+ search: "laptop",
743
+ maxDistance: 2,
744
+ });
745
+ sqlite_fuzzy_match({
746
+ table: "products",
747
+ column: "name",
748
+ search: "laptob",
749
+ maxDistance: 2,
750
+ tokenize: false,
751
+ }); // full value
752
+
753
+ // Phonetic matching \u2014 finds words that sound alike (matches against any word in value)
754
+ // Use includeRowData: false for lighter payloads when only matching values are needed
755
+ sqlite_phonetic_match({
756
+ table: "products",
757
+ column: "name",
758
+ search: "laptop",
759
+ algorithm: "soundex",
760
+ });
761
+ sqlite_phonetic_match({
762
+ table: "products",
763
+ column: "name",
764
+ search: "laptop",
765
+ includeRowData: false,
766
+ }); // lighter
767
+
768
+ // Advanced search \u2014 combines exact, fuzzy, and phonetic techniques
769
+ // fuzzyThreshold: 0.3-0.4 = loose matching (more results), 0.6-0.8 = strict matching (fewer results)
770
+ sqlite_advanced_search({
771
+ table: "products",
772
+ column: "name",
773
+ searchTerm: "laptop",
774
+ techniques: ["exact", "fuzzy", "phonetic"],
775
+ fuzzyThreshold: 0.4,
776
+ });
777
+
778
+ // Sentiment analysis \u2014 pure text analysis, no database query needed
779
+ sqlite_text_sentiment({ text: "This product is amazing and wonderful!" });
780
+ // \u2192 { sentiment: "very_positive", score: 2, confidence: "medium" }
781
+
782
+ sqlite_text_sentiment({ text: "Great service but slow delivery", returnWords: true });
783
+ // \u2192 { sentiment: "neutral", score: 0, matchedPositive: ["great"], matchedNegative: ["slow"] }
784
+ \`\`\``
785
+ ],
786
+ [
787
+ "transactions",
788
+ `# db-mcp Help \u2014 Transactions (8 tools, Native only)
789
+
790
+ ## Atomic Execution (preferred for simple cases)
791
+
792
+ \`\`\`javascript
793
+ sqlite_transaction_execute({
794
+ statements: ["UPDATE a SET x=1", "UPDATE b SET y=2"],
795
+ });
796
+ \`\`\`
797
+
798
+ ## Manual Transaction Control
799
+
800
+ \`\`\`javascript
801
+ sqlite_transaction_begin({ mode: "immediate" }); // or "deferred", "exclusive"
802
+ sqlite_transaction_savepoint({ name: "checkpoint" });
803
+ sqlite_transaction_rollback_to({ name: "checkpoint" });
804
+ sqlite_transaction_release({ name: "checkpoint" });
805
+ sqlite_transaction_commit();
806
+ sqlite_transaction_rollback();
807
+ \`\`\`
808
+
809
+ ## Transaction State (read-only)
810
+
811
+ \`\`\`javascript
812
+ sqlite_transaction_status(); // \u2192 { status: "active" | "none", active: true/false }
813
+ \`\`\`
814
+
815
+ ## \u26A0\uFE0F Gotchas
816
+
817
+ - Transaction tools are **Native only** \u2014 WASM adapter does not support transactions
818
+ - Use \`sqlite_transaction_execute\` for simple multi-statement operations; manual \`begin\`/\`commit\` for complex flows with savepoints
819
+ - \`sqlite_transaction_status\` is read-only and requires only \`read\` scope; all other transaction tools require \`write\` scope`
820
+ ],
821
+ [
822
+ "vector",
823
+ `# db-mcp Help \u2014 Vector/Semantic Search (11 tools)
824
+
825
+ \`\`\`javascript
826
+ // Create vector table with metadata columns
827
+ sqlite_vector_create_table({ tableName: "docs", dimensions: 384, additionalColumns: [{ name: "content", type: "TEXT" }] });
828
+
829
+ // Store vectors (single and batch)
830
+ sqlite_vector_store({ table: "docs", idColumn: "id", vectorColumn: "emb", id: 1, vector: [...] });
831
+ sqlite_vector_batch_store({ table: "docs", idColumn: "id", vectorColumn: "emb", items: [{ id: 1, vector: [...] }, { id: 2, vector: [...] }] });
832
+
833
+ // Search vectors (returnColumns omits vector data from results for smaller payloads)
834
+ sqlite_vector_search({ table: "docs", vectorColumn: "emb", queryVector: [...], limit: 10, returnColumns: ["id", "title"] }); // returns { results: [...] } instead of rows
835
+
836
+ // Retrieve and delete vectors
837
+ // Note: sqlite_vector_get returns parsed 'vector' array + additional columns in a 'metadata' object for flexibility
838
+ sqlite_vector_get({ table: "docs", idColumn: "id", vectorColumn: "emb", id: 1 });
839
+ sqlite_vector_delete({ table: "docs", idColumn: "id", ids: [1, 2, 3] });
840
+
841
+ // Vector metadata
842
+ sqlite_vector_count({ table: "docs" }); // or with dimensions filter: { table: "docs", dimensions: 384 }
843
+ sqlite_vector_dimensions({ table: "docs", vectorColumn: "emb" });
844
+ sqlite_vector_stats({ table: "docs", vectorColumn: "emb" }); // returns sampleSize, magnitudeStats (min/max/avg)
845
+
846
+ // Utility tools for preprocessing
847
+ sqlite_vector_normalize({ vector: [3, 4, 0, 0] }); // returns unit vector [0.6, 0.8, 0, 0]
848
+ sqlite_vector_distance({ vector1: [...], vector2: [...], metric: "cosine" }); // returns { value: <number> }
849
+ \`\`\``
850
+ ]
851
+ ]);
852
+ var BUFFER_HIGH_WATER = 50;
853
+ var FLUSH_INTERVAL_MS = 100;
854
+ var DEFAULT_RECENT_COUNT = 50;
855
+ var STDERR_SENTINEL = "stderr";
856
+ var TAIL_READ_BYTES = 65536;
857
+ var AuditLogger = class {
858
+ config;
859
+ buffer = [];
860
+ flushTimer = null;
861
+ activeFlush = null;
862
+ closed = false;
863
+ dirEnsured = false;
864
+ stderrMode;
865
+ constructor(config) {
866
+ this.config = config;
867
+ this.stderrMode = config.logPath.toLowerCase() === STDERR_SENTINEL;
868
+ if (config.enabled) {
869
+ this.flushTimer = setInterval(() => {
870
+ void this.flush();
871
+ }, FLUSH_INTERVAL_MS);
872
+ this.flushTimer.unref();
873
+ }
874
+ }
875
+ /**
876
+ * Eagerly create the log directory and touch the log file on startup.
877
+ * Provides immediate confirmation that audit is configured correctly.
878
+ * Non-throwing — falls back silently if the path is inaccessible.
879
+ */
880
+ async init() {
881
+ if (this.stderrMode || !this.config.enabled) return;
882
+ try {
883
+ await this.ensureDirectory();
884
+ const fh = await open(this.config.logPath, "a");
885
+ await fh.close();
886
+ } catch {
887
+ process.stderr.write(
888
+ `[AUDIT] Failed to initialise log file: ${this.config.logPath}
889
+ `
890
+ );
891
+ }
892
+ }
893
+ /**
894
+ * Append an audit entry to the buffer.
895
+ * Non-blocking — the entry is serialised and queued; the
896
+ * actual file write happens on the next flush cycle.
897
+ */
898
+ log(entry) {
899
+ if (this.closed || !this.config.enabled) return;
900
+ this.buffer.push(JSON.stringify(entry));
901
+ if (this.buffer.length >= BUFFER_HIGH_WATER) {
902
+ void this.flush();
903
+ }
904
+ }
905
+ /**
906
+ * Flush the buffer to disk.
907
+ * Safe to call concurrently — serialises via `this.activeFlush` Promise.
908
+ */
909
+ async flush() {
910
+ if (this.activeFlush) {
911
+ await this.activeFlush;
912
+ if (this.buffer.length === 0) return;
913
+ }
914
+ if (this.buffer.length === 0) return;
915
+ const doFlush = async () => {
916
+ await this.rotateIfNeeded();
917
+ const lines = this.buffer;
918
+ this.buffer = [];
919
+ try {
920
+ if (this.stderrMode) {
921
+ process.stderr.write(lines.join("\n") + "\n");
922
+ } else {
923
+ await this.ensureDirectory();
924
+ await appendFile(
925
+ this.config.logPath,
926
+ lines.join("\n") + "\n",
927
+ "utf-8"
928
+ );
929
+ }
930
+ } catch (err) {
931
+ const message = err instanceof Error ? err.message : String(err);
932
+ process.stderr.write(`[AUDIT] Write failed: ${message}
933
+ `);
934
+ this.buffer.unshift(...lines);
935
+ }
936
+ };
937
+ this.activeFlush = doFlush();
938
+ try {
939
+ await this.activeFlush;
940
+ } finally {
941
+ this.activeFlush = null;
942
+ }
943
+ }
944
+ /**
945
+ * Gracefully close the logger — flush remaining entries and stop the timer.
946
+ */
947
+ async close() {
948
+ this.closed = true;
949
+ if (this.flushTimer) {
950
+ clearInterval(this.flushTimer);
951
+ this.flushTimer = null;
952
+ }
953
+ await this.flush();
954
+ }
955
+ /**
956
+ * Read the most recent audit entries from the log file.
957
+ * Uses a streaming tail-read: only the last TAIL_READ_BYTES (64 KB) are
958
+ * read from disk, preventing O(n) memory spikes for large audit logs.
959
+ * Used by the `sqlite://audit` resource.
960
+ *
961
+ * @param count Maximum number of entries to return (default 50)
962
+ */
963
+ async recent(count = DEFAULT_RECENT_COUNT) {
964
+ if (this.stderrMode) return [];
965
+ await this.flush();
966
+ try {
967
+ let fh;
968
+ try {
969
+ fh = await open(this.config.logPath, "r");
970
+ } catch {
971
+ return [];
972
+ }
973
+ try {
974
+ const info = await stat(this.config.logPath);
975
+ const fileSize = info.size;
976
+ if (fileSize === 0) return [];
977
+ const readSize = Math.min(fileSize, TAIL_READ_BYTES);
978
+ const startOffset = fileSize - readSize;
979
+ const buf = Buffer.alloc(readSize);
980
+ await fh.read(buf, 0, readSize, startOffset);
981
+ const chunk = buf.toString("utf-8");
982
+ const rawLines = chunk.split("\n").filter(Boolean);
983
+ const lines = startOffset > 0 ? rawLines.slice(1) : rawLines;
984
+ const tail = lines.slice(-count);
985
+ return tail.reduce((acc, line) => {
986
+ try {
987
+ acc.push(JSON.parse(line));
988
+ } catch {
989
+ }
990
+ return acc;
991
+ }, []);
992
+ } finally {
993
+ await fh.close();
994
+ }
995
+ } catch {
996
+ return [];
997
+ }
998
+ }
999
+ /**
1000
+ * Ensure the parent directory of the log file exists.
1001
+ */
1002
+ async ensureDirectory() {
1003
+ if (this.dirEnsured) return;
1004
+ try {
1005
+ await mkdir(dirname(this.config.logPath), { recursive: true });
1006
+ this.dirEnsured = true;
1007
+ } catch {
1008
+ this.dirEnsured = true;
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Rotate the log file if it exceeds the configured size limit.
1013
+ * Keeps up to 5 rotated files (`.1` through `.5`); older data is discarded.
1014
+ * Rotation failure is non-fatal — audit must not block tool execution.
1015
+ */
1016
+ async rotateIfNeeded() {
1017
+ if (this.stderrMode || !this.config.maxSizeBytes) return;
1018
+ try {
1019
+ const info = await stat(this.config.logPath).catch(() => null);
1020
+ if (!info || info.size < this.config.maxSizeBytes) return;
1021
+ for (let i = 4; i >= 1; i--) {
1022
+ const oldFile = `${this.config.logPath}.${String(i)}`;
1023
+ const newFile = `${this.config.logPath}.${String(i + 1)}`;
1024
+ await rename(oldFile, newFile).catch(() => null);
1025
+ }
1026
+ const rotatedPath = `${this.config.logPath}.1`;
1027
+ await rename(this.config.logPath, rotatedPath);
1028
+ } catch {
1029
+ }
1030
+ }
1031
+ };
1032
+ var gzipAsync = promisify(gzip);
1033
+ var SNAPSHOT_TOOL_ARGS = {
1034
+ sqlite_drop_table: { targetKey: "table" },
1035
+ sqlite_drop_index: { targetKey: "index" },
1036
+ sqlite_drop_view: { targetKey: "view" },
1037
+ sqlite_import_csv: { targetKey: "table" },
1038
+ sqlite_backup: { targetKey: "path" }
1039
+ };
1040
+ var SNAPSHOT_EXT = ".snapshot.json.gz";
1041
+ var SNAPSHOT_EXT_LEGACY = ".snapshot.json";
1042
+ var MAX_SAMPLE_ROWS = 100;
1043
+ var DEFAULT_MAX_DATA_SIZE_BYTES = 50 * 1024 * 1024;
1044
+ var BackupManager = class {
1045
+ config;
1046
+ snapshotDir;
1047
+ dirEnsured = false;
1048
+ pendingWrites = /* @__PURE__ */ new Set();
1049
+ constructor(config, auditLogPath) {
1050
+ this.config = config;
1051
+ const logDir = dirname(auditLogPath);
1052
+ this.snapshotDir = join(logDir, "snapshots");
1053
+ }
1054
+ /**
1055
+ * Check if a tool should receive a pre-mutation snapshot.
1056
+ */
1057
+ shouldSnapshot(toolName) {
1058
+ return this.config.enabled && toolName in SNAPSHOT_TOOL_ARGS;
1059
+ }
1060
+ /**
1061
+ * Create a pre-mutation snapshot of the target object.
1062
+ *
1063
+ * @returns Relative path to the snapshot file, or undefined if skipped/failed
1064
+ */
1065
+ async createSnapshot(toolName, args, requestId, adapter, logAs) {
1066
+ if (!this.shouldSnapshot(toolName)) return void 0;
1067
+ try {
1068
+ const mapping = SNAPSHOT_TOOL_ARGS[toolName];
1069
+ if (!mapping) return void 0;
1070
+ const rawTarget = args[mapping.targetKey];
1071
+ const target = typeof rawTarget === "string" ? rawTarget : "unknown";
1072
+ return await this.captureObjectSnapshot(
1073
+ logAs ?? toolName,
1074
+ target,
1075
+ requestId,
1076
+ adapter
1077
+ );
1078
+ } catch (err) {
1079
+ const message = err instanceof Error ? err.message : String(err);
1080
+ process.stderr.write(
1081
+ `[AUDIT-BACKUP] Snapshot failed for ${toolName}: ${message}
1082
+ `
1083
+ );
1084
+ return void 0;
1085
+ }
1086
+ }
1087
+ /**
1088
+ * List available snapshots with metadata.
1089
+ */
1090
+ async listSnapshots() {
1091
+ try {
1092
+ await this.ensureDirectory();
1093
+ const files = await readdir(this.snapshotDir);
1094
+ const snapshots = [];
1095
+ for (const file of files) {
1096
+ if (!file.endsWith(SNAPSHOT_EXT) && !file.endsWith(SNAPSHOT_EXT_LEGACY))
1097
+ continue;
1098
+ try {
1099
+ const parsed = await this.readSnapshotFile(file);
1100
+ if (parsed) {
1101
+ snapshots.push({ ...parsed.metadata, filename: file });
1102
+ }
1103
+ } catch {
1104
+ }
1105
+ }
1106
+ snapshots.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
1107
+ return snapshots;
1108
+ } catch {
1109
+ return [];
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Read a specific snapshot by filename.
1114
+ */
1115
+ async getSnapshot(filename) {
1116
+ try {
1117
+ const safe = basename(filename);
1118
+ return await this.readSnapshotFile(safe);
1119
+ } catch {
1120
+ return null;
1121
+ }
1122
+ }
1123
+ /**
1124
+ * Apply retention policy — delete oldest snapshots that exceed limits.
1125
+ */
1126
+ async cleanup() {
1127
+ if (!this.config.enabled) return 0;
1128
+ try {
1129
+ const files = await readdir(this.snapshotDir);
1130
+ const snapshotFiles = files.filter(
1131
+ (f) => f.endsWith(SNAPSHOT_EXT) || f.endsWith(SNAPSHOT_EXT_LEGACY)
1132
+ );
1133
+ if (snapshotFiles.length === 0) return 0;
1134
+ const fileInfos = [];
1135
+ for (const file of snapshotFiles) {
1136
+ const filePath = join(this.snapshotDir, file);
1137
+ try {
1138
+ const stats = await stat(filePath);
1139
+ fileInfos.push({ name: file, mtime: stats.mtime, path: filePath });
1140
+ } catch {
1141
+ }
1142
+ }
1143
+ fileInfos.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
1144
+ let deleted = 0;
1145
+ const now = Date.now();
1146
+ const maxAgeMs = this.config.maxAgeDays * 24 * 60 * 60 * 1e3;
1147
+ for (const info of fileInfos) {
1148
+ const age = now - info.mtime.getTime();
1149
+ const overAge = age > maxAgeMs;
1150
+ const overCount = fileInfos.length - deleted > this.config.maxCount;
1151
+ if (overAge || overCount) {
1152
+ try {
1153
+ await unlink(info.path);
1154
+ deleted++;
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ }
1159
+ if (deleted > 0) {
1160
+ process.stderr.write(
1161
+ `[AUDIT-BACKUP] Cleaned up ${String(deleted)} snapshot(s)
1162
+ `
1163
+ );
1164
+ }
1165
+ return deleted;
1166
+ } catch {
1167
+ return 0;
1168
+ }
1169
+ }
1170
+ /**
1171
+ * Flush all pending async snapshot writes.
1172
+ * Call during graceful shutdown to ensure all snapshots are persisted.
1173
+ */
1174
+ async flush() {
1175
+ if (this.pendingWrites.size > 0) {
1176
+ await Promise.allSettled(this.pendingWrites);
1177
+ }
1178
+ }
1179
+ async getStats() {
1180
+ try {
1181
+ const files = await readdir(this.snapshotDir);
1182
+ const snapshotFiles = files.filter(
1183
+ (f) => f.endsWith(SNAPSHOT_EXT) || f.endsWith(SNAPSHOT_EXT_LEGACY)
1184
+ );
1185
+ let totalSize = 0;
1186
+ let oldestMtime;
1187
+ for (const file of snapshotFiles) {
1188
+ try {
1189
+ const stats = await stat(join(this.snapshotDir, file));
1190
+ totalSize += stats.size;
1191
+ if (!oldestMtime || stats.mtime < oldestMtime) {
1192
+ oldestMtime = stats.mtime;
1193
+ }
1194
+ } catch {
1195
+ }
1196
+ }
1197
+ return {
1198
+ count: snapshotFiles.length,
1199
+ ...oldestMtime && { oldestAge: oldestMtime.toISOString() },
1200
+ totalSizeKB: Math.round(totalSize / 1024)
1201
+ };
1202
+ } catch {
1203
+ return { count: 0, totalSizeKB: 0 };
1204
+ }
1205
+ }
1206
+ // =========================================================================
1207
+ // Private snapshot capture methods
1208
+ // =========================================================================
1209
+ async captureObjectSnapshot(toolName, target, requestId, adapter) {
1210
+ const ddl = await this.buildDdl(target, adapter);
1211
+ const { rowCount } = await this.captureVolumeMetadata(target, adapter);
1212
+ const { data, dataSkipped, dataSkippedReason } = await this.captureTableData(target, adapter);
1213
+ return this.writeSnapshot(toolName, target, requestId, ddl, data, {
1214
+ ...rowCount !== void 0 && { rowCount },
1215
+ ...dataSkipped && { dataSkipped },
1216
+ ...dataSkippedReason !== void 0 && { dataSkippedReason }
1217
+ });
1218
+ }
1219
+ /**
1220
+ * Build a DDL string from sqlite_master for the given object.
1221
+ * Works for tables, views, indexes, and triggers.
1222
+ */
1223
+ async buildDdl(objectName, adapter) {
1224
+ try {
1225
+ const result = await adapter.executeQuery(
1226
+ "SELECT sql FROM sqlite_master WHERE name = ?",
1227
+ [objectName]
1228
+ );
1229
+ if (result.rows && result.rows.length > 0) {
1230
+ const row = result.rows[0];
1231
+ if (row?.["sql"] != null) {
1232
+ const val = row["sql"];
1233
+ return typeof val === "string" ? val : "";
1234
+ }
1235
+ }
1236
+ } catch (e) {
1237
+ const msg = e instanceof Error ? e.message : String(e);
1238
+ process.stderr.write(
1239
+ `[AUDIT-BACKUP] DDL capture failed for ${objectName}: ${msg}
1240
+ `
1241
+ );
1242
+ }
1243
+ return `-- Object "${objectName}" does not exist or cannot be described`;
1244
+ }
1245
+ /**
1246
+ * Capture row count using COUNT(*).
1247
+ * Near-zero cost for small tables; failures are silently ignored (best-effort).
1248
+ */
1249
+ async captureVolumeMetadata(tableName, adapter) {
1250
+ try {
1251
+ const typeResult = await adapter.executeQuery(
1252
+ "SELECT type FROM sqlite_master WHERE name = ?",
1253
+ [tableName]
1254
+ );
1255
+ const objType = typeResult.rows?.[0]?.["type"];
1256
+ if (objType !== "table") return {};
1257
+ const countResult = await adapter.executeQuery(
1258
+ `SELECT COUNT(*) as row_count FROM "${tableName}"`
1259
+ );
1260
+ const rawCount = countResult.rows?.[0]?.["row_count"];
1261
+ if (typeof rawCount === "number") {
1262
+ return { rowCount: rawCount };
1263
+ }
1264
+ } catch {
1265
+ }
1266
+ return {};
1267
+ }
1268
+ /**
1269
+ * Capture row data as INSERT statements, subject to config limits.
1270
+ * Returns empty output when `includeData` is disabled.
1271
+ */
1272
+ async captureTableData(tableName, adapter) {
1273
+ if (!this.config.includeData) {
1274
+ return { dataSkipped: false };
1275
+ }
1276
+ const maxDataSize = this.config.maxDataSizeBytes || DEFAULT_MAX_DATA_SIZE_BYTES;
1277
+ try {
1278
+ const typeResult = await adapter.executeQuery(
1279
+ "SELECT type FROM sqlite_master WHERE name = ?",
1280
+ [tableName]
1281
+ );
1282
+ const objType = typeResult.rows?.[0]?.["type"];
1283
+ if (objType !== "table") return { dataSkipped: false };
1284
+ } catch {
1285
+ return { dataSkipped: false };
1286
+ }
1287
+ try {
1288
+ const pageResult = await adapter.executeQuery(
1289
+ "SELECT (SELECT page_count FROM pragma_page_count()) * (SELECT page_size FROM pragma_page_size()) as total_size"
1290
+ );
1291
+ const rawSize = pageResult.rows?.[0]?.["total_size"];
1292
+ const totalSizeBytes = typeof rawSize === "number" ? rawSize : 0;
1293
+ if (totalSizeBytes > maxDataSize) {
1294
+ const sizeMB = Math.round(totalSizeBytes / (1024 * 1024));
1295
+ const thresholdMB = Math.round(maxDataSize / (1024 * 1024));
1296
+ return {
1297
+ dataSkipped: true,
1298
+ dataSkippedReason: `Database size ~${String(sizeMB)}MB exceeds ${String(thresholdMB)}MB threshold`
1299
+ };
1300
+ }
1301
+ } catch {
1302
+ }
1303
+ try {
1304
+ const result = await adapter.executeQuery(
1305
+ `SELECT * FROM "${tableName}" LIMIT ${String(MAX_SAMPLE_ROWS)}`
1306
+ );
1307
+ if (result.rows && result.rows.length > 0) {
1308
+ const firstRow = result.rows[0];
1309
+ if (firstRow) {
1310
+ const cols = Object.keys(firstRow).map((c) => `"${c}"`).join(", ");
1311
+ const data = result.rows.map((row) => {
1312
+ const vals = Object.values(row).map((v) => {
1313
+ if (v === null) return "NULL";
1314
+ if (typeof v === "string")
1315
+ return `'${v.replace(/'/g, "''")}'`;
1316
+ if (typeof v === "number" || typeof v === "boolean")
1317
+ return String(v);
1318
+ return `'${JSON.stringify(v).replace(/'/g, "''")}'`;
1319
+ }).join(", ");
1320
+ return `INSERT INTO "${tableName}" (${cols}) VALUES (${vals});`;
1321
+ }).join("\n");
1322
+ return { data, dataSkipped: false };
1323
+ }
1324
+ }
1325
+ } catch {
1326
+ }
1327
+ return { dataSkipped: false };
1328
+ }
1329
+ async writeSnapshot(tool, target, requestId, ddl, data, volumeMeta) {
1330
+ await this.ensureDirectory();
1331
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1332
+ const safeTarget = target.replace(/[^a-zA-Z0-9_-]/g, "_");
1333
+ const safeTool = tool.replace(/[^a-zA-Z0-9_-]/g, "_");
1334
+ const filename = `${timestamp.replace(/[:.]/g, "-")}_${safeTool}_${safeTarget}${SNAPSHOT_EXT}`;
1335
+ const content = {
1336
+ metadata: {
1337
+ timestamp,
1338
+ tool,
1339
+ target,
1340
+ type: data ? "ddl+data" : "ddl",
1341
+ requestId,
1342
+ sizeBytes: 0,
1343
+ // Updated after serialization
1344
+ ...volumeMeta?.rowCount !== void 0 && {
1345
+ rowCount: volumeMeta.rowCount
1346
+ },
1347
+ ...volumeMeta?.dataSkipped && { dataSkipped: true },
1348
+ ...volumeMeta?.dataSkippedReason && {
1349
+ dataSkippedReason: volumeMeta.dataSkippedReason
1350
+ }
1351
+ },
1352
+ ddl,
1353
+ data
1354
+ };
1355
+ const json = JSON.stringify(content, null, 2);
1356
+ const sizeBytes = Buffer.byteLength(json, "utf-8");
1357
+ const finalJson = json.replace(
1358
+ '"sizeBytes": 0',
1359
+ `"sizeBytes": ${String(sizeBytes)}`
1360
+ );
1361
+ const compressed = await gzipAsync(Buffer.from(finalJson, "utf-8"));
1362
+ const filePath = join(this.snapshotDir, filename);
1363
+ const writePromise = mkdtemp(`${this.snapshotDir}${sep}.tmp-`).then(async (tempDir) => {
1364
+ const tmpPath = join(tempDir, filename);
1365
+ await writeFile(tmpPath, compressed, { flag: "wx" });
1366
+ await rename(tmpPath, filePath);
1367
+ await rmdir(tempDir).catch(() => null);
1368
+ }).catch((err) => {
1369
+ const msg = err instanceof Error ? err.message : String(err);
1370
+ process.stderr.write(
1371
+ `[AUDIT-BACKUP] Async write failed for ${filename}: ${msg}
1372
+ `
1373
+ );
1374
+ });
1375
+ this.pendingWrites.add(writePromise);
1376
+ void writePromise.finally(() => {
1377
+ this.pendingWrites.delete(writePromise);
1378
+ });
1379
+ return filename;
1380
+ }
1381
+ /**
1382
+ * Read and decompress a snapshot file (supports both gzip and legacy JSON).
1383
+ */
1384
+ async readSnapshotFile(filename) {
1385
+ const filePath = join(this.snapshotDir, filename);
1386
+ const raw = await readFile(filePath);
1387
+ if (raw[0] === 31 && raw[1] === 139) {
1388
+ const decompressed = gunzipSync(raw);
1389
+ return JSON.parse(decompressed.toString("utf-8"));
1390
+ }
1391
+ return JSON.parse(raw.toString("utf-8"));
1392
+ }
1393
+ async ensureDirectory() {
1394
+ if (this.dirEnsured) return;
1395
+ try {
1396
+ await mkdir(this.snapshotDir, { recursive: true });
1397
+ this.dirEnsured = true;
1398
+ } catch {
1399
+ this.dirEnsured = true;
1400
+ }
1401
+ }
1402
+ };
1403
+
1404
+ // src/auth/scope-map.ts
1405
+ var toolScopeMap = /* @__PURE__ */ new Map();
1406
+ for (const [group, tools] of Object.entries(TOOL_GROUPS)) {
1407
+ const scope = TOOL_GROUP_SCOPES[group];
1408
+ if (scope) {
1409
+ for (const toolName of tools) {
1410
+ toolScopeMap.set(toolName, scope);
1411
+ toolScopeMap.set(`sqlite_${toolName}`, scope);
1412
+ }
1413
+ }
1414
+ }
1415
+ function getRequiredScope(toolName) {
1416
+ return toolScopeMap.get(toolName) ?? SCOPES.READ;
1417
+ }
1418
+
1419
+ // src/audit/interceptor.ts
1420
+ var ALWAYS_AUDITED_SCOPES = /* @__PURE__ */ new Set(["write", "admin"]);
1421
+ function scopeToCategory(scope) {
1422
+ if (scope === "admin") return "admin";
1423
+ if (scope === "read") return "read";
1424
+ return "write";
1425
+ }
1426
+ function createAuditInterceptor(auditLogger, backupManager, queryAdapter) {
1427
+ const auditReads = auditLogger.config.auditReads;
1428
+ return {
1429
+ async around(toolName, args, requestId, fn, options) {
1430
+ const scope = getRequiredScope(toolName);
1431
+ if (!ALWAYS_AUDITED_SCOPES.has(scope) && !auditReads) {
1432
+ return fn();
1433
+ }
1434
+ const isReadScope = scope === "read";
1435
+ const authCtx = getAuthContext();
1436
+ const start = performance.now();
1437
+ let success = true;
1438
+ let error;
1439
+ let backupRef;
1440
+ let tokenEstimate;
1441
+ try {
1442
+ const result = await fn();
1443
+ if (typeof result === "object" && result !== null) {
1444
+ try {
1445
+ const json = JSON.stringify({
1446
+ ...result,
1447
+ _meta: { tokenEstimate: 0 }
1448
+ });
1449
+ tokenEstimate = Math.ceil(Buffer.byteLength(json, "utf8") / 4);
1450
+ } catch {
1451
+ }
1452
+ } else if (typeof result === "string") {
1453
+ tokenEstimate = Math.ceil(Buffer.byteLength(result, "utf8") / 4);
1454
+ }
1455
+ return result;
1456
+ } catch (err) {
1457
+ success = false;
1458
+ error = err instanceof Error ? err.message : String(err);
1459
+ const errorResult = {
1460
+ success: false,
1461
+ error,
1462
+ code: "INTERNAL_ERROR",
1463
+ category: "internal",
1464
+ recoverable: false
1465
+ };
1466
+ const enriched = JSON.stringify({
1467
+ ...errorResult,
1468
+ _meta: { tokenEstimate: 0 }
1469
+ });
1470
+ tokenEstimate = Math.ceil(Buffer.byteLength(enriched, "utf8") / 4);
1471
+ throw err;
1472
+ } finally {
1473
+ const durationMs = Math.round(performance.now() - start);
1474
+ if (isReadScope) {
1475
+ auditLogger.log({
1476
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1477
+ requestId,
1478
+ tool: options?.logAs ?? toolName,
1479
+ category: "read",
1480
+ scope,
1481
+ durationMs,
1482
+ success,
1483
+ error,
1484
+ tokenEstimate
1485
+ });
1486
+ } else {
1487
+ auditLogger.log({
1488
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1489
+ requestId,
1490
+ tool: options?.logAs ?? toolName,
1491
+ category: scopeToCategory(scope),
1492
+ scope,
1493
+ user: authCtx?.claims?.sub ?? null,
1494
+ scopes: authCtx?.scopes ?? [],
1495
+ durationMs,
1496
+ success,
1497
+ error,
1498
+ args: auditLogger.config.redact ? void 0 : args,
1499
+ backup: backupRef,
1500
+ tokenEstimate
1501
+ });
1502
+ }
1503
+ }
1504
+ }
1505
+ };
1506
+ }
1507
+ var DbMcpServer = class {
1508
+ server;
1509
+ adapters = /* @__PURE__ */ new Map();
1510
+ toolFilter;
1511
+ config;
1512
+ auditLogger = null;
1513
+ backupManager = null;
1514
+ auditInterceptor = null;
1515
+ auditInitPromise = null;
1516
+ constructor(config) {
1517
+ this.config = config;
1518
+ this.toolFilter = config.toolFilter ? parseToolFilter(config.toolFilter) : getToolFilterFromEnv();
1519
+ this.server = new McpServer(
1520
+ {
1521
+ name: config.name,
1522
+ version: config.version
1523
+ },
1524
+ {
1525
+ capabilities: {
1526
+ logging: {}
1527
+ },
1528
+ instructions: INSTRUCTIONS
1529
+ }
1530
+ );
1531
+ logger.info(getFilterSummary(this.toolFilter), { module: "FILTER" });
1532
+ logger.info("MCP Server initialized", {
1533
+ module: "SERVER",
1534
+ name: config.name,
1535
+ version: config.version,
1536
+ toolFilter: config.toolFilter ?? "none",
1537
+ capabilities: ["logging"]
1538
+ });
1539
+ this.registerBuiltInTools();
1540
+ this.registerHelpResources();
1541
+ if (config.audit?.enabled) {
1542
+ this.auditInitPromise = this.initializeAudit(config);
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Register a database adapter
1547
+ */
1548
+ async registerAdapter(adapter, config) {
1549
+ const adapterId = `${adapter.type}:${config.connectionString ?? "default"}`;
1550
+ if (this.auditInitPromise) {
1551
+ await this.auditInitPromise;
1552
+ }
1553
+ await adapter.connect(config);
1554
+ this.adapters.set(adapterId, adapter);
1555
+ if (this.auditInterceptor && "setAuditInterceptor" in adapter) {
1556
+ adapter.setAuditInterceptor(this.auditInterceptor);
1557
+ }
1558
+ if (this.backupManager && "setBackupManager" in adapter) {
1559
+ adapter.setBackupManager(this.backupManager);
1560
+ }
1561
+ adapter.registerTools(this.server, this.toolFilter);
1562
+ adapter.registerResources(this.server);
1563
+ adapter.registerPrompts(this.server);
1564
+ logger.info(`Registered adapter: ${adapter.name} (${adapterId})`, {
1565
+ module: "SERVER"
1566
+ });
1567
+ }
1568
+ /**
1569
+ * Get an adapter by ID
1570
+ */
1571
+ getAdapter(adapterId) {
1572
+ return this.adapters.get(adapterId);
1573
+ }
1574
+ /**
1575
+ * Get all registered adapters
1576
+ */
1577
+ getAdapters() {
1578
+ return this.adapters;
1579
+ }
1580
+ /**
1581
+ * Register built-in server tools (health, info, etc.)
1582
+ */
1583
+ registerBuiltInTools() {
1584
+ const serverInfoOpts = {
1585
+ title: "Server Info",
1586
+ description: "Get information about the db-mcp server and registered adapters",
1587
+ icons: SERVER_ICONS,
1588
+ annotations: READ_ONLY
1589
+ };
1590
+ this.server.registerTool("server_info", serverInfoOpts, () => {
1591
+ const adapterInfo = [];
1592
+ for (const [id, adapter] of this.adapters) {
1593
+ adapterInfo.push({
1594
+ id,
1595
+ ...adapter.getInfo()
1596
+ });
1597
+ }
1598
+ return {
1599
+ content: [
1600
+ {
1601
+ type: "text",
1602
+ text: JSON.stringify(
1603
+ {
1604
+ name: this.config.name,
1605
+ version: this.config.version,
1606
+ transport: this.config.transport,
1607
+ adapters: adapterInfo,
1608
+ toolFilter: {
1609
+ raw: this.toolFilter.raw,
1610
+ enabledGroups: [...this.toolFilter.enabledGroups]
1611
+ }
1612
+ },
1613
+ null,
1614
+ 2
1615
+ )
1616
+ }
1617
+ ]
1618
+ };
1619
+ });
1620
+ const healthOpts = {
1621
+ title: "Server Health",
1622
+ description: "Check health status of all database connections",
1623
+ icons: SERVER_ICONS,
1624
+ annotations: READ_ONLY
1625
+ };
1626
+ this.server.registerTool("server_health", healthOpts, async () => {
1627
+ const health = {
1628
+ server: "healthy",
1629
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1630
+ adapters: {}
1631
+ };
1632
+ for (const [id, adapter] of this.adapters) {
1633
+ try {
1634
+ const adapterHealth = await adapter.getHealth();
1635
+ health["adapters"][id] = adapterHealth;
1636
+ } catch (error) {
1637
+ health["adapters"][id] = {
1638
+ connected: false,
1639
+ error: error instanceof Error ? error.message : "Unknown error"
1640
+ };
1641
+ }
1642
+ }
1643
+ return {
1644
+ content: [
1645
+ {
1646
+ type: "text",
1647
+ text: JSON.stringify(health, null, 2)
1648
+ }
1649
+ ]
1650
+ };
1651
+ });
1652
+ const listAdaptersOpts = {
1653
+ title: "List Adapters",
1654
+ description: "List all registered database adapters",
1655
+ icons: SERVER_ICONS,
1656
+ annotations: READ_ONLY
1657
+ };
1658
+ this.server.registerTool("list_adapters", listAdaptersOpts, () => {
1659
+ const adapters = [];
1660
+ for (const [id, adapter] of this.adapters) {
1661
+ adapters.push({
1662
+ id,
1663
+ type: adapter.type,
1664
+ name: adapter.name,
1665
+ version: adapter.version,
1666
+ connected: adapter.isConnected()
1667
+ });
1668
+ }
1669
+ return {
1670
+ content: [
1671
+ {
1672
+ type: "text",
1673
+ text: JSON.stringify(adapters, null, 2)
1674
+ }
1675
+ ]
1676
+ };
1677
+ });
1678
+ }
1679
+ /**
1680
+ * Register sqlite://help resources for on-demand reference documentation.
1681
+ * Always registers sqlite://help (gotchas). Group-specific help is filtered
1682
+ * by the tool filter configuration.
1683
+ */
1684
+ registerHelpResources() {
1685
+ const gotchasContent = HELP_CONTENT.get("gotchas");
1686
+ if (gotchasContent) {
1687
+ this.server.registerResource(
1688
+ "sqlite_help",
1689
+ "sqlite://help",
1690
+ {
1691
+ description: "Critical gotchas, WASM vs Native comparison, and Code Mode API reference",
1692
+ mimeType: "text/markdown"
1693
+ },
1694
+ () => ({
1695
+ contents: [
1696
+ {
1697
+ uri: "sqlite://help",
1698
+ mimeType: "text/markdown",
1699
+ text: gotchasContent
1700
+ }
1701
+ ]
1702
+ })
1703
+ );
1704
+ }
1705
+ const groupHelpKeys = [
1706
+ { group: "core", key: "core" },
1707
+ { group: "json", key: "json" },
1708
+ { group: "text", key: "text" },
1709
+ { group: "stats", key: "stats" },
1710
+ { group: "vector", key: "vector" },
1711
+ { group: "geo", key: "geo" },
1712
+ { group: "admin", key: "admin" },
1713
+ { group: "transactions", key: "transactions" },
1714
+ { group: "introspection", key: "introspection" },
1715
+ { group: "migration", key: "migration" }
1716
+ ];
1717
+ for (const { group, key } of groupHelpKeys) {
1718
+ const isCodemodeOnly = this.toolFilter.enabledGroups.size === 1 && this.toolFilter.enabledGroups.has("codemode");
1719
+ if (!this.toolFilter.enabledGroups.has(group) && !isCodemodeOnly) {
1720
+ continue;
1721
+ }
1722
+ const content = HELP_CONTENT.get(key);
1723
+ if (!content) continue;
1724
+ this.server.registerResource(
1725
+ `sqlite_help_${key}`,
1726
+ `sqlite://help/${key}`,
1727
+ {
1728
+ description: `Tool reference for the ${group} tool group`,
1729
+ mimeType: "text/markdown"
1730
+ },
1731
+ () => ({
1732
+ contents: [
1733
+ {
1734
+ uri: `sqlite://help/${key}`,
1735
+ mimeType: "text/markdown",
1736
+ text: content
1737
+ }
1738
+ ]
1739
+ })
1740
+ );
1741
+ }
1742
+ const registeredHelp = ["sqlite://help"];
1743
+ for (const { group, key } of groupHelpKeys) {
1744
+ if (this.toolFilter.enabledGroups.has(group)) {
1745
+ registeredHelp.push(`sqlite://help/${key}`);
1746
+ }
1747
+ }
1748
+ logger.info(`Help resources: ${registeredHelp.join(", ")}`, {
1749
+ module: "SERVER"
1750
+ });
1751
+ }
1752
+ /**
1753
+ * Start the server with the configured transport
1754
+ */
1755
+ async start() {
1756
+ switch (this.config.transport) {
1757
+ case "stdio":
1758
+ await this.startStdio();
1759
+ break;
1760
+ case "http":
1761
+ await this.startHttp();
1762
+ break;
1763
+ default:
1764
+ throw new DbMcpError(
1765
+ `Unsupported transport: ${this.config.transport}`,
1766
+ "SERVER_START_FAILED",
1767
+ "config" /* CONFIGURATION */
1768
+ );
1769
+ }
1770
+ }
1771
+ /**
1772
+ * Start server with stdio transport
1773
+ */
1774
+ async startStdio() {
1775
+ const transport = new StdioServerTransport();
1776
+ await this.server.connect(transport);
1777
+ logger.info(`db-mcp server started (stdio transport)`, {
1778
+ module: "SERVER"
1779
+ });
1780
+ }
1781
+ /**
1782
+ * Start server with HTTP transport (Streamable HTTP with SSE support)
1783
+ */
1784
+ async startHttp() {
1785
+ const { HttpTransport } = await import('./http-6KF4ULDI.js');
1786
+ const oauthConfig = {
1787
+ enabled: this.config.oauth?.enabled ?? false,
1788
+ authorizationServerUrl: this.config.oauth?.authorizationServerUrl ?? "",
1789
+ audience: this.config.oauth?.audience ?? this.config.name
1790
+ };
1791
+ if (this.config.oauth?.issuer !== void 0) {
1792
+ oauthConfig.issuer = this.config.oauth.issuer;
1793
+ }
1794
+ if (this.config.oauth?.jwksUri !== void 0) {
1795
+ oauthConfig.jwksUri = this.config.oauth.jwksUri;
1796
+ }
1797
+ if (this.config.oauth?.clockTolerance !== void 0) {
1798
+ oauthConfig.clockTolerance = this.config.oauth.clockTolerance;
1799
+ }
1800
+ if (this.config.oauth?.publicPaths !== void 0) {
1801
+ oauthConfig.publicPaths = this.config.oauth.publicPaths;
1802
+ }
1803
+ const transport = new HttpTransport({
1804
+ port: this.config.port ?? 3e3,
1805
+ ...this.config.host !== void 0 && { host: this.config.host },
1806
+ ...this.config.authToken !== void 0 && {
1807
+ authToken: this.config.authToken
1808
+ },
1809
+ ...this.config.enableHSTS !== void 0 && {
1810
+ enableHSTS: this.config.enableHSTS
1811
+ },
1812
+ oauth: oauthConfig,
1813
+ stateless: this.config.statelessHttp ?? false
1814
+ });
1815
+ await transport.initialize(this.server);
1816
+ await transport.start();
1817
+ const mode = this.config.statelessHttp ? "stateless" : "stateful";
1818
+ const host = this.config.host ?? "0.0.0.0";
1819
+ const port = String(this.config.port ?? 3e3);
1820
+ logger.info(
1821
+ `db-mcp server started (HTTP transport on ${host}:${port}, ${mode} mode)`,
1822
+ { module: "TRANSPORT", mode }
1823
+ );
1824
+ }
1825
+ /**
1826
+ * Gracefully shut down the server
1827
+ */
1828
+ async shutdown() {
1829
+ logger.info("Shutting down db-mcp server...", { module: "SERVER" });
1830
+ for (const [id, adapter] of this.adapters) {
1831
+ try {
1832
+ await adapter.disconnect();
1833
+ logger.info(`Disconnected adapter: ${id}`, { module: "SERVER" });
1834
+ } catch (error) {
1835
+ logger.error(`Error disconnecting adapter ${id}`, {
1836
+ module: "SERVER",
1837
+ error: error instanceof Error ? error : void 0
1838
+ });
1839
+ }
1840
+ }
1841
+ if (this.auditLogger) {
1842
+ await this.auditLogger.close();
1843
+ logger.info("Audit logger closed", { module: "AUDIT" });
1844
+ }
1845
+ if (this.backupManager) {
1846
+ await this.backupManager.flush();
1847
+ logger.info("Backup manager flushed", { module: "AUDIT" });
1848
+ }
1849
+ await this.server.close();
1850
+ logger.info("Server shutdown complete", { module: "SERVER" });
1851
+ }
1852
+ /**
1853
+ * Initialize the audit subsystem: logger, interceptor, backup manager,
1854
+ * sqlite://audit resource, and audit backup tools.
1855
+ */
1856
+ async initializeAudit(config) {
1857
+ const auditConfig = config.audit;
1858
+ if (!auditConfig?.enabled) return;
1859
+ this.auditLogger = new AuditLogger(auditConfig);
1860
+ await this.auditLogger.init();
1861
+ if (auditConfig.backup?.enabled) {
1862
+ this.backupManager = new BackupManager(
1863
+ auditConfig.backup,
1864
+ auditConfig.logPath
1865
+ );
1866
+ }
1867
+ this.auditInterceptor = createAuditInterceptor(
1868
+ this.auditLogger,
1869
+ this.backupManager ?? void 0);
1870
+ this.registerAuditResource();
1871
+ if (this.backupManager && this.toolFilter.enabledGroups.has("admin")) {
1872
+ this.registerAuditBackupTools();
1873
+ }
1874
+ logger.info(
1875
+ `Audit logging enabled (${auditConfig.logPath}, redact=${String(auditConfig.redact)}, reads=${String(auditConfig.auditReads)}, backup=${String(!!auditConfig.backup?.enabled)})`,
1876
+ { module: "AUDIT" }
1877
+ );
1878
+ }
1879
+ /**
1880
+ * Register the sqlite://audit resource for agent access to audit log.
1881
+ */
1882
+ registerAuditResource() {
1883
+ if (!this.auditLogger) return;
1884
+ const auditLogger = this.auditLogger;
1885
+ const backupManager = this.backupManager;
1886
+ this.server.registerResource(
1887
+ "sqlite_audit",
1888
+ "sqlite://audit",
1889
+ {
1890
+ description: "Recent audit log entries and backup statistics. Shows the last 50 tool invocations with timing, outcomes, and token estimates.",
1891
+ mimeType: "application/json"
1892
+ },
1893
+ async () => {
1894
+ const recent = await auditLogger.recent(50);
1895
+ const backupStats = backupManager ? await backupManager.getStats() : void 0;
1896
+ const payload = {
1897
+ entries: recent,
1898
+ stats: {
1899
+ totalEntries: recent.length,
1900
+ ...backupStats && { backups: backupStats }
1901
+ }
1902
+ };
1903
+ return {
1904
+ contents: [
1905
+ {
1906
+ uri: "sqlite://audit",
1907
+ mimeType: "application/json",
1908
+ text: JSON.stringify(payload, null, 2)
1909
+ }
1910
+ ]
1911
+ };
1912
+ }
1913
+ );
1914
+ }
1915
+ /**
1916
+ * Register audit backup tools for snapshot management.
1917
+ */
1918
+ registerAuditBackupTools() {
1919
+ const backupManager = this.backupManager;
1920
+ if (!backupManager) return;
1921
+ this.server.registerTool(
1922
+ "sqlite_audit_list_backups",
1923
+ {
1924
+ title: "List Audit Backups",
1925
+ description: "List pre-mutation DDL snapshots captured before destructive operations. Returns metadata for each snapshot including timestamp, tool, target, and size.",
1926
+ annotations: {
1927
+ readOnlyHint: true,
1928
+ destructiveHint: false,
1929
+ idempotentHint: true,
1930
+ openWorldHint: false
1931
+ }
1932
+ },
1933
+ async () => {
1934
+ const snapshots = await backupManager.listSnapshots();
1935
+ return {
1936
+ content: [
1937
+ {
1938
+ type: "text",
1939
+ text: JSON.stringify(
1940
+ {
1941
+ snapshots,
1942
+ count: snapshots.length,
1943
+ _meta: {
1944
+ tokenEstimate: Math.ceil(
1945
+ Buffer.byteLength(JSON.stringify(snapshots), "utf8") / 4
1946
+ )
1947
+ }
1948
+ },
1949
+ null,
1950
+ 2
1951
+ )
1952
+ }
1953
+ ]
1954
+ };
1955
+ }
1956
+ );
1957
+ this.server.registerTool(
1958
+ "sqlite_audit_get_backup",
1959
+ {
1960
+ title: "Get Audit Backup",
1961
+ description: "Retrieve a specific pre-mutation DDL snapshot by filename. Returns the full snapshot content including DDL and optional data.",
1962
+ inputSchema: z.object({
1963
+ filename: z.string().describe(
1964
+ "Snapshot filename from sqlite_audit_list_backups results"
1965
+ )
1966
+ }),
1967
+ annotations: {
1968
+ readOnlyHint: true,
1969
+ destructiveHint: false,
1970
+ idempotentHint: true,
1971
+ openWorldHint: false
1972
+ }
1973
+ },
1974
+ async (args) => {
1975
+ const { filename } = args;
1976
+ const snapshot = await backupManager.getSnapshot(filename);
1977
+ if (!snapshot) {
1978
+ return {
1979
+ content: [
1980
+ {
1981
+ type: "text",
1982
+ text: JSON.stringify(
1983
+ {
1984
+ success: false,
1985
+ error: `Snapshot not found: ${filename}`
1986
+ },
1987
+ null,
1988
+ 2
1989
+ )
1990
+ }
1991
+ ],
1992
+ isError: true
1993
+ };
1994
+ }
1995
+ return {
1996
+ content: [
1997
+ {
1998
+ type: "text",
1999
+ text: JSON.stringify(snapshot, null, 2)
2000
+ }
2001
+ ]
2002
+ };
2003
+ }
2004
+ );
2005
+ this.server.registerTool(
2006
+ "sqlite_audit_cleanup",
2007
+ {
2008
+ title: "Cleanup Audit Backups",
2009
+ description: "Apply retention policy to audit backup snapshots. Deletes snapshots exceeding age or count limits.",
2010
+ annotations: {
2011
+ readOnlyHint: false,
2012
+ destructiveHint: true,
2013
+ idempotentHint: true,
2014
+ openWorldHint: false
2015
+ }
2016
+ },
2017
+ async () => {
2018
+ const deleted = await backupManager.cleanup();
2019
+ return {
2020
+ content: [
2021
+ {
2022
+ type: "text",
2023
+ text: JSON.stringify(
2024
+ {
2025
+ success: true,
2026
+ deletedCount: deleted,
2027
+ message: deleted > 0 ? `Cleaned up ${String(deleted)} snapshot(s)` : "No snapshots to clean up"
2028
+ },
2029
+ null,
2030
+ 2
2031
+ )
2032
+ }
2033
+ ]
2034
+ };
2035
+ }
2036
+ );
2037
+ logger.info(
2038
+ "Registered audit backup tools: sqlite_audit_list_backups, sqlite_audit_get_backup, sqlite_audit_cleanup",
2039
+ { module: "AUDIT" }
2040
+ );
2041
+ }
2042
+ };
2043
+ function createServer(config) {
2044
+ return new DbMcpServer(config);
2045
+ }
2046
+ var DEFAULT_CONFIG = {
2047
+ name: NAME,
2048
+ version: VERSION,
2049
+ transport: "stdio",
2050
+ databases: []
2051
+ };
2052
+
2053
+ export { DEFAULT_CONFIG, DbMcpServer, createServer };