sqlite-hub 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/bin/sqlite-hub-mcp.js +8 -0
- package/bin/sqlite-hub.js +555 -122
- package/docs/API.md +30 -0
- package/docs/CLI.md +22 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/MCP.md +85 -0
- package/docs/changelog.md +13 -1
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
- package/docs/todo.md +1 -14
- package/frontend/js/api.js +49 -0
- package/frontend/js/app.js +202 -2
- package/frontend/js/components/connectionCard.js +3 -1
- package/frontend/js/components/modal.js +356 -15
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +483 -7
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +52 -0
- package/frontend/js/views/data.js +16 -0
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/settings.js +266 -30
- package/frontend/js/views/structure.js +6 -0
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/components.css +149 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +31 -0
- package/package.json +4 -2
- package/server/mcp/stdioServer.js +272 -0
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +285 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/settings.js +47 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +284 -2
- package/server/services/mcpStatusService.js +140 -0
- package/server/services/mcpToolService.js +274 -0
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +821 -169
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
2
|
const { NotFoundError, ReadOnlyError, ValidationError } = require("../utils/errors");
|
|
3
3
|
const { ConnectionManager } = require("./sqlite/connectionManager");
|
|
4
|
+
const { BackupService } = require("./sqlite/backupService");
|
|
4
5
|
const { DataBrowserService } = require("./sqlite/dataBrowserService");
|
|
5
6
|
const { ExportService } = require("./sqlite/exportService");
|
|
6
|
-
const { getTableDetail } = require("./sqlite/introspection");
|
|
7
|
-
const {
|
|
7
|
+
const { getTableDetail, listSchema } = require("./sqlite/introspection");
|
|
8
|
+
const { OverviewService } = require("./sqlite/overviewService");
|
|
9
|
+
const { SqlExecutor, splitSqlStatements } = require("./sqlite/sqlExecutor");
|
|
8
10
|
const { TypeGenerationService } = require("./typeGenerationService");
|
|
11
|
+
const { detectQueryType } = require("./storage/queryHistoryUtils");
|
|
9
12
|
|
|
10
13
|
function normalizeLookupValue(value, label) {
|
|
11
14
|
const normalized = String(value ?? "").trim();
|
|
@@ -64,6 +67,99 @@ function normalizeMarkdownExportFilename(filename, fallback = "document.md") {
|
|
|
64
67
|
return normalizedFilename;
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
function stripSqlTerminators(sql) {
|
|
71
|
+
return String(sql ?? "").trim().replace(/;+\s*$/g, "").trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeLeadingSqlComments(sql) {
|
|
75
|
+
let text = String(sql ?? "").trim();
|
|
76
|
+
let changed = true;
|
|
77
|
+
|
|
78
|
+
while (changed) {
|
|
79
|
+
changed = false;
|
|
80
|
+
const nextText = text
|
|
81
|
+
.replace(/^--[^\n]*(?:\n|$)/, "")
|
|
82
|
+
.replace(/^\/\*[\s\S]*?\*\//, "")
|
|
83
|
+
.trim();
|
|
84
|
+
|
|
85
|
+
if (nextText !== text) {
|
|
86
|
+
changed = true;
|
|
87
|
+
text = nextText;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return text;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function explainInnerSql(statement) {
|
|
95
|
+
return removeLeadingSqlComments(statement)
|
|
96
|
+
.replace(/^EXPLAIN\s+QUERY\s+PLAN\s+/i, "")
|
|
97
|
+
.replace(/^EXPLAIN\s+/i, "")
|
|
98
|
+
.trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isAllowedReadonlyStatement(statement) {
|
|
102
|
+
const text = removeLeadingSqlComments(statement);
|
|
103
|
+
const queryType = detectQueryType(text);
|
|
104
|
+
|
|
105
|
+
if (queryType === "select" || queryType === "pragma") {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (/^EXPLAIN\b/i.test(text)) {
|
|
110
|
+
const innerType = detectQueryType(explainInnerSql(text));
|
|
111
|
+
return innerType === "select" || innerType === "pragma";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertReadOnlySql(db, sql) {
|
|
118
|
+
const statements = splitSqlStatements(sql);
|
|
119
|
+
|
|
120
|
+
if (!statements.length) {
|
|
121
|
+
throw new ValidationError("No executable SQL statements were found.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
statements.forEach((statement, index) => {
|
|
125
|
+
if (!isAllowedReadonlyStatement(statement)) {
|
|
126
|
+
throw new ValidationError(
|
|
127
|
+
`Statement ${index + 1} is not allowed for read-only MCP queries. Only SELECT, PRAGMA, and EXPLAIN statements are allowed.`,
|
|
128
|
+
{ code: "MCP_READONLY_SQL_REQUIRED" }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const prepared = db.prepare(statement);
|
|
133
|
+
|
|
134
|
+
if (!prepared.reader) {
|
|
135
|
+
throw new ValidationError(
|
|
136
|
+
`Statement ${index + 1} does not return rows and is blocked by the read-only query guard.`,
|
|
137
|
+
{ code: "MCP_READONLY_SQL_REQUIRED" }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return statements;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildExplainQueryPlanSql(sql) {
|
|
146
|
+
const stripped = stripSqlTerminators(sql);
|
|
147
|
+
|
|
148
|
+
if (/^EXPLAIN\s+QUERY\s+PLAN\b/i.test(removeLeadingSqlComments(stripped))) {
|
|
149
|
+
return stripped;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return `EXPLAIN QUERY PLAN ${stripped}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildQueryPlanHints(rows = []) {
|
|
156
|
+
return rows
|
|
157
|
+
.map((row) => String(row.detail ?? row["QUERY PLAN"] ?? "").trim())
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.filter((detail) => /\bSCAN\b/i.test(detail) && !/\bUSING\s+(?:COVERING\s+)?INDEX\b/i.test(detail))
|
|
160
|
+
.map((detail) => `Review whether an index would help: ${detail}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
67
163
|
function coerceIdentityValue(column, value) {
|
|
68
164
|
const text = String(value ?? "");
|
|
69
165
|
const affinity = String(column?.affinity ?? "").toUpperCase();
|
|
@@ -215,6 +311,7 @@ class DatabaseCommandService {
|
|
|
215
311
|
connectionManager,
|
|
216
312
|
sqlExecutor,
|
|
217
313
|
}),
|
|
314
|
+
overviewService: new OverviewService({ connectionManager }),
|
|
218
315
|
sqlExecutor,
|
|
219
316
|
close() {
|
|
220
317
|
connectionManager.closeCurrent();
|
|
@@ -241,10 +338,25 @@ class DatabaseCommandService {
|
|
|
241
338
|
}
|
|
242
339
|
}
|
|
243
340
|
|
|
341
|
+
async withDatabaseAsync(databaseReference, callback, options = {}) {
|
|
342
|
+
const connection = this.getDatabase(databaseReference);
|
|
343
|
+
const runtime = this.runtimeFactory(connection, options);
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
return await callback({ connection, runtime });
|
|
347
|
+
} finally {
|
|
348
|
+
runtime.close?.();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
244
352
|
listTables(databaseReference) {
|
|
245
353
|
return this.withDatabase(databaseReference, ({ runtime }) => runtime.dataBrowserService.listTables());
|
|
246
354
|
}
|
|
247
355
|
|
|
356
|
+
getDatabaseOverview(databaseReference) {
|
|
357
|
+
return this.withDatabase(databaseReference, ({ runtime }) => runtime.overviewService.getOverview());
|
|
358
|
+
}
|
|
359
|
+
|
|
248
360
|
getTable(databaseReference, tableName) {
|
|
249
361
|
const normalizedTableName = normalizeLookupValue(tableName, "Table name");
|
|
250
362
|
return this.withDatabase(databaseReference, ({ runtime }) =>
|
|
@@ -252,6 +364,33 @@ class DatabaseCommandService {
|
|
|
252
364
|
);
|
|
253
365
|
}
|
|
254
366
|
|
|
367
|
+
getSchema(databaseReference) {
|
|
368
|
+
return this.withDatabase(databaseReference, ({ runtime }) => listSchema(runtime.db));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getIndexes(databaseReference, tableName = null) {
|
|
372
|
+
const normalizedTableName = normalizeOptionalValue(tableName);
|
|
373
|
+
|
|
374
|
+
if (normalizedTableName) {
|
|
375
|
+
return this.getTable(databaseReference, normalizedTableName).indexes ?? [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return this.getSchema(databaseReference).indexes ?? [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getForeignKeys(databaseReference, tableName = null) {
|
|
382
|
+
const normalizedTableName = normalizeOptionalValue(tableName);
|
|
383
|
+
|
|
384
|
+
if (normalizedTableName) {
|
|
385
|
+
return this.getTable(databaseReference, normalizedTableName).foreignKeys ?? [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return this.getSchema(databaseReference).tables.map((table) => ({
|
|
389
|
+
tableName: table.name,
|
|
390
|
+
foreignKeys: table.foreignKeys ?? [],
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
|
|
255
394
|
generateTableTypes(databaseReference, tableName, target, options = {}) {
|
|
256
395
|
const normalizedTableName = normalizeLookupValue(tableName, "Table name");
|
|
257
396
|
return this.withDatabase(databaseReference, ({ runtime }) =>
|
|
@@ -264,6 +403,98 @@ class DatabaseCommandService {
|
|
|
264
403
|
);
|
|
265
404
|
}
|
|
266
405
|
|
|
406
|
+
generateTypes(databaseReference, { tableName = null, allTables = false, target, options = {} } = {}) {
|
|
407
|
+
const normalizedTableName = normalizeOptionalValue(tableName);
|
|
408
|
+
|
|
409
|
+
return this.withDatabase(databaseReference, ({ runtime }) => {
|
|
410
|
+
const tables = allTables || !normalizedTableName
|
|
411
|
+
? runtime.dataBrowserService.listTables().map((table) => table.name)
|
|
412
|
+
: [normalizedTableName];
|
|
413
|
+
const files = tables.map((name) =>
|
|
414
|
+
this.typeGenerationService.generateTypesFromDatabase(runtime.db, name, target, options)
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
target: files[0]?.target ?? target,
|
|
419
|
+
files,
|
|
420
|
+
warnings: files.flatMap((file) =>
|
|
421
|
+
(file.warnings ?? []).map((warning) => `${file.tableName}: ${warning}`)
|
|
422
|
+
),
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
executeReadOnlyQuery(databaseReference, sql, options = {}) {
|
|
428
|
+
return this.withDatabase(databaseReference, ({ runtime }) => {
|
|
429
|
+
const statements = assertReadOnlySql(runtime.db, sql);
|
|
430
|
+
const result = runtime.sqlExecutor.execute(sql, {
|
|
431
|
+
executedBy: options.executedBy ?? "mcp",
|
|
432
|
+
maxRows: options.maxRows ?? 500,
|
|
433
|
+
persistHistory: options.persistHistory,
|
|
434
|
+
requireReader: true,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
statementsValidated: statements.length,
|
|
439
|
+
result,
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
explainQueryPlan(databaseReference, sql) {
|
|
445
|
+
return this.withDatabase(databaseReference, ({ runtime }) => {
|
|
446
|
+
assertReadOnlySql(runtime.db, sql);
|
|
447
|
+
const explainSql = buildExplainQueryPlanSql(sql);
|
|
448
|
+
const rows = runtime.db.prepare(explainSql).all();
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
sql: explainSql,
|
|
452
|
+
rows,
|
|
453
|
+
hints: buildQueryPlanHints(rows),
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
createChartFromQuery(databaseReference, { sql, name, chartType = "bar", config, resultColumns, tableVisible = true } = {}) {
|
|
459
|
+
const normalizedSql = normalizeLookupValue(sql, "SQL");
|
|
460
|
+
|
|
461
|
+
if (/^EXPLAIN\b/i.test(removeLeadingSqlComments(normalizedSql)) || detectQueryType(normalizedSql) !== "select") {
|
|
462
|
+
throw new ValidationError("Charts can only be created from read-only SELECT queries.", {
|
|
463
|
+
code: "CHART_SELECT_QUERY_REQUIRED",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return this.withDatabase(databaseReference, ({ connection, runtime }) => {
|
|
468
|
+
assertReadOnlySql(runtime.db, normalizedSql);
|
|
469
|
+
const result = runtime.sqlExecutor.execute(normalizedSql, {
|
|
470
|
+
executedBy: "mcp",
|
|
471
|
+
maxRows: 500,
|
|
472
|
+
requireReader: true,
|
|
473
|
+
});
|
|
474
|
+
const chart = this.appStateStore.createQueryHistoryChart({
|
|
475
|
+
databaseKey: connection.id,
|
|
476
|
+
queryHistoryId: result.historyId,
|
|
477
|
+
name,
|
|
478
|
+
chartType,
|
|
479
|
+
config,
|
|
480
|
+
resultColumns: resultColumns ?? result.columns.map((column) => ({
|
|
481
|
+
name: column,
|
|
482
|
+
type: "unknown",
|
|
483
|
+
})),
|
|
484
|
+
tableVisible,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
chart,
|
|
489
|
+
queryHistoryId: result.historyId,
|
|
490
|
+
export: {
|
|
491
|
+
png: false,
|
|
492
|
+
message: "Charts are saved in SQLite Hub. PNG export is available from the Charts UI.",
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
267
498
|
getTableRow(databaseReference, tableName, exportTarget) {
|
|
268
499
|
const normalizedTableName = normalizeLookupValue(tableName, "Table name");
|
|
269
500
|
normalizeLookupValue(exportTarget, "Row key");
|
|
@@ -381,6 +612,39 @@ class DatabaseCommandService {
|
|
|
381
612
|
};
|
|
382
613
|
}
|
|
383
614
|
|
|
615
|
+
async createBackup(databaseReference, options = {}) {
|
|
616
|
+
const backupOptions = {
|
|
617
|
+
name: normalizeOptionalValue(options.name),
|
|
618
|
+
notes: normalizeOptionalValue(options.notes),
|
|
619
|
+
type: normalizeOptionalValue(options.type) ?? "manual",
|
|
620
|
+
context: normalizeOptionalValue(options.context) ?? "automation",
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
return this.withDatabaseAsync(
|
|
624
|
+
databaseReference,
|
|
625
|
+
async ({ runtime }) => {
|
|
626
|
+
const backupService = new BackupService({
|
|
627
|
+
appStateStore: this.appStateStore,
|
|
628
|
+
connectionManager: runtime.connectionManager,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
return backupService.createActiveBackup(backupOptions);
|
|
632
|
+
},
|
|
633
|
+
{ readOnly: true }
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
listBackups(databaseReference) {
|
|
638
|
+
return this.withDatabase(databaseReference, ({ connection, runtime }) => {
|
|
639
|
+
const backupService = new BackupService({
|
|
640
|
+
appStateStore: this.appStateStore,
|
|
641
|
+
connectionManager: runtime.connectionManager,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
return backupService.listBackups({ connectionId: connection.id });
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
384
648
|
exportSavedQuery(databaseReference, queryName, format = "csv") {
|
|
385
649
|
const query = this.requireQuery(databaseReference, queryName);
|
|
386
650
|
const result = this.withDatabase(databaseReference, ({ runtime }) =>
|
|
@@ -395,6 +659,22 @@ class DatabaseCommandService {
|
|
|
395
659
|
return this.appStateStore.listDatabaseDocuments(connection.id);
|
|
396
660
|
}
|
|
397
661
|
|
|
662
|
+
readDocuments(databaseReference, documentName = null) {
|
|
663
|
+
const normalizedDocumentName = normalizeOptionalValue(documentName);
|
|
664
|
+
|
|
665
|
+
if (normalizedDocumentName) {
|
|
666
|
+
return {
|
|
667
|
+
items: [this.getDocument(databaseReference, normalizedDocumentName)],
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
items: this.listDocuments(databaseReference).map((document) =>
|
|
673
|
+
this.appStateStore.getDatabaseDocument(this.getDatabase(databaseReference).id, document.id)
|
|
674
|
+
),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
398
678
|
findDocument(databaseReference, documentName) {
|
|
399
679
|
const connection = this.getDatabase(databaseReference);
|
|
400
680
|
const normalizedDocumentName = normalizeLookupValue(documentName, "Document name").toLowerCase();
|
|
@@ -449,8 +729,10 @@ class DatabaseCommandService {
|
|
|
449
729
|
|
|
450
730
|
module.exports = {
|
|
451
731
|
DatabaseCommandService,
|
|
732
|
+
assertReadOnlySql,
|
|
452
733
|
buildIdentityFromExportTarget,
|
|
453
734
|
buildRowJsonObject,
|
|
735
|
+
isAllowedReadonlyStatement,
|
|
454
736
|
getDocumentTitle,
|
|
455
737
|
getQueryTitle,
|
|
456
738
|
normalizeMarkdownExportFilename,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const DEFAULT_MCP_STATUS = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
serverRunning: false,
|
|
4
|
+
connected: false,
|
|
5
|
+
activeClientCount: 0,
|
|
6
|
+
lastConnectedAt: null,
|
|
7
|
+
lastDisconnectedAt: null,
|
|
8
|
+
lastToolCallAt: null,
|
|
9
|
+
lastToolName: null,
|
|
10
|
+
transport: "unknown",
|
|
11
|
+
exposedTools: [],
|
|
12
|
+
error: null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function normalizeToolNames(exposedTools = []) {
|
|
16
|
+
return Array.from(
|
|
17
|
+
new Set(
|
|
18
|
+
(Array.isArray(exposedTools) ? exposedTools : [])
|
|
19
|
+
.map((tool) => String(typeof tool === "string" ? tool : tool?.name ?? "").trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class McpStatusService {
|
|
26
|
+
constructor({ appStateStore, exposedTools = [], transport = "unknown" } = {}) {
|
|
27
|
+
this.appStateStore = appStateStore;
|
|
28
|
+
this.transport = transport;
|
|
29
|
+
this.exposedTools = normalizeToolNames(exposedTools);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getDefaultStatus() {
|
|
33
|
+
return {
|
|
34
|
+
...DEFAULT_MCP_STATUS,
|
|
35
|
+
transport: this.transport,
|
|
36
|
+
exposedTools: this.exposedTools,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getStatus() {
|
|
41
|
+
const status = this.appStateStore?.getMcpStatus?.(this.getDefaultStatus()) ?? this.getDefaultStatus();
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...this.getDefaultStatus(),
|
|
45
|
+
...status,
|
|
46
|
+
exposedTools: this.exposedTools.length ? this.exposedTools : normalizeToolNames(status.exposedTools),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
patchStatus(patch) {
|
|
51
|
+
const nextStatus = this.appStateStore?.patchMcpStatus?.(
|
|
52
|
+
{
|
|
53
|
+
...patch,
|
|
54
|
+
exposedTools: this.exposedTools,
|
|
55
|
+
},
|
|
56
|
+
this.getDefaultStatus()
|
|
57
|
+
) ?? {
|
|
58
|
+
...this.getStatus(),
|
|
59
|
+
...patch,
|
|
60
|
+
exposedTools: this.exposedTools,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...this.getDefaultStatus(),
|
|
65
|
+
...nextStatus,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
markServerRunning() {
|
|
70
|
+
return this.patchStatus({
|
|
71
|
+
enabled: true,
|
|
72
|
+
serverRunning: true,
|
|
73
|
+
transport: this.transport,
|
|
74
|
+
error: null,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
markConnected() {
|
|
79
|
+
const current = this.getStatus();
|
|
80
|
+
|
|
81
|
+
return this.patchStatus({
|
|
82
|
+
enabled: true,
|
|
83
|
+
serverRunning: true,
|
|
84
|
+
connected: true,
|
|
85
|
+
activeClientCount: Math.max(1, Number(current.activeClientCount ?? 0)),
|
|
86
|
+
lastConnectedAt: new Date().toISOString(),
|
|
87
|
+
transport: this.transport,
|
|
88
|
+
error: null,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
markDisconnected() {
|
|
93
|
+
const current = this.getStatus();
|
|
94
|
+
const activeClientCount = Math.max(0, Number(current.activeClientCount ?? 0) - 1);
|
|
95
|
+
|
|
96
|
+
return this.patchStatus({
|
|
97
|
+
connected: activeClientCount > 0,
|
|
98
|
+
serverRunning: activeClientCount > 0,
|
|
99
|
+
activeClientCount,
|
|
100
|
+
lastDisconnectedAt: new Date().toISOString(),
|
|
101
|
+
transport: this.transport,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
markToolCall(toolName) {
|
|
106
|
+
return this.patchStatus({
|
|
107
|
+
enabled: true,
|
|
108
|
+
serverRunning: true,
|
|
109
|
+
connected: true,
|
|
110
|
+
activeClientCount: Math.max(1, Number(this.getStatus().activeClientCount ?? 0)),
|
|
111
|
+
lastToolCallAt: new Date().toISOString(),
|
|
112
|
+
lastToolName: String(toolName ?? "") || null,
|
|
113
|
+
transport: this.transport,
|
|
114
|
+
error: null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
markError(error) {
|
|
119
|
+
return this.patchStatus({
|
|
120
|
+
error: error?.message ?? String(error ?? "Unknown MCP error"),
|
|
121
|
+
transport: this.transport,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
markStopped() {
|
|
126
|
+
return this.patchStatus({
|
|
127
|
+
serverRunning: false,
|
|
128
|
+
connected: false,
|
|
129
|
+
activeClientCount: 0,
|
|
130
|
+
lastDisconnectedAt: new Date().toISOString(),
|
|
131
|
+
transport: this.transport,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
DEFAULT_MCP_STATUS,
|
|
138
|
+
McpStatusService,
|
|
139
|
+
normalizeToolNames,
|
|
140
|
+
};
|