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,12 @@
|
|
|
1
1
|
import { renderPageHeader } from '../components/pageHeader.js';
|
|
2
2
|
import { renderTextInput } from '../components/formControls.js';
|
|
3
|
-
import { escapeHtml } from '../utils/format.js';
|
|
3
|
+
import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
|
|
4
4
|
|
|
5
5
|
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,262 @@ 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
|
+
Local_Command
|
|
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>Local stdio MCP is allowed because it runs on the same machine as SQLite Hub and does not expose network credentials.</div>
|
|
269
|
+
</div>
|
|
270
|
+
</section>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderApiTokenRow(token, tokenSaving) {
|
|
276
|
+
const callCount = Number(token.callCount ?? 0);
|
|
277
|
+
const createdAt = token.createdAt ? formatCompactDateTime(token.createdAt) : 'Unknown';
|
|
278
|
+
const lastCallAt = token.lastCallAt ? formatCompactDateTime(token.lastCallAt) : 'Never';
|
|
279
|
+
|
|
280
|
+
return `
|
|
281
|
+
<div class="grid grid-cols-1 gap-4 border-t border-outline-variant/10 bg-surface-container-high px-4 py-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(8rem,0.75fr)_7rem_minmax(8rem,0.85fr)_8rem] xl:items-center">
|
|
282
|
+
<div class="min-w-0">
|
|
283
|
+
<div class="truncate text-sm font-semibold text-on-surface" title="${escapeHtml(token.name)}">
|
|
284
|
+
${escapeHtml(token.name)}
|
|
285
|
+
</div>
|
|
286
|
+
<div
|
|
287
|
+
class="mt-2 inline-flex max-w-full border border-outline-variant/10 bg-surface-container-lowest px-3 py-1 font-mono text-[11px] text-primary-container"
|
|
288
|
+
title="${escapeHtml(token.tokenPrefix ?? '')}"
|
|
289
|
+
>
|
|
290
|
+
<span class="truncate">${escapeHtml(token.tokenPrefix)}...</span>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
<div>
|
|
294
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Created</div>
|
|
295
|
+
<div class="mt-1 font-mono text-[11px] text-on-surface-variant/75 xl:mt-0" title="${escapeHtml(token.createdAt ?? 'unknown')}">
|
|
296
|
+
${escapeHtml(createdAt)}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div>
|
|
300
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Calls</div>
|
|
301
|
+
<div class="mt-1 font-mono text-lg font-bold text-on-surface xl:mt-0">
|
|
302
|
+
${escapeHtml(formatNumber(callCount))}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div>
|
|
306
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Last Call</div>
|
|
307
|
+
<div class="mt-1 font-mono text-[11px] text-on-surface-variant/75 xl:mt-0" title="${escapeHtml(token.lastCallAt ?? 'never')}">
|
|
308
|
+
${escapeHtml(lastCallAt)}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="flex xl:justify-end">
|
|
312
|
+
<button
|
|
313
|
+
class="delete-button"
|
|
314
|
+
data-action="open-delete-api-token-modal"
|
|
315
|
+
data-token-id="${escapeHtml(token.id)}"
|
|
316
|
+
type="button"
|
|
317
|
+
${tokenSaving ? 'disabled' : ''}
|
|
318
|
+
>
|
|
319
|
+
<span class="material-symbols-outlined text-sm">delete</span>
|
|
320
|
+
Delete
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderApiTokenRows(apiTokens, tokenSaving) {
|
|
328
|
+
if (!apiTokens.length) {
|
|
329
|
+
return `
|
|
330
|
+
<div class="border border-outline-variant/10 bg-surface-container-high px-4 py-5 text-sm text-on-surface-variant">
|
|
331
|
+
No API tokens exist for this database.
|
|
332
|
+
</div>
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return `
|
|
337
|
+
<div class="overflow-hidden border border-outline-variant/10">
|
|
338
|
+
<div class="hidden bg-surface-container-highest px-4 py-3 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55 xl:grid xl:grid-cols-[minmax(0,1.35fr)_minmax(8rem,0.75fr)_7rem_minmax(8rem,0.85fr)_8rem]">
|
|
339
|
+
<div>Token</div>
|
|
340
|
+
<div>Created</div>
|
|
341
|
+
<div>Calls</div>
|
|
342
|
+
<div>Last Call</div>
|
|
343
|
+
<div class="text-right">Actions</div>
|
|
344
|
+
</div>
|
|
345
|
+
${apiTokens.map(token => renderApiTokenRow(token, tokenSaving)).join('')}
|
|
346
|
+
</div>
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
|
|
93
350
|
function renderSettingsContent(state) {
|
|
94
351
|
if (state.settings.loading && !state.settings.appVersion) {
|
|
95
352
|
return `
|
|
@@ -113,7 +370,8 @@ function renderSettingsContent(state) {
|
|
|
113
370
|
`;
|
|
114
371
|
}
|
|
115
372
|
|
|
116
|
-
const activeSection =
|
|
373
|
+
const activeSection =
|
|
374
|
+
state.settings.section === 'api-tokens' ? 'api-tokens' : state.settings.section === 'mcp' ? 'mcp' : 'information';
|
|
117
375
|
const appVersion = escapeHtml(state.settings.appVersion ?? '0.0.0');
|
|
118
376
|
const sqliteVersion = escapeHtml(state.settings.sqliteVersion ?? 'unknown');
|
|
119
377
|
const versionCheckLoading = Boolean(state.settings.versionCheckLoading);
|
|
@@ -121,32 +379,7 @@ function renderSettingsContent(state) {
|
|
|
121
379
|
const apiTokens = state.settings.apiTokens ?? [];
|
|
122
380
|
const createdApiToken = state.settings.createdApiToken;
|
|
123
381
|
const tokenSaving = Boolean(state.settings.tokenSaving);
|
|
124
|
-
const tokenItems = apiTokens
|
|
125
|
-
? apiTokens
|
|
126
|
-
.map(
|
|
127
|
-
token => `
|
|
128
|
-
<div class="flex flex-col gap-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
129
|
-
<div class="min-w-0">
|
|
130
|
-
<div class="truncate text-sm font-semibold text-on-surface">${escapeHtml(token.name)}</div>
|
|
131
|
-
<div class="mt-1 font-mono text-[10px] text-on-surface-variant/60">
|
|
132
|
-
${escapeHtml(token.tokenPrefix)}... · Created ${escapeHtml(token.createdAt ?? 'unknown')}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
<button
|
|
136
|
-
class="delete-button"
|
|
137
|
-
data-action="open-delete-api-token-modal"
|
|
138
|
-
data-token-id="${escapeHtml(token.id)}"
|
|
139
|
-
type="button"
|
|
140
|
-
${tokenSaving ? 'disabled' : ''}
|
|
141
|
-
>
|
|
142
|
-
<span class="material-symbols-outlined text-sm">delete</span>
|
|
143
|
-
Delete
|
|
144
|
-
</button>
|
|
145
|
-
</div>
|
|
146
|
-
`,
|
|
147
|
-
)
|
|
148
|
-
.join('')
|
|
149
|
-
: '<div class="text-sm text-on-surface-variant">No API tokens exist for this database.</div>';
|
|
382
|
+
const tokenItems = renderApiTokenRows(apiTokens, tokenSaving);
|
|
150
383
|
const tokenSection = tokenDatabase
|
|
151
384
|
? `
|
|
152
385
|
<div class="space-y-5">
|
|
@@ -327,11 +560,12 @@ function renderSettingsContent(state) {
|
|
|
327
560
|
</div>
|
|
328
561
|
</section>
|
|
329
562
|
`;
|
|
563
|
+
const mcpSection = renderMcpSection(state.settings);
|
|
330
564
|
|
|
331
565
|
return `
|
|
332
566
|
<div class="space-y-6">
|
|
333
567
|
${renderSettingsNavigation(activeSection)}
|
|
334
|
-
${activeSection === 'api-tokens' ? apiTokensSection : informationSection}
|
|
568
|
+
${activeSection === 'api-tokens' ? apiTokensSection : activeSection === 'mcp' ? mcpSection : informationSection}
|
|
335
569
|
</div>
|
|
336
570
|
`;
|
|
337
571
|
}
|
|
@@ -346,7 +580,9 @@ export function renderSettingsView(state) {
|
|
|
346
580
|
subtitle:
|
|
347
581
|
state.settings.section === 'api-tokens'
|
|
348
582
|
? 'Security // Database API access'
|
|
349
|
-
:
|
|
583
|
+
: state.settings.section === 'mcp'
|
|
584
|
+
? 'Agents // Local MCP access'
|
|
585
|
+
: 'Application // Build + Credits',
|
|
350
586
|
})}
|
|
351
587
|
${renderSettingsContent(state)}
|
|
352
588
|
</div>
|
|
@@ -248,6 +248,12 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
|
|
|
248
248
|
label: 'Table Designer',
|
|
249
249
|
target: tableName => `/table-designer/${encodeURIComponent(tableName)}`,
|
|
250
250
|
},
|
|
251
|
+
{
|
|
252
|
+
icon: 'troubleshoot',
|
|
253
|
+
key: 'table-advisor',
|
|
254
|
+
label: 'Table Advisor',
|
|
255
|
+
target: tableName => `/table-advisor/${encodeURIComponent(tableName)}`,
|
|
256
|
+
},
|
|
251
257
|
{
|
|
252
258
|
key: 'sql-editor',
|
|
253
259
|
},
|