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.
- package/.gitleaks.toml +9 -0
- package/.trivyignore +8 -0
- package/README.md +193 -120
- package/dist/{chunk-TVIZ3XJH.js → chunk-5Y42NPBP.js} +4168 -2411
- package/dist/{chunk-AOUL5SHS.js → chunk-645ZEFLA.js} +70 -20
- package/dist/chunk-OKOVZ5QE.js +28 -0
- package/dist/chunk-SFJQCNG7.js +131 -0
- package/dist/{chunk-FW7UCRLN.js → chunk-VIDSICEL.js} +3 -0
- package/dist/chunk-WBER5YY4.js +2053 -0
- package/dist/{chunk-4IA3DB5C.js → chunk-X3MUUOWM.js} +19 -2
- package/dist/{chunk-Z2GFQU3G.js → chunk-Z7C2TM4L.js} +114 -21
- package/dist/cli.js +79 -5
- package/dist/{http-VSB7DBJR.js → http-6KF4ULDI.js} +199 -77
- package/dist/index.d.ts +231 -8
- package/dist/index.js +6 -5
- package/dist/{sqlite-26V3Y4MK.js → sqlite-U5KSYQXK.js} +62 -138
- package/dist/{sqlite-native-5O7FZJGB.js → sqlite-native-JXMCFQBA.js} +575 -113
- package/dist/worker-script.js +34 -10
- package/logs/.gitkeep +1 -0
- package/mcp-config-example.json +83 -0
- package/package.json +9 -8
- package/playwright.config.ts +1 -1
- package/scripts/update-badges.ts +99 -0
- package/server.json +7 -5
- package/test-server/README.md +20 -23
- package/test-server/code-map.md +45 -33
- package/test-server/reset-database.ps1 +59 -17
- package/test-server/scripts/README.md +27 -0
- package/test-server/{test-help-resources.mjs → scripts/test-help-resources.mjs} +12 -5
- package/test-server/scripts/test-prompts.mjs +251 -0
- package/test-server/{test-tool-annotations.mjs → scripts/test-tool-annotations.mjs} +9 -4
- package/test-server/test-advanced/README.md +70 -0
- package/test-server/test-advanced/test-codemode-advanced-admin.md +174 -0
- package/test-server/test-advanced/test-codemode-advanced-core.md +193 -0
- package/test-server/test-advanced/test-codemode-advanced-geo.md +157 -0
- package/test-server/test-advanced/test-codemode-advanced-introspection.md +171 -0
- package/test-server/test-advanced/test-codemode-advanced-json.md +169 -0
- package/test-server/test-advanced/test-codemode-advanced-migration.md +160 -0
- package/test-server/test-advanced/test-codemode-advanced-stats.md +185 -0
- package/test-server/test-advanced/test-codemode-advanced-text.md +183 -0
- package/test-server/test-advanced/test-codemode-advanced-transactions.md +136 -0
- package/test-server/test-advanced/test-codemode-advanced-vector.md +141 -0
- package/test-server/test-codemode/README.md +121 -0
- package/test-server/test-codemode/test-codemode-admin.md +223 -0
- package/test-server/test-codemode/test-codemode-core.md +286 -0
- package/test-server/test-codemode/test-codemode-geo.md +177 -0
- package/test-server/test-codemode/test-codemode-introspection.md +190 -0
- package/test-server/test-codemode/test-codemode-json.md +237 -0
- package/test-server/test-codemode/test-codemode-migration.md +278 -0
- package/test-server/test-codemode/test-codemode-sandbox.md +413 -0
- package/test-server/test-codemode/test-codemode-stats.md +232 -0
- package/test-server/test-codemode/test-codemode-text.md +237 -0
- package/test-server/test-codemode/test-codemode-transactions.md +236 -0
- package/test-server/test-codemode/test-codemode-vector.md +244 -0
- package/test-server/test-codemode/test-codemode-wasm-degradation.md +394 -0
- package/test-server/test-database.sql +37 -1
- package/test-server/test-resources.md +43 -16
- package/test-server/test-tool-groups/README.md +100 -0
- package/test-server/test-tool-groups/test-admin-core.md +165 -0
- package/test-server/test-tool-groups/test-admin-extensions.md +133 -0
- package/test-server/{test-tools.md → test-tool-groups/test-core-data.md} +103 -17
- package/test-server/test-tool-groups/test-core-schema.md +240 -0
- package/test-server/test-tool-groups/test-geo-haversine.md +130 -0
- package/test-server/test-tool-groups/test-geo-spatialite.md +110 -0
- package/test-server/test-tool-groups/test-introspection-diagnostics.md +123 -0
- package/test-server/test-tool-groups/test-introspection-schema.md +133 -0
- package/test-server/test-tool-groups/test-json-read.md +219 -0
- package/test-server/test-tool-groups/test-json-write.md +157 -0
- package/test-server/test-tool-groups/test-migration.md +193 -0
- package/test-server/test-tool-groups/test-stats-advanced.md +118 -0
- package/test-server/test-tool-groups/test-stats-basic.md +156 -0
- package/test-server/test-tool-groups/test-text-advanced.md +169 -0
- package/test-server/test-tool-groups/test-text-basic.md +177 -0
- package/test-server/test-tool-groups/test-transactions.md +179 -0
- package/test-server/test-tool-groups/test-vector-read.md +130 -0
- package/test-server/test-tool-groups/test-vector-write.md +115 -0
- package/test-server/tool-reference.md +79 -58
- package/tsconfig.build.json +6 -0
- package/dist/chunk-RHVEZ42P.js +0 -873
- package/test-server/test-agent-experience.md +0 -243
- package/test-server/test-group-tools.md +0 -861
- package/test-server/test-tools-advanced-1.md +0 -517
- package/test-server/test-tools-advanced-2.md +0 -487
- 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 };
|