sqlite-hub 1.5.0 → 2.0.1
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 +5 -1
- package/bin/sqlite-hub-mcp.js +8 -0
- package/docs/MCP.md +109 -0
- package/docs/changelog.md +5 -0
- package/docs/todo.md +0 -1
- package/frontend/js/api.js +4 -0
- package/frontend/js/app.js +10 -0
- package/frontend/js/store.js +35 -7
- package/frontend/js/views/settings.js +189 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +23 -0
- package/package.json +4 -2
- package/server/mcp/httpRouter.js +95 -0
- package/server/mcp/stdioServer.js +272 -0
- package/server/routes/settings.js +69 -0
- package/server/server.js +15 -0
- package/server/services/databaseCommandService.js +239 -2
- package/server/services/mcpStatusService.js +140 -0
- package/server/services/mcpToolService.js +274 -0
- package/server/services/storage/appStateStore.js +48 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const { ValidationError } = require("../utils/errors");
|
|
2
|
+
|
|
3
|
+
function objectSchema(properties = {}, required = []) {
|
|
4
|
+
return {
|
|
5
|
+
type: "object",
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
properties,
|
|
8
|
+
required,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function databaseIdProperty() {
|
|
13
|
+
return {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "SQLite Hub database id or exact imported database label.",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MCP_TOOL_DEFINITIONS = [
|
|
20
|
+
{
|
|
21
|
+
name: "list_connections",
|
|
22
|
+
description: "List imported SQLite Hub database connections without exposing API tokens.",
|
|
23
|
+
inputSchema: objectSchema(),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "get_database_overview",
|
|
27
|
+
description: "Return operational overview, SQLite runtime metadata, and schema statistics for a database.",
|
|
28
|
+
inputSchema: objectSchema({ databaseId: databaseIdProperty() }, ["databaseId"]),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "list_tables",
|
|
32
|
+
description: "List tables in a database with column counts.",
|
|
33
|
+
inputSchema: objectSchema({ databaseId: databaseIdProperty() }, ["databaseId"]),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "describe_table",
|
|
37
|
+
description: "Describe one table including columns, indexes, foreign keys, triggers, and row count.",
|
|
38
|
+
inputSchema: objectSchema(
|
|
39
|
+
{
|
|
40
|
+
databaseId: databaseIdProperty(),
|
|
41
|
+
tableName: { type: "string" },
|
|
42
|
+
},
|
|
43
|
+
["databaseId", "tableName"]
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "get_schema",
|
|
48
|
+
description: "Return the database schema: tables, views, indexes, triggers, and schema entries.",
|
|
49
|
+
inputSchema: objectSchema({ databaseId: databaseIdProperty() }, ["databaseId"]),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "get_indexes",
|
|
53
|
+
description: "Return all indexes or indexes for one table.",
|
|
54
|
+
inputSchema: objectSchema({
|
|
55
|
+
databaseId: databaseIdProperty(),
|
|
56
|
+
tableName: { type: "string", description: "Optional table name." },
|
|
57
|
+
}, ["databaseId"]),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "get_foreign_keys",
|
|
61
|
+
description: "Return all foreign-key metadata or foreign keys for one table.",
|
|
62
|
+
inputSchema: objectSchema({
|
|
63
|
+
databaseId: databaseIdProperty(),
|
|
64
|
+
tableName: { type: "string", description: "Optional table name." },
|
|
65
|
+
}, ["databaseId"]),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "run_readonly_query",
|
|
69
|
+
description: "Run a read-only SELECT, PRAGMA, or EXPLAIN query. Mutating SQL is blocked server-side.",
|
|
70
|
+
inputSchema: objectSchema(
|
|
71
|
+
{
|
|
72
|
+
databaseId: databaseIdProperty(),
|
|
73
|
+
sql: { type: "string" },
|
|
74
|
+
maxRows: { type: "integer", minimum: 1, maximum: 5000, default: 500 },
|
|
75
|
+
},
|
|
76
|
+
["databaseId", "sql"]
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "explain_query_plan",
|
|
81
|
+
description: "Run SQLite EXPLAIN QUERY PLAN for a read-only query and return structured plan rows.",
|
|
82
|
+
inputSchema: objectSchema(
|
|
83
|
+
{
|
|
84
|
+
databaseId: databaseIdProperty(),
|
|
85
|
+
sql: { type: "string" },
|
|
86
|
+
},
|
|
87
|
+
["databaseId", "sql"]
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "read_documents",
|
|
92
|
+
description: "Read database-scoped Markdown documents, optionally filtered by filename or title.",
|
|
93
|
+
inputSchema: objectSchema({
|
|
94
|
+
databaseId: databaseIdProperty(),
|
|
95
|
+
documentName: { type: "string", description: "Optional document id, filename, or title." },
|
|
96
|
+
}, ["databaseId"]),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "create_backup",
|
|
100
|
+
description: "Create and verify a managed SQLite Hub backup using the existing backup mechanism.",
|
|
101
|
+
inputSchema: objectSchema({
|
|
102
|
+
databaseId: databaseIdProperty(),
|
|
103
|
+
name: { type: "string" },
|
|
104
|
+
notes: { type: "string" },
|
|
105
|
+
}, ["databaseId"]),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "generate_types",
|
|
109
|
+
description: "Generate TypeScript, Rust, Kotlin, or Swift types from one table or all tables.",
|
|
110
|
+
inputSchema: objectSchema(
|
|
111
|
+
{
|
|
112
|
+
databaseId: databaseIdProperty(),
|
|
113
|
+
tableName: { type: "string", description: "Optional table name. Omit when allTables is true." },
|
|
114
|
+
allTables: { type: "boolean", default: false },
|
|
115
|
+
target: { type: "string", enum: ["typescript", "rust", "kotlin", "swift"] },
|
|
116
|
+
options: { type: "object", additionalProperties: true },
|
|
117
|
+
},
|
|
118
|
+
["databaseId", "target"]
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "create_chart_from_query",
|
|
123
|
+
description: "Create a saved SQLite Hub chart from a read-only SELECT query without writing export files.",
|
|
124
|
+
inputSchema: objectSchema(
|
|
125
|
+
{
|
|
126
|
+
databaseId: databaseIdProperty(),
|
|
127
|
+
sql: { type: "string" },
|
|
128
|
+
name: { type: "string" },
|
|
129
|
+
chartType: { type: "string", enum: ["bar", "line", "pie", "scatter"] },
|
|
130
|
+
config: { type: "object", additionalProperties: true },
|
|
131
|
+
resultColumns: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
132
|
+
tableVisible: { type: "boolean", default: true },
|
|
133
|
+
},
|
|
134
|
+
["databaseId", "sql", "chartType", "config"]
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
function redactConnection(connection = {}) {
|
|
140
|
+
return {
|
|
141
|
+
id: connection.id,
|
|
142
|
+
label: connection.label,
|
|
143
|
+
readOnly: Boolean(connection.readOnly),
|
|
144
|
+
sizeBytes: connection.sizeBytes ?? null,
|
|
145
|
+
lastOpenedAt: connection.lastOpenedAt ?? null,
|
|
146
|
+
lastModifiedAt: connection.lastModifiedAt ?? null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function redactOverview(overview = {}) {
|
|
151
|
+
return {
|
|
152
|
+
...overview,
|
|
153
|
+
connection: redactConnection(overview.connection ?? {}),
|
|
154
|
+
file: {
|
|
155
|
+
filename: overview.file?.filename ?? overview.connection?.label ?? null,
|
|
156
|
+
sizeBytes: overview.file?.sizeBytes ?? null,
|
|
157
|
+
lastModifiedAt: overview.file?.lastModifiedAt ?? null,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function summarizeBackup(backup = {}) {
|
|
163
|
+
return {
|
|
164
|
+
backupId: backup.id,
|
|
165
|
+
id: backup.id,
|
|
166
|
+
createdAt: backup.createdAt ?? null,
|
|
167
|
+
databaseId: backup.connectionId ?? null,
|
|
168
|
+
sizeBytes: backup.sizeBytes ?? null,
|
|
169
|
+
verified: backup.status === "verified",
|
|
170
|
+
status: backup.status,
|
|
171
|
+
name: backup.name,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class McpToolService {
|
|
176
|
+
constructor({ databaseService, statusService } = {}) {
|
|
177
|
+
this.databaseService = databaseService;
|
|
178
|
+
this.statusService = statusService;
|
|
179
|
+
this.toolDefinitions = MCP_TOOL_DEFINITIONS;
|
|
180
|
+
this.toolNames = this.toolDefinitions.map((tool) => tool.name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
listTools() {
|
|
184
|
+
return this.toolDefinitions;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
requireTool(name) {
|
|
188
|
+
const tool = this.toolDefinitions.find((definition) => definition.name === name);
|
|
189
|
+
|
|
190
|
+
if (!tool) {
|
|
191
|
+
throw new ValidationError(`Unknown MCP tool: ${name}`, { code: "MCP_TOOL_NOT_FOUND" });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return tool;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async callTool(name, args = {}) {
|
|
198
|
+
this.requireTool(name);
|
|
199
|
+
this.statusService?.markToolCall?.(name);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
switch (name) {
|
|
203
|
+
case "list_connections":
|
|
204
|
+
return {
|
|
205
|
+
items: this.databaseService.listDatabases().map(redactConnection),
|
|
206
|
+
};
|
|
207
|
+
case "get_database_overview":
|
|
208
|
+
return redactOverview(this.databaseService.getDatabaseOverview(args.databaseId));
|
|
209
|
+
case "list_tables":
|
|
210
|
+
return {
|
|
211
|
+
items: this.databaseService.listTables(args.databaseId),
|
|
212
|
+
};
|
|
213
|
+
case "describe_table":
|
|
214
|
+
return this.databaseService.getTable(args.databaseId, args.tableName);
|
|
215
|
+
case "get_schema":
|
|
216
|
+
return this.databaseService.getSchema(args.databaseId);
|
|
217
|
+
case "get_indexes":
|
|
218
|
+
return {
|
|
219
|
+
items: this.databaseService.getIndexes(args.databaseId, args.tableName),
|
|
220
|
+
};
|
|
221
|
+
case "get_foreign_keys":
|
|
222
|
+
return {
|
|
223
|
+
items: this.databaseService.getForeignKeys(args.databaseId, args.tableName),
|
|
224
|
+
};
|
|
225
|
+
case "run_readonly_query":
|
|
226
|
+
return this.databaseService.executeReadOnlyQuery(args.databaseId, args.sql, {
|
|
227
|
+
executedBy: "mcp",
|
|
228
|
+
maxRows: args.maxRows,
|
|
229
|
+
});
|
|
230
|
+
case "explain_query_plan":
|
|
231
|
+
return this.databaseService.explainQueryPlan(args.databaseId, args.sql);
|
|
232
|
+
case "read_documents":
|
|
233
|
+
return this.databaseService.readDocuments(args.databaseId, args.documentName);
|
|
234
|
+
case "create_backup":
|
|
235
|
+
return summarizeBackup(
|
|
236
|
+
await this.databaseService.createBackup(args.databaseId, {
|
|
237
|
+
name: args.name,
|
|
238
|
+
notes: args.notes,
|
|
239
|
+
context: "mcp",
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
case "generate_types":
|
|
243
|
+
return this.databaseService.generateTypes(args.databaseId, {
|
|
244
|
+
tableName: args.tableName,
|
|
245
|
+
allTables: Boolean(args.allTables),
|
|
246
|
+
target: args.target,
|
|
247
|
+
options: args.options ?? {},
|
|
248
|
+
});
|
|
249
|
+
case "create_chart_from_query":
|
|
250
|
+
return this.databaseService.createChartFromQuery(args.databaseId, {
|
|
251
|
+
sql: args.sql,
|
|
252
|
+
name: args.name,
|
|
253
|
+
chartType: args.chartType,
|
|
254
|
+
config: args.config,
|
|
255
|
+
resultColumns: args.resultColumns,
|
|
256
|
+
tableVisible: args.tableVisible,
|
|
257
|
+
});
|
|
258
|
+
default:
|
|
259
|
+
throw new ValidationError(`Unknown MCP tool: ${name}`, { code: "MCP_TOOL_NOT_FOUND" });
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.statusService?.markError?.(error);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
MCP_TOOL_DEFINITIONS,
|
|
270
|
+
McpToolService,
|
|
271
|
+
redactConnection,
|
|
272
|
+
redactOverview,
|
|
273
|
+
summarizeBackup,
|
|
274
|
+
};
|
|
@@ -1004,6 +1004,54 @@ class AppStateStore {
|
|
|
1004
1004
|
.run(key, String(value));
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
getJsonMetaValue(key, fallback = null) {
|
|
1008
|
+
const value = this.getMetaValue(key);
|
|
1009
|
+
|
|
1010
|
+
if (!value) {
|
|
1011
|
+
return fallback;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
return JSON.parse(value);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return fallback;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
setJsonMetaValue(key, value) {
|
|
1022
|
+
this.setMetaValue(key, JSON.stringify(value ?? null));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
getMcpStatus(defaultStatus = {}) {
|
|
1026
|
+
const storedStatus = this.getJsonMetaValue("mcpStatus", {});
|
|
1027
|
+
return {
|
|
1028
|
+
...defaultStatus,
|
|
1029
|
+
...(storedStatus && typeof storedStatus === "object" && !Array.isArray(storedStatus)
|
|
1030
|
+
? storedStatus
|
|
1031
|
+
: {}),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
setMcpStatus(status, defaultStatus = {}) {
|
|
1036
|
+
const nextStatus = {
|
|
1037
|
+
...defaultStatus,
|
|
1038
|
+
...(status && typeof status === "object" && !Array.isArray(status) ? status : {}),
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
this.setJsonMetaValue("mcpStatus", nextStatus);
|
|
1042
|
+
return nextStatus;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
patchMcpStatus(patch, defaultStatus = {}) {
|
|
1046
|
+
return this.setMcpStatus(
|
|
1047
|
+
{
|
|
1048
|
+
...this.getMcpStatus(defaultStatus),
|
|
1049
|
+
...(patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {}),
|
|
1050
|
+
},
|
|
1051
|
+
defaultStatus
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1007
1055
|
normalizeQueryHistoryText(value) {
|
|
1008
1056
|
const text = String(value ?? "").trim();
|
|
1009
1057
|
return text ? text : null;
|