db-mcp 3.0.1 → 4.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 (92) hide show
  1. package/README.md +92 -75
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/database-adapter.d.ts +2 -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/resources.d.ts +1 -1
  9. package/dist/adapters/sqlite/resources.d.ts.map +1 -1
  10. package/dist/adapters/sqlite/schemas/admin.d.ts +29 -15
  11. package/dist/adapters/sqlite/schemas/admin.d.ts.map +1 -1
  12. package/dist/adapters/sqlite/schemas/core.d.ts +2 -0
  13. package/dist/adapters/sqlite/schemas/core.d.ts.map +1 -1
  14. package/dist/adapters/sqlite/schemas/fts.d.ts +4 -0
  15. package/dist/adapters/sqlite/schemas/fts.d.ts.map +1 -1
  16. package/dist/adapters/sqlite/schemas/introspection.d.ts +20 -11
  17. package/dist/adapters/sqlite/schemas/introspection.d.ts.map +1 -1
  18. package/dist/adapters/sqlite/schemas/spatialite.d.ts +1 -0
  19. package/dist/adapters/sqlite/schemas/spatialite.d.ts.map +1 -1
  20. package/dist/adapters/sqlite/schemas/text.d.ts +36 -0
  21. package/dist/adapters/sqlite/schemas/text.d.ts.map +1 -1
  22. package/dist/adapters/sqlite/sqlite-adapter.d.ts.map +1 -1
  23. package/dist/adapters/sqlite/tools/admin/index.d.ts.map +1 -1
  24. package/dist/adapters/sqlite/tools/admin/pragma.d.ts +0 -4
  25. package/dist/adapters/sqlite/tools/admin/pragma.d.ts.map +1 -1
  26. package/dist/adapters/sqlite/tools/core/queries.d.ts.map +1 -1
  27. package/dist/adapters/sqlite/tools/core/tables.d.ts.map +1 -1
  28. package/dist/adapters/sqlite/tools/fts.d.ts.map +1 -1
  29. package/dist/adapters/sqlite/tools/introspection/diagnostics/indexes.d.ts.map +1 -1
  30. package/dist/adapters/sqlite/tools/json-helpers/read.d.ts.map +1 -1
  31. package/dist/adapters/sqlite/tools/json-operations/crud.d.ts.map +1 -1
  32. package/dist/adapters/sqlite/tools/json-operations/query.d.ts.map +1 -1
  33. package/dist/adapters/sqlite/tools/text/helpers.d.ts +9 -0
  34. package/dist/adapters/sqlite/tools/text/helpers.d.ts.map +1 -1
  35. package/dist/adapters/sqlite/tools/text/index.d.ts.map +1 -1
  36. package/dist/adapters/sqlite/tools/text/search.d.ts +4 -0
  37. package/dist/adapters/sqlite/tools/text/search.d.ts.map +1 -1
  38. package/dist/adapters/sqlite/types.d.ts +4 -0
  39. package/dist/adapters/sqlite/types.d.ts.map +1 -1
  40. package/dist/adapters/sqlite-native/native-sqlite-adapter.d.ts.map +1 -1
  41. package/dist/adapters/sqlite-native/tools/spatialite/tools.d.ts.map +1 -1
  42. package/dist/audit/interceptor.d.ts.map +1 -1
  43. package/dist/audit/logger.d.ts +26 -25
  44. package/dist/audit/logger.d.ts.map +1 -1
  45. package/dist/{chunk-W5WQVNVX.js → chunk-EQF3WZHX.js} +10 -17
  46. package/dist/{chunk-37G4DLL6.js → chunk-NIC5Z2CA.js} +732 -138
  47. package/dist/chunk-R5M4WKDM.js +987 -0
  48. package/dist/{chunk-NV34JSQM.js → chunk-TGE7LFAR.js} +708 -996
  49. package/dist/{chunk-L552U3QS.js → chunk-Y57WFKAP.js} +227 -9
  50. package/dist/cli.js +103 -10
  51. package/dist/codemode/sandbox.d.ts.map +1 -1
  52. package/dist/constants/server-instructions.d.ts +1 -1
  53. package/dist/constants/server-instructions.d.ts.map +1 -1
  54. package/dist/filtering/tool-constants.d.ts +9 -9
  55. package/dist/{http-HWTUVFIA.js → http-2WGR3E5K.js} +13 -1
  56. package/dist/index.js +4 -4
  57. package/dist/observability/metrics.d.ts +36 -0
  58. package/dist/observability/metrics.d.ts.map +1 -0
  59. package/dist/observability/system-db.d.ts +13 -0
  60. package/dist/observability/system-db.d.ts.map +1 -0
  61. package/dist/server/mcp-server.d.ts +3 -0
  62. package/dist/server/mcp-server.d.ts.map +1 -1
  63. package/dist/server/registration/admin-tools.d.ts +6 -0
  64. package/dist/server/registration/admin-tools.d.ts.map +1 -0
  65. package/dist/server/registration/audit-tools.d.ts +4 -0
  66. package/dist/server/registration/audit-tools.d.ts.map +1 -1
  67. package/dist/server/registration/help-resources.d.ts.map +1 -1
  68. package/dist/server/registration/index.d.ts +3 -1
  69. package/dist/server/registration/index.d.ts.map +1 -1
  70. package/dist/server/registration/observability-resources.d.ts +11 -0
  71. package/dist/server/registration/observability-resources.d.ts.map +1 -0
  72. package/dist/server/subscription-manager.d.ts +35 -0
  73. package/dist/server/subscription-manager.d.ts.map +1 -0
  74. package/dist/{sqlite-TB56Z7G3.js → sqlite-D4WCNPU4.js} +8 -5
  75. package/dist/{sqlite-native-IF75BB3O.js → sqlite-native-DLVS2T52.js} +159 -34
  76. package/dist/transports/http/sessions/legacy-sse.d.ts.map +1 -1
  77. package/dist/transports/http/sessions/stateful.d.ts.map +1 -1
  78. package/dist/transports/http/transport.d.ts.map +1 -1
  79. package/dist/transports/http/types.d.ts +2 -0
  80. package/dist/transports/http/types.d.ts.map +1 -1
  81. package/dist/types/server.d.ts +2 -0
  82. package/dist/types/server.d.ts.map +1 -1
  83. package/dist/utils/index.d.ts +1 -0
  84. package/dist/utils/index.d.ts.map +1 -1
  85. package/dist/utils/logger/types.d.ts +1 -1
  86. package/dist/utils/logger/types.d.ts.map +1 -1
  87. package/dist/utils/tokens.d.ts +15 -0
  88. package/dist/utils/tokens.d.ts.map +1 -0
  89. package/package.json +10 -7
  90. package/dist/chunk-E5IESRTK.js +0 -489
  91. package/dist/utils/insights-manager.d.ts +0 -39
  92. 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, redactObject, getRequiredScope, estimateTokens, AuditListBackupsOutputSchema, AuditListBackupsSchema, AuditGetBackupOutputSchema, AuditCleanupOutputSchema, AuditDiffBackupOutputSchema, AuditRestoreBackupOutputSchema, AuditSearchOutputSchema, AuditSearchSchema } from './chunk-R5M4WKDM.js';
2
+ import { parseToolFilter, getToolFilterFromEnv, logger, getFilterSummary, DbMcpError, getAuthContext, scopesGrantToolAccess, registerToolScopes, metrics, InsufficientScopeError, sanitizeErrorMessage, formatHandlerError } from './chunk-Y57WFKAP.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
 
@@ -33,7 +36,7 @@ Only help resources for your enabled tool groups are registered.`;
33
36
  var HELP_CONTENT = /* @__PURE__ */ new Map([
34
37
  [
35
38
  "admin",
36
- `# db-mcp Help \u2014 Database Administration (32N/31W tools) + Server Audit (5 tools)
39
+ `# db-mcp Help \u2014 Database Administration (31N/30W tools) + Server Audit (7 tools)
37
40
 
38
41
  ## Maintenance
39
42
 
@@ -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({ query: "DROP TABLE", limit: 50 }); // search and filter structured audit logs
69
74
  \`\`\`
70
75
 
71
76
  ## PRAGMA
@@ -140,12 +145,6 @@ 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
148
  \`\`\``
150
149
  ],
151
150
  [
@@ -154,7 +153,7 @@ sqlite_append_insight({ insight: "Q4 revenue increased 23% YoY" }); // add to me
154
153
 
155
154
  ## Basic Queries
156
155
 
157
- - \`sqlite_read_query({ query: "SELECT * FROM users LIMIT 10" })\` \u2014 execute SELECT, PRAGMA, EXPLAIN, or WITH statements
156
+ - \`sqlite_read_query({ query: "SELECT * FROM users LIMIT 10", cursor: "..." })\` \u2014 execute SELECT, PRAGMA, EXPLAIN, or WITH statements. Supports \`cursor\` for offset-based pagination (returns \`nextCursor\`). **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.
158
157
  - \`sqlite_write_query({ query: "INSERT INTO users (name) VALUES ('Alice')" })\` \u2014 execute INSERT, UPDATE, DELETE, REPLACE, or trigger DDL (CREATE/DROP TRIGGER)
159
158
 
160
159
  ## Tables & Schema
@@ -321,6 +320,7 @@ sqlite_spatialite_index({
321
320
  19. **sqlite_batch_insert**: All rows must have the same keys \u2014 inconsistent column sets across rows will cause errors or unexpected NULLs
322
321
  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
322
  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\`
323
+ 22. **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.
324
324
 
325
325
  ## WASM vs Native
326
326
 
@@ -333,6 +333,7 @@ sqlite_spatialite_index({
333
333
  | Backup/Restore/Dump/VacuumInto/Verify (5 tools) | \u2705 | \u274C | Graceful error |
334
334
  | R-Tree spatial indexing | \u2705 | \u274C | Graceful error |
335
335
  | CSV virtual tables | \u2705 | \u274C | Graceful error |
336
+ | SQLCipher Encryption at Rest | \u2705 | \u274C | Graceful error |
336
337
  | generate_series | JS fallback | JS fallback | \u2014 |
337
338
  | dbstat | \u2705 native (per-table) | \u274C | JS (counts only) |
338
339
  | soundex() | \u2705 native | \u274C | JS |
@@ -434,6 +435,13 @@ sqlite_storage_analysis({ limit: 10 }); // top 10 tables only
434
435
  // Audit index effectiveness \u2014 find redundant, missing FK, unindexed large tables
435
436
  sqlite_index_audit({ excludeSystemTables: true });
436
437
  sqlite_index_audit({ table: "orders", minSeverity: "warning" }); // reduce payload
438
+ // Run EXPLAIN QUERY PLAN on target queries to recommend composite/partial indexes
439
+ sqlite_index_audit({
440
+ recommendComposite: true,
441
+ queriesToAnalyze: [
442
+ "SELECT * FROM orders WHERE user_id = 1 AND status = 'active'",
443
+ ],
444
+ });
437
445
 
438
446
  // EXPLAIN QUERY PLAN with scan-type classification and optimization suggestions (SELECT/WITH only)
439
447
  sqlite_query_plan({ sql: "SELECT * FROM orders WHERE status = 'active'" });
@@ -693,7 +701,7 @@ sqlite_window_moving_avg({
693
701
  ],
694
702
  [
695
703
  "text",
696
- `# db-mcp Help \u2014 Text Processing & FTS5 (19N/14W: 14 text + 5 FTS5 [NATIVE ONLY])
704
+ `# db-mcp Help \u2014 Text Processing & FTS5 (20N/15W: 15 text + 5 FTS5 [NATIVE ONLY])
697
705
 
698
706
  ## Full-Text Search / FTS5 (5 tools, Native only)
699
707
 
@@ -713,6 +721,8 @@ sqlite_fts_search({
713
721
  table: "articles_fts",
714
722
  query: "machine learning",
715
723
  limit: 10,
724
+ includeFacets: true, // optionally return breakdown by technique
725
+ cursor: "...", // Optional offset-based cursor for pagination (returns nextCursor)
716
726
  });
717
727
  sqlite_fts_match_info({ table: "articles_fts", query: "machine learning" }); // bm25 ranking info
718
728
 
@@ -732,7 +742,7 @@ sqlite_fts_headline({
732
742
 
733
743
  \u26A0\uFE0F FTS5 virtual tables (\`*_fts\`) and shadow tables (\`*_fts_*\`) are hidden from \`sqlite_list_tables\` for cleaner output
734
744
 
735
- ## Text Processing (14 tools)
745
+ ## Text Processing (15 tools)
736
746
 
737
747
  \`\`\`javascript
738
748
  // Regex patterns: \u26A0\uFE0F Double-escape backslashes (\\\\) when passing through JSON/MCP
@@ -818,6 +828,17 @@ sqlite_advanced_search({
818
828
  searchTerm: "laptop",
819
829
  techniques: ["exact", "fuzzy", "phonetic"],
820
830
  fuzzyThreshold: 0.4,
831
+ includeFacets: true,
832
+ });
833
+
834
+ // Hybrid Search \u2014 combines FTS5 text search and vector embedding search via Reciprocal Rank Fusion (RRF)
835
+ sqlite_hybrid_search({
836
+ table: "articles_fts",
837
+ query: "machine learning",
838
+ vectorColumn: "embedding",
839
+ queryVector: [0.1, 0.2, 0.3], // vector data from external embedding API
840
+ metric: "cosine",
841
+ limit: 10,
821
842
  });
822
843
 
823
844
  // Sentiment analysis \u2014 text analysis (can analyze raw text or database columns)
@@ -899,21 +920,23 @@ sqlite_vector_distance({ vector1: [...], vector2: [...], metric: "cosine" }); //
899
920
  \`\`\``
900
921
  ]
901
922
  ]);
923
+
924
+ // src/audit/logger.ts
902
925
  var BUFFER_HIGH_WATER = 50;
903
926
  var FLUSH_INTERVAL_MS = 100;
904
927
  var DEFAULT_RECENT_COUNT = 50;
905
928
  var STDERR_SENTINEL = "stderr";
906
- var TAIL_READ_BYTES = 65536;
907
929
  var AuditLogger = class {
908
930
  config;
909
931
  buffer = [];
910
932
  flushTimer = null;
911
933
  activeFlush = null;
912
934
  closed = false;
913
- dirEnsured = false;
914
935
  stderrMode;
915
- constructor(config) {
936
+ systemDb;
937
+ constructor(config, systemDb = null) {
916
938
  this.config = config;
939
+ this.systemDb = systemDb;
917
940
  this.stderrMode = config.logPath.toLowerCase() === STDERR_SENTINEL;
918
941
  if (config.enabled) {
919
942
  this.flushTimer = setInterval(() => {
@@ -923,39 +946,27 @@ var AuditLogger = class {
923
946
  }
924
947
  }
925
948
  /**
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.
949
+ * Initialization is handled externally by SystemDb.
929
950
  */
951
+ // eslint-disable-next-line @typescript-eslint/require-await
930
952
  async init() {
931
953
  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
- }
943
954
  }
944
955
  /**
945
956
  * 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.
957
+ * Non-blocking — the entry is queued; the
958
+ * actual DB write happens on the next flush cycle.
948
959
  */
949
960
  log(entry) {
950
961
  if (this.closed || !this.config.enabled) return;
951
962
  const safeEntry = redactObject(entry, 0, 5);
952
- this.buffer.push(JSON.stringify(safeEntry));
963
+ this.buffer.push(safeEntry);
953
964
  if (this.buffer.length >= BUFFER_HIGH_WATER) {
954
965
  void this.flush();
955
966
  }
956
967
  }
957
968
  /**
958
- * Flush the buffer to disk.
969
+ * Flush the buffer to SystemDb or stderr.
959
970
  * Safe to call concurrently — serialises via `this.activeFlush` Promise.
960
971
  */
961
972
  async flush() {
@@ -965,25 +976,45 @@ var AuditLogger = class {
965
976
  }
966
977
  if (this.buffer.length === 0) return;
967
978
  const doFlush = async () => {
968
- await this.rotateIfNeeded();
969
- const lines = this.buffer;
979
+ await Promise.resolve();
980
+ const entries = this.buffer;
970
981
  this.buffer = [];
971
982
  try {
972
983
  if (this.stderrMode) {
984
+ const lines = entries.map((e) => JSON.stringify(e));
973
985
  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
- );
986
+ } else if (this.systemDb) {
987
+ const db = this.systemDb.getDb();
988
+ const stmt = db.prepare(`
989
+ INSERT INTO audit_logs (timestamp, requestId, tool, category, scope, user, scopesJson, durationMs, success, tokenEstimate, error, argsJson, backupPath)
990
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
991
+ `);
992
+ const transaction = db.transaction((logs) => {
993
+ for (const log of logs) {
994
+ stmt.run(
995
+ log.timestamp,
996
+ log.requestId,
997
+ log.tool,
998
+ log.category,
999
+ log.scope,
1000
+ log.user ?? null,
1001
+ JSON.stringify(log.scopes),
1002
+ log.durationMs,
1003
+ log.success ? 1 : 0,
1004
+ log.tokenEstimate ?? null,
1005
+ log.error ?? null,
1006
+ log.args ? JSON.stringify(log.args) : null,
1007
+ log.backup ?? null
1008
+ );
1009
+ }
1010
+ });
1011
+ transaction(entries);
981
1012
  }
982
1013
  } catch (err) {
983
1014
  const message = err instanceof Error ? err.message : String(err);
984
1015
  process.stderr.write(`[AUDIT] Write failed: ${message}
985
1016
  `);
986
- this.buffer.unshift(...lines);
1017
+ this.buffer.unshift(...entries);
987
1018
  }
988
1019
  };
989
1020
  this.activeFlush = doFlush();
@@ -1005,79 +1036,197 @@ var AuditLogger = class {
1005
1036
  await this.flush();
1006
1037
  }
1007
1038
  /**
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)
1039
+ * Read the most recent audit entries from the SystemDb.
1040
+ * Retrieve recent entries directly from the SystemDb.
1014
1041
  */
1015
- async recent(count = DEFAULT_RECENT_COUNT) {
1016
- if (this.stderrMode) return [];
1042
+ async recent(limit = DEFAULT_RECENT_COUNT) {
1043
+ if (this.stderrMode || !this.systemDb) {
1044
+ return [];
1045
+ }
1017
1046
  await this.flush();
1018
1047
  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
- }
1048
+ const db = this.systemDb.getDb();
1049
+ const rows = db.prepare(
1050
+ `
1051
+ SELECT * FROM audit_logs
1052
+ ORDER BY timestamp DESC
1053
+ LIMIT ?
1054
+ `
1055
+ ).all(limit);
1056
+ return rows.map((row) => ({
1057
+ timestamp: row.timestamp,
1058
+ requestId: row.requestId,
1059
+ tool: row.tool,
1060
+ category: row.category,
1061
+ scope: row.scope,
1062
+ user: row.user,
1063
+ scopes: row.scopesJson ? JSON.parse(row.scopesJson) : [],
1064
+ durationMs: row.durationMs,
1065
+ success: row.success === 1,
1066
+ tokenEstimate: row.tokenEstimate ?? void 0,
1067
+ error: row.error ?? void 0,
1068
+ args: row.argsJson ? JSON.parse(row.argsJson) : void 0,
1069
+ backup: row.backupPath ?? void 0
1070
+ }));
1047
1071
  } catch {
1048
1072
  return [];
1049
1073
  }
1050
1074
  }
1051
1075
  /**
1052
- * Ensure the parent directory of the log file exists.
1076
+ * Search and filter audit entries from the SystemDb.
1053
1077
  */
1054
- async ensureDirectory() {
1055
- if (this.dirEnsured) return;
1078
+ async search(filters) {
1079
+ if (this.stderrMode || !this.systemDb) {
1080
+ return { entries: [], totalCount: 0 };
1081
+ }
1082
+ await this.flush();
1056
1083
  try {
1057
- await mkdir(dirname(this.config.logPath), { recursive: true });
1058
- this.dirEnsured = true;
1084
+ const db = this.systemDb.getDb();
1085
+ let sql = "SELECT * FROM audit_logs WHERE 1=1";
1086
+ let countSql = "SELECT COUNT(*) as c FROM audit_logs WHERE 1=1";
1087
+ const params = [];
1088
+ if (filters.tool) {
1089
+ sql += " AND tool = ?";
1090
+ countSql += " AND tool = ?";
1091
+ params.push(filters.tool);
1092
+ }
1093
+ if (filters.category) {
1094
+ sql += " AND category = ?";
1095
+ countSql += " AND category = ?";
1096
+ params.push(filters.category);
1097
+ }
1098
+ if (filters.success !== void 0) {
1099
+ sql += " AND success = ?";
1100
+ countSql += " AND success = ?";
1101
+ params.push(filters.success ? 1 : 0);
1102
+ }
1103
+ if (filters.requestId) {
1104
+ sql += " AND requestId = ?";
1105
+ countSql += " AND requestId = ?";
1106
+ params.push(filters.requestId);
1107
+ }
1108
+ if (filters.fromTimestamp) {
1109
+ sql += " AND timestamp >= ?";
1110
+ countSql += " AND timestamp >= ?";
1111
+ params.push(filters.fromTimestamp);
1112
+ }
1113
+ if (filters.toTimestamp) {
1114
+ sql += " AND timestamp <= ?";
1115
+ countSql += " AND timestamp <= ?";
1116
+ params.push(filters.toTimestamp);
1117
+ }
1118
+ const totalCount = db.prepare(countSql).get(...params).c;
1119
+ sql += " ORDER BY timestamp DESC LIMIT ? OFFSET ?";
1120
+ params.push(filters.limit ?? 50);
1121
+ params.push(filters.offset ?? 0);
1122
+ const rows = db.prepare(sql).all(...params);
1123
+ const entries = rows.map((row) => ({
1124
+ timestamp: row.timestamp,
1125
+ requestId: row.requestId,
1126
+ tool: row.tool,
1127
+ category: row.category,
1128
+ scope: row.scope,
1129
+ user: row.user,
1130
+ scopes: row.scopesJson ? JSON.parse(row.scopesJson) : [],
1131
+ durationMs: row.durationMs,
1132
+ success: row.success === 1,
1133
+ tokenEstimate: row.tokenEstimate ?? void 0,
1134
+ error: row.error ?? void 0,
1135
+ args: row.argsJson ? JSON.parse(row.argsJson) : void 0,
1136
+ backup: row.backupPath ?? void 0
1137
+ }));
1138
+ return { entries, totalCount };
1059
1139
  } catch {
1060
- this.dirEnsured = true;
1140
+ return { entries: [], totalCount: 0 };
1061
1141
  }
1062
1142
  }
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;
1143
+ };
1144
+ var SystemDb = class {
1145
+ db = null;
1146
+ config;
1147
+ constructor(config) {
1148
+ this.config = config;
1149
+ }
1150
+ async init() {
1070
1151
  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);
1077
- }
1078
- const rotatedPath = `${this.config.logPath}.1`;
1079
- await rename(this.config.logPath, rotatedPath);
1080
- } catch {
1152
+ const encryptionKey = process.env["DB_ENCRYPTION_KEY"];
1153
+ let BetterSqlite3;
1154
+ if (encryptionKey) {
1155
+ BetterSqlite3 = (await import('better-sqlite3-multiple-ciphers')).default;
1156
+ } else {
1157
+ BetterSqlite3 = (await import('better-sqlite3')).default;
1158
+ }
1159
+ if (this.config.dbPath !== ":memory:") {
1160
+ mkdirSync(dirname(this.config.dbPath), { recursive: true });
1161
+ }
1162
+ this.db = new BetterSqlite3(this.config.dbPath);
1163
+ if (encryptionKey && this.config.dbPath !== ":memory:" && this.config.dbPath !== "") {
1164
+ const escapedKey = encryptionKey.replace(/"/g, '""');
1165
+ this.db.pragma(`key = "${escapedKey}"`);
1166
+ }
1167
+ this.db.pragma("journal_mode = WAL");
1168
+ this.db.pragma("synchronous = NORMAL");
1169
+ this.db.exec(`
1170
+ CREATE TABLE IF NOT EXISTS audit_logs (
1171
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1172
+ timestamp TEXT NOT NULL,
1173
+ requestId TEXT NOT NULL,
1174
+ tool TEXT NOT NULL,
1175
+ category TEXT NOT NULL,
1176
+ scope TEXT NOT NULL,
1177
+ user TEXT,
1178
+ durationMs INTEGER NOT NULL,
1179
+ success INTEGER NOT NULL,
1180
+ tokenEstimate INTEGER,
1181
+ error TEXT,
1182
+ argsJson TEXT,
1183
+ scopesJson TEXT,
1184
+ backupPath TEXT
1185
+ );
1186
+
1187
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
1188
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_tool ON audit_logs(tool);
1189
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_category ON audit_logs(category);
1190
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_success ON audit_logs(success);
1191
+
1192
+ CREATE TABLE IF NOT EXISTS metrics_snapshots (
1193
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1194
+ timestamp TEXT NOT NULL,
1195
+ tool TEXT NOT NULL,
1196
+ calls INTEGER NOT NULL,
1197
+ errors INTEGER NOT NULL,
1198
+ p50 INTEGER NOT NULL,
1199
+ p95 INTEGER NOT NULL,
1200
+ p99 INTEGER NOT NULL,
1201
+ tokens INTEGER NOT NULL
1202
+ );
1203
+
1204
+ CREATE INDEX IF NOT EXISTS idx_metrics_snapshots_timestamp ON metrics_snapshots(timestamp);
1205
+ `);
1206
+ logger.info(`System database initialized at ${this.config.dbPath}`, {
1207
+ module: "SYSTEM_DB"
1208
+ });
1209
+ } catch (err) {
1210
+ logger.error(
1211
+ "Failed to initialize SystemDb. better-sqlite3 may not be installed.",
1212
+ {
1213
+ module: "SYSTEM_DB",
1214
+ error: err instanceof Error ? err : new Error(String(err))
1215
+ }
1216
+ );
1217
+ throw err;
1218
+ }
1219
+ }
1220
+ getDb() {
1221
+ if (!this.db) {
1222
+ throw new Error("SystemDb not initialized");
1223
+ }
1224
+ return this.db;
1225
+ }
1226
+ close() {
1227
+ if (this.db) {
1228
+ this.db.close();
1229
+ this.db = null;
1081
1230
  }
1082
1231
  }
1083
1232
  };
@@ -1538,11 +1687,14 @@ function createAuditInterceptor(auditLogger, backupManager) {
1538
1687
  ...result,
1539
1688
  _meta: { tokenEstimate: 0 }
1540
1689
  });
1541
- tokenEstimate = Math.ceil(Buffer.byteLength(json, "utf8") / 4);
1690
+ tokenEstimate = estimateTokens(json, "json");
1542
1691
  } catch {
1543
1692
  }
1544
1693
  } else if (typeof result === "string") {
1545
- tokenEstimate = Math.ceil(Buffer.byteLength(result, "utf8") / 4);
1694
+ const isSql = /^\s*(?:SELECT|INSERT|UPDATE|DELETE|WITH|CREATE|ALTER|DROP|PRAGMA)\b/i.test(
1695
+ result
1696
+ );
1697
+ tokenEstimate = estimateTokens(result, isSql ? "sql" : "text");
1546
1698
  }
1547
1699
  return result;
1548
1700
  } catch (err) {
@@ -1561,10 +1713,16 @@ function createAuditInterceptor(auditLogger, backupManager) {
1561
1713
  ...errorResult,
1562
1714
  _meta: { tokenEstimate: 0 }
1563
1715
  });
1564
- tokenEstimate = Math.ceil(Buffer.byteLength(enriched, "utf8") / 4);
1716
+ tokenEstimate = estimateTokens(enriched, "json");
1565
1717
  throw err;
1566
1718
  } finally {
1567
1719
  const durationMs = Math.round(performance.now() - start);
1720
+ metrics.recordToolCall(
1721
+ options?.logAs ?? toolName,
1722
+ durationMs,
1723
+ success,
1724
+ tokenEstimate ?? 0
1725
+ );
1568
1726
  if (isReadScope) {
1569
1727
  auditLogger.log({
1570
1728
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1730,15 +1888,18 @@ function registerHelpResources(server, toolFilter) {
1730
1888
  mimeType: "text/markdown",
1731
1889
  annotations: ASSISTANT_FOCUSED
1732
1890
  },
1733
- () => ({
1734
- contents: [
1735
- {
1736
- uri: "sqlite://help",
1737
- mimeType: "text/markdown",
1738
- text: gotchasContent
1739
- }
1740
- ]
1741
- })
1891
+ () => {
1892
+ metrics.recordResourceRead("sqlite://help");
1893
+ return {
1894
+ contents: [
1895
+ {
1896
+ uri: "sqlite://help",
1897
+ mimeType: "text/markdown",
1898
+ text: gotchasContent
1899
+ }
1900
+ ]
1901
+ };
1902
+ }
1742
1903
  );
1743
1904
  }
1744
1905
  const groupHelpKeys = [
@@ -1768,15 +1929,18 @@ function registerHelpResources(server, toolFilter) {
1768
1929
  mimeType: "text/markdown",
1769
1930
  annotations: ASSISTANT_FOCUSED
1770
1931
  },
1771
- () => ({
1772
- contents: [
1773
- {
1774
- uri: `sqlite://help/${key}`,
1775
- mimeType: "text/markdown",
1776
- text: content
1777
- }
1778
- ]
1779
- })
1932
+ () => {
1933
+ metrics.recordResourceRead(`sqlite://help/${key}`);
1934
+ return {
1935
+ contents: [
1936
+ {
1937
+ uri: `sqlite://help/${key}`,
1938
+ mimeType: "text/markdown",
1939
+ text: content
1940
+ }
1941
+ ]
1942
+ };
1943
+ }
1780
1944
  );
1781
1945
  }
1782
1946
  const registeredHelp = ["sqlite://help"];
@@ -1816,6 +1980,7 @@ function registerAuditResource(server, auditLogger, backupManager) {
1816
1980
  mimeType: "application/json"
1817
1981
  },
1818
1982
  async () => {
1983
+ metrics.recordResourceRead("sqlite://audit");
1819
1984
  const recent = await auditLogger.recent(50);
1820
1985
  const backupStats = backupManager ? await backupManager.getStats() : void 0;
1821
1986
  const payload = {
@@ -2365,16 +2530,373 @@ function registerAuditBackupTools(server, backupManager, adaptersMap) {
2365
2530
  { module: "AUDIT" }
2366
2531
  );
2367
2532
  }
2533
+ function registerAuditSearchTool(server, auditLogger) {
2534
+ if (!auditLogger) return;
2535
+ server.registerTool(
2536
+ "sqlite_audit_search",
2537
+ {
2538
+ title: "Search Audit Log",
2539
+ description: "Search and filter structured audit logs from the System Database. Returns recent tool invocations, outcomes, token estimates, and parameters.",
2540
+ inputSchema: AuditSearchSchema,
2541
+ outputSchema: AuditSearchOutputSchema,
2542
+ annotations: {
2543
+ readOnlyHint: true,
2544
+ destructiveHint: false,
2545
+ idempotentHint: true,
2546
+ openWorldHint: false
2547
+ }
2548
+ },
2549
+ async (args) => {
2550
+ const authCtx = getAuthContext();
2551
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "sqlite_audit_search")) {
2552
+ throw new InsufficientScopeError(["admin", "full"], authCtx.scopes);
2553
+ }
2554
+ let parsed;
2555
+ try {
2556
+ parsed = AuditSearchSchema.parse(args ?? {});
2557
+ } catch (error) {
2558
+ const structured = formatHandlerError(error);
2559
+ return {
2560
+ content: [
2561
+ {
2562
+ type: "text",
2563
+ text: JSON.stringify(structured, null, 2)
2564
+ }
2565
+ ],
2566
+ isError: true,
2567
+ structuredContent: structured
2568
+ };
2569
+ }
2570
+ const { entries, totalCount } = await auditLogger.search(parsed);
2571
+ const result = {
2572
+ success: true,
2573
+ entries,
2574
+ count: entries.length,
2575
+ totalCount
2576
+ };
2577
+ const tokenEstimate = Math.ceil(
2578
+ Buffer.byteLength(JSON.stringify(result), "utf8") / 4
2579
+ );
2580
+ return {
2581
+ content: [
2582
+ {
2583
+ type: "text",
2584
+ text: JSON.stringify(
2585
+ { ...result, _meta: { tokenEstimate } },
2586
+ null,
2587
+ 2
2588
+ )
2589
+ }
2590
+ ],
2591
+ structuredContent: result
2592
+ };
2593
+ }
2594
+ );
2595
+ registerToolScopes(/* @__PURE__ */ new Map([["sqlite_audit_search", ["admin", "full"]]]));
2596
+ logger.info("Registered audit search tool: sqlite_audit_search", {
2597
+ module: "AUDIT"
2598
+ });
2599
+ }
2600
+
2601
+ // src/server/registration/observability-resources.ts
2602
+ function registerObservabilityResources(server) {
2603
+ server.registerResource(
2604
+ "sqlite://metrics",
2605
+ "sqlite://metrics",
2606
+ {
2607
+ description: "Returns in-memory streaming metrics including p50/p95/p99 latency percentiles and token usage.",
2608
+ mimeType: "application/json"
2609
+ },
2610
+ () => {
2611
+ const summary = metrics.getSummary();
2612
+ return {
2613
+ contents: [
2614
+ {
2615
+ uri: "sqlite://metrics",
2616
+ mimeType: "application/json",
2617
+ text: JSON.stringify(summary, null, 2)
2618
+ }
2619
+ ]
2620
+ };
2621
+ }
2622
+ );
2623
+ }
2624
+ function registerAdminTools(server) {
2625
+ server.registerTool(
2626
+ "sqlite_server_config",
2627
+ {
2628
+ title: "Server Configuration",
2629
+ description: "Get or update runtime configuration values for the server. Currently supports updating the log level.",
2630
+ inputSchema: z.object({
2631
+ action: z.enum(["get", "set"]).describe("Whether to get or set the configuration value"),
2632
+ setting: z.enum(["logLevel"]).optional().describe("The setting to modify"),
2633
+ value: z.string().optional().describe(
2634
+ "The new value for the setting (e.g., 'debug', 'info', 'warning')"
2635
+ )
2636
+ }),
2637
+ annotations: {
2638
+ readOnlyHint: false,
2639
+ destructiveHint: false,
2640
+ idempotentHint: false,
2641
+ openWorldHint: false
2642
+ }
2643
+ },
2644
+ (args) => {
2645
+ const authCtx = getAuthContext();
2646
+ if (authCtx && !scopesGrantToolAccess(authCtx.scopes, "sqlite_server_config")) {
2647
+ throw new InsufficientScopeError(["admin"], authCtx.scopes);
2648
+ }
2649
+ try {
2650
+ const parsed = z.object({
2651
+ action: z.enum(["get", "set"]),
2652
+ setting: z.enum(["logLevel"]).optional(),
2653
+ value: z.string().optional()
2654
+ }).parse(args ?? {});
2655
+ const { action, setting, value } = parsed;
2656
+ if (action === "get") {
2657
+ return {
2658
+ content: [
2659
+ {
2660
+ type: "text",
2661
+ text: JSON.stringify(
2662
+ {
2663
+ success: true,
2664
+ config: {
2665
+ logLevel: logger.getLevel()
2666
+ }
2667
+ },
2668
+ null,
2669
+ 2
2670
+ )
2671
+ }
2672
+ ]
2673
+ };
2674
+ }
2675
+ if (action === "set") {
2676
+ if (setting === "logLevel" && value) {
2677
+ const validLevels = [
2678
+ "debug",
2679
+ "info",
2680
+ "notice",
2681
+ "warning",
2682
+ "error",
2683
+ "critical",
2684
+ "alert",
2685
+ "emergency"
2686
+ ];
2687
+ if (!validLevels.includes(value.toLowerCase())) {
2688
+ return {
2689
+ content: [
2690
+ {
2691
+ type: "text",
2692
+ text: JSON.stringify(
2693
+ {
2694
+ success: false,
2695
+ error: `Invalid log level: ${value}. Must be one of: ${validLevels.join(", ")}`,
2696
+ code: "INVALID_CONFIG"
2697
+ },
2698
+ null,
2699
+ 2
2700
+ )
2701
+ }
2702
+ ],
2703
+ isError: true
2704
+ };
2705
+ }
2706
+ logger.setLevel(value.toLowerCase());
2707
+ logger.info(
2708
+ `Log level dynamically changed to ${value} via sqlite_server_config tool`,
2709
+ {
2710
+ module: "SERVER"
2711
+ }
2712
+ );
2713
+ return {
2714
+ content: [
2715
+ {
2716
+ type: "text",
2717
+ text: JSON.stringify(
2718
+ {
2719
+ success: true,
2720
+ message: `Log level successfully updated to ${value}`
2721
+ },
2722
+ null,
2723
+ 2
2724
+ )
2725
+ }
2726
+ ]
2727
+ };
2728
+ }
2729
+ return {
2730
+ content: [
2731
+ {
2732
+ type: "text",
2733
+ text: JSON.stringify(
2734
+ {
2735
+ success: false,
2736
+ error: "Missing setting or value for set action",
2737
+ code: "INVALID_CONFIG"
2738
+ },
2739
+ null,
2740
+ 2
2741
+ )
2742
+ }
2743
+ ],
2744
+ isError: true
2745
+ };
2746
+ }
2747
+ return {
2748
+ content: [
2749
+ {
2750
+ type: "text",
2751
+ text: JSON.stringify({ success: false, error: "Invalid action" })
2752
+ }
2753
+ ],
2754
+ isError: true
2755
+ };
2756
+ } catch (error) {
2757
+ return {
2758
+ content: [
2759
+ {
2760
+ type: "text",
2761
+ text: JSON.stringify(
2762
+ {
2763
+ success: false,
2764
+ error: error instanceof Error ? error.message : String(error)
2765
+ },
2766
+ null,
2767
+ 2
2768
+ )
2769
+ }
2770
+ ],
2771
+ isError: true
2772
+ };
2773
+ }
2774
+ }
2775
+ );
2776
+ registerToolScopes(/* @__PURE__ */ new Map([["sqlite_server_config", ["admin"]]]));
2777
+ }
2778
+
2779
+ // src/server/subscription-manager.ts
2780
+ var SubscriptionManager = class {
2781
+ server;
2782
+ // Map of URI string -> Set of Session IDs
2783
+ subscriptions = /* @__PURE__ */ new Map();
2784
+ constructor(server) {
2785
+ this.server = server;
2786
+ }
2787
+ /**
2788
+ * Subscribe a session to a resource URI.
2789
+ */
2790
+ subscribe(uri, sessionId) {
2791
+ if (!sessionId) {
2792
+ logger.debug(
2793
+ `Subscription requested for ${uri} but no sessionId provided. Assuming stateless transport.`
2794
+ );
2795
+ return;
2796
+ }
2797
+ let sessions = this.subscriptions.get(uri);
2798
+ if (!sessions) {
2799
+ sessions = /* @__PURE__ */ new Set();
2800
+ this.subscriptions.set(uri, sessions);
2801
+ }
2802
+ if (!sessions.has(sessionId)) {
2803
+ sessions.add(sessionId);
2804
+ logger.debug(`Session ${sessionId} subscribed to resource ${uri}`, {
2805
+ module: "SERVER"
2806
+ });
2807
+ }
2808
+ }
2809
+ /**
2810
+ * Unsubscribe a session from a resource URI.
2811
+ */
2812
+ unsubscribe(uri, sessionId) {
2813
+ if (!sessionId) {
2814
+ return;
2815
+ }
2816
+ const sessions = this.subscriptions.get(uri);
2817
+ if (sessions?.has(sessionId)) {
2818
+ sessions.delete(sessionId);
2819
+ logger.debug(`Session ${sessionId} unsubscribed from resource ${uri}`, {
2820
+ module: "SERVER"
2821
+ });
2822
+ if (sessions.size === 0) {
2823
+ this.subscriptions.delete(uri);
2824
+ }
2825
+ }
2826
+ }
2827
+ /**
2828
+ * Unsubscribe a session from all resources (useful on transport disconnect).
2829
+ */
2830
+ unsubscribeSession(sessionId) {
2831
+ let count = 0;
2832
+ for (const [uri, sessions] of this.subscriptions.entries()) {
2833
+ if (sessions.has(sessionId)) {
2834
+ sessions.delete(sessionId);
2835
+ count++;
2836
+ if (sessions.size === 0) {
2837
+ this.subscriptions.delete(uri);
2838
+ }
2839
+ }
2840
+ }
2841
+ if (count > 0) {
2842
+ logger.debug(
2843
+ `Session ${sessionId} disconnected, removed ${count} subscriptions`,
2844
+ { module: "SERVER" }
2845
+ );
2846
+ }
2847
+ }
2848
+ /**
2849
+ * Notifies all subscribed clients that a resource has changed.
2850
+ */
2851
+ async notifyResourceUpdated(uri) {
2852
+ if (!this.hasSubscribers(uri)) {
2853
+ return;
2854
+ }
2855
+ try {
2856
+ await this.server.server.sendResourceUpdated({ uri });
2857
+ logger.debug(`Notified subscribers of update to ${uri}`, {
2858
+ module: "SERVER"
2859
+ });
2860
+ } catch (error) {
2861
+ logger.error(`Failed to notify subscribers for ${uri}`, {
2862
+ module: "SERVER",
2863
+ error: error instanceof Error ? error : new Error(String(error))
2864
+ });
2865
+ }
2866
+ }
2867
+ /**
2868
+ * Notifies subscribers of schema-related resources (schema, tables, and specific tables).
2869
+ */
2870
+ async notifySchemaSubscribers() {
2871
+ const urisToNotify = /* @__PURE__ */ new Set([
2872
+ "sqlite://schema",
2873
+ "sqlite://tables"
2874
+ ]);
2875
+ for (const uri of this.subscriptions.keys()) {
2876
+ if (uri.startsWith("sqlite://table/")) {
2877
+ urisToNotify.add(uri);
2878
+ }
2879
+ }
2880
+ const promises = Array.from(urisToNotify).map(
2881
+ (uri) => this.notifyResourceUpdated(uri)
2882
+ );
2883
+ await Promise.allSettled(promises);
2884
+ }
2885
+ /**
2886
+ * Check if there are any active subscribers for a specific URI.
2887
+ */
2888
+ hasSubscribers(uri) {
2889
+ const sessions = this.subscriptions.get(uri);
2890
+ return sessions ? sessions.size > 0 : false;
2891
+ }
2892
+ };
2368
2893
 
2369
2894
  // src/server/mcp-server.ts
2370
2895
  var proto = McpServer.prototype;
2371
2896
  if (typeof proto["createToolError"] === "function") {
2372
2897
  const originalCreateToolError = proto["createToolError"];
2373
2898
  proto["createToolError"] = function(errorMessage) {
2374
- const result = originalCreateToolError.call(
2375
- this,
2376
- errorMessage
2377
- );
2899
+ const result = originalCreateToolError.call(this, errorMessage);
2378
2900
  if (result.content?.[0]?.type === "text") {
2379
2901
  const rawError = result.content[0].text;
2380
2902
  if (rawError.includes("Input validation error")) {
@@ -2402,8 +2924,10 @@ var DbMcpServer = class {
2402
2924
  config;
2403
2925
  auditLogger = null;
2404
2926
  backupManager = null;
2927
+ systemDb = null;
2405
2928
  auditInterceptor = null;
2406
2929
  auditInitPromise = null;
2930
+ subscriptionManager;
2407
2931
  constructor(config) {
2408
2932
  this.config = config;
2409
2933
  this.toolFilter = config.toolFilter ? parseToolFilter(config.toolFilter) : getToolFilterFromEnv();
@@ -2419,6 +2943,48 @@ var DbMcpServer = class {
2419
2943
  instructions: INSTRUCTIONS
2420
2944
  }
2421
2945
  );
2946
+ this.subscriptionManager = new SubscriptionManager(this.server);
2947
+ this.server.subscriptionManager = this.subscriptionManager;
2948
+ this.server.server.registerCapabilities({
2949
+ resources: {
2950
+ subscribe: true
2951
+ }
2952
+ });
2953
+ this.server.server.setRequestHandler(
2954
+ SubscribeRequestSchema,
2955
+ (request, extra) => {
2956
+ const uri = request.params.uri;
2957
+ let sessionId = extra.sessionId ?? extra.requestInfo?.headers["mcp-session-id"] ?? void 0;
2958
+ if (sessionId === void 0 && this.config.transport === "stdio") {
2959
+ sessionId = "stdio";
2960
+ }
2961
+ if (!["sqlite://schema", "sqlite://tables", "sqlite://health"].includes(
2962
+ uri
2963
+ ) && !uri.startsWith("sqlite://table/")) {
2964
+ throw new Error(`Resource ${uri} is not subscribable`);
2965
+ }
2966
+ this.subscriptionManager.subscribe(
2967
+ uri,
2968
+ sessionId
2969
+ );
2970
+ return {};
2971
+ }
2972
+ );
2973
+ this.server.server.setRequestHandler(
2974
+ UnsubscribeRequestSchema,
2975
+ (request, extra) => {
2976
+ const uri = request.params.uri;
2977
+ let sessionId = extra.sessionId ?? extra.requestInfo?.headers["mcp-session-id"] ?? void 0;
2978
+ if (sessionId === void 0 && this.config.transport === "stdio") {
2979
+ sessionId = "stdio";
2980
+ }
2981
+ this.subscriptionManager.unsubscribe(
2982
+ uri,
2983
+ sessionId
2984
+ );
2985
+ return {};
2986
+ }
2987
+ );
2422
2988
  logger.info(getFilterSummary(this.toolFilter), { module: "FILTER" });
2423
2989
  logger.info("MCP Server initialized", {
2424
2990
  module: "SERVER",
@@ -2434,6 +3000,10 @@ var DbMcpServer = class {
2434
3000
  this.toolFilter
2435
3001
  );
2436
3002
  registerHelpResources(this.server, this.toolFilter);
3003
+ registerObservabilityResources(this.server);
3004
+ if (this.toolFilter.enabledGroups.has("admin")) {
3005
+ registerAdminTools(this.server);
3006
+ }
2437
3007
  const internalMcp = this.server;
2438
3008
  const handlers = internalMcp.server._requestHandlers;
2439
3009
  if (!handlers?.has("tools/list")) {
@@ -2459,6 +3029,11 @@ var DbMcpServer = class {
2459
3029
  if (config.audit?.enabled) {
2460
3030
  this.auditInitPromise = this.initializeAudit(config);
2461
3031
  }
3032
+ setInterval(() => {
3033
+ if (this.subscriptionManager.hasSubscribers("sqlite://health")) {
3034
+ void this.subscriptionManager.notifyResourceUpdated("sqlite://health");
3035
+ }
3036
+ }, 6e4).unref();
2462
3037
  }
2463
3038
  /**
2464
3039
  * Register a database adapter
@@ -2482,6 +3057,9 @@ var DbMcpServer = class {
2482
3057
  adapter.registerTools(this.server, this.toolFilter);
2483
3058
  adapter.registerResources(this.server);
2484
3059
  adapter.registerPrompts(this.server);
3060
+ adapter.on("schemaChanged", () => {
3061
+ void this.subscriptionManager.notifySchemaSubscribers();
3062
+ });
2485
3063
  const toolDefs = adapter.getToolDefinitions();
2486
3064
  const scopesMap = /* @__PURE__ */ new Map();
2487
3065
  for (const tool of toolDefs) {
@@ -2544,7 +3122,7 @@ var DbMcpServer = class {
2544
3122
  * Start server with HTTP transport (Streamable HTTP with SSE support)
2545
3123
  */
2546
3124
  async startHttp() {
2547
- const { HttpTransport } = await import('./http-HWTUVFIA.js');
3125
+ const { HttpTransport } = await import('./http-2WGR3E5K.js');
2548
3126
  const oauthConfig = {
2549
3127
  enabled: this.config.oauth?.enabled ?? false,
2550
3128
  authorizationServerUrl: this.config.oauth?.authorizationServerUrl ?? "",
@@ -2573,7 +3151,10 @@ var DbMcpServer = class {
2573
3151
  },
2574
3152
  oauth: oauthConfig,
2575
3153
  stateless: this.config.statelessHttp ?? false,
2576
- noAuthEnforcement: this.config.noAuthEnforcement ?? false
3154
+ noAuthEnforcement: this.config.noAuthEnforcement ?? false,
3155
+ ...this.config.metricsExport !== void 0 && {
3156
+ metricsExport: this.config.metricsExport
3157
+ }
2577
3158
  });
2578
3159
  await transport.initialize(this.server);
2579
3160
  await transport.start();
@@ -2605,6 +3186,11 @@ var DbMcpServer = class {
2605
3186
  await this.auditLogger.close();
2606
3187
  logger.info("Audit logger closed", { module: "AUDIT" });
2607
3188
  }
3189
+ metrics.close();
3190
+ if (this.systemDb) {
3191
+ this.systemDb.close();
3192
+ logger.info("System database closed", { module: "SYSTEM_DB" });
3193
+ }
2608
3194
  if (this.backupManager) {
2609
3195
  await this.backupManager.flush();
2610
3196
  logger.info("Backup manager flushed", { module: "AUDIT" });
@@ -2619,7 +3205,12 @@ var DbMcpServer = class {
2619
3205
  async initializeAudit(config) {
2620
3206
  const auditConfig = config.audit;
2621
3207
  if (!auditConfig?.enabled) return;
2622
- this.auditLogger = new AuditLogger(auditConfig);
3208
+ if (auditConfig.logPath !== "stderr") {
3209
+ this.systemDb = new SystemDb({ dbPath: auditConfig.logPath });
3210
+ await this.systemDb.init();
3211
+ metrics.setSystemDb(this.systemDb);
3212
+ }
3213
+ this.auditLogger = new AuditLogger(auditConfig, this.systemDb);
2623
3214
  await this.auditLogger.init();
2624
3215
  if (auditConfig.backup?.enabled) {
2625
3216
  this.backupManager = new BackupManager(
@@ -2635,6 +3226,9 @@ var DbMcpServer = class {
2635
3226
  if (this.backupManager && this.toolFilter.enabledGroups.has("admin")) {
2636
3227
  registerAuditBackupTools(this.server, this.backupManager, this.adapters);
2637
3228
  }
3229
+ if (this.toolFilter.enabledGroups.has("admin")) {
3230
+ registerAuditSearchTool(this.server, this.auditLogger);
3231
+ }
2638
3232
  logger.info(
2639
3233
  `Audit logging enabled (${auditConfig.logPath}, redact=${String(auditConfig.redact)}, reads=${String(auditConfig.auditReads)}, backup=${String(!!auditConfig.backup?.enabled)})`,
2640
3234
  { module: "AUDIT" }