db-mcp 3.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +112 -75
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/database-adapter.d.ts +11 -1
  4. package/dist/adapters/database-adapter.d.ts.map +1 -1
  5. package/dist/adapters/registration/resources.d.ts.map +1 -1
  6. package/dist/adapters/registration/tools.d.ts.map +1 -1
  7. package/dist/adapters/sqlite/prompts/analysis.d.ts.map +1 -1
  8. package/dist/adapters/sqlite/query-executor.d.ts.map +1 -1
  9. package/dist/adapters/sqlite/read-write-lock.d.ts +49 -0
  10. package/dist/adapters/sqlite/read-write-lock.d.ts.map +1 -0
  11. package/dist/adapters/sqlite/resources.d.ts +1 -1
  12. package/dist/adapters/sqlite/resources.d.ts.map +1 -1
  13. package/dist/adapters/sqlite/schemas/admin.d.ts +33 -19
  14. package/dist/adapters/sqlite/schemas/admin.d.ts.map +1 -1
  15. package/dist/adapters/sqlite/schemas/core.d.ts +96 -0
  16. package/dist/adapters/sqlite/schemas/core.d.ts.map +1 -1
  17. package/dist/adapters/sqlite/schemas/fts.d.ts +4 -0
  18. package/dist/adapters/sqlite/schemas/fts.d.ts.map +1 -1
  19. package/dist/adapters/sqlite/schemas/introspection.d.ts +25 -15
  20. package/dist/adapters/sqlite/schemas/introspection.d.ts.map +1 -1
  21. package/dist/adapters/sqlite/schemas/migration.d.ts +2 -2
  22. package/dist/adapters/sqlite/schemas/migration.d.ts.map +1 -1
  23. package/dist/adapters/sqlite/schemas/spatialite.d.ts +1 -0
  24. package/dist/adapters/sqlite/schemas/spatialite.d.ts.map +1 -1
  25. package/dist/adapters/sqlite/schemas/text.d.ts +36 -0
  26. package/dist/adapters/sqlite/schemas/text.d.ts.map +1 -1
  27. package/dist/adapters/sqlite/sqlite-adapter.d.ts +6 -0
  28. package/dist/adapters/sqlite/sqlite-adapter.d.ts.map +1 -1
  29. package/dist/adapters/sqlite/tools/admin/backup/create.d.ts.map +1 -1
  30. package/dist/adapters/sqlite/tools/admin/backup/dump.d.ts.map +1 -1
  31. package/dist/adapters/sqlite/tools/admin/backup/restore.d.ts.map +1 -1
  32. package/dist/adapters/sqlite/tools/admin/index.d.ts.map +1 -1
  33. package/dist/adapters/sqlite/tools/admin/pragma.d.ts +0 -4
  34. package/dist/adapters/sqlite/tools/admin/pragma.d.ts.map +1 -1
  35. package/dist/adapters/sqlite/tools/admin/verify.d.ts.map +1 -1
  36. package/dist/adapters/sqlite/tools/codemode.d.ts.map +1 -1
  37. package/dist/adapters/sqlite/tools/core/convenience.d.ts.map +1 -1
  38. package/dist/adapters/sqlite/tools/core/index.d.ts.map +1 -1
  39. package/dist/adapters/sqlite/tools/core/queries.d.ts.map +1 -1
  40. package/dist/adapters/sqlite/tools/core/tables.d.ts.map +1 -1
  41. package/dist/adapters/sqlite/tools/core/versioning.d.ts +19 -0
  42. package/dist/adapters/sqlite/tools/core/versioning.d.ts.map +1 -0
  43. package/dist/adapters/sqlite/tools/fts.d.ts.map +1 -1
  44. package/dist/adapters/sqlite/tools/introspection/diagnostics/indexes.d.ts.map +1 -1
  45. package/dist/adapters/sqlite/tools/introspection/graph/tools.d.ts.map +1 -1
  46. package/dist/adapters/sqlite/tools/json-helpers/read.d.ts.map +1 -1
  47. package/dist/adapters/sqlite/tools/json-operations/crud.d.ts.map +1 -1
  48. package/dist/adapters/sqlite/tools/json-operations/query.d.ts.map +1 -1
  49. package/dist/adapters/sqlite/tools/migration/helpers.d.ts.map +1 -1
  50. package/dist/adapters/sqlite/tools/text/helpers.d.ts +9 -0
  51. package/dist/adapters/sqlite/tools/text/helpers.d.ts.map +1 -1
  52. package/dist/adapters/sqlite/tools/text/index.d.ts.map +1 -1
  53. package/dist/adapters/sqlite/tools/text/search.d.ts +4 -0
  54. package/dist/adapters/sqlite/tools/text/search.d.ts.map +1 -1
  55. package/dist/adapters/sqlite/tools/vector/metadata.d.ts.map +1 -1
  56. package/dist/adapters/sqlite/tools/vector/search.d.ts.map +1 -1
  57. package/dist/adapters/sqlite/tools/vector/storage.d.ts.map +1 -1
  58. package/dist/adapters/sqlite/tools/virtual/vtable/analyze-csv.d.ts.map +1 -1
  59. package/dist/adapters/sqlite/tools/virtual/vtable/csv.d.ts.map +1 -1
  60. package/dist/adapters/sqlite/tools/virtual/vtable/info.d.ts.map +1 -1
  61. package/dist/adapters/sqlite/types.d.ts +5 -1
  62. package/dist/adapters/sqlite/types.d.ts.map +1 -1
  63. package/dist/adapters/sqlite-native/index.d.ts +1 -1
  64. package/dist/adapters/sqlite-native/index.d.ts.map +1 -1
  65. package/dist/adapters/sqlite-native/native-sqlite-adapter.d.ts.map +1 -1
  66. package/dist/adapters/sqlite-native/tools/spatialite/tools.d.ts.map +1 -1
  67. package/dist/adapters/sqlite-native/tools/window/helpers.d.ts +52 -0
  68. package/dist/adapters/sqlite-native/tools/window/helpers.d.ts.map +1 -0
  69. package/dist/adapters/sqlite-native/tools/window/index.d.ts +7 -0
  70. package/dist/adapters/sqlite-native/tools/window/index.d.ts.map +1 -0
  71. package/dist/adapters/sqlite-native/tools/window/lag-lead.d.ts +7 -0
  72. package/dist/adapters/sqlite-native/tools/window/lag-lead.d.ts.map +1 -0
  73. package/dist/adapters/sqlite-native/tools/window/moving-average.d.ts +7 -0
  74. package/dist/adapters/sqlite-native/tools/window/moving-average.d.ts.map +1 -0
  75. package/dist/adapters/sqlite-native/tools/window/ntile.d.ts +7 -0
  76. package/dist/adapters/sqlite-native/tools/window/ntile.d.ts.map +1 -0
  77. package/dist/adapters/sqlite-native/tools/window/rank.d.ts +7 -0
  78. package/dist/adapters/sqlite-native/tools/window/rank.d.ts.map +1 -0
  79. package/dist/adapters/sqlite-native/tools/window/row-number.d.ts +7 -0
  80. package/dist/adapters/sqlite-native/tools/window/row-number.d.ts.map +1 -0
  81. package/dist/adapters/sqlite-native/tools/window/running-total.d.ts +7 -0
  82. package/dist/adapters/sqlite-native/tools/window/running-total.d.ts.map +1 -0
  83. package/dist/adapters/sqlite-native/tools/window/schemas.d.ts +159 -0
  84. package/dist/adapters/sqlite-native/tools/window/schemas.d.ts.map +1 -0
  85. package/dist/adapters/sqlite-native/transaction-methods.d.ts.map +1 -1
  86. package/dist/audit/interceptor.d.ts.map +1 -1
  87. package/dist/audit/logger.d.ts +26 -25
  88. package/dist/audit/logger.d.ts.map +1 -1
  89. package/dist/auth/authorization-server-discovery.d.ts.map +1 -1
  90. package/dist/auth/scopes/mapping.d.ts.map +1 -1
  91. package/dist/auth/token-validator.d.ts.map +1 -1
  92. package/dist/{chunk-W5WQVNVX.js → chunk-27C5EE6U.js} +23 -17
  93. package/dist/chunk-2TYICG7B.js +1183 -0
  94. package/dist/{chunk-37G4DLL6.js → chunk-LLIAIJPO.js} +1146 -539
  95. package/dist/{chunk-NV34JSQM.js → chunk-UE7GVOBV.js} +1757 -1331
  96. package/dist/{chunk-L552U3QS.js → chunk-XLIYO3YA.js} +401 -77
  97. package/dist/cli.js +116 -11
  98. package/dist/codemode/api-constants.d.ts.map +1 -1
  99. package/dist/codemode/api.d.ts.map +1 -1
  100. package/dist/codemode/sandbox-factory.d.ts.map +1 -1
  101. package/dist/codemode/sandbox.d.ts +0 -3
  102. package/dist/codemode/sandbox.d.ts.map +1 -1
  103. package/dist/codemode/security.d.ts.map +1 -1
  104. package/dist/codemode/types.d.ts +2 -0
  105. package/dist/codemode/types.d.ts.map +1 -1
  106. package/dist/constants/server-instructions.d.ts +1 -1
  107. package/dist/constants/server-instructions.d.ts.map +1 -1
  108. package/dist/filtering/tool-constants.d.ts +11 -11
  109. package/dist/filtering/tool-constants.d.ts.map +1 -1
  110. package/dist/{http-HWTUVFIA.js → http-ZDMK46QU.js} +195 -32
  111. package/dist/index.js +4 -4
  112. package/dist/observability/metrics.d.ts +36 -0
  113. package/dist/observability/metrics.d.ts.map +1 -0
  114. package/dist/observability/system-db.d.ts +13 -0
  115. package/dist/observability/system-db.d.ts.map +1 -0
  116. package/dist/server/mcp-server.d.ts +3 -0
  117. package/dist/server/mcp-server.d.ts.map +1 -1
  118. package/dist/server/registration/admin-tools.d.ts +6 -0
  119. package/dist/server/registration/admin-tools.d.ts.map +1 -0
  120. package/dist/server/registration/audit-tools/backup.d.ts +8 -0
  121. package/dist/server/registration/audit-tools/backup.d.ts.map +1 -0
  122. package/dist/server/registration/audit-tools/helpers.d.ts +11 -0
  123. package/dist/server/registration/audit-tools/helpers.d.ts.map +1 -0
  124. package/dist/server/registration/audit-tools/index.d.ts +4 -0
  125. package/dist/server/registration/audit-tools/index.d.ts.map +1 -0
  126. package/dist/server/registration/audit-tools/resource.d.ts +8 -0
  127. package/dist/server/registration/audit-tools/resource.d.ts.map +1 -0
  128. package/dist/server/registration/audit-tools/search.d.ts +7 -0
  129. package/dist/server/registration/audit-tools/search.d.ts.map +1 -0
  130. package/dist/server/registration/help-resources.d.ts.map +1 -1
  131. package/dist/server/registration/index.d.ts +3 -1
  132. package/dist/server/registration/index.d.ts.map +1 -1
  133. package/dist/server/registration/observability-resources.d.ts +11 -0
  134. package/dist/server/registration/observability-resources.d.ts.map +1 -0
  135. package/dist/server/subscription-manager.d.ts +35 -0
  136. package/dist/server/subscription-manager.d.ts.map +1 -0
  137. package/dist/{sqlite-TB56Z7G3.js → sqlite-XWZPEZJO.js} +195 -19
  138. package/dist/{sqlite-native-IF75BB3O.js → sqlite-native-5GQ3WKCZ.js} +311 -147
  139. package/dist/transports/http/middleware.d.ts.map +1 -1
  140. package/dist/transports/http/oauth.d.ts.map +1 -1
  141. package/dist/transports/http/sessions/legacy-sse.d.ts.map +1 -1
  142. package/dist/transports/http/sessions/stateful.d.ts +5 -1
  143. package/dist/transports/http/sessions/stateful.d.ts.map +1 -1
  144. package/dist/transports/http/transport.d.ts +2 -0
  145. package/dist/transports/http/transport.d.ts.map +1 -1
  146. package/dist/transports/http/types.d.ts +11 -0
  147. package/dist/transports/http/types.d.ts.map +1 -1
  148. package/dist/types/auth.d.ts +4 -0
  149. package/dist/types/auth.d.ts.map +1 -1
  150. package/dist/types/server.d.ts +5 -0
  151. package/dist/types/server.d.ts.map +1 -1
  152. package/dist/utils/annotations.d.ts +8 -0
  153. package/dist/utils/annotations.d.ts.map +1 -1
  154. package/dist/utils/errors/categories.d.ts +16 -11
  155. package/dist/utils/errors/categories.d.ts.map +1 -1
  156. package/dist/utils/errors/classes.d.ts +37 -0
  157. package/dist/utils/errors/classes.d.ts.map +1 -1
  158. package/dist/utils/errors/format.d.ts +2 -2
  159. package/dist/utils/errors/format.d.ts.map +1 -1
  160. package/dist/utils/errors/index.d.ts +1 -1
  161. package/dist/utils/errors/index.d.ts.map +1 -1
  162. package/dist/utils/errors/suggestions.d.ts +1 -0
  163. package/dist/utils/errors/suggestions.d.ts.map +1 -1
  164. package/dist/utils/index.d.ts +3 -0
  165. package/dist/utils/index.d.ts.map +1 -1
  166. package/dist/utils/logger/types.d.ts +1 -1
  167. package/dist/utils/logger/types.d.ts.map +1 -1
  168. package/dist/utils/security-utils.d.ts +29 -0
  169. package/dist/utils/security-utils.d.ts.map +1 -0
  170. package/dist/utils/stream-utils.d.ts +14 -0
  171. package/dist/utils/stream-utils.d.ts.map +1 -0
  172. package/dist/utils/tokens.d.ts +15 -0
  173. package/dist/utils/tokens.d.ts.map +1 -0
  174. package/dist/utils/where-clause.d.ts +0 -13
  175. package/dist/utils/where-clause.d.ts.map +1 -1
  176. package/package.json +43 -44
  177. package/dist/adapters/sqlite-native/tools/window.d.ts +0 -7
  178. package/dist/adapters/sqlite-native/tools/window.d.ts.map +0 -1
  179. package/dist/chunk-E5IESRTK.js +0 -489
  180. package/dist/server/registration/audit-tools.d.ts +0 -13
  181. package/dist/server/registration/audit-tools.d.ts.map +0 -1
  182. package/dist/utils/insights-manager.d.ts +0 -39
  183. package/dist/utils/insights-manager.d.ts.map +0 -1
@@ -1,25 +1,28 @@
1
- import { VERSION, NAME, READ_ONLY, SERVER_ICONS, ASSISTANT_FOCUSED, redactObject, getRequiredScope, AuditListBackupsOutputSchema, AuditListBackupsSchema, AuditGetBackupOutputSchema, AuditCleanupOutputSchema, AuditDiffBackupOutputSchema, AuditRestoreBackupOutputSchema } from './chunk-E5IESRTK.js';
2
- import { parseToolFilter, getToolFilterFromEnv, logger, getFilterSummary, DbMcpError, getAuthContext, scopesGrantToolAccess, registerToolScopes, InsufficientScopeError, sanitizeErrorMessage, formatHandlerError } from './chunk-L552U3QS.js';
1
+ import { VERSION, NAME, READ_ONLY, SERVER_ICONS, ASSISTANT_FOCUSED, toSDK, redactObject, getRequiredScope, estimateTokens, AuditListBackupsOutputSchema, AuditListBackupsSchema, AuditGetBackupOutputSchema, AuditCleanupOutputSchema, AuditDiffBackupOutputSchema, AuditRestoreBackupOutputSchema, AuditSearchOutputSchema, AuditSearchSchema } from './chunk-2TYICG7B.js';
2
+ import { ErrorCategory, logger, parseToolFilter, getToolFilterFromEnv, ValidationError, getFilterSummary, DbMcpError, getAuthContext, scopesGrantToolAccess, registerToolScopes, metrics, InsufficientScopeError, formatHandlerError, InternalError, sanitizeErrorMessage } from './chunk-XLIYO3YA.js';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { open, stat, mkdir, rename, readdir, unlink, mkdtemp, writeFile, rmdir, readFile, appendFile } from 'fs/promises';
6
5
  import { dirname, join, basename, sep } from 'path';
6
+ import { mkdirSync } from 'fs';
7
+ import { readdir, stat, unlink, mkdtemp, writeFile, rename, rmdir, readFile, mkdir } from 'fs/promises';
7
8
  import { gzip, gunzipSync } from 'zlib';
8
9
  import { promisify } from 'util';
9
10
  import { performance } from 'perf_hooks';
10
11
  import { z } from 'zod';
12
+ import { SubscribeRequestSchema, UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';
11
13
 
12
14
  // src/constants/server-instructions.ts
13
15
  var INSTRUCTIONS = `# db-mcp (SQLite MCP Server)
14
16
 
15
17
  ## Quick Access
16
18
 
17
- | Purpose | Action |
18
- | --------------- | -------------------------- |
19
- | Health check | \`server_health\` tool |
20
- | Server info | \`server_info\` tool |
21
- | Database schema | \`sqlite://schema\` resource |
22
- | Tool help | \`sqlite://help\` resource |
19
+ | Purpose | Action |
20
+ | ------------------ | -------------------------- |
21
+ | Health check | \`server_health\` tool |
22
+ | Live server health | \`sqlite://health\` resource |
23
+ | Server info | \`server_info\` tool |
24
+ | Database schema | \`sqlite://schema\` resource |
25
+ | Tool help | \`sqlite://help\` resource |
23
26
 
24
27
  ## Built-in Tools
25
28
 
@@ -31,9 +34,7 @@ Read \`sqlite://help\` for gotchas and critical usage patterns.
31
34
  Read \`sqlite://help/{group}\` for group-specific tool reference (core, json, text, stats, vector, geo, admin, transactions, introspection, migration).
32
35
  Only help resources for your enabled tool groups are registered.`;
33
36
  var HELP_CONTENT = /* @__PURE__ */ new Map([
34
- [
35
- "admin",
36
- `# db-mcp Help \u2014 Database Administration (32N/31W tools) + Server Audit (5 tools)
37
+ ["admin", `# db-mcp Help \u2014 Database Administration (31N/30W tools) + Server Audit (7 tools)
37
38
 
38
39
  ## Maintenance
39
40
 
@@ -42,7 +43,7 @@ sqlite_integrity_check({ maxErrors: 10 }); // check for corruption
42
43
  sqlite_optimize({ analyze: true, reindex: true }); // optimize performance
43
44
  sqlite_vacuum(); // reclaim space
44
45
  sqlite_analyze({ table: "orders" }); // update statistics for query planner
45
- sqlite_dbstat({ summarize: true }); // storage stats (\u26A0\uFE0F summarize native-only; WASM returns counts only)
46
+ sqlite_dbstat(); // storage stats (\u26A0\uFE0F summarize native-only; WASM returns counts only)
46
47
  sqlite_reindex(); // rebuild all indexes
47
48
  sqlite_reindex({ target: "idx_users_email" }); // rebuild specific index
48
49
  sqlite_reindex({ target: "orders" }); // rebuild all indexes for a table
@@ -50,6 +51,8 @@ sqlite_reindex({ target: "orders" }); // rebuild all indexes for a table
50
51
 
51
52
  ## Backup/Restore (Native only)
52
53
 
54
+ \u26A0\uFE0F Paths must be absolute and within explicitly authorized \`ALLOWED_IO_ROOTS\`.
55
+
53
56
  \`\`\`javascript
54
57
  sqlite_backup({ targetPath: "/path/to/backup.db" });
55
58
  sqlite_vacuum_into({ outputPath: "/path/to/compact.db" }); // create defragmented copy
@@ -58,14 +61,16 @@ sqlite_verify_backup({ backupPath: "/path/to/backup.db" }); // check integrity w
58
61
  sqlite_restore({ sourcePath: "/path/to/backup.db" }); // \u26A0\uFE0F WARNING: Replaces current database
59
62
  \`\`\`
60
63
 
61
- ## Audit Backups
64
+ ## Server Management & Audit
62
65
 
63
66
  \`\`\`javascript
67
+ sqlite_server_config({ action: "set", setting: "logLevel", value: "debug" }); // get or update runtime server configuration
64
68
  sqlite_audit_list_backups({ limit: 10, offset: 0 }); // list pre-mutation DDL snapshots
65
69
  sqlite_audit_get_backup({ filename: "snapshot_123.json" }); // retrieve specific snapshot
66
70
  sqlite_audit_diff_backup({ filename: "snapshot_123.json" }); // compare snapshot against live schema
67
71
  sqlite_audit_restore_backup({ filename: "snapshot_123.json", dryRun: true }); // restore schema from snapshot
68
72
  sqlite_audit_cleanup(); // apply retention policy and delete old snapshots
73
+ sqlite_audit_search({ tool: "sqlite_drop_table", limit: 10 }); // search and filter structured audit logs
69
74
  \`\`\`
70
75
 
71
76
  ## PRAGMA
@@ -76,7 +81,7 @@ sqlite_pragma_settings({ pragma: "cache_size", value: 10000 }); // set value
76
81
  sqlite_pragma_table_info({ table: "users" }); // column details
77
82
  sqlite_pragma_compile_options({ filter: "FTS" }); // \u26A0\uFE0F WASM may show FTS3, not FTS5
78
83
  sqlite_pragma_database_list(); // list attached databases
79
- sqlite_attach_database({ filepath: "/path/to/other.db", alias: "archive" }); // attach external DB
84
+ sqlite_attach_database({ filepath: "/path/to/other.db", alias: "archive" }); // attach external DB (\u26A0\uFE0F subject to ALLOWED_IO_ROOTS)
80
85
  sqlite_detach_database({ alias: "archive" }); // detach DB
81
86
  sqlite_pragma_optimize(); // run PRAGMA optimize
82
87
  \`\`\`
@@ -132,7 +137,7 @@ sqlite_create_rtree_table({ tableName: "locations_idx", dimensions: 2 }); // 2D:
132
137
 
133
138
  ## CSV Virtual Tables (Native only)
134
139
 
135
- \u26A0\uFE0F Requires ABSOLUTE file paths
140
+ \u26A0\uFE0F Requires ABSOLUTE file paths that must fall within explicitly authorized \`ALLOWED_IO_ROOTS\`.
136
141
 
137
142
  \`\`\`javascript
138
143
  sqlite_analyze_csv_schema({ filePath: "/absolute/path/to/data.csv" }); // analyze CSV structure
@@ -140,22 +145,13 @@ sqlite_create_csv_table({
140
145
  tableName: "csv_data",
141
146
  filePath: "/absolute/path/to/data.csv",
142
147
  });
143
- \`\`\`
144
-
145
- ## Business Insights
146
-
147
- \`\`\`javascript
148
- sqlite_append_insight({ insight: "Q4 revenue increased 23% YoY" }); // add to memo://insights
149
- \`\`\``
150
- ],
151
- [
152
- "core",
153
- `# db-mcp Help \u2014 Core Operations (21 tools)
148
+ \`\`\``],
149
+ ["core", `# db-mcp Help \u2014 Core Operations (25 tools)
154
150
 
155
151
  ## Basic Queries
156
152
 
157
- - \`sqlite_read_query({ query: "SELECT * FROM users LIMIT 10" })\` \u2014 execute SELECT, PRAGMA, EXPLAIN, or WITH statements
158
- - \`sqlite_write_query({ query: "INSERT INTO users (name) VALUES ('Alice')" })\` \u2014 execute INSERT, UPDATE, DELETE, REPLACE, or trigger DDL (CREATE/DROP TRIGGER)
153
+ - \`sqlite_read_query({ query: "SELECT * FROM users LIMIT 10", cursor: "...", stream: true, chunkSize: 10 })\` \u2014 execute SELECT, PRAGMA, EXPLAIN, or WITH statements. Supports \`cursor\` for offset-based pagination (returns \`nextCursor\`). Set \`stream: true\` to return row-by-row chunks via progress notifications instead of full response buffering (requires client progressToken support; gracefully falls back if unavailable). **Agent Tip:** Avoid \`SELECT *\` on wide tables with large text/JSON columns to conserve token context; use \`sqlite_describe_table\` first and select specific columns.
154
+ - \`sqlite_write_query({ query: "INSERT INTO users (name) VALUES ('Alice')" })\` \u2014 execute INSERT, UPDATE, DELETE, REPLACE, or trigger DDL (CREATE/DROP TRIGGER). Supports \`expectedVersion\` for OCC.
159
155
 
160
156
  ## Tables & Schema
161
157
 
@@ -175,19 +171,30 @@ sqlite_append_insight({ insight: "Q4 revenue increased 23% YoY" }); // add to me
175
171
  - \`sqlite_create_index({ indexName: "idx_users_email", table: "users", columns: ["email"], unique?: true })\` \u2014 create a new index
176
172
  - \`sqlite_drop_index({ indexName: "idx_users_email", ifExists?: true })\` \u2014 drop an existing index
177
173
 
174
+ ## Versioning & Concurrency (OCC)
175
+
176
+ - \`sqlite_enable_versioning({ table: "users" })\` \u2014 add a \`_version\` column and a BEFORE UPDATE trigger to enforce optimistic concurrency control on a table
177
+ - \`sqlite_disable_versioning({ table: "users" })\` \u2014 remove the \`_version\` column and concurrency trigger
178
+ - \`sqlite_check_version({ table: "users", rowId: 1 })\` \u2014 get the current \`_version\` of a row
179
+ - \`sqlite_conditional_update({ table: "users", conditions: [{ column: "id", operator: "=", value: 1 }], expectedVersion: 2, data: { name: "Bob" } })\` \u2014 safely update a row, incrementing \`_version\` atomically. Will fail if \`expectedVersion\` does not match
180
+
178
181
  ## Convenience Tools (High-Level Data Operations)
179
182
 
180
- - \`sqlite_upsert({ table: "users", data: { id: 1, name: "Alice" }, conflictColumns: ["id"], updateColumns: ["name"], returning: true })\` \u2014 insert or update a row using \`ON CONFLICT\` (or \`REPLACE\` fallback). Supports \`returning: true\` or array of columns.
183
+ - \`sqlite_upsert({ table: "users", data: { id: 1, name: "Alice" }, conflictColumns: ["id"], updateColumns: ["name"], returning: true })\` \u2014 insert or update a row using \`ON CONFLICT\` (or \`REPLACE\` fallback). Supports \`returning: true\` or array of columns, and \`expectedVersion\` for OCC.
181
184
  - \`sqlite_batch_insert({ table: "users", rows: [{ 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.
182
185
  - \`sqlite_count({ table: "users", where?: "status = 'active'" })\` \u2014 count rows in a table (faster than a full query)
183
186
  - \`sqlite_exists({ table: "users", where: "email = 'test@example.com'" })\` \u2014 check if a row exists (stops at first match)
184
187
  - \`sqlite_truncate({ table: "users" })\` \u2014 quickly delete all rows from a table (executes \`DELETE FROM table\`)
185
188
  - \`sqlite_date_add({ table: "users", column: "created_at", amount: 7, unit: "days", whereClause: "id = 1" })\` \u2014 add or subtract time intervals from a date column. By default returns only the computed column; use \`selectColumns\` to return additional context.
186
- - \`sqlite_date_diff({ table: "users", column1: "ended_at", column2: "started_at", unit: "days", whereClause: "id = 1" })\` \u2014 calculate the difference between two date columns. By default returns only the computed column; use \`selectColumns\` to return additional context.`
187
- ],
188
- [
189
- "geo",
190
- `# db-mcp Help \u2014 Geospatial Operations (11N/4W: 4 basic + 7 SpatiaLite [NATIVE ONLY])
189
+ - \`sqlite_date_diff({ table: "users", column1: "ended_at", column2: "started_at", unit: "days", whereClause: "id = 1" })\` \u2014 calculate the difference between two date columns. By default returns only the computed column; use \`selectColumns\` to return additional context.
190
+
191
+ ## \u26A0\uFE0F Core Gotchas
192
+
193
+ 1. **sqlite_write_query**: DML only (INSERT/UPDATE/DELETE/REPLACE) \u2014 use \`sqlite_read_query\` for SELECT, and dedicated DDL tools for schema modifications.
194
+ 2. **sqlite_upsert**: Always specify \`conflictColumns\` \u2014 without it, falls back to \`REPLACE\` which deletes and re-inserts the row, potentially losing columns not included in \`data\`.
195
+ 3. **Optimistic Concurrency Control (OCC)**: When a table is versioned (\`sqlite_enable_versioning\`), you MUST supply \`expectedVersion\` to \`sqlite_write_query\`, \`sqlite_upsert\`, and \`sqlite_conditional_update\`. Omitting it will result in a \`ConflictError\` to strictly prevent lost updates.
196
+ 4. **sqlite_batch_insert**: All rows must have the same keys \u2014 inconsistent column sets across rows will cause errors or unexpected NULLs.`],
197
+ ["geo", `# db-mcp Help \u2014 Geospatial Operations (11N/4W: 4 basic + 7 SpatiaLite [NATIVE ONLY])
191
198
 
192
199
  ## Basic Geo (always available \u2014 Haversine formula)
193
200
 
@@ -292,35 +299,15 @@ sqlite_spatialite_index({
292
299
  geometryColumn: "geom",
293
300
  action: "create",
294
301
  }); // create, drop, or check
295
- \`\`\``
296
- ],
297
- [
298
- "gotchas",
299
- `# db-mcp Help \u2014 Gotchas & Code Mode
300
-
301
- ## \u26A0\uFE0F Critical Gotchas
302
-
303
- 1. **sqlite_write_query**: DML only (INSERT/UPDATE/DELETE/REPLACE) \u2014 use \`sqlite_read_query\` for SELECT, and dedicated DDL tools like \`sqlite_create_table\`, \`sqlite_create_trigger\`, and \`sqlite_drop_trigger\` for schema modifications.
304
- 2. **Regex patterns**: Double-escape backslashes (\`\\\\\\\\\`) when passing through JSON/MCP
305
- 3. **FTS5 virtual tables**: \`*_fts\` and shadow tables \`*_fts_*\` are hidden from \`sqlite_list_tables\` for cleaner output
306
- 4. **FTS5 boolean logic**: Uses AND by default \u2014 \`"machine learning"\` = rows with BOTH words. Use OR explicitly: \`"machine OR learning"\`
307
- 5. **json_each row multiplication**: Expands arrays to rows \u2014 use \`limit\` param for large arrays
308
- 6. **json_group_object without groupByColumn**: Each row creates a key-value pair; duplicate keys result if key values aren't unique
309
- 7. **allowExpressions**: For column extraction ONLY (e.g., \`json_extract\`), NOT aggregate functions \u2014 use \`aggregateFunction\` param instead
310
- 8. **sqlite_json_normalize_column**: Defaults to \`preserve\` (maintains original format); use \`outputFormat: 'text'\` to force text
311
- 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
312
- 10. **SpatiaLite distances**: \`nearest_neighbor\`/\`distance_matrix\` return CARTESIAN distance (degrees), not geodetic (km/miles)
313
- 11. **SpatiaLite buffer**: Auto-simplifies output by default (tolerance=0.0001). Use \`simplifyTolerance: 0\` to disable
314
- 12. **sqlite_stats_top_n**: Returns all columns by default which creates large payloads for wide tables \u2014 always pass \`selectColumns\` to control output size
315
- 13. **CSV virtual tables**: Require ABSOLUTE file paths
316
- 14. **sqlite_create_series_table**: Creates a REGULAR table (not virtual) \u2014 use \`sqlite_drop_table\` to remove
317
- 15. **sqlite_dbstat**: \`summarize\` only works in native; WASM returns counts only
318
- 16. **PRAGMA compile options**: WASM may show FTS3, not FTS5
319
- 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\`).
320
- 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.
321
- 19. **sqlite_batch_insert**: All rows must have the same keys \u2014 inconsistent column sets across rows will cause errors or unexpected NULLs
322
- 20. **sqlite_schema_diff**: \`baseline\` and \`target\` accept either the string \`"current"\` (queries live DB) or an inline snapshot object from a prior \`sqlite_schema_snapshot\` call. At least one side must be \`"current"\` unless doing an offline comparison
323
- 21. **sqlite_upsert**: Always specify \`conflictColumns\` \u2014 without it, falls back to \`REPLACE\` which deletes and re-inserts the row, potentially losing columns not included in \`data\`
302
+ \`\`\``],
303
+ ["gotchas", `# db-mcp Help \u2014 Gotchas & Code Mode
304
+
305
+ ## Server-Level Rules
306
+
307
+ 1. **CSV virtual tables & Backups**: Require ABSOLUTE file paths. Operations will be strictly blocked if paths do not fall within the explicitly authorized \`ALLOWED_IO_ROOTS\` directory list. Unconfigured stdio transports default to no filesystem access.
308
+ 2. **Resource Subscriptions**: The \`sqlite://schema\` and \`sqlite://health\` resources support MCP subscriptions, allowing the client to receive real-time push notifications when DDL changes occur or health metrics update without needing to poll.
309
+ 3. **HTTP Session Timeouts**: Stateful HTTP sessions automatically expire after 30 minutes of inactivity, or 24 hours total. Sessions are swept automatically; ensure client workflows re-authenticate or handle broken sessions gracefully.
310
+ 4. **Structured Errors**: The server returns strongly-typed JSON errors instead of generic stack traces. Execution timeouts surface as recoverable \`TimeoutError\` (category: \`timeout\`). Rate limit violations surface as \`RateLimitError\` (category: \`rate_limit\`). Input/schema rejections surface as \`ValidationError\` (category: \`validation\`). Use this metadata (\`code\`, \`category\`, \`recoverable\`) to programmatically retry or backoff.
324
311
 
325
312
  ## WASM vs Native
326
313
 
@@ -333,6 +320,7 @@ sqlite_spatialite_index({
333
320
  | Backup/Restore/Dump/VacuumInto/Verify (5 tools) | \u2705 | \u274C | Graceful error |
334
321
  | R-Tree spatial indexing | \u2705 | \u274C | Graceful error |
335
322
  | CSV virtual tables | \u2705 | \u274C | Graceful error |
323
+ | SQLCipher Encryption at Rest | \u2705 | \u274C | Graceful error |
336
324
  | generate_series | JS fallback | JS fallback | \u2014 |
337
325
  | dbstat | \u2705 native (per-table) | \u274C | JS (counts only) |
338
326
  | soundex() | \u2705 native | \u274C | JS |
@@ -346,22 +334,19 @@ sqlite_spatialite_index({
346
334
  **Usage**: \`sqlite_execute_code({ code: "const tables = await sqlite.core.listTables(); return tables;" })\`
347
335
  **Discover**: \`sqlite.help()\` for all groups, \`sqlite.<group>.help()\` for methods.
348
336
  **Progress**: Use \`await sqlite.reportProgress(current, total, "Message")\` for custom long-running tasks.
349
- **Groups**: \`sqlite.core\`, \`sqlite.json\`, \`sqlite.text\`, \`sqlite.stats\`, \`sqlite.vector\`, \`sqlite.admin\`, \`sqlite.transactions\` (Native-only), \`sqlite.geo\`, \`sqlite.introspection\`, \`sqlite.migration\`
337
+ **Groups**: \`sqlite.core\`, \`sqlite.json\`, \`sqlite.text\`, \`sqlite.stats\`, \`sqlite.vector\`, \`sqlite.admin\`, \`sqlite.transactions\` (Native-only), \`sqlite.geo\`, \`sqlite.introspection\`, \`sqlite.migration\` (opt-in)
350
338
 
351
339
  > **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.
352
340
 
353
341
  ## Code Mode API Mapping
354
342
 
355
- \`sqlite_group_action\` \u2192 \`sqlite.group.action()\` (group prefixes dropped: \`sqlite_json_insert\` \u2192 \`sqlite.json.insert()\`)
356
- **Exception**: \`stats\` and \`migration\` keep their prefix: \`sqlite_stats_basic\` \u2192 \`sqlite.stats.statsBasic()\`, \`sqlite_migration_apply\` \u2192 \`sqlite.migration.migrationApply()\`
343
+ Code Mode maps standard tools to an object-oriented sandbox API (\`sqlite_group_action\` \u2192 \`sqlite.group.action()\`).
357
344
 
358
- **Positional args work**: \`sqlite.core.readQuery("SELECT...")\`, \`sqlite.json.insert("docs", "data", {...})\`
359
-
360
- **Discovery**: \`sqlite.help()\` returns all groups and methods. \`sqlite.core.help()\`, \`sqlite.json.help()\` for group-specific methods.`
361
- ],
362
- [
363
- "introspection",
364
- `# db-mcp Help \u2014 Schema Introspection (10 tools)
345
+ - **Flexible Parameters**: Parameters are forgiven for casing discrepancies (e.g. \`conflict_columns\` vs \`conflictColumns\`).
346
+ - **Flexible Methods**: Methods mapped into the Code Mode proxy forgive casing discrepancies (e.g. \`sqlite.core.read_query\` executes \`sqlite.core.readQuery()\`).
347
+ - Methods support both an options object and **positional arguments** (e.g. \`sqlite.core.readQuery("SELECT...")\`, \`sqlite.json.insert("docs", "data", {...})\`).
348
+ - **Discovery**: Call \`sqlite.help()\` or \`sqlite.core.help()\` to see available methods directly within the sandbox.`],
349
+ ["introspection", `# db-mcp Help \u2014 Schema Introspection (10 tools)
365
350
 
366
351
  All introspection tools are **read-only** \u2014 they query PRAGMAs and sqlite_master, never modify data.
367
352
 
@@ -434,6 +419,13 @@ sqlite_storage_analysis({ limit: 10 }); // top 10 tables only
434
419
  // Audit index effectiveness \u2014 find redundant, missing FK, unindexed large tables
435
420
  sqlite_index_audit({ excludeSystemTables: true });
436
421
  sqlite_index_audit({ table: "orders", minSeverity: "warning" }); // reduce payload
422
+ // Run EXPLAIN QUERY PLAN on target queries to recommend composite/partial indexes
423
+ sqlite_index_audit({
424
+ recommendComposite: true,
425
+ queriesToAnalyze: [
426
+ "SELECT * FROM orders WHERE user_id = 1 AND status = 'active'",
427
+ ],
428
+ });
437
429
 
438
430
  // EXPLAIN QUERY PLAN with scan-type classification and optimization suggestions (SELECT/WITH only)
439
431
  sqlite_query_plan({ sql: "SELECT * FROM orders WHERE status = 'active'" });
@@ -442,11 +434,9 @@ sqlite_query_plan({ sql: "SELECT * FROM orders WHERE status = 'active'" });
442
434
  ## \u26A0\uFE0F Gotchas
443
435
 
444
436
  - \`excludeSystemTables\` defaults to \`true\` \u2014 SpatiaLite system tables are hidden for cleaner output. Pass \`false\` to include them
445
- - \`sqlite_migration_risks\` analyzes DDL text statically \u2014 it does NOT execute the statements`
446
- ],
447
- [
448
- "json",
449
- `# db-mcp Help \u2014 JSON Operations (25 tools)
437
+ - \`sqlite_migration_risks\` analyzes DDL text statically \u2014 it does NOT execute the statements
438
+ - \`sqlite_schema_diff\`: \`baseline\` and \`target\` accept either the string \`"current"\` (queries live DB) or an inline snapshot object from a prior \`sqlite_schema_snapshot\` call. At least one side must be \`"current"\` unless doing an offline comparison.`],
439
+ ["json", `# db-mcp Help \u2014 JSON Operations (25 tools)
450
440
 
451
441
  ## Collection & CRUD
452
442
 
@@ -520,11 +510,8 @@ sqlite_json_group_object({
520
510
 
521
511
  - \`sqlite_json_storage_info({ table, column })\` \u2014 check text vs JSONB format
522
512
  - \`sqlite_jsonb_convert({ table, column })\` \u2014 convert to JSONB for faster queries
523
- - \`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`
524
- ],
525
- [
526
- "migration",
527
- `# db-mcp Help \u2014 Migration Tracking (6 tools)
513
+ - \`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`],
514
+ ["migration", `# db-mcp Help \u2014 Migration Tracking (6 tools)
528
515
 
529
516
  \u26A0\uFE0F Must call \`sqlite_migration_init()\` before using any other migration tool \u2014 it creates the tracking table.
530
517
 
@@ -566,11 +553,8 @@ sqlite_migration_status();
566
553
  ## \u26A0\uFE0F Gotchas
567
554
 
568
555
  - Rollback requires \`rollbackSql\` to have been provided when the migration was recorded/applied
569
- - Migration group is **opt-in** \u2014 not included by default. Enable via \`--tool-filter\` flag: use \`dev-schema\` (core+introspection+migration+codemode) or \`full\` (all groups)`
570
- ],
571
- [
572
- "stats",
573
- `# db-mcp Help \u2014 Statistical Analysis (23N/17W: 17 core + 6 window [NATIVE ONLY])
556
+ - Migration group is **opt-in** \u2014 not included by default. Enable via \`--tool-filter\` flag: use \`dev-schema\` (core+introspection+migration+codemode) or \`full\` (all groups)`],
557
+ ["stats", `# db-mcp Help \u2014 Statistical Analysis (23N/17W: 17 core + 6 window [NATIVE ONLY])
574
558
 
575
559
  ## Core Statistics (always available)
576
560
 
@@ -689,11 +673,8 @@ sqlite_window_moving_avg({
689
673
  windowSize: 7,
690
674
  selectColumns: ["date"],
691
675
  });
692
- \`\`\``
693
- ],
694
- [
695
- "text",
696
- `# db-mcp Help \u2014 Text Processing & FTS5 (19N/14W: 14 text + 5 FTS5 [NATIVE ONLY])
676
+ \`\`\``],
677
+ ["text", `# db-mcp Help \u2014 Text Processing & FTS5 (20N/15W: 15 text + 5 FTS5 [NATIVE ONLY])
697
678
 
698
679
  ## Full-Text Search / FTS5 (5 tools, Native only)
699
680
 
@@ -713,6 +694,8 @@ sqlite_fts_search({
713
694
  table: "articles_fts",
714
695
  query: "machine learning",
715
696
  limit: 10,
697
+ includeFacets: true, // optionally return breakdown by technique
698
+ cursor: "...", // Optional offset-based cursor for pagination (returns nextCursor)
716
699
  });
717
700
  sqlite_fts_match_info({ table: "articles_fts", query: "machine learning" }); // bm25 ranking info
718
701
 
@@ -732,7 +715,7 @@ sqlite_fts_headline({
732
715
 
733
716
  \u26A0\uFE0F FTS5 virtual tables (\`*_fts\`) and shadow tables (\`*_fts_*\`) are hidden from \`sqlite_list_tables\` for cleaner output
734
717
 
735
- ## Text Processing (14 tools)
718
+ ## Text Processing (15 tools)
736
719
 
737
720
  \`\`\`javascript
738
721
  // Regex patterns: \u26A0\uFE0F Double-escape backslashes (\\\\) when passing through JSON/MCP
@@ -818,7 +801,19 @@ sqlite_advanced_search({
818
801
  searchTerm: "laptop",
819
802
  techniques: ["exact", "fuzzy", "phonetic"],
820
803
  fuzzyThreshold: 0.4,
804
+ includeFacets: true,
805
+ });
806
+
807
+ // Hybrid Search \u2014 combines FTS5 text search and vector embedding search via Reciprocal Rank Fusion (RRF)
808
+ sqlite_hybrid_search({
809
+ table: "articles_fts",
810
+ query: "machine learning",
811
+ vectorColumn: "embedding",
812
+ queryVector: [0.1, 0.2, 0.3], // vector data from external embedding API
813
+ metric: "cosine",
814
+ limit: 10,
821
815
  });
816
+ // \u26A0\uFE0F Requires an FTS5 table (Native only) and a vector column in the source table
822
817
 
823
818
  // Sentiment analysis \u2014 text analysis (can analyze raw text or database columns)
824
819
  sqlite_text_sentiment({ text: "This product is amazing and wonderful!" });
@@ -831,11 +826,8 @@ sqlite_text_sentiment({
831
826
  returnWords: true,
832
827
  });
833
828
  // \u2192 { sentiment: "neutral", score: 0, matchedPositive: ["great"], matchedNegative: ["slow"] }
834
- \`\`\``
835
- ],
836
- [
837
- "transactions",
838
- `# db-mcp Help \u2014 Transactions (8 tools, Native only)
829
+ \`\`\``],
830
+ ["transactions", `# db-mcp Help \u2014 Transactions (8 tools, Native only)
839
831
 
840
832
  ## Atomic Execution (preferred for simple cases)
841
833
 
@@ -866,11 +858,8 @@ sqlite_transaction_status(); // \u2192 { status: "active" | "none", active: true
866
858
 
867
859
  - Transaction tools are **Native only** \u2014 WASM adapter does not support transactions
868
860
  - Use \`sqlite_transaction_execute\` for simple multi-statement operations; manual \`begin\`/\`commit\` for complex flows with savepoints
869
- - \`sqlite_transaction_status\` is read-only and requires only \`read\` scope; all other transaction tools require \`write\` scope`
870
- ],
871
- [
872
- "vector",
873
- `# db-mcp Help \u2014 Vector/Semantic Search (11 tools)
861
+ - \`sqlite_transaction_status\` is read-only and requires only \`read\` scope; all other transaction tools require \`write\` scope`],
862
+ ["vector", `# db-mcp Help \u2014 Vector/Semantic Search (11 tools)
874
863
 
875
864
  \`\`\`javascript
876
865
  // Create vector table with metadata columns
@@ -896,24 +885,29 @@ sqlite_vector_stats({ table: "docs", vectorColumn: "emb" }); // returns sampleSi
896
885
  // Utility tools for preprocessing
897
886
  sqlite_vector_normalize({ vector: [3, 4, 0, 0] }); // returns unit vector [0.6, 0.8, 0, 0]
898
887
  sqlite_vector_distance({ vector1: [...], vector2: [...], metric: "cosine" }); // returns { value: <number> }
899
- \`\`\``
900
- ]
888
+ \`\`\`
889
+
890
+ ## \u26A0\uFE0F Vector Gotchas
891
+
892
+ - **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\`).`]
901
893
  ]);
894
+
895
+ // src/audit/logger.ts
902
896
  var BUFFER_HIGH_WATER = 50;
903
897
  var FLUSH_INTERVAL_MS = 100;
904
898
  var DEFAULT_RECENT_COUNT = 50;
905
899
  var STDERR_SENTINEL = "stderr";
906
- var TAIL_READ_BYTES = 65536;
907
900
  var AuditLogger = class {
908
901
  config;
909
902
  buffer = [];
910
903
  flushTimer = null;
911
904
  activeFlush = null;
912
905
  closed = false;
913
- dirEnsured = false;
914
906
  stderrMode;
915
- constructor(config) {
907
+ systemDb;
908
+ constructor(config, systemDb = null) {
916
909
  this.config = config;
910
+ this.systemDb = systemDb;
917
911
  this.stderrMode = config.logPath.toLowerCase() === STDERR_SENTINEL;
918
912
  if (config.enabled) {
919
913
  this.flushTimer = setInterval(() => {
@@ -923,39 +917,27 @@ var AuditLogger = class {
923
917
  }
924
918
  }
925
919
  /**
926
- * Eagerly create the log directory and touch the log file on startup.
927
- * Provides immediate confirmation that audit is configured correctly.
928
- * Non-throwing — falls back silently if the path is inaccessible.
920
+ * Initialization is handled externally by SystemDb.
929
921
  */
930
- async init() {
931
- if (this.stderrMode || !this.config.enabled) return;
932
- try {
933
- await this.ensureDirectory();
934
- const fh = await open(this.config.logPath, "a");
935
- await fh.close();
936
- } catch (err) {
937
- process.stderr.write(
938
- `[AUDIT] Failed to initialise log file: ${this.config.logPath}
939
- `
940
- );
941
- throw err;
942
- }
922
+ init() {
923
+ if (this.stderrMode || !this.config.enabled) return Promise.resolve();
924
+ return Promise.resolve();
943
925
  }
944
926
  /**
945
927
  * Append an audit entry to the buffer.
946
- * Non-blocking — the entry is serialised and queued; the
947
- * actual file write happens on the next flush cycle.
928
+ * Non-blocking — the entry is queued; the
929
+ * actual DB write happens on the next flush cycle.
948
930
  */
949
931
  log(entry) {
950
932
  if (this.closed || !this.config.enabled) return;
951
933
  const safeEntry = redactObject(entry, 0, 5);
952
- this.buffer.push(JSON.stringify(safeEntry));
934
+ this.buffer.push(safeEntry);
953
935
  if (this.buffer.length >= BUFFER_HIGH_WATER) {
954
936
  void this.flush();
955
937
  }
956
938
  }
957
939
  /**
958
- * Flush the buffer to disk.
940
+ * Flush the buffer to SystemDb or stderr.
959
941
  * Safe to call concurrently — serialises via `this.activeFlush` Promise.
960
942
  */
961
943
  async flush() {
@@ -965,25 +947,45 @@ var AuditLogger = class {
965
947
  }
966
948
  if (this.buffer.length === 0) return;
967
949
  const doFlush = async () => {
968
- await this.rotateIfNeeded();
969
- const lines = this.buffer;
950
+ await Promise.resolve();
951
+ const entries = this.buffer;
970
952
  this.buffer = [];
971
953
  try {
972
954
  if (this.stderrMode) {
955
+ const lines = entries.map((e) => JSON.stringify(e));
973
956
  process.stderr.write(lines.join("\n") + "\n");
974
- } else {
975
- await this.ensureDirectory();
976
- await appendFile(
977
- this.config.logPath,
978
- lines.join("\n") + "\n",
979
- "utf-8"
980
- );
957
+ } else if (this.systemDb) {
958
+ const db = this.systemDb.getDb();
959
+ const stmt = db.prepare(`
960
+ INSERT INTO audit_logs (timestamp, requestId, tool, category, scope, user, scopesJson, durationMs, success, tokenEstimate, error, argsJson, backupPath)
961
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
962
+ `);
963
+ const transaction = db.transaction((logs) => {
964
+ for (const log of logs) {
965
+ stmt.run(
966
+ log.timestamp,
967
+ log.requestId,
968
+ log.tool,
969
+ log.category,
970
+ log.scope,
971
+ log.user ?? null,
972
+ JSON.stringify(log.scopes),
973
+ log.durationMs,
974
+ log.success ? 1 : 0,
975
+ log.tokenEstimate ?? null,
976
+ log.error ?? null,
977
+ log.args ? JSON.stringify(log.args) : null,
978
+ log.backup ?? null
979
+ );
980
+ }
981
+ });
982
+ transaction(entries);
981
983
  }
982
984
  } catch (err) {
983
985
  const message = err instanceof Error ? err.message : String(err);
984
986
  process.stderr.write(`[AUDIT] Write failed: ${message}
985
987
  `);
986
- this.buffer.unshift(...lines);
988
+ this.buffer.unshift(...entries);
987
989
  }
988
990
  };
989
991
  this.activeFlush = doFlush();
@@ -1005,79 +1007,197 @@ var AuditLogger = class {
1005
1007
  await this.flush();
1006
1008
  }
1007
1009
  /**
1008
- * Read the most recent audit entries from the log file.
1009
- * Uses a streaming tail-read: only the last TAIL_READ_BYTES (64 KB) are
1010
- * read from disk, preventing O(n) memory spikes for large audit logs.
1011
- * Used by the `sqlite://audit` resource.
1012
- *
1013
- * @param count Maximum number of entries to return (default 50)
1010
+ * Read the most recent audit entries from the SystemDb.
1011
+ * Retrieve recent entries directly from the SystemDb.
1014
1012
  */
1015
- async recent(count = DEFAULT_RECENT_COUNT) {
1016
- if (this.stderrMode) return [];
1013
+ async recent(limit = DEFAULT_RECENT_COUNT) {
1014
+ if (this.stderrMode || !this.systemDb) {
1015
+ return [];
1016
+ }
1017
1017
  await this.flush();
1018
1018
  try {
1019
- let fh;
1020
- try {
1021
- fh = await open(this.config.logPath, "r");
1022
- } catch {
1023
- return [];
1024
- }
1025
- try {
1026
- const info = await stat(this.config.logPath);
1027
- const fileSize = info.size;
1028
- if (fileSize === 0) return [];
1029
- const readSize = Math.min(fileSize, TAIL_READ_BYTES);
1030
- const startOffset = fileSize - readSize;
1031
- const buf = Buffer.alloc(readSize);
1032
- await fh.read(buf, 0, readSize, startOffset);
1033
- const chunk = buf.toString("utf-8");
1034
- const rawLines = chunk.split("\n").filter(Boolean);
1035
- const lines = startOffset > 0 ? rawLines.slice(1) : rawLines;
1036
- const tail = lines.slice(-count);
1037
- return tail.reduce((acc, line) => {
1038
- try {
1039
- acc.push(JSON.parse(line));
1040
- } catch {
1041
- }
1042
- return acc;
1043
- }, []);
1044
- } finally {
1045
- await fh.close();
1046
- }
1019
+ const db = this.systemDb.getDb();
1020
+ const rows = db.prepare(
1021
+ `
1022
+ SELECT * FROM audit_logs
1023
+ ORDER BY timestamp DESC
1024
+ LIMIT ?
1025
+ `
1026
+ ).all(limit);
1027
+ return rows.map((row) => ({
1028
+ timestamp: row.timestamp,
1029
+ requestId: row.requestId,
1030
+ tool: row.tool,
1031
+ category: row.category,
1032
+ scope: row.scope,
1033
+ user: row.user,
1034
+ scopes: row.scopesJson ? JSON.parse(row.scopesJson) : [],
1035
+ durationMs: row.durationMs,
1036
+ success: row.success === 1,
1037
+ tokenEstimate: row.tokenEstimate ?? void 0,
1038
+ error: row.error ?? void 0,
1039
+ args: row.argsJson ? JSON.parse(row.argsJson) : void 0,
1040
+ backup: row.backupPath ?? void 0
1041
+ }));
1047
1042
  } catch {
1048
1043
  return [];
1049
1044
  }
1050
1045
  }
1051
1046
  /**
1052
- * Ensure the parent directory of the log file exists.
1047
+ * Search and filter audit entries from the SystemDb.
1053
1048
  */
1054
- async ensureDirectory() {
1055
- if (this.dirEnsured) return;
1049
+ async search(filters) {
1050
+ if (this.stderrMode || !this.systemDb) {
1051
+ return { entries: [], totalCount: 0 };
1052
+ }
1053
+ await this.flush();
1056
1054
  try {
1057
- await mkdir(dirname(this.config.logPath), { recursive: true });
1058
- this.dirEnsured = true;
1055
+ const db = this.systemDb.getDb();
1056
+ let sql = "SELECT * FROM audit_logs WHERE 1=1";
1057
+ let countSql = "SELECT COUNT(*) as c FROM audit_logs WHERE 1=1";
1058
+ const params = [];
1059
+ if (filters.tool) {
1060
+ sql += " AND tool = ?";
1061
+ countSql += " AND tool = ?";
1062
+ params.push(filters.tool);
1063
+ }
1064
+ if (filters.category) {
1065
+ sql += " AND category = ?";
1066
+ countSql += " AND category = ?";
1067
+ params.push(filters.category);
1068
+ }
1069
+ if (filters.success !== void 0) {
1070
+ sql += " AND success = ?";
1071
+ countSql += " AND success = ?";
1072
+ params.push(filters.success ? 1 : 0);
1073
+ }
1074
+ if (filters.requestId) {
1075
+ sql += " AND requestId = ?";
1076
+ countSql += " AND requestId = ?";
1077
+ params.push(filters.requestId);
1078
+ }
1079
+ if (filters.fromTimestamp) {
1080
+ sql += " AND timestamp >= ?";
1081
+ countSql += " AND timestamp >= ?";
1082
+ params.push(filters.fromTimestamp);
1083
+ }
1084
+ if (filters.toTimestamp) {
1085
+ sql += " AND timestamp <= ?";
1086
+ countSql += " AND timestamp <= ?";
1087
+ params.push(filters.toTimestamp);
1088
+ }
1089
+ const totalCount = db.prepare(countSql).get(...params).c;
1090
+ sql += " ORDER BY timestamp DESC LIMIT ? OFFSET ?";
1091
+ params.push(filters.limit ?? 50);
1092
+ params.push(filters.offset ?? 0);
1093
+ const rows = db.prepare(sql).all(...params);
1094
+ const entries = rows.map((row) => ({
1095
+ timestamp: row.timestamp,
1096
+ requestId: row.requestId,
1097
+ tool: row.tool,
1098
+ category: row.category,
1099
+ scope: row.scope,
1100
+ user: row.user,
1101
+ scopes: row.scopesJson ? JSON.parse(row.scopesJson) : [],
1102
+ durationMs: row.durationMs,
1103
+ success: row.success === 1,
1104
+ tokenEstimate: row.tokenEstimate ?? void 0,
1105
+ error: row.error ?? void 0,
1106
+ args: row.argsJson ? JSON.parse(row.argsJson) : void 0,
1107
+ backup: row.backupPath ?? void 0
1108
+ }));
1109
+ return { entries, totalCount };
1059
1110
  } catch {
1060
- this.dirEnsured = true;
1111
+ return { entries: [], totalCount: 0 };
1061
1112
  }
1062
1113
  }
1063
- /**
1064
- * Rotate the log file if it exceeds the configured size limit.
1065
- * Keeps up to 5 rotated files (`.1` through `.5`); older data is discarded.
1066
- * Rotation failure is non-fatal — audit must not block tool execution.
1067
- */
1068
- async rotateIfNeeded() {
1069
- if (this.stderrMode || !this.config.maxSizeBytes) return;
1114
+ };
1115
+ var SystemDb = class {
1116
+ db = null;
1117
+ config;
1118
+ constructor(config) {
1119
+ this.config = config;
1120
+ }
1121
+ async init() {
1070
1122
  try {
1071
- const info = await stat(this.config.logPath).catch(() => null);
1072
- if (!info || info.size < this.config.maxSizeBytes) return;
1073
- for (let i = 4; i >= 1; i--) {
1074
- const oldFile = `${this.config.logPath}.${String(i)}`;
1075
- const newFile = `${this.config.logPath}.${String(i + 1)}`;
1076
- await rename(oldFile, newFile).catch(() => null);
1123
+ const encryptionKey = process.env["DB_ENCRYPTION_KEY"];
1124
+ let BetterSqlite3;
1125
+ if (encryptionKey) {
1126
+ BetterSqlite3 = (await import('better-sqlite3-multiple-ciphers')).default;
1127
+ } else {
1128
+ BetterSqlite3 = (await import('better-sqlite3')).default;
1077
1129
  }
1078
- const rotatedPath = `${this.config.logPath}.1`;
1079
- await rename(this.config.logPath, rotatedPath);
1080
- } catch {
1130
+ if (this.config.dbPath !== ":memory:") {
1131
+ mkdirSync(dirname(this.config.dbPath), { recursive: true });
1132
+ }
1133
+ this.db = new BetterSqlite3(this.config.dbPath);
1134
+ if (encryptionKey && this.config.dbPath !== ":memory:" && this.config.dbPath !== "") {
1135
+ const escapedKey = encryptionKey.replace(/"/g, '""');
1136
+ this.db.pragma(`key = "${escapedKey}"`);
1137
+ }
1138
+ this.db.pragma("journal_mode = WAL");
1139
+ this.db.pragma("synchronous = NORMAL");
1140
+ this.db.exec(`
1141
+ CREATE TABLE IF NOT EXISTS audit_logs (
1142
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1143
+ timestamp TEXT NOT NULL,
1144
+ requestId TEXT NOT NULL,
1145
+ tool TEXT NOT NULL,
1146
+ category TEXT NOT NULL,
1147
+ scope TEXT NOT NULL,
1148
+ user TEXT,
1149
+ durationMs INTEGER NOT NULL,
1150
+ success INTEGER NOT NULL,
1151
+ tokenEstimate INTEGER,
1152
+ error TEXT,
1153
+ argsJson TEXT,
1154
+ scopesJson TEXT,
1155
+ backupPath TEXT
1156
+ );
1157
+
1158
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
1159
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_tool ON audit_logs(tool);
1160
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_category ON audit_logs(category);
1161
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_success ON audit_logs(success);
1162
+
1163
+ CREATE TABLE IF NOT EXISTS metrics_snapshots (
1164
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1165
+ timestamp TEXT NOT NULL,
1166
+ tool TEXT NOT NULL,
1167
+ calls INTEGER NOT NULL,
1168
+ errors INTEGER NOT NULL,
1169
+ p50 INTEGER NOT NULL,
1170
+ p95 INTEGER NOT NULL,
1171
+ p99 INTEGER NOT NULL,
1172
+ tokens INTEGER NOT NULL
1173
+ );
1174
+
1175
+ CREATE INDEX IF NOT EXISTS idx_metrics_snapshots_timestamp ON metrics_snapshots(timestamp);
1176
+ `);
1177
+ logger.info(`System database initialized at ${this.config.dbPath}`, {
1178
+ module: "SYSTEM_DB"
1179
+ });
1180
+ } catch (err) {
1181
+ logger.error(
1182
+ "Failed to initialize SystemDb. better-sqlite3 may not be installed.",
1183
+ {
1184
+ module: "SYSTEM_DB",
1185
+ error: err instanceof Error ? err : new Error(String(err))
1186
+ }
1187
+ );
1188
+ throw err;
1189
+ }
1190
+ }
1191
+ getDb() {
1192
+ if (!this.db) {
1193
+ throw new InternalError("SystemDb not initialized");
1194
+ }
1195
+ return this.db;
1196
+ }
1197
+ close() {
1198
+ if (this.db) {
1199
+ this.db.close();
1200
+ this.db = null;
1081
1201
  }
1082
1202
  }
1083
1203
  };
@@ -1538,11 +1658,14 @@ function createAuditInterceptor(auditLogger, backupManager) {
1538
1658
  ...result,
1539
1659
  _meta: { tokenEstimate: 0 }
1540
1660
  });
1541
- tokenEstimate = Math.ceil(Buffer.byteLength(json, "utf8") / 4);
1661
+ tokenEstimate = estimateTokens(json, "json");
1542
1662
  } catch {
1543
1663
  }
1544
1664
  } else if (typeof result === "string") {
1545
- tokenEstimate = Math.ceil(Buffer.byteLength(result, "utf8") / 4);
1665
+ const isSql = /^\s*(?:SELECT|INSERT|UPDATE|DELETE|WITH|CREATE|ALTER|DROP|PRAGMA)\b/i.test(
1666
+ result
1667
+ );
1668
+ tokenEstimate = estimateTokens(result, isSql ? "sql" : "text");
1546
1669
  }
1547
1670
  return result;
1548
1671
  } catch (err) {
@@ -1561,10 +1684,16 @@ function createAuditInterceptor(auditLogger, backupManager) {
1561
1684
  ...errorResult,
1562
1685
  _meta: { tokenEstimate: 0 }
1563
1686
  });
1564
- tokenEstimate = Math.ceil(Buffer.byteLength(enriched, "utf8") / 4);
1687
+ tokenEstimate = estimateTokens(enriched, "json");
1565
1688
  throw err;
1566
1689
  } finally {
1567
1690
  const durationMs = Math.round(performance.now() - start);
1691
+ metrics.recordToolCall(
1692
+ options?.logAs ?? toolName,
1693
+ durationMs,
1694
+ success,
1695
+ tokenEstimate ?? 0
1696
+ );
1568
1697
  if (isReadScope) {
1569
1698
  auditLogger.log({
1570
1699
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1599,280 +1728,95 @@ function createAuditInterceptor(auditLogger, backupManager) {
1599
1728
  };
1600
1729
  }
1601
1730
 
1602
- // src/server/registration/built-in-tools.ts
1603
- function registerBuiltInTools(server, adaptersMap, config, toolFilter) {
1604
- const serverInfoOpts = {
1605
- title: "Server Info",
1606
- description: "Get information about the db-mcp server and registered adapters",
1607
- icons: SERVER_ICONS,
1608
- annotations: READ_ONLY
1609
- };
1610
- server.registerTool("server_info", serverInfoOpts, () => {
1611
- const authCtx = getAuthContext();
1612
- if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "server_info")) {
1613
- throw new InsufficientScopeError(["read"], authCtx.scopes);
1614
- }
1615
- const adapterInfo = [];
1616
- for (const [id, adapter] of adaptersMap) {
1617
- adapterInfo.push({
1618
- id,
1619
- ...adapter.getInfo()
1620
- });
1621
- }
1622
- return {
1623
- content: [
1624
- {
1625
- type: "text",
1626
- text: JSON.stringify(
1627
- {
1628
- name: config.name,
1629
- version: config.version,
1630
- transport: config.transport,
1631
- adapters: adapterInfo,
1632
- toolFilter: {
1633
- raw: toolFilter.raw,
1634
- enabledGroups: [...toolFilter.enabledGroups]
1635
- }
1636
- },
1637
- null,
1638
- 2
1639
- )
1731
+ // src/server/registration/audit-tools/helpers.ts
1732
+ function redactSqlLiterals(text) {
1733
+ return text.replace(/'(?:''|[^'])*'/g, "'***'");
1734
+ }
1735
+ function validateDdl(sql) {
1736
+ const cleanSql = sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--.*$/gm, "");
1737
+ const upperSql = cleanSql.toUpperCase();
1738
+ if (upperSql.includes("ATTACH ") || upperSql.includes("DETACH ") || upperSql.includes("PRAGMA ") || upperSql.includes("LOAD_EXTENSION(")) {
1739
+ throw new ValidationError(
1740
+ `DDL validation failed: unauthorized command or function call`
1741
+ );
1742
+ }
1743
+ if (upperSql.includes(" ON MAIN.") || upperSql.includes(" ON TEMP.")) {
1744
+ throw new ValidationError(
1745
+ `DDL validation failed: trigger attempts to target a specific database`
1746
+ );
1747
+ }
1748
+ }
1749
+
1750
+ // src/server/registration/audit-tools/resource.ts
1751
+ function registerAuditResource(server, auditLogger, backupManager) {
1752
+ if (!auditLogger) return;
1753
+ server.registerResource(
1754
+ "sqlite_audit",
1755
+ "sqlite://audit",
1756
+ {
1757
+ description: "Recent audit log entries and backup statistics. Shows the last 50 tool invocations with timing, outcomes, and token estimates.",
1758
+ mimeType: "application/json"
1759
+ },
1760
+ async () => {
1761
+ metrics.recordResourceRead("sqlite://audit");
1762
+ const recent = await auditLogger.recent(50);
1763
+ const backupStats = backupManager ? await backupManager.getStats() : void 0;
1764
+ const payload = {
1765
+ entries: recent,
1766
+ stats: {
1767
+ totalEntries: recent.length,
1768
+ ...backupStats && { backups: backupStats }
1640
1769
  }
1641
- ]
1642
- };
1643
- });
1644
- const healthOpts = {
1645
- title: "Server Health",
1646
- description: "Check health status of all database connections",
1647
- icons: SERVER_ICONS,
1648
- annotations: READ_ONLY
1649
- };
1650
- server.registerTool("server_health", healthOpts, async () => {
1651
- const authCtx = getAuthContext();
1652
- if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "server_health")) {
1653
- throw new InsufficientScopeError(["read"], authCtx.scopes);
1770
+ };
1771
+ const payloadStr = JSON.stringify(payload, null, 2);
1772
+ return {
1773
+ contents: [
1774
+ {
1775
+ uri: "sqlite://audit",
1776
+ mimeType: "application/json",
1777
+ text: redactSqlLiterals(payloadStr)
1778
+ }
1779
+ ]
1780
+ };
1654
1781
  }
1655
- const health = {
1656
- server: "healthy",
1657
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1658
- adapters: {}
1659
- };
1660
- for (const [id, adapter] of adaptersMap) {
1782
+ );
1783
+ }
1784
+ function registerAuditBackupTools(server, backupManager, adaptersMap) {
1785
+ if (!backupManager) return;
1786
+ server.registerTool(
1787
+ "sqlite_audit_list_backups",
1788
+ {
1789
+ title: "List Audit Backups",
1790
+ description: "List pre-mutation DDL snapshots captured before destructive operations. Returns metadata for each snapshot including timestamp, tool, target, and size.",
1791
+ inputSchema: AuditListBackupsSchema,
1792
+ outputSchema: AuditListBackupsOutputSchema,
1793
+ annotations: toSDK({
1794
+ readOnlyHint: true,
1795
+ destructiveHint: false,
1796
+ idempotentHint: true,
1797
+ openWorldHint: false,
1798
+ sensitiveHint: true
1799
+ })
1800
+ },
1801
+ async (args) => {
1802
+ const authCtx = getAuthContext();
1803
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "sqlite_audit_list_backups")) {
1804
+ throw new InsufficientScopeError(["admin", "full"], authCtx.scopes);
1805
+ }
1806
+ let parsed;
1661
1807
  try {
1662
- const adapterHealth = await adapter.getHealth();
1663
- health["adapters"][id] = adapterHealth;
1808
+ parsed = AuditListBackupsSchema.parse(args ?? {});
1664
1809
  } catch (error) {
1665
- let errorMsg = error instanceof Error ? error.message : "Unknown error";
1666
- errorMsg = errorMsg.replace(/(:[^:@/]+@)/g, ":***@");
1667
- health["adapters"][id] = {
1668
- connected: false,
1669
- error: errorMsg
1670
- };
1671
- }
1672
- }
1673
- return {
1674
- content: [
1675
- {
1676
- type: "text",
1677
- text: JSON.stringify(health, null, 2)
1678
- }
1679
- ]
1680
- };
1681
- });
1682
- const listAdaptersOpts = {
1683
- title: "List Adapters",
1684
- description: "List all registered database adapters",
1685
- icons: SERVER_ICONS,
1686
- annotations: READ_ONLY
1687
- };
1688
- server.registerTool("list_adapters", listAdaptersOpts, () => {
1689
- const authCtx = getAuthContext();
1690
- if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "list_adapters")) {
1691
- throw new InsufficientScopeError(["read"], authCtx.scopes);
1692
- }
1693
- const adapters = [];
1694
- for (const [, adapter] of adaptersMap) {
1695
- adapters.push({
1696
- id: `${adapter.type}:default`,
1697
- type: adapter.type,
1698
- name: adapter.name,
1699
- version: adapter.version,
1700
- connected: adapter.isConnected()
1701
- });
1702
- }
1703
- return {
1704
- content: [
1705
- {
1706
- type: "text",
1707
- text: JSON.stringify(adapters, null, 2)
1708
- }
1709
- ]
1710
- };
1711
- });
1712
- registerToolScopes(
1713
- /* @__PURE__ */ new Map([
1714
- ["server_info", ["read"]],
1715
- ["server_health", ["read"]],
1716
- ["list_adapters", ["read"]]
1717
- ])
1718
- );
1719
- }
1720
-
1721
- // src/server/registration/help-resources.ts
1722
- function registerHelpResources(server, toolFilter) {
1723
- const gotchasContent = HELP_CONTENT.get("gotchas");
1724
- if (gotchasContent) {
1725
- server.registerResource(
1726
- "sqlite_help",
1727
- "sqlite://help",
1728
- {
1729
- description: "Critical gotchas, WASM vs Native comparison, and Code Mode API reference",
1730
- mimeType: "text/markdown",
1731
- annotations: ASSISTANT_FOCUSED
1732
- },
1733
- () => ({
1734
- contents: [
1735
- {
1736
- uri: "sqlite://help",
1737
- mimeType: "text/markdown",
1738
- text: gotchasContent
1739
- }
1740
- ]
1741
- })
1742
- );
1743
- }
1744
- const groupHelpKeys = [
1745
- { group: "core", key: "core" },
1746
- { group: "json", key: "json" },
1747
- { group: "text", key: "text" },
1748
- { group: "stats", key: "stats" },
1749
- { group: "vector", key: "vector" },
1750
- { group: "geo", key: "geo" },
1751
- { group: "admin", key: "admin" },
1752
- { group: "transactions", key: "transactions" },
1753
- { group: "introspection", key: "introspection" },
1754
- { group: "migration", key: "migration" }
1755
- ];
1756
- for (const { group, key } of groupHelpKeys) {
1757
- const isCodemodeOnly = toolFilter.enabledGroups.size === 1 && toolFilter.enabledGroups.has("codemode");
1758
- if (!toolFilter.enabledGroups.has(group) && !isCodemodeOnly) {
1759
- continue;
1760
- }
1761
- const content = HELP_CONTENT.get(key);
1762
- if (!content) continue;
1763
- server.registerResource(
1764
- `sqlite_help_${key}`,
1765
- `sqlite://help/${key}`,
1766
- {
1767
- description: `Tool reference for the ${group} tool group`,
1768
- mimeType: "text/markdown",
1769
- annotations: ASSISTANT_FOCUSED
1770
- },
1771
- () => ({
1772
- contents: [
1773
- {
1774
- uri: `sqlite://help/${key}`,
1775
- mimeType: "text/markdown",
1776
- text: content
1777
- }
1778
- ]
1779
- })
1780
- );
1781
- }
1782
- const registeredHelp = ["sqlite://help"];
1783
- for (const { group, key } of groupHelpKeys) {
1784
- if (toolFilter.enabledGroups.has(group)) {
1785
- registeredHelp.push(`sqlite://help/${key}`);
1786
- }
1787
- }
1788
- logger.info(`Help resources: ${registeredHelp.join(", ")}`, {
1789
- module: "SERVER"
1790
- });
1791
- }
1792
- function redactSqlLiterals(text) {
1793
- return text.replace(/'(?:''|[^'])*'/g, "'***'");
1794
- }
1795
- function validateDdl(sql) {
1796
- const cleanSql = sql.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--.*$/gm, "");
1797
- const upperSql = cleanSql.toUpperCase();
1798
- if (upperSql.includes("ATTACH ") || upperSql.includes("DETACH ") || upperSql.includes("PRAGMA ") || upperSql.includes("LOAD_EXTENSION(")) {
1799
- throw new Error(
1800
- `DDL validation failed: unauthorized command or function call`
1801
- );
1802
- }
1803
- if (upperSql.includes(" ON MAIN.") || upperSql.includes(" ON TEMP.")) {
1804
- throw new Error(
1805
- `DDL validation failed: trigger attempts to target a specific database`
1806
- );
1807
- }
1808
- }
1809
- function registerAuditResource(server, auditLogger, backupManager) {
1810
- if (!auditLogger) return;
1811
- server.registerResource(
1812
- "sqlite_audit",
1813
- "sqlite://audit",
1814
- {
1815
- description: "Recent audit log entries and backup statistics. Shows the last 50 tool invocations with timing, outcomes, and token estimates.",
1816
- mimeType: "application/json"
1817
- },
1818
- async () => {
1819
- const recent = await auditLogger.recent(50);
1820
- const backupStats = backupManager ? await backupManager.getStats() : void 0;
1821
- const payload = {
1822
- entries: recent,
1823
- stats: {
1824
- totalEntries: recent.length,
1825
- ...backupStats && { backups: backupStats }
1826
- }
1827
- };
1828
- const payloadStr = JSON.stringify(payload, null, 2);
1829
- return {
1830
- contents: [
1831
- {
1832
- uri: "sqlite://audit",
1833
- mimeType: "application/json",
1834
- text: redactSqlLiterals(payloadStr)
1835
- }
1836
- ]
1837
- };
1838
- }
1839
- );
1840
- }
1841
- function registerAuditBackupTools(server, backupManager, adaptersMap) {
1842
- if (!backupManager) return;
1843
- server.registerTool(
1844
- "sqlite_audit_list_backups",
1845
- {
1846
- title: "List Audit Backups",
1847
- description: "List pre-mutation DDL snapshots captured before destructive operations. Returns metadata for each snapshot including timestamp, tool, target, and size.",
1848
- inputSchema: AuditListBackupsSchema,
1849
- outputSchema: AuditListBackupsOutputSchema,
1850
- annotations: {
1851
- readOnlyHint: true,
1852
- destructiveHint: false,
1853
- idempotentHint: true,
1854
- openWorldHint: false
1855
- }
1856
- },
1857
- async (args) => {
1858
- const authCtx = getAuthContext();
1859
- if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "sqlite_audit_list_backups")) {
1860
- throw new InsufficientScopeError(["admin", "full"], authCtx.scopes);
1861
- }
1862
- let parsed;
1863
- try {
1864
- parsed = AuditListBackupsSchema.parse(args ?? {});
1865
- } catch (error) {
1866
- const structured = formatHandlerError(error);
1867
- return {
1868
- content: [
1869
- {
1870
- type: "text",
1871
- text: JSON.stringify(structured, null, 2)
1872
- }
1873
- ],
1874
- isError: true,
1875
- structuredContent: structured
1810
+ const structured = formatHandlerError(error);
1811
+ return {
1812
+ content: [
1813
+ {
1814
+ type: "text",
1815
+ text: JSON.stringify(structured, null, 2)
1816
+ }
1817
+ ],
1818
+ isError: true,
1819
+ structuredContent: structured
1876
1820
  };
1877
1821
  }
1878
1822
  const limit = parsed.limit ?? 10;
@@ -1914,12 +1858,13 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
1914
1858
  filename: z.string().default("").describe("Snapshot filename from sqlite_audit_list_backups results")
1915
1859
  }),
1916
1860
  outputSchema: AuditGetBackupOutputSchema,
1917
- annotations: {
1861
+ annotations: toSDK({
1918
1862
  readOnlyHint: true,
1919
1863
  destructiveHint: false,
1920
1864
  idempotentHint: true,
1921
- openWorldHint: false
1922
- }
1865
+ openWorldHint: false,
1866
+ sensitiveHint: true
1867
+ })
1923
1868
  },
1924
1869
  async (args) => {
1925
1870
  const authCtx = getAuthContext();
@@ -1947,7 +1892,9 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
1947
1892
  if (!snapshot) {
1948
1893
  const errRes = {
1949
1894
  success: false,
1950
- error: `Snapshot not found: ${filename}`
1895
+ error: `Snapshot not found: ${filename}`,
1896
+ code: "NOT_FOUND",
1897
+ category: "not_found"
1951
1898
  };
1952
1899
  return {
1953
1900
  content: [
@@ -1979,12 +1926,13 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
1979
1926
  title: "Cleanup Audit Backups",
1980
1927
  description: "Apply retention policy to audit backup snapshots. Deletes snapshots exceeding age or count limits.",
1981
1928
  outputSchema: AuditCleanupOutputSchema,
1982
- annotations: {
1929
+ annotations: toSDK({
1983
1930
  readOnlyHint: false,
1984
1931
  destructiveHint: true,
1985
1932
  idempotentHint: true,
1986
- openWorldHint: false
1987
- }
1933
+ openWorldHint: false,
1934
+ sensitiveHint: true
1935
+ })
1988
1936
  },
1989
1937
  async () => {
1990
1938
  const authCtx = getAuthContext();
@@ -2019,12 +1967,13 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2019
1967
  )
2020
1968
  }),
2021
1969
  outputSchema: AuditDiffBackupOutputSchema,
2022
- annotations: {
1970
+ annotations: toSDK({
2023
1971
  readOnlyHint: true,
2024
1972
  destructiveHint: false,
2025
1973
  idempotentHint: true,
2026
- openWorldHint: false
2027
- }
1974
+ openWorldHint: false,
1975
+ sensitiveHint: true
1976
+ })
2028
1977
  },
2029
1978
  async (args) => {
2030
1979
  const authCtx = getAuthContext();
@@ -2052,7 +2001,9 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2052
2001
  if (!snapshot) {
2053
2002
  const errRes = {
2054
2003
  success: false,
2055
- error: `Snapshot not found: ${filename}`
2004
+ error: `Snapshot not found: ${filename}`,
2005
+ code: "NOT_FOUND",
2006
+ category: "not_found"
2056
2007
  };
2057
2008
  return {
2058
2009
  content: [
@@ -2069,7 +2020,9 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2069
2020
  if (!adapter) {
2070
2021
  const errRes = {
2071
2022
  success: false,
2072
- error: "No connected adapter available"
2023
+ error: "No connected adapter available",
2024
+ code: "INTERNAL_ERROR",
2025
+ category: "internal"
2073
2026
  };
2074
2027
  return {
2075
2028
  content: [
@@ -2158,19 +2111,16 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2158
2111
  structuredContent: result
2159
2112
  };
2160
2113
  } catch (error) {
2161
- const errRes = {
2162
- success: false,
2163
- error: error instanceof Error ? error.message : String(error)
2164
- };
2114
+ const structured = formatHandlerError(error);
2165
2115
  return {
2166
2116
  content: [
2167
2117
  {
2168
2118
  type: "text",
2169
- text: JSON.stringify(errRes, null, 2)
2119
+ text: JSON.stringify(structured, null, 2)
2170
2120
  }
2171
2121
  ],
2172
2122
  isError: true,
2173
- structuredContent: errRes
2123
+ structuredContent: structured
2174
2124
  };
2175
2125
  }
2176
2126
  }
@@ -2185,12 +2135,13 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2185
2135
  dryRun: z.unknown().optional().describe("If true, returns the DDL without executing it (boolean)")
2186
2136
  }),
2187
2137
  outputSchema: AuditRestoreBackupOutputSchema,
2188
- annotations: {
2138
+ annotations: toSDK({
2189
2139
  readOnlyHint: false,
2190
2140
  destructiveHint: true,
2191
2141
  idempotentHint: false,
2192
- openWorldHint: false
2193
- }
2142
+ openWorldHint: false,
2143
+ sensitiveHint: true
2144
+ })
2194
2145
  },
2195
2146
  async (args) => {
2196
2147
  const authCtx = getAuthContext();
@@ -2223,7 +2174,9 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2223
2174
  if (!snapshot) {
2224
2175
  const errRes = {
2225
2176
  success: false,
2226
- error: `Snapshot not found: ${filename}`
2177
+ error: `Snapshot not found: ${filename}`,
2178
+ code: "NOT_FOUND",
2179
+ category: "not_found"
2227
2180
  };
2228
2181
  return {
2229
2182
  content: [
@@ -2240,7 +2193,9 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2240
2193
  if (!ddl.trim()) {
2241
2194
  const errRes = {
2242
2195
  success: false,
2243
- error: "Snapshot contains no DDL statements"
2196
+ error: "Snapshot contains no DDL statements",
2197
+ code: "VALIDATION_ERROR",
2198
+ category: "validation"
2244
2199
  };
2245
2200
  return {
2246
2201
  content: [
@@ -2257,7 +2212,9 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2257
2212
  if (!adapter) {
2258
2213
  const errRes = {
2259
2214
  success: false,
2260
- error: "No connected adapter available"
2215
+ error: "No connected adapter available",
2216
+ code: "INTERNAL_ERROR",
2217
+ category: "internal"
2261
2218
  };
2262
2219
  return {
2263
2220
  content: [
@@ -2334,19 +2291,16 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2334
2291
  structuredContent: result
2335
2292
  };
2336
2293
  } catch (error) {
2337
- const errRes = {
2338
- success: false,
2339
- error: error instanceof Error ? error.message : String(error)
2340
- };
2294
+ const structured = formatHandlerError(error);
2341
2295
  return {
2342
2296
  content: [
2343
2297
  {
2344
2298
  type: "text",
2345
- text: JSON.stringify(errRes, null, 2)
2299
+ text: JSON.stringify(structured, null, 2)
2346
2300
  }
2347
2301
  ],
2348
2302
  isError: true,
2349
- structuredContent: errRes
2303
+ structuredContent: structured
2350
2304
  };
2351
2305
  }
2352
2306
  }
@@ -2366,46 +2320,626 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2366
2320
  );
2367
2321
  }
2368
2322
 
2369
- // src/server/mcp-server.ts
2370
- var proto = McpServer.prototype;
2371
- if (typeof proto["createToolError"] === "function") {
2372
- const originalCreateToolError = proto["createToolError"];
2373
- proto["createToolError"] = function(errorMessage) {
2374
- const result = originalCreateToolError.call(
2375
- this,
2376
- errorMessage
2377
- );
2378
- if (result.content?.[0]?.type === "text") {
2379
- const rawError = result.content[0].text;
2380
- if (rawError.includes("Input validation error")) {
2381
- const cleanError = rawError.replace(
2382
- /^MCP error -32602: Input validation error: /,
2383
- "Validation error: "
2323
+ // src/server/registration/audit-tools/search.ts
2324
+ function registerAuditSearchTool(server, auditLogger) {
2325
+ if (!auditLogger) return;
2326
+ server.registerTool(
2327
+ "sqlite_audit_search",
2328
+ {
2329
+ title: "Search Audit Log",
2330
+ description: "Search and filter structured audit logs from the System Database. Returns recent tool invocations, outcomes, token estimates, and parameters.",
2331
+ inputSchema: AuditSearchSchema,
2332
+ outputSchema: AuditSearchOutputSchema,
2333
+ annotations: toSDK({
2334
+ readOnlyHint: true,
2335
+ destructiveHint: false,
2336
+ idempotentHint: true,
2337
+ openWorldHint: false,
2338
+ sensitiveHint: true
2339
+ })
2340
+ },
2341
+ async (args) => {
2342
+ try {
2343
+ const authCtx = getAuthContext();
2344
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "sqlite_audit_search")) {
2345
+ throw new InsufficientScopeError(["admin", "full"], authCtx.scopes);
2346
+ }
2347
+ const parsed = AuditSearchSchema.parse(args ?? {});
2348
+ const { entries, totalCount } = await auditLogger.search(parsed);
2349
+ const result = {
2350
+ success: true,
2351
+ entries,
2352
+ count: entries.length,
2353
+ totalCount
2354
+ };
2355
+ const tokenEstimate = Math.ceil(
2356
+ Buffer.byteLength(JSON.stringify(result), "utf8") / 4
2384
2357
  );
2385
- const structured = {
2386
- success: false,
2387
- error: cleanError,
2388
- code: "VALIDATION_ERROR",
2389
- category: "validation" /* VALIDATION */
2358
+ return {
2359
+ content: [
2360
+ {
2361
+ type: "text",
2362
+ text: JSON.stringify(
2363
+ { ...result, _meta: { tokenEstimate } },
2364
+ null,
2365
+ 2
2366
+ )
2367
+ }
2368
+ ],
2369
+ structuredContent: result
2390
2370
  };
2391
- result.content[0].text = JSON.stringify(structured, null, 2);
2392
- result.isError = true;
2393
- }
2394
- }
2395
- return result;
2396
- };
2397
- }
2398
- var DbMcpServer = class {
2399
- server;
2400
- adapters = /* @__PURE__ */ new Map();
2401
- toolFilter;
2402
- config;
2403
- auditLogger = null;
2371
+ } catch (error) {
2372
+ const structured = formatHandlerError(error);
2373
+ return {
2374
+ content: [
2375
+ {
2376
+ type: "text",
2377
+ text: JSON.stringify(structured, null, 2)
2378
+ }
2379
+ ],
2380
+ isError: true,
2381
+ structuredContent: structured
2382
+ };
2383
+ }
2384
+ }
2385
+ );
2386
+ registerToolScopes(/* @__PURE__ */ new Map([["sqlite_audit_search", ["admin", "full"]]]));
2387
+ logger.info("Registered audit search tool: sqlite_audit_search", {
2388
+ module: "AUDIT"
2389
+ });
2390
+ }
2391
+
2392
+ // src/server/registration/built-in-tools.ts
2393
+ function registerBuiltInTools(server, adaptersMap, config, toolFilter) {
2394
+ const serverInfoOpts = {
2395
+ title: "Server Info",
2396
+ description: "Get information about the db-mcp server and registered adapters",
2397
+ icons: SERVER_ICONS,
2398
+ annotations: READ_ONLY
2399
+ };
2400
+ server.registerTool("server_info", serverInfoOpts, () => {
2401
+ const authCtx = getAuthContext();
2402
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "server_info")) {
2403
+ throw new InsufficientScopeError(["read"], authCtx.scopes);
2404
+ }
2405
+ const adapterInfo = [];
2406
+ for (const [id, adapter] of adaptersMap) {
2407
+ adapterInfo.push({
2408
+ id,
2409
+ ...adapter.getInfo()
2410
+ });
2411
+ }
2412
+ return {
2413
+ content: [
2414
+ {
2415
+ type: "text",
2416
+ text: JSON.stringify(
2417
+ {
2418
+ name: config.name,
2419
+ version: config.version,
2420
+ transport: config.transport,
2421
+ adapters: adapterInfo,
2422
+ toolFilter: {
2423
+ raw: toolFilter.raw,
2424
+ enabledGroups: [...toolFilter.enabledGroups]
2425
+ }
2426
+ },
2427
+ null,
2428
+ 2
2429
+ )
2430
+ }
2431
+ ]
2432
+ };
2433
+ });
2434
+ const healthOpts = {
2435
+ title: "Server Health",
2436
+ description: "Check health status of all database connections",
2437
+ icons: SERVER_ICONS,
2438
+ annotations: READ_ONLY
2439
+ };
2440
+ server.registerTool("server_health", healthOpts, async () => {
2441
+ const authCtx = getAuthContext();
2442
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "server_health")) {
2443
+ throw new InsufficientScopeError(["read"], authCtx.scopes);
2444
+ }
2445
+ const health = {
2446
+ server: "healthy",
2447
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2448
+ adapters: {}
2449
+ };
2450
+ for (const [id, adapter] of adaptersMap) {
2451
+ try {
2452
+ const adapterHealth = await adapter.getHealth();
2453
+ health["adapters"][id] = adapterHealth;
2454
+ } catch (error) {
2455
+ let errorMsg = error instanceof Error ? error.message : "Unknown error";
2456
+ errorMsg = errorMsg.replace(/(:[^:@/]+@)/g, ":***@");
2457
+ health["adapters"][id] = {
2458
+ connected: false,
2459
+ error: errorMsg
2460
+ };
2461
+ }
2462
+ }
2463
+ return {
2464
+ content: [
2465
+ {
2466
+ type: "text",
2467
+ text: JSON.stringify(health, null, 2)
2468
+ }
2469
+ ]
2470
+ };
2471
+ });
2472
+ const listAdaptersOpts = {
2473
+ title: "List Adapters",
2474
+ description: "List all registered database adapters",
2475
+ icons: SERVER_ICONS,
2476
+ annotations: READ_ONLY
2477
+ };
2478
+ server.registerTool("list_adapters", listAdaptersOpts, () => {
2479
+ const authCtx = getAuthContext();
2480
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "list_adapters")) {
2481
+ throw new InsufficientScopeError(["read"], authCtx.scopes);
2482
+ }
2483
+ const adapters = [];
2484
+ for (const [, adapter] of adaptersMap) {
2485
+ adapters.push({
2486
+ id: `${adapter.type}:default`,
2487
+ type: adapter.type,
2488
+ name: adapter.name,
2489
+ version: adapter.version,
2490
+ connected: adapter.isConnected()
2491
+ });
2492
+ }
2493
+ return {
2494
+ content: [
2495
+ {
2496
+ type: "text",
2497
+ text: JSON.stringify(adapters, null, 2)
2498
+ }
2499
+ ]
2500
+ };
2501
+ });
2502
+ registerToolScopes(
2503
+ /* @__PURE__ */ new Map([
2504
+ ["server_info", ["read"]],
2505
+ ["server_health", ["read"]],
2506
+ ["list_adapters", ["read"]]
2507
+ ])
2508
+ );
2509
+ }
2510
+
2511
+ // src/server/registration/help-resources.ts
2512
+ function registerHelpResources(server, toolFilter) {
2513
+ const gotchasContent = HELP_CONTENT.get("gotchas");
2514
+ if (gotchasContent) {
2515
+ server.registerResource(
2516
+ "sqlite_help",
2517
+ "sqlite://help",
2518
+ {
2519
+ description: "Critical gotchas, WASM vs Native comparison, and Code Mode API reference",
2520
+ mimeType: "text/markdown",
2521
+ annotations: ASSISTANT_FOCUSED
2522
+ },
2523
+ () => {
2524
+ metrics.recordResourceRead("sqlite://help");
2525
+ return {
2526
+ contents: [
2527
+ {
2528
+ uri: "sqlite://help",
2529
+ mimeType: "text/markdown",
2530
+ text: gotchasContent
2531
+ }
2532
+ ]
2533
+ };
2534
+ }
2535
+ );
2536
+ }
2537
+ const groupHelpKeys = [
2538
+ { group: "core", key: "core" },
2539
+ { group: "json", key: "json" },
2540
+ { group: "text", key: "text" },
2541
+ { group: "stats", key: "stats" },
2542
+ { group: "vector", key: "vector" },
2543
+ { group: "geo", key: "geo" },
2544
+ { group: "admin", key: "admin" },
2545
+ { group: "transactions", key: "transactions" },
2546
+ { group: "introspection", key: "introspection" },
2547
+ { group: "migration", key: "migration" }
2548
+ ];
2549
+ for (const { group, key } of groupHelpKeys) {
2550
+ const isCodemodeOnly = toolFilter.enabledGroups.size === 1 && toolFilter.enabledGroups.has("codemode");
2551
+ if (!toolFilter.enabledGroups.has(group) && !isCodemodeOnly) {
2552
+ continue;
2553
+ }
2554
+ const content = HELP_CONTENT.get(key);
2555
+ if (!content) continue;
2556
+ server.registerResource(
2557
+ `sqlite_help_${key}`,
2558
+ `sqlite://help/${key}`,
2559
+ {
2560
+ description: `Tool reference for the ${group} tool group`,
2561
+ mimeType: "text/markdown",
2562
+ annotations: ASSISTANT_FOCUSED
2563
+ },
2564
+ () => {
2565
+ metrics.recordResourceRead(`sqlite://help/${key}`);
2566
+ return {
2567
+ contents: [
2568
+ {
2569
+ uri: `sqlite://help/${key}`,
2570
+ mimeType: "text/markdown",
2571
+ text: content
2572
+ }
2573
+ ]
2574
+ };
2575
+ }
2576
+ );
2577
+ }
2578
+ const registeredHelp = ["sqlite://help"];
2579
+ for (const { group, key } of groupHelpKeys) {
2580
+ if (toolFilter.enabledGroups.has(group)) {
2581
+ registeredHelp.push(`sqlite://help/${key}`);
2582
+ }
2583
+ }
2584
+ logger.info(`Help resources: ${registeredHelp.join(", ")}`, {
2585
+ module: "SERVER"
2586
+ });
2587
+ }
2588
+
2589
+ // src/server/registration/observability-resources.ts
2590
+ function registerObservabilityResources(server) {
2591
+ server.registerResource(
2592
+ "sqlite://metrics",
2593
+ "sqlite://metrics",
2594
+ {
2595
+ description: "Returns in-memory streaming metrics including p50/p95/p99 latency percentiles and token usage.",
2596
+ mimeType: "application/json"
2597
+ },
2598
+ () => {
2599
+ const summary = metrics.getSummary();
2600
+ return {
2601
+ contents: [
2602
+ {
2603
+ uri: "sqlite://metrics",
2604
+ mimeType: "application/json",
2605
+ text: JSON.stringify(summary, null, 2)
2606
+ }
2607
+ ]
2608
+ };
2609
+ }
2610
+ );
2611
+ }
2612
+ function registerAdminTools(server) {
2613
+ server.registerTool(
2614
+ "sqlite_server_config",
2615
+ {
2616
+ title: "Server Configuration",
2617
+ description: "Get or update runtime configuration values for the server. Currently supports updating the log level.",
2618
+ inputSchema: z.object({
2619
+ action: z.enum(["get", "set"]).describe("Whether to get or set the configuration value"),
2620
+ setting: z.enum(["logLevel"]).optional().describe("The setting to modify"),
2621
+ value: z.string().optional().describe(
2622
+ "The new value for the setting (e.g., 'debug', 'info', 'warning')"
2623
+ )
2624
+ }),
2625
+ annotations: toSDK({
2626
+ readOnlyHint: false,
2627
+ destructiveHint: false,
2628
+ idempotentHint: false,
2629
+ openWorldHint: false,
2630
+ sensitiveHint: true
2631
+ })
2632
+ },
2633
+ (args) => {
2634
+ const authCtx = getAuthContext();
2635
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "sqlite_server_config")) {
2636
+ throw new InsufficientScopeError(["admin"], authCtx.scopes);
2637
+ }
2638
+ try {
2639
+ const parsed = z.object({
2640
+ action: z.enum(["get", "set"]),
2641
+ setting: z.enum(["logLevel"]).optional(),
2642
+ value: z.string().optional()
2643
+ }).parse(args ?? {});
2644
+ const { action, setting, value } = parsed;
2645
+ if (action === "get") {
2646
+ return {
2647
+ content: [
2648
+ {
2649
+ type: "text",
2650
+ text: JSON.stringify(
2651
+ {
2652
+ success: true,
2653
+ config: {
2654
+ logLevel: logger.getLevel()
2655
+ }
2656
+ },
2657
+ null,
2658
+ 2
2659
+ )
2660
+ }
2661
+ ]
2662
+ };
2663
+ }
2664
+ if (action === "set") {
2665
+ if (setting === "logLevel" && value) {
2666
+ const validLevels = [
2667
+ "debug",
2668
+ "info",
2669
+ "notice",
2670
+ "warning",
2671
+ "error",
2672
+ "critical",
2673
+ "alert",
2674
+ "emergency"
2675
+ ];
2676
+ if (!validLevels.includes(value.toLowerCase())) {
2677
+ return {
2678
+ content: [
2679
+ {
2680
+ type: "text",
2681
+ text: JSON.stringify(
2682
+ {
2683
+ success: false,
2684
+ error: `Invalid log level: ${value}. Must be one of: ${validLevels.join(", ")}`,
2685
+ code: "INVALID_CONFIG",
2686
+ category: "validation"
2687
+ },
2688
+ null,
2689
+ 2
2690
+ )
2691
+ }
2692
+ ],
2693
+ isError: true
2694
+ };
2695
+ }
2696
+ logger.setLevel(value.toLowerCase());
2697
+ logger.info(
2698
+ `Log level dynamically changed to ${value} via sqlite_server_config tool`,
2699
+ {
2700
+ module: "SERVER"
2701
+ }
2702
+ );
2703
+ return {
2704
+ content: [
2705
+ {
2706
+ type: "text",
2707
+ text: JSON.stringify(
2708
+ {
2709
+ success: true,
2710
+ message: `Log level successfully updated to ${value}`
2711
+ },
2712
+ null,
2713
+ 2
2714
+ )
2715
+ }
2716
+ ]
2717
+ };
2718
+ }
2719
+ return {
2720
+ content: [
2721
+ {
2722
+ type: "text",
2723
+ text: JSON.stringify(
2724
+ {
2725
+ success: false,
2726
+ error: "Missing setting or value for set action",
2727
+ code: "INVALID_CONFIG",
2728
+ category: "validation"
2729
+ },
2730
+ null,
2731
+ 2
2732
+ )
2733
+ }
2734
+ ],
2735
+ isError: true
2736
+ };
2737
+ }
2738
+ return {
2739
+ content: [
2740
+ {
2741
+ type: "text",
2742
+ text: JSON.stringify({
2743
+ success: false,
2744
+ error: "Invalid action",
2745
+ code: "INVALID_CONFIG",
2746
+ category: "validation"
2747
+ })
2748
+ }
2749
+ ],
2750
+ isError: true
2751
+ };
2752
+ } catch (error) {
2753
+ const structured = formatHandlerError(error);
2754
+ return {
2755
+ content: [
2756
+ {
2757
+ type: "text",
2758
+ text: JSON.stringify(structured, null, 2)
2759
+ }
2760
+ ],
2761
+ isError: true,
2762
+ structuredContent: structured
2763
+ };
2764
+ }
2765
+ }
2766
+ );
2767
+ registerToolScopes(/* @__PURE__ */ new Map([["sqlite_server_config", ["admin"]]]));
2768
+ }
2769
+
2770
+ // src/server/subscription-manager.ts
2771
+ var SubscriptionManager = class {
2772
+ server;
2773
+ // Map of URI string -> Set of Session IDs
2774
+ subscriptions = /* @__PURE__ */ new Map();
2775
+ constructor(server) {
2776
+ this.server = server;
2777
+ }
2778
+ /**
2779
+ * Subscribe a session to a resource URI.
2780
+ */
2781
+ subscribe(uri, sessionId) {
2782
+ if (!sessionId) {
2783
+ logger.debug(
2784
+ `Subscription requested for ${uri} but no sessionId provided. Assuming stateless transport.`
2785
+ );
2786
+ return;
2787
+ }
2788
+ let sessions = this.subscriptions.get(uri);
2789
+ if (!sessions) {
2790
+ sessions = /* @__PURE__ */ new Set();
2791
+ this.subscriptions.set(uri, sessions);
2792
+ }
2793
+ if (!sessions.has(sessionId)) {
2794
+ sessions.add(sessionId);
2795
+ logger.debug(`Session ${sessionId} subscribed to resource ${uri}`, {
2796
+ module: "SERVER"
2797
+ });
2798
+ }
2799
+ }
2800
+ /**
2801
+ * Unsubscribe a session from a resource URI.
2802
+ */
2803
+ unsubscribe(uri, sessionId) {
2804
+ if (!sessionId) {
2805
+ return;
2806
+ }
2807
+ const sessions = this.subscriptions.get(uri);
2808
+ if (sessions?.has(sessionId)) {
2809
+ sessions.delete(sessionId);
2810
+ logger.debug(`Session ${sessionId} unsubscribed from resource ${uri}`, {
2811
+ module: "SERVER"
2812
+ });
2813
+ if (sessions.size === 0) {
2814
+ this.subscriptions.delete(uri);
2815
+ }
2816
+ }
2817
+ }
2818
+ /**
2819
+ * Unsubscribe a session from all resources (useful on transport disconnect).
2820
+ */
2821
+ unsubscribeSession(sessionId) {
2822
+ let count = 0;
2823
+ for (const [uri, sessions] of this.subscriptions.entries()) {
2824
+ if (sessions.has(sessionId)) {
2825
+ sessions.delete(sessionId);
2826
+ count++;
2827
+ if (sessions.size === 0) {
2828
+ this.subscriptions.delete(uri);
2829
+ }
2830
+ }
2831
+ }
2832
+ if (count > 0) {
2833
+ logger.debug(
2834
+ `Session ${sessionId} disconnected, removed ${count} subscriptions`,
2835
+ { module: "SERVER" }
2836
+ );
2837
+ }
2838
+ }
2839
+ /**
2840
+ * Notifies all subscribed clients that a resource has changed.
2841
+ */
2842
+ async notifyResourceUpdated(uri) {
2843
+ if (!this.hasSubscribers(uri)) {
2844
+ return;
2845
+ }
2846
+ try {
2847
+ await this.server.server.sendResourceUpdated({ uri });
2848
+ logger.debug(`Notified subscribers of update to ${uri}`, {
2849
+ module: "SERVER"
2850
+ });
2851
+ } catch (error) {
2852
+ logger.error(`Failed to notify subscribers for ${uri}`, {
2853
+ module: "SERVER",
2854
+ error: error instanceof Error ? error : new Error(String(error))
2855
+ });
2856
+ }
2857
+ }
2858
+ /**
2859
+ * Notifies subscribers of schema-related resources (schema, tables, and specific tables).
2860
+ */
2861
+ async notifySchemaSubscribers() {
2862
+ const urisToNotify = /* @__PURE__ */ new Set([
2863
+ "sqlite://schema",
2864
+ "sqlite://tables"
2865
+ ]);
2866
+ for (const uri of this.subscriptions.keys()) {
2867
+ if (uri.startsWith("sqlite://table/")) {
2868
+ urisToNotify.add(uri);
2869
+ }
2870
+ }
2871
+ const promises = Array.from(urisToNotify).map(
2872
+ (uri) => this.notifyResourceUpdated(uri)
2873
+ );
2874
+ await Promise.allSettled(promises);
2875
+ }
2876
+ /**
2877
+ * Check if there are any active subscribers for a specific URI.
2878
+ */
2879
+ hasSubscribers(uri) {
2880
+ const sessions = this.subscriptions.get(uri);
2881
+ return sessions ? sessions.size > 0 : false;
2882
+ }
2883
+ };
2884
+
2885
+ // src/server/mcp-server.ts
2886
+ var proto = McpServer.prototype;
2887
+ if (typeof proto["createToolError"] === "function") {
2888
+ const originalCreateToolError = proto["createToolError"];
2889
+ proto["createToolError"] = function(errorMessage) {
2890
+ const result = originalCreateToolError.call(this, errorMessage);
2891
+ if (result.content?.[0]?.type === "text") {
2892
+ const rawError = result.content[0].text;
2893
+ if (rawError.includes("Input validation error")) {
2894
+ const cleanError = rawError.replace(
2895
+ /^MCP error -32602: Input validation error: /,
2896
+ "Validation error: "
2897
+ );
2898
+ const structured = {
2899
+ success: false,
2900
+ error: cleanError,
2901
+ code: "VALIDATION_ERROR",
2902
+ category: ErrorCategory.VALIDATION
2903
+ };
2904
+ result.content[0].text = JSON.stringify(structured, null, 2);
2905
+ result.isError = true;
2906
+ }
2907
+ }
2908
+ return result;
2909
+ };
2910
+ }
2911
+ var DbMcpServer = class {
2912
+ server;
2913
+ adapters = /* @__PURE__ */ new Map();
2914
+ toolFilter;
2915
+ config;
2916
+ auditLogger = null;
2404
2917
  backupManager = null;
2918
+ systemDb = null;
2405
2919
  auditInterceptor = null;
2406
2920
  auditInitPromise = null;
2921
+ subscriptionManager;
2407
2922
  constructor(config) {
2408
2923
  this.config = config;
2924
+ let allowedIoRoots = config.allowedIoRoots;
2925
+ if (!allowedIoRoots) {
2926
+ if (config.transport === "http") {
2927
+ const errorMsg = `FATAL: Refusing to bind HTTP transport without explicit ALLOWED_IO_ROOTS. You MUST specify --allowed-io-roots (or ALLOWED_IO_ROOTS env var) to prevent ambient filesystem authority.`;
2928
+ logger.error(errorMsg, { module: "SERVER" });
2929
+ process.exit(1);
2930
+ }
2931
+ allowedIoRoots = [];
2932
+ logger.warning(
2933
+ "\u26A0\uFE0F SECURITY WARNING: ALLOWED_IO_ROOTS not explicitly provided. Defaulting to empty array (NO filesystem access). You MUST specify --allowed-io-roots (or ALLOWED_IO_ROOTS env var) to enable filesystem tools.",
2934
+ { module: "SERVER" }
2935
+ );
2936
+ } else {
2937
+ logger.info("IO sandbox configured", {
2938
+ module: "SERVER",
2939
+ allowedRoots: allowedIoRoots
2940
+ });
2941
+ }
2942
+ this.config.allowedIoRoots = allowedIoRoots;
2409
2943
  this.toolFilter = config.toolFilter ? parseToolFilter(config.toolFilter) : getToolFilterFromEnv();
2410
2944
  this.server = new McpServer(
2411
2945
  {
@@ -2419,6 +2953,48 @@ var DbMcpServer = class {
2419
2953
  instructions: INSTRUCTIONS
2420
2954
  }
2421
2955
  );
2956
+ this.subscriptionManager = new SubscriptionManager(this.server);
2957
+ this.server.subscriptionManager = this.subscriptionManager;
2958
+ this.server.server.registerCapabilities({
2959
+ resources: {
2960
+ subscribe: true
2961
+ }
2962
+ });
2963
+ this.server.server.setRequestHandler(
2964
+ SubscribeRequestSchema,
2965
+ (request, extra) => {
2966
+ const uri = request.params.uri;
2967
+ let sessionId = extra.sessionId ?? extra.requestInfo?.headers["mcp-session-id"] ?? void 0;
2968
+ if (sessionId === void 0 && this.config.transport === "stdio") {
2969
+ sessionId = "stdio";
2970
+ }
2971
+ if (!["sqlite://schema", "sqlite://tables", "sqlite://health"].includes(
2972
+ uri
2973
+ ) && !uri.startsWith("sqlite://table/")) {
2974
+ throw new ValidationError(`Resource ${uri} is not subscribable`);
2975
+ }
2976
+ this.subscriptionManager.subscribe(
2977
+ uri,
2978
+ sessionId
2979
+ );
2980
+ return {};
2981
+ }
2982
+ );
2983
+ this.server.server.setRequestHandler(
2984
+ UnsubscribeRequestSchema,
2985
+ (request, extra) => {
2986
+ const uri = request.params.uri;
2987
+ let sessionId = extra.sessionId ?? extra.requestInfo?.headers["mcp-session-id"] ?? void 0;
2988
+ if (sessionId === void 0 && this.config.transport === "stdio") {
2989
+ sessionId = "stdio";
2990
+ }
2991
+ this.subscriptionManager.unsubscribe(
2992
+ uri,
2993
+ sessionId
2994
+ );
2995
+ return {};
2996
+ }
2997
+ );
2422
2998
  logger.info(getFilterSummary(this.toolFilter), { module: "FILTER" });
2423
2999
  logger.info("MCP Server initialized", {
2424
3000
  module: "SERVER",
@@ -2434,13 +3010,17 @@ var DbMcpServer = class {
2434
3010
  this.toolFilter
2435
3011
  );
2436
3012
  registerHelpResources(this.server, this.toolFilter);
3013
+ registerObservabilityResources(this.server);
3014
+ if (this.toolFilter.enabledGroups.has("admin")) {
3015
+ registerAdminTools(this.server);
3016
+ }
2437
3017
  const internalMcp = this.server;
2438
3018
  const handlers = internalMcp.server._requestHandlers;
2439
3019
  if (!handlers?.has("tools/list")) {
2440
3020
  throw new DbMcpError(
2441
3021
  "Security: SDK _requestHandlers monkey-patch failed. Scope filtering is disabled.",
2442
3022
  "SERVER_START_FAILED",
2443
- "internal" /* INTERNAL */
3023
+ ErrorCategory.INTERNAL
2444
3024
  );
2445
3025
  }
2446
3026
  const originalListToolsHandler = handlers.get("tools/list");
@@ -2459,6 +3039,11 @@ var DbMcpServer = class {
2459
3039
  if (config.audit?.enabled) {
2460
3040
  this.auditInitPromise = this.initializeAudit(config);
2461
3041
  }
3042
+ setInterval(() => {
3043
+ if (this.subscriptionManager.hasSubscribers("sqlite://health")) {
3044
+ void this.subscriptionManager.notifyResourceUpdated("sqlite://health");
3045
+ }
3046
+ }, 6e4).unref();
2462
3047
  }
2463
3048
  /**
2464
3049
  * Register a database adapter
@@ -2470,6 +3055,9 @@ var DbMcpServer = class {
2470
3055
  }
2471
3056
  await adapter.connect(config);
2472
3057
  this.adapters.set(adapterId, adapter);
3058
+ if ("setAllowedIoRoots" in adapter) {
3059
+ adapter.setAllowedIoRoots(this.config.allowedIoRoots);
3060
+ }
2473
3061
  if (this.auditInterceptor && "setAuditInterceptor" in adapter) {
2474
3062
  adapter.setAuditInterceptor(this.auditInterceptor);
2475
3063
  this.auditInterceptor.setQueryAdapter({
@@ -2482,6 +3070,9 @@ var DbMcpServer = class {
2482
3070
  adapter.registerTools(this.server, this.toolFilter);
2483
3071
  adapter.registerResources(this.server);
2484
3072
  adapter.registerPrompts(this.server);
3073
+ adapter.on("schemaChanged", () => {
3074
+ void this.subscriptionManager.notifySchemaSubscribers();
3075
+ });
2485
3076
  const toolDefs = adapter.getToolDefinitions();
2486
3077
  const scopesMap = /* @__PURE__ */ new Map();
2487
3078
  for (const tool of toolDefs) {
@@ -2522,7 +3113,7 @@ var DbMcpServer = class {
2522
3113
  throw new DbMcpError(
2523
3114
  `Unsupported transport: ${this.config.transport}`,
2524
3115
  "SERVER_START_FAILED",
2525
- "config" /* CONFIGURATION */
3116
+ ErrorCategory.CONFIGURATION
2526
3117
  );
2527
3118
  }
2528
3119
  }
@@ -2544,7 +3135,7 @@ var DbMcpServer = class {
2544
3135
  * Start server with HTTP transport (Streamable HTTP with SSE support)
2545
3136
  */
2546
3137
  async startHttp() {
2547
- const { HttpTransport } = await import('./http-HWTUVFIA.js');
3138
+ const { HttpTransport } = await import('./http-ZDMK46QU.js');
2548
3139
  const oauthConfig = {
2549
3140
  enabled: this.config.oauth?.enabled ?? false,
2550
3141
  authorizationServerUrl: this.config.oauth?.authorizationServerUrl ?? "",
@@ -2573,7 +3164,10 @@ var DbMcpServer = class {
2573
3164
  },
2574
3165
  oauth: oauthConfig,
2575
3166
  stateless: this.config.statelessHttp ?? false,
2576
- noAuthEnforcement: this.config.noAuthEnforcement ?? false
3167
+ noAuthEnforcement: this.config.noAuthEnforcement ?? false,
3168
+ ...this.config.metricsExport !== void 0 && {
3169
+ metricsExport: this.config.metricsExport
3170
+ }
2577
3171
  });
2578
3172
  await transport.initialize(this.server);
2579
3173
  await transport.start();
@@ -2605,6 +3199,11 @@ var DbMcpServer = class {
2605
3199
  await this.auditLogger.close();
2606
3200
  logger.info("Audit logger closed", { module: "AUDIT" });
2607
3201
  }
3202
+ metrics.close();
3203
+ if (this.systemDb) {
3204
+ this.systemDb.close();
3205
+ logger.info("System database closed", { module: "SYSTEM_DB" });
3206
+ }
2608
3207
  if (this.backupManager) {
2609
3208
  await this.backupManager.flush();
2610
3209
  logger.info("Backup manager flushed", { module: "AUDIT" });
@@ -2619,7 +3218,12 @@ var DbMcpServer = class {
2619
3218
  async initializeAudit(config) {
2620
3219
  const auditConfig = config.audit;
2621
3220
  if (!auditConfig?.enabled) return;
2622
- this.auditLogger = new AuditLogger(auditConfig);
3221
+ if (auditConfig.logPath !== "stderr") {
3222
+ this.systemDb = new SystemDb({ dbPath: auditConfig.logPath });
3223
+ await this.systemDb.init();
3224
+ metrics.setSystemDb(this.systemDb);
3225
+ }
3226
+ this.auditLogger = new AuditLogger(auditConfig, this.systemDb);
2623
3227
  await this.auditLogger.init();
2624
3228
  if (auditConfig.backup?.enabled) {
2625
3229
  this.backupManager = new BackupManager(
@@ -2635,6 +3239,9 @@ var DbMcpServer = class {
2635
3239
  if (this.backupManager && this.toolFilter.enabledGroups.has("admin")) {
2636
3240
  registerAuditBackupTools(this.server, this.backupManager, this.adapters);
2637
3241
  }
3242
+ if (this.toolFilter.enabledGroups.has("admin")) {
3243
+ registerAuditSearchTool(this.server, this.auditLogger);
3244
+ }
2638
3245
  logger.info(
2639
3246
  `Audit logging enabled (${auditConfig.logPath}, redact=${String(auditConfig.redact)}, reads=${String(auditConfig.auditReads)}, backup=${String(!!auditConfig.backup?.enabled)})`,
2640
3247
  { module: "AUDIT" }