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.
- package/README.md +92 -75
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/database-adapter.d.ts +2 -1
- package/dist/adapters/database-adapter.d.ts.map +1 -1
- package/dist/adapters/registration/resources.d.ts.map +1 -1
- package/dist/adapters/registration/tools.d.ts.map +1 -1
- package/dist/adapters/sqlite/prompts/analysis.d.ts.map +1 -1
- package/dist/adapters/sqlite/resources.d.ts +1 -1
- package/dist/adapters/sqlite/resources.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/admin.d.ts +29 -15
- package/dist/adapters/sqlite/schemas/admin.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/core.d.ts +2 -0
- package/dist/adapters/sqlite/schemas/core.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/fts.d.ts +4 -0
- package/dist/adapters/sqlite/schemas/fts.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/introspection.d.ts +20 -11
- package/dist/adapters/sqlite/schemas/introspection.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/spatialite.d.ts +1 -0
- package/dist/adapters/sqlite/schemas/spatialite.d.ts.map +1 -1
- package/dist/adapters/sqlite/schemas/text.d.ts +36 -0
- package/dist/adapters/sqlite/schemas/text.d.ts.map +1 -1
- package/dist/adapters/sqlite/sqlite-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/index.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/admin/pragma.d.ts +0 -4
- package/dist/adapters/sqlite/tools/admin/pragma.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/core/queries.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/core/tables.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/fts.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/introspection/diagnostics/indexes.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-helpers/read.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-operations/crud.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/json-operations/query.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/text/helpers.d.ts +9 -0
- package/dist/adapters/sqlite/tools/text/helpers.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/text/index.d.ts.map +1 -1
- package/dist/adapters/sqlite/tools/text/search.d.ts +4 -0
- package/dist/adapters/sqlite/tools/text/search.d.ts.map +1 -1
- package/dist/adapters/sqlite/types.d.ts +4 -0
- package/dist/adapters/sqlite/types.d.ts.map +1 -1
- package/dist/adapters/sqlite-native/native-sqlite-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite-native/tools/spatialite/tools.d.ts.map +1 -1
- package/dist/audit/interceptor.d.ts.map +1 -1
- package/dist/audit/logger.d.ts +26 -25
- package/dist/audit/logger.d.ts.map +1 -1
- package/dist/{chunk-W5WQVNVX.js → chunk-EQF3WZHX.js} +10 -17
- package/dist/{chunk-37G4DLL6.js → chunk-NIC5Z2CA.js} +732 -138
- package/dist/chunk-R5M4WKDM.js +987 -0
- package/dist/{chunk-NV34JSQM.js → chunk-TGE7LFAR.js} +708 -996
- package/dist/{chunk-L552U3QS.js → chunk-Y57WFKAP.js} +227 -9
- package/dist/cli.js +103 -10
- package/dist/codemode/sandbox.d.ts.map +1 -1
- package/dist/constants/server-instructions.d.ts +1 -1
- package/dist/constants/server-instructions.d.ts.map +1 -1
- package/dist/filtering/tool-constants.d.ts +9 -9
- package/dist/{http-HWTUVFIA.js → http-2WGR3E5K.js} +13 -1
- package/dist/index.js +4 -4
- package/dist/observability/metrics.d.ts +36 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/system-db.d.ts +13 -0
- package/dist/observability/system-db.d.ts.map +1 -0
- package/dist/server/mcp-server.d.ts +3 -0
- package/dist/server/mcp-server.d.ts.map +1 -1
- package/dist/server/registration/admin-tools.d.ts +6 -0
- package/dist/server/registration/admin-tools.d.ts.map +1 -0
- package/dist/server/registration/audit-tools.d.ts +4 -0
- package/dist/server/registration/audit-tools.d.ts.map +1 -1
- package/dist/server/registration/help-resources.d.ts.map +1 -1
- package/dist/server/registration/index.d.ts +3 -1
- package/dist/server/registration/index.d.ts.map +1 -1
- package/dist/server/registration/observability-resources.d.ts +11 -0
- package/dist/server/registration/observability-resources.d.ts.map +1 -0
- package/dist/server/subscription-manager.d.ts +35 -0
- package/dist/server/subscription-manager.d.ts.map +1 -0
- package/dist/{sqlite-TB56Z7G3.js → sqlite-D4WCNPU4.js} +8 -5
- package/dist/{sqlite-native-IF75BB3O.js → sqlite-native-DLVS2T52.js} +159 -34
- package/dist/transports/http/sessions/legacy-sse.d.ts.map +1 -1
- package/dist/transports/http/sessions/stateful.d.ts.map +1 -1
- package/dist/transports/http/transport.d.ts.map +1 -1
- package/dist/transports/http/types.d.ts +2 -0
- package/dist/transports/http/types.d.ts.map +1 -1
- package/dist/types/server.d.ts +2 -0
- package/dist/types/server.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/logger/types.d.ts +1 -1
- package/dist/utils/logger/types.d.ts.map +1 -1
- package/dist/utils/tokens.d.ts +15 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/package.json +10 -7
- package/dist/chunk-E5IESRTK.js +0 -489
- package/dist/utils/insights-manager.d.ts +0 -39
- 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-
|
|
2
|
-
import { parseToolFilter, getToolFilterFromEnv, logger, getFilterSummary, DbMcpError, getAuthContext, scopesGrantToolAccess, registerToolScopes, InsufficientScopeError, sanitizeErrorMessage, formatHandlerError } from './chunk-
|
|
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
|
|
18
|
-
|
|
|
19
|
-
| Health check
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
947
|
-
* actual
|
|
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(
|
|
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
|
|
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
|
|
969
|
-
const
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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(...
|
|
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
|
|
1009
|
-
*
|
|
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(
|
|
1016
|
-
if (this.stderrMode)
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
*
|
|
1076
|
+
* Search and filter audit entries from the SystemDb.
|
|
1053
1077
|
*/
|
|
1054
|
-
async
|
|
1055
|
-
if (this.
|
|
1078
|
+
async search(filters) {
|
|
1079
|
+
if (this.stderrMode || !this.systemDb) {
|
|
1080
|
+
return { entries: [], totalCount: 0 };
|
|
1081
|
+
}
|
|
1082
|
+
await this.flush();
|
|
1056
1083
|
try {
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1140
|
+
return { entries: [], totalCount: 0 };
|
|
1061
1141
|
}
|
|
1062
1142
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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 =
|
|
1690
|
+
tokenEstimate = estimateTokens(json, "json");
|
|
1542
1691
|
} catch {
|
|
1543
1692
|
}
|
|
1544
1693
|
} else if (typeof result === "string") {
|
|
1545
|
-
|
|
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 =
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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-
|
|
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
|
-
|
|
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" }
|