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 CHANGED
@@ -265,7 +265,11 @@ rows as JSON, generating schema types, and working with Markdown documents. See
265
265
 
266
266
  ## API
267
267
 
268
- SQLite Hub also provides a local JSON API for app info, database metadata, tables, saved queries, exports, documents, and schema type generation. `/api/v1/info` returns the same app/version status as `sqlite-hub --info`; database data is protected by database-specific API itokens created in Settings. See the [API documentation](./docs/API.md) for authentication, endpoints, and examples.
268
+ SQLite Hub also provides a local JSON API for app info, database metadata, tables, saved queries, exports, documents, and schema type generation. `/api/v1/info` returns the same app/version status as `sqlite-hub --info`; database data is protected by database-specific API tokens created in Settings. See the [API documentation](./docs/API.md) for authentication, endpoints, and examples.
269
+
270
+ ## MCP
271
+
272
+ SQLite Hub includes a local MCP server for agents such as Codex. When SQLite Hub is running, the MCP endpoint is available at `/mcp`; a stdio fallback is also available through `sqlite-hub-mcp`. It exposes the shared API/CLI service layer as guarded tools for schema inspection, read-only queries, query-plan explanation, backups, type generation, documents, and chart creation. See the [MCP documentation](./docs/MCP.md) for setup, tool names, and security boundaries.
269
273
 
270
274
  ## Changelog
271
275
 
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { startMcpStdioServer } = require("../server/mcp/stdioServer");
4
+
5
+ startMcpStdioServer().catch((error) => {
6
+ process.stderr.write(`SQLite Hub MCP failed to start: ${error.message}\n`);
7
+ process.exitCode = 1;
8
+ });
package/docs/MCP.md ADDED
@@ -0,0 +1,109 @@
1
+ # SQLite Hub MCP
2
+
3
+ SQLite Hub ships a local MCP server so agents can inspect and automate the same imported SQLite databases that the UI, CLI, and local API use.
4
+
5
+ The MCP server uses the shared SQLite Hub service layer. API, CLI, and MCP calls all go through the same database registry, query execution, type generation, backup, document, and chart logic.
6
+
7
+ ## Start With SQLite Hub
8
+
9
+ When SQLite Hub is running, the MCP server is available on the same local web server:
10
+
11
+ ```toml
12
+ [mcp_servers.sqlitehub]
13
+ url = "http://127.0.0.1:4173/mcp"
14
+ startup_timeout_sec = 10
15
+ tool_timeout_sec = 60
16
+ ```
17
+
18
+ If SQLite Hub is started with a custom port, use that port in the URL:
19
+
20
+ ```toml
21
+ [mcp_servers.sqlitehub]
22
+ url = "http://127.0.0.1:PORT/mcp"
23
+ startup_timeout_sec = 10
24
+ tool_timeout_sec = 60
25
+ ```
26
+
27
+ The Settings `MCP` tab shows a copyable Codex config with the active host and port.
28
+
29
+ ## Stdio Fallback
30
+
31
+ When SQLite Hub is installed from npm, use:
32
+
33
+ ```bash
34
+ sqlite-hub-mcp
35
+ ```
36
+
37
+ For a local checkout, Codex can also spawn the stdio server directly:
38
+
39
+ ```toml
40
+ [mcp_servers.sqlitehub]
41
+ command = "node"
42
+ args = ["/absolute/path/to/sqlite-hub/bin/sqlite-hub-mcp.js"]
43
+ startup_timeout_sec = 10
44
+ tool_timeout_sec = 60
45
+ ```
46
+
47
+ The stdio fallback is intended for local agents running on the same machine as SQLite Hub. It does not expose a network listener and does not require API tokens for local stdio use.
48
+
49
+ ## Tools
50
+
51
+ Read-only and safe tools:
52
+
53
+ - `list_connections`: list imported SQLite Hub database ids and labels.
54
+ - `get_database_overview`: inspect database health, SQLite metadata, table counts, and schema-map statistics.
55
+ - `list_tables`: list database tables.
56
+ - `describe_table`: inspect columns, indexes, foreign keys, triggers, and row counts for one table.
57
+ - `get_schema`: return tables, views, indexes, triggers, and raw schema entries.
58
+ - `get_indexes`: return all indexes or indexes for one table.
59
+ - `get_foreign_keys`: return all foreign keys or foreign keys for one table.
60
+ - `run_readonly_query`: execute a read-only `SELECT`, `PRAGMA`, or `EXPLAIN` query.
61
+ - `explain_query_plan`: run `EXPLAIN QUERY PLAN` and return structured plan rows plus index hints when a table scan appears.
62
+ - `read_documents`: read database-scoped Markdown documents.
63
+
64
+ Controlled write tools:
65
+
66
+ - `create_backup`: create a verified backup through SQLite Hub's existing backup mechanism.
67
+ - `generate_types`: generate TypeScript, Rust, Kotlin, or Swift types from one table or all tables.
68
+ - `create_chart_from_query`: create a saved chart from a read-only `SELECT` query. It writes chart metadata to SQLite Hub but does not export files.
69
+
70
+ ## Security
71
+
72
+ `run_readonly_query` validates SQL server-side before execution. Only `SELECT`, `PRAGMA`, and `EXPLAIN` statements that return rows are allowed.
73
+
74
+ These statements are blocked in `run_readonly_query`:
75
+
76
+ - `INSERT`
77
+ - `UPDATE`
78
+ - `DELETE`
79
+ - `DROP`
80
+ - `ALTER`
81
+ - `CREATE`
82
+ - `ATTACH`
83
+ - `DETACH`
84
+ - `VACUUM`
85
+ - other non-reader or mutating statements
86
+
87
+ Backups are always created through SQLite Hub's managed backup service. Chart creation stores chart metadata only. The MCP server does not write arbitrary local files.
88
+
89
+ The HTTP MCP endpoint runs on SQLite Hub's existing loopback server. It is local-only and uses the same localhost request guard as the internal API. API tokens are not exposed through MCP tool responses.
90
+
91
+ ## Settings Status
92
+
93
+ The Settings view has an `MCP` tab. It shows whether the MCP server is running, whether an agent is connected, active client count, the last connection time, last tool call, transport, exposed tools, and a copyable Codex config example.
94
+
95
+ For HTTP, connection state is tracked from MCP `initialize` and tool calls against `/mcp`. For stdio, connection state is tracked from MCP `initialize` and tool calls in the spawned process. When the stdio MCP process exits cleanly, SQLite Hub marks the session as disconnected.
96
+
97
+ ## Example Prompts
98
+
99
+ ```text
100
+ Use SQLite Hub MCP to inspect my current database schema and suggest missing indexes.
101
+ ```
102
+
103
+ ```text
104
+ Use SQLite Hub MCP to create a backup before generating TypeScript types.
105
+ ```
106
+
107
+ ```text
108
+ Use SQLite Hub MCP to explain the query plan for this SQL query.
109
+ ```
package/docs/changelog.md CHANGED
@@ -1,3 +1,8 @@
1
+ # v2.0.0
2
+
3
+ - mcp
4
+ - mcp http endpoint starts with sqlite hub
5
+
1
6
  # v1.5.0
2
7
 
3
8
  - synthetic data generation
package/docs/todo.md CHANGED
@@ -1,4 +1,3 @@
1
1
  # Maybe
2
2
 
3
3
  - Data Pipeline Light/hook, programmable triggers?
4
- - MCP
@@ -558,6 +558,10 @@ export function checkAppVersion() {
558
558
  return request("/api/settings/version-check");
559
559
  }
560
560
 
561
+ export function getSettingsMcpStatus() {
562
+ return request("/api/settings/mcp");
563
+ }
564
+
561
565
  export function createApiToken(name) {
562
566
  return request("/api/settings/api-tokens", {
563
567
  method: "POST",
@@ -2577,6 +2577,16 @@ async function handleAction(actionNode) {
2577
2577
  }
2578
2578
  return;
2579
2579
  }
2580
+ case 'copy-mcp-config': {
2581
+ const configNode = document.querySelector('[data-mcp-config]');
2582
+ const config = configNode instanceof HTMLTextAreaElement ? configNode.value : '';
2583
+
2584
+ if (config && navigator.clipboard?.writeText) {
2585
+ await navigator.clipboard.writeText(config);
2586
+ showToast('MCP config copied.', 'success');
2587
+ }
2588
+ return;
2589
+ }
2580
2590
  case 'open-row-editor-url':
2581
2591
  openRowEditorUrl(actionNode);
2582
2592
  return;
@@ -332,6 +332,9 @@ const state = {
332
332
  versionCheck: null,
333
333
  versionCheckLoading: false,
334
334
  versionCheckError: null,
335
+ mcpStatus: null,
336
+ mcpStatusLoading: false,
337
+ mcpStatusError: null,
335
338
  },
336
339
  overview: {
337
340
  data: null,
@@ -1568,6 +1571,22 @@ async function refreshSettingsState() {
1568
1571
  }
1569
1572
  }
1570
1573
 
1574
+ async function refreshSettingsMcpStatus() {
1575
+ state.settings.mcpStatusLoading = true;
1576
+ state.settings.mcpStatusError = null;
1577
+ emitChange();
1578
+
1579
+ try {
1580
+ const response = await api.getSettingsMcpStatus();
1581
+ state.settings.mcpStatus = response.data ?? null;
1582
+ } catch (error) {
1583
+ state.settings.mcpStatusError = normalizeError(error);
1584
+ } finally {
1585
+ state.settings.mcpStatusLoading = false;
1586
+ emitChange();
1587
+ }
1588
+ }
1589
+
1571
1590
  function buildLogRequestOptions({ append = false } = {}) {
1572
1591
  const filters = normalizeLogFilters(state.logs.filters);
1573
1592
 
@@ -1674,7 +1693,8 @@ export async function createSettingsApiToken(name) {
1674
1693
  }
1675
1694
 
1676
1695
  export function setSettingsSection(section) {
1677
- const normalizedSection = section === 'api-tokens' ? 'api-tokens' : 'information';
1696
+ const normalizedSection =
1697
+ section === 'api-tokens' ? 'api-tokens' : section === 'mcp' ? 'mcp' : 'information';
1678
1698
 
1679
1699
  if (state.settings.section === normalizedSection) {
1680
1700
  return;
@@ -1726,7 +1746,9 @@ export async function loadMoreLogs() {
1726
1746
  await loadLogs({ append: true });
1727
1747
  }
1728
1748
 
1729
- export async function checkSettingsAppVersion() {
1749
+ export async function checkSettingsAppVersion(options = {}) {
1750
+ const silent = Boolean(options.silent);
1751
+
1730
1752
  state.settings.versionCheckLoading = true;
1731
1753
  state.settings.versionCheckError = null;
1732
1754
  emitChange();
@@ -1736,14 +1758,18 @@ export async function checkSettingsAppVersion() {
1736
1758
  const result = response.data ?? null;
1737
1759
 
1738
1760
  state.settings.versionCheck = result;
1739
- pushToast(
1740
- result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
1741
- 'success',
1742
- );
1761
+ if (!silent) {
1762
+ pushToast(
1763
+ result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
1764
+ 'success',
1765
+ );
1766
+ }
1743
1767
  return result;
1744
1768
  } catch (error) {
1745
1769
  state.settings.versionCheckError = normalizeError(error);
1746
- pushToast('Version check failed.', 'alert');
1770
+ if (!silent) {
1771
+ pushToast('Version check failed.', 'alert');
1772
+ }
1747
1773
  return null;
1748
1774
  } finally {
1749
1775
  state.settings.versionCheckLoading = false;
@@ -2964,6 +2990,8 @@ async function loadRouteData(route, options = {}) {
2964
2990
  return;
2965
2991
  case 'settings':
2966
2992
  await refreshSettingsState();
2993
+ await refreshSettingsMcpStatus();
2994
+ await checkSettingsAppVersion({ silent: true });
2967
2995
  return;
2968
2996
  default:
2969
2997
  }
@@ -6,6 +6,7 @@ function renderSettingsNavigation(activeSection) {
6
6
  const items = [
7
7
  { id: 'information', icon: 'info', label: 'Information' },
8
8
  { id: 'api-tokens', icon: 'key', label: 'API Tokens' },
9
+ { id: 'mcp', icon: 'hub', label: 'MCP' },
9
10
  ];
10
11
 
11
12
  return `
@@ -90,6 +91,187 @@ function renderVersionCheckStatus(settings) {
90
91
  `;
91
92
  }
92
93
 
94
+ function renderMcpTimestamp(value) {
95
+ return value ? formatCompactDateTime(value) : 'Never';
96
+ }
97
+
98
+ function renderMcpStatusSummary(status) {
99
+ if (status?.error) {
100
+ return {
101
+ label: 'Server Error',
102
+ icon: 'error',
103
+ badgeClass: 'status-badge border-error/30 bg-error-container/20 text-error',
104
+ message: status.error,
105
+ };
106
+ }
107
+
108
+ if (status?.connected) {
109
+ return {
110
+ label: 'Agent Connected',
111
+ icon: 'check_circle',
112
+ badgeClass: 'status-badge status-badge--success',
113
+ message: `Last tool call: ${status.lastToolName || 'none'} at ${renderMcpTimestamp(status.lastToolCallAt)}`,
114
+ };
115
+ }
116
+
117
+ if (status?.enabled) {
118
+ return {
119
+ label: 'Waiting For Agent',
120
+ icon: 'radio_button_unchecked',
121
+ badgeClass: 'status-badge',
122
+ message: status.lastConnectedAt
123
+ ? `Last agent connection: ${renderMcpTimestamp(status.lastConnectedAt)}`
124
+ : 'No MCP agent connected yet.',
125
+ };
126
+ }
127
+
128
+ return {
129
+ label: 'Stopped',
130
+ icon: 'block',
131
+ badgeClass: 'status-badge',
132
+ message: 'No MCP agent connected yet.',
133
+ };
134
+ }
135
+
136
+ function renderMcpMetric(label, value) {
137
+ return `
138
+ <div class="border border-outline-variant/10 bg-surface-container-high px-4 py-3">
139
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
140
+ ${escapeHtml(label)}
141
+ </div>
142
+ <div class="mt-2 break-words font-mono text-sm text-on-surface">
143
+ ${escapeHtml(value)}
144
+ </div>
145
+ </div>
146
+ `;
147
+ }
148
+
149
+ function renderMcpSection(settings) {
150
+ if (settings.mcpStatusLoading && !settings.mcpStatus) {
151
+ return `
152
+ <div class="flex min-h-[240px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
153
+ <div class="text-center text-on-surface-variant/40">
154
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
155
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_MCP_STATUS</p>
156
+ </div>
157
+ </div>
158
+ `;
159
+ }
160
+
161
+ if (settings.mcpStatusError) {
162
+ return `
163
+ <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
164
+ <div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
165
+ ${escapeHtml(settings.mcpStatusError.code ?? 'MCP_STATUS_FAILED')}
166
+ </div>
167
+ <div class="mt-2">${escapeHtml(settings.mcpStatusError.message ?? 'MCP status could not be loaded.')}</div>
168
+ </div>
169
+ `;
170
+ }
171
+
172
+ const status = settings.mcpStatus ?? {};
173
+ const summary = renderMcpStatusSummary(status);
174
+ const tools = status.toolDetails?.length
175
+ ? status.toolDetails
176
+ : (status.exposedTools ?? []).map(name => ({ name, description: '' }));
177
+ const codexConfig = status.codexConfig ?? '';
178
+ const command = status.command ?? 'sqlite-hub-mcp';
179
+
180
+ return `
181
+ <div class="space-y-6">
182
+ <section class="shell-section overflow-hidden">
183
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
184
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">MCP Status</span>
185
+ <span class="material-symbols-outlined text-xs text-primary-container">hub</span>
186
+ </div>
187
+ <div class="space-y-5 p-6">
188
+ <div class="flex flex-col gap-3 border border-outline-variant/10 bg-surface-container-high px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
189
+ <div class="min-w-0">
190
+ <div class="flex items-center gap-3">
191
+ <span class="material-symbols-outlined text-base text-primary-container">${summary.icon}</span>
192
+ <span class="${summary.badgeClass}">${escapeHtml(summary.label)}</span>
193
+ </div>
194
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant">
195
+ ${escapeHtml(summary.message)}
196
+ </div>
197
+ </div>
198
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/60">
199
+ ${escapeHtml(status.transport ?? 'unknown')}
200
+ </div>
201
+ </div>
202
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-3">
203
+ ${renderMcpMetric('MCP Server', status.serverRunning ? 'Running' : 'Stopped')}
204
+ ${renderMcpMetric('Agent connected', status.connected ? 'Yes' : 'No')}
205
+ ${renderMcpMetric('Active clients', String(status.activeClientCount ?? 0))}
206
+ ${renderMcpMetric('Last connected', renderMcpTimestamp(status.lastConnectedAt))}
207
+ ${renderMcpMetric('Last disconnected', renderMcpTimestamp(status.lastDisconnectedAt))}
208
+ ${renderMcpMetric('Last tool call', status.lastToolCallAt ? renderMcpTimestamp(status.lastToolCallAt) : 'Never')}
209
+ ${renderMcpMetric('Last tool name', status.lastToolName ?? 'None')}
210
+ ${renderMcpMetric('Transport', status.transport ?? 'unknown')}
211
+ ${renderMcpMetric('Exposed tools', String(status.exposedTools?.length ?? tools.length))}
212
+ </div>
213
+ </div>
214
+ </section>
215
+
216
+ <section class="shell-section overflow-hidden">
217
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
218
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Tools</span>
219
+ <span class="material-symbols-outlined text-xs text-primary-container">construction</span>
220
+ </div>
221
+ <div class="grid grid-cols-1 gap-3 p-6 md:grid-cols-2">
222
+ ${tools
223
+ .map(
224
+ tool => `
225
+ <div class="border border-outline-variant/10 bg-surface-container-high px-4 py-3">
226
+ <div class="font-mono text-xs font-bold text-primary-container">${escapeHtml(tool.name)}</div>
227
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant">${escapeHtml(tool.description || 'SQLite Hub MCP tool.')}</div>
228
+ </div>
229
+ `,
230
+ )
231
+ .join('')}
232
+ </div>
233
+ </section>
234
+
235
+ <section class="shell-section overflow-hidden">
236
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
237
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Codex Setup</span>
238
+ <span class="material-symbols-outlined text-xs text-primary-container">terminal</span>
239
+ </div>
240
+ <div class="space-y-4 p-6">
241
+ <div>
242
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
243
+ MCP_ENDPOINT
244
+ </div>
245
+ <div class="mt-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 font-mono text-sm text-primary-container">
246
+ ${escapeHtml(command)}
247
+ </div>
248
+ </div>
249
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
250
+ <textarea class="settings-mcp-config-input custom-scrollbar control-input font-mono text-xs leading-6" readonly data-mcp-config>${escapeHtml(codexConfig)}</textarea>
251
+ <button class="standard-button self-start" data-action="copy-mcp-config" type="button">
252
+ <span class="material-symbols-outlined text-sm">content_copy</span>
253
+ Copy Config
254
+ </button>
255
+ </div>
256
+ </div>
257
+ </section>
258
+
259
+ <section class="shell-section overflow-hidden">
260
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
261
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Security</span>
262
+ <span class="material-symbols-outlined text-xs text-primary-container">shield</span>
263
+ </div>
264
+ <div class="space-y-2 p-6 text-sm leading-6 text-on-surface-variant">
265
+ <div>Read-only queries are limited to SELECT, PRAGMA, and EXPLAIN.</div>
266
+ <div>Mutating SQL statements are blocked server-side before execution.</div>
267
+ <div>Destructive actions must use existing SQLite Hub safety mechanisms such as verified backups.</div>
268
+ <div>The local HTTP MCP endpoint is bound to SQLite Hub's loopback server and does not expose API tokens.</div>
269
+ </div>
270
+ </section>
271
+ </div>
272
+ `;
273
+ }
274
+
93
275
  function renderApiTokenRow(token, tokenSaving) {
94
276
  const callCount = Number(token.callCount ?? 0);
95
277
  const createdAt = token.createdAt ? formatCompactDateTime(token.createdAt) : 'Unknown';
@@ -188,7 +370,8 @@ function renderSettingsContent(state) {
188
370
  `;
189
371
  }
190
372
 
191
- const activeSection = state.settings.section === 'api-tokens' ? 'api-tokens' : 'information';
373
+ const activeSection =
374
+ state.settings.section === 'api-tokens' ? 'api-tokens' : state.settings.section === 'mcp' ? 'mcp' : 'information';
192
375
  const appVersion = escapeHtml(state.settings.appVersion ?? '0.0.0');
193
376
  const sqliteVersion = escapeHtml(state.settings.sqliteVersion ?? 'unknown');
194
377
  const versionCheckLoading = Boolean(state.settings.versionCheckLoading);
@@ -377,11 +560,12 @@ function renderSettingsContent(state) {
377
560
  </div>
378
561
  </section>
379
562
  `;
563
+ const mcpSection = renderMcpSection(state.settings);
380
564
 
381
565
  return `
382
566
  <div class="space-y-6">
383
567
  ${renderSettingsNavigation(activeSection)}
384
- ${activeSection === 'api-tokens' ? apiTokensSection : informationSection}
568
+ ${activeSection === 'api-tokens' ? apiTokensSection : activeSection === 'mcp' ? mcpSection : informationSection}
385
569
  </div>
386
570
  `;
387
571
  }
@@ -396,7 +580,9 @@ export function renderSettingsView(state) {
396
580
  subtitle:
397
581
  state.settings.section === 'api-tokens'
398
582
  ? 'Security // Database API access'
399
- : 'Application // Build + Credits',
583
+ : state.settings.section === 'mcp'
584
+ ? 'Agents // Local MCP access'
585
+ : 'Application // Build + Credits',
400
586
  })}
401
587
  ${renderSettingsContent(state)}
402
588
  </div>