lokicms-plugin-sql 1.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/dist/federation/cache-layer.d.ts +58 -0
- package/dist/federation/cache-layer.d.ts.map +1 -0
- package/dist/federation/cache-layer.js +210 -0
- package/dist/federation/cache-layer.js.map +1 -0
- package/dist/federation/index.d.ts +12 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +9 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/federation/source-manager.d.ts +57 -0
- package/dist/federation/source-manager.d.ts.map +1 -0
- package/dist/federation/source-manager.js +238 -0
- package/dist/federation/source-manager.js.map +1 -0
- package/dist/federation/sync-engine.d.ts +68 -0
- package/dist/federation/sync-engine.d.ts.map +1 -0
- package/dist/federation/sync-engine.js +288 -0
- package/dist/federation/sync-engine.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +28 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +798 -0
- package/dist/plugin.js.map +1 -0
- package/dist/providers/base.d.ts +142 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +161 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/index.d.ts +22 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +74 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mariadb.d.ts +83 -0
- package/dist/providers/mariadb.d.ts.map +1 -0
- package/dist/providers/mariadb.js +293 -0
- package/dist/providers/mariadb.js.map +1 -0
- package/dist/providers/mysql.d.ts +78 -0
- package/dist/providers/mysql.d.ts.map +1 -0
- package/dist/providers/mysql.js +284 -0
- package/dist/providers/mysql.js.map +1 -0
- package/dist/providers/postgresql.d.ts +77 -0
- package/dist/providers/postgresql.d.ts.map +1 -0
- package/dist/providers/postgresql.js +296 -0
- package/dist/providers/postgresql.js.map +1 -0
- package/dist/providers/sqlite.d.ts +80 -0
- package/dist/providers/sqlite.d.ts.map +1 -0
- package/dist/providers/sqlite.js +283 -0
- package/dist/providers/sqlite.js.map +1 -0
- package/dist/query/builder.d.ts +74 -0
- package/dist/query/builder.d.ts.map +1 -0
- package/dist/query/builder.js +279 -0
- package/dist/query/builder.js.map +1 -0
- package/dist/query/index.d.ts +10 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +8 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/transformer.d.ts +74 -0
- package/dist/query/transformer.d.ts.map +1 -0
- package/dist/query/transformer.js +236 -0
- package/dist/query/transformer.js.map +1 -0
- package/dist/types.d.ts +350 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +38 -0
- package/dist/types.js.map +1 -0
- package/dist/vectors/adapter.d.ts +128 -0
- package/dist/vectors/adapter.d.ts.map +1 -0
- package/dist/vectors/adapter.js +79 -0
- package/dist/vectors/adapter.js.map +1 -0
- package/dist/vectors/index.d.ts +41 -0
- package/dist/vectors/index.d.ts.map +1 -0
- package/dist/vectors/index.js +87 -0
- package/dist/vectors/index.js.map +1 -0
- package/dist/vectors/lokijs-vector.d.ts +112 -0
- package/dist/vectors/lokijs-vector.d.ts.map +1 -0
- package/dist/vectors/lokijs-vector.js +217 -0
- package/dist/vectors/lokijs-vector.js.map +1 -0
- package/dist/vectors/mariadb-vector.d.ts +56 -0
- package/dist/vectors/mariadb-vector.d.ts.map +1 -0
- package/dist/vectors/mariadb-vector.js +263 -0
- package/dist/vectors/mariadb-vector.js.map +1 -0
- package/dist/vectors/mysql-vector.d.ts +56 -0
- package/dist/vectors/mysql-vector.d.ts.map +1 -0
- package/dist/vectors/mysql-vector.js +235 -0
- package/dist/vectors/mysql-vector.js.map +1 -0
- package/dist/vectors/pgvector.d.ts +52 -0
- package/dist/vectors/pgvector.d.ts.map +1 -0
- package/dist/vectors/pgvector.js +190 -0
- package/dist/vectors/pgvector.js.map +1 -0
- package/dist/vectors/sqlite-vec.d.ts +80 -0
- package/dist/vectors/sqlite-vec.d.ts.map +1 -0
- package/dist/vectors/sqlite-vec.js +362 -0
- package/dist/vectors/sqlite-vec.js.map +1 -0
- package/package.json +64 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LokiCMS SQL Plugin
|
|
3
|
+
*
|
|
4
|
+
* Universal SQL adapter with multi-database support, data federation,
|
|
5
|
+
* split storage, and vector capabilities.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { DEFAULT_TABLE_PREFIX, DEFAULT_VECTOR_DIMENSIONS, DEFAULT_CACHE_TTL } from './types.js';
|
|
9
|
+
import { createSourceManager } from './federation/source-manager.js';
|
|
10
|
+
import { createSyncEngine } from './federation/sync-engine.js';
|
|
11
|
+
import { createCacheLayer } from './federation/cache-layer.js';
|
|
12
|
+
import { createVectorAdapter } from './vectors/index.js';
|
|
13
|
+
// Plugin state
|
|
14
|
+
let sourceManager = null;
|
|
15
|
+
let syncEngine = null;
|
|
16
|
+
let cacheLayer = null;
|
|
17
|
+
let vectorAdapter = null;
|
|
18
|
+
let pluginConfig = null;
|
|
19
|
+
/**
|
|
20
|
+
* Get source manager for code usage
|
|
21
|
+
*/
|
|
22
|
+
export function getSourceManager() {
|
|
23
|
+
if (!sourceManager) {
|
|
24
|
+
throw new Error('SQL plugin not initialized');
|
|
25
|
+
}
|
|
26
|
+
return sourceManager;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get sync engine for code usage
|
|
30
|
+
*/
|
|
31
|
+
export function getSyncEngine() {
|
|
32
|
+
if (!syncEngine) {
|
|
33
|
+
throw new Error('SQL plugin not initialized');
|
|
34
|
+
}
|
|
35
|
+
return syncEngine;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get vector adapter for code usage
|
|
39
|
+
*/
|
|
40
|
+
export function getVectorAdapter() {
|
|
41
|
+
return vectorAdapter;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* LokiCMS SQL Plugin Definition
|
|
45
|
+
*/
|
|
46
|
+
const plugin = {
|
|
47
|
+
name: 'lokicms-sql',
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
description: 'Universal SQL adapter with multi-database support and data federation',
|
|
50
|
+
async register(api) {
|
|
51
|
+
pluginConfig = api.config.sql || {};
|
|
52
|
+
const tablePrefix = pluginConfig.tablePrefix ?? DEFAULT_TABLE_PREFIX;
|
|
53
|
+
api.logger.info('Initializing SQL plugin');
|
|
54
|
+
// Create source manager
|
|
55
|
+
sourceManager = createSourceManager(api.logger, tablePrefix);
|
|
56
|
+
// Set up primary if configured
|
|
57
|
+
if (pluginConfig.primary) {
|
|
58
|
+
try {
|
|
59
|
+
await sourceManager.setPrimary(pluginConfig.primary.provider, pluginConfig.primary.mode, pluginConfig.primary.connection);
|
|
60
|
+
// Create vector adapter if configured
|
|
61
|
+
const primary = sourceManager.getPrimary();
|
|
62
|
+
if (primary && pluginConfig.vectors) {
|
|
63
|
+
vectorAdapter = createVectorAdapter(primary, api.logger, {
|
|
64
|
+
tableName: `${tablePrefix}vectors`,
|
|
65
|
+
dimensions: pluginConfig.vectors.dimensions ?? DEFAULT_VECTOR_DIMENSIONS,
|
|
66
|
+
});
|
|
67
|
+
await vectorAdapter.initialize(pluginConfig.vectors.dimensions ?? DEFAULT_VECTOR_DIMENSIONS);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
api.logger.error(`Failed to connect to primary: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Add configured sources
|
|
75
|
+
if (pluginConfig.sources) {
|
|
76
|
+
for (const sourceConfig of pluginConfig.sources) {
|
|
77
|
+
try {
|
|
78
|
+
await sourceManager.addSource(sourceConfig);
|
|
79
|
+
if (sourceConfig.autoSync) {
|
|
80
|
+
await sourceManager.connect(sourceConfig.name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
api.logger.error(`Failed to add source ${sourceConfig.name}: ${error}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Create sync engine (requires LokiJS db from services)
|
|
89
|
+
const lokiDb = api.services.db;
|
|
90
|
+
if (lokiDb) {
|
|
91
|
+
syncEngine = createSyncEngine(sourceManager, lokiDb, api.logger);
|
|
92
|
+
// Create cache layer
|
|
93
|
+
const cacheConfig = pluginConfig.cache ?? {
|
|
94
|
+
enabled: true,
|
|
95
|
+
ttlMinutes: DEFAULT_CACHE_TTL,
|
|
96
|
+
};
|
|
97
|
+
cacheLayer = createCacheLayer(lokiDb, cacheConfig, api.logger);
|
|
98
|
+
// Start auto-sync for enabled sources
|
|
99
|
+
if (pluginConfig.sources) {
|
|
100
|
+
for (const sourceConfig of pluginConfig.sources) {
|
|
101
|
+
if (sourceConfig.autoSync) {
|
|
102
|
+
syncEngine.startAutoSync(sourceConfig.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Register MCP tools
|
|
108
|
+
registerMcpTools(api);
|
|
109
|
+
api.logger.info('SQL plugin registered');
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Register all 15 MCP tools
|
|
114
|
+
*/
|
|
115
|
+
function registerMcpTools(api) {
|
|
116
|
+
// ========================================================================
|
|
117
|
+
// Tool 1: sql_status
|
|
118
|
+
// ========================================================================
|
|
119
|
+
api.mcp.registerTool('sql_status', {
|
|
120
|
+
description: 'Get status of all SQL connections including primary, sources, vectors, and cache',
|
|
121
|
+
inputSchema: z.object({}),
|
|
122
|
+
handler: async () => {
|
|
123
|
+
if (!sourceManager) {
|
|
124
|
+
return { error: 'SQL plugin not initialized' };
|
|
125
|
+
}
|
|
126
|
+
const primary = sourceManager.getPrimary();
|
|
127
|
+
let primaryStatus = null;
|
|
128
|
+
if (primary) {
|
|
129
|
+
primaryStatus = await primary.getStatus();
|
|
130
|
+
}
|
|
131
|
+
const sourceStatuses = await sourceManager.getAllStatuses();
|
|
132
|
+
const sources = {};
|
|
133
|
+
for (const [name, status] of sourceStatuses) {
|
|
134
|
+
sources[name] = status;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
primary: primaryStatus,
|
|
138
|
+
sources,
|
|
139
|
+
vectors: vectorAdapter ? {
|
|
140
|
+
supported: vectorAdapter.isSupported(),
|
|
141
|
+
type: vectorAdapter.type,
|
|
142
|
+
} : null,
|
|
143
|
+
cache: cacheLayer ? cacheLayer.getStats() : null,
|
|
144
|
+
syncJobs: syncEngine ? syncEngine.getJobs().length : 0,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// ========================================================================
|
|
149
|
+
// Tool 2: sql_connect
|
|
150
|
+
// ========================================================================
|
|
151
|
+
api.mcp.registerTool('sql_connect', {
|
|
152
|
+
description: 'Connect to a configured data source',
|
|
153
|
+
inputSchema: z.object({
|
|
154
|
+
source: z.string().describe('Source name to connect to'),
|
|
155
|
+
}),
|
|
156
|
+
handler: async (input) => {
|
|
157
|
+
if (!sourceManager) {
|
|
158
|
+
return { error: 'SQL plugin not initialized' };
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await sourceManager.connect(input.source);
|
|
162
|
+
const status = await sourceManager.getStatus(input.source);
|
|
163
|
+
return {
|
|
164
|
+
success: true,
|
|
165
|
+
source: input.source,
|
|
166
|
+
status,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: error instanceof Error ? error.message : String(error),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
// ========================================================================
|
|
178
|
+
// Tool 3: sql_disconnect
|
|
179
|
+
// ========================================================================
|
|
180
|
+
api.mcp.registerTool('sql_disconnect', {
|
|
181
|
+
description: 'Disconnect from a data source',
|
|
182
|
+
inputSchema: z.object({
|
|
183
|
+
source: z.string().describe('Source name to disconnect from'),
|
|
184
|
+
}),
|
|
185
|
+
handler: async (input) => {
|
|
186
|
+
if (!sourceManager) {
|
|
187
|
+
return { error: 'SQL plugin not initialized' };
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
// Stop auto-sync if running
|
|
191
|
+
if (syncEngine) {
|
|
192
|
+
syncEngine.stopAutoSync(input.source);
|
|
193
|
+
}
|
|
194
|
+
await sourceManager.disconnect(input.source);
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
source: input.source,
|
|
198
|
+
message: `Disconnected from ${input.source}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
// ========================================================================
|
|
210
|
+
// Tool 4: sql_sources
|
|
211
|
+
// ========================================================================
|
|
212
|
+
api.mcp.registerTool('sql_sources', {
|
|
213
|
+
description: 'List all configured data sources',
|
|
214
|
+
inputSchema: z.object({}),
|
|
215
|
+
handler: async () => {
|
|
216
|
+
if (!sourceManager) {
|
|
217
|
+
return { error: 'SQL plugin not initialized' };
|
|
218
|
+
}
|
|
219
|
+
const sources = sourceManager.getAllSources();
|
|
220
|
+
return {
|
|
221
|
+
count: sources.length,
|
|
222
|
+
sources: sources.map((s) => ({
|
|
223
|
+
name: s.name,
|
|
224
|
+
provider: s.config.provider,
|
|
225
|
+
mode: s.config.mode,
|
|
226
|
+
status: s.status,
|
|
227
|
+
tables: Object.keys(s.config.tables),
|
|
228
|
+
autoSync: s.config.autoSync ?? false,
|
|
229
|
+
lastConnected: s.lastConnected,
|
|
230
|
+
error: s.error,
|
|
231
|
+
})),
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
// ========================================================================
|
|
236
|
+
// Tool 5: sql_import
|
|
237
|
+
// ========================================================================
|
|
238
|
+
api.mcp.registerTool('sql_import', {
|
|
239
|
+
description: 'Import data from a readonly source into LokiJS collections',
|
|
240
|
+
inputSchema: z.object({
|
|
241
|
+
source: z.string().describe('Source name to import from'),
|
|
242
|
+
table: z.string().optional().describe('Specific table to import (omit for all)'),
|
|
243
|
+
fullRefresh: z.boolean().optional().describe('Clear existing data before import'),
|
|
244
|
+
}),
|
|
245
|
+
handler: async (input) => {
|
|
246
|
+
if (!syncEngine) {
|
|
247
|
+
return { error: 'Sync engine not initialized' };
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const result = await syncEngine.import({
|
|
251
|
+
source: input.source,
|
|
252
|
+
table: input.table,
|
|
253
|
+
fullRefresh: input.fullRefresh ?? false,
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
success: result.success,
|
|
257
|
+
source: result.source,
|
|
258
|
+
table: result.table,
|
|
259
|
+
collection: result.collection,
|
|
260
|
+
inserted: result.inserted,
|
|
261
|
+
updated: result.updated,
|
|
262
|
+
deleted: result.deleted,
|
|
263
|
+
errors: result.errors,
|
|
264
|
+
duration: `${result.duration}ms`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: error instanceof Error ? error.message : String(error),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
// ========================================================================
|
|
276
|
+
// Tool 6: sql_sync
|
|
277
|
+
// ========================================================================
|
|
278
|
+
api.mcp.registerTool('sql_sync', {
|
|
279
|
+
description: 'Sync data from all or specific source',
|
|
280
|
+
inputSchema: z.object({
|
|
281
|
+
source: z.string().optional().describe('Source to sync (omit for all)'),
|
|
282
|
+
}),
|
|
283
|
+
handler: async (input) => {
|
|
284
|
+
if (!syncEngine) {
|
|
285
|
+
return { error: 'Sync engine not initialized' };
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const results = input.source
|
|
289
|
+
? await syncEngine.sync(input.source)
|
|
290
|
+
: await syncEngine.syncAll();
|
|
291
|
+
return {
|
|
292
|
+
success: results.every((r) => r.success),
|
|
293
|
+
synced: results.length,
|
|
294
|
+
results: results.map((r) => ({
|
|
295
|
+
source: r.source,
|
|
296
|
+
table: r.table,
|
|
297
|
+
collection: r.collection,
|
|
298
|
+
inserted: r.inserted,
|
|
299
|
+
updated: r.updated,
|
|
300
|
+
errors: r.errors.length,
|
|
301
|
+
})),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: error instanceof Error ? error.message : String(error),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
// ========================================================================
|
|
313
|
+
// Tool 7: sql_query
|
|
314
|
+
// ========================================================================
|
|
315
|
+
api.mcp.registerTool('sql_query', {
|
|
316
|
+
description: 'Execute a SQL query on primary or specified source',
|
|
317
|
+
inputSchema: z.object({
|
|
318
|
+
sql: z.string().describe('SQL query to execute'),
|
|
319
|
+
source: z.string().optional().describe('Source name (omit for primary)'),
|
|
320
|
+
params: z.array(z.unknown()).optional().describe('Query parameters'),
|
|
321
|
+
}),
|
|
322
|
+
handler: async (input) => {
|
|
323
|
+
if (!sourceManager) {
|
|
324
|
+
return { error: 'SQL plugin not initialized' };
|
|
325
|
+
}
|
|
326
|
+
const provider = input.source
|
|
327
|
+
? sourceManager.getProvider(input.source)
|
|
328
|
+
: sourceManager.getPrimary();
|
|
329
|
+
if (!provider) {
|
|
330
|
+
return { error: input.source ? `Source '${input.source}' not found` : 'No primary configured' };
|
|
331
|
+
}
|
|
332
|
+
if (!provider.isConnected()) {
|
|
333
|
+
return { error: 'Provider not connected' };
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const result = await provider.query(input.sql, input.params);
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
rowCount: result.rowCount,
|
|
340
|
+
rows: result.rows.slice(0, 100), // Limit response size
|
|
341
|
+
truncated: result.rowCount > 100,
|
|
342
|
+
fields: result.fields,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: error instanceof Error ? error.message : String(error),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
// ========================================================================
|
|
354
|
+
// Tool 8: sql_search
|
|
355
|
+
// ========================================================================
|
|
356
|
+
api.mcp.registerTool('sql_search', {
|
|
357
|
+
description: 'Perform full-text search on a table',
|
|
358
|
+
inputSchema: z.object({
|
|
359
|
+
table: z.string().describe('Table to search'),
|
|
360
|
+
query: z.string().describe('Search query text'),
|
|
361
|
+
columns: z.array(z.string()).optional().describe('Columns to search'),
|
|
362
|
+
limit: z.number().optional().describe('Maximum results'),
|
|
363
|
+
source: z.string().optional().describe('Source name (omit for primary)'),
|
|
364
|
+
}),
|
|
365
|
+
handler: async (input) => {
|
|
366
|
+
if (!sourceManager) {
|
|
367
|
+
return { error: 'SQL plugin not initialized' };
|
|
368
|
+
}
|
|
369
|
+
const provider = input.source
|
|
370
|
+
? sourceManager.getProvider(input.source)
|
|
371
|
+
: sourceManager.getPrimary();
|
|
372
|
+
if (!provider) {
|
|
373
|
+
return { error: 'Provider not found' };
|
|
374
|
+
}
|
|
375
|
+
const caps = await provider.getCapabilities();
|
|
376
|
+
if (!caps.fullText) {
|
|
377
|
+
return { error: 'Full-text search not supported by this provider' };
|
|
378
|
+
}
|
|
379
|
+
const limit = input.limit ?? 20;
|
|
380
|
+
const columns = input.columns ?? ['*'];
|
|
381
|
+
const escapedTable = provider.escapeIdentifier(input.table);
|
|
382
|
+
let sql;
|
|
383
|
+
const params = [];
|
|
384
|
+
// Build provider-specific full-text query
|
|
385
|
+
if (provider.type === 'postgresql') {
|
|
386
|
+
// PostgreSQL tsvector search
|
|
387
|
+
const searchColumns = columns.join(' || \' \' || ');
|
|
388
|
+
sql = `SELECT * FROM ${escapedTable}
|
|
389
|
+
WHERE to_tsvector('english', ${searchColumns}) @@ plainto_tsquery('english', $1)
|
|
390
|
+
LIMIT $2`;
|
|
391
|
+
params.push(input.query, limit);
|
|
392
|
+
}
|
|
393
|
+
else if (provider.type === 'mysql' || provider.type === 'mariadb') {
|
|
394
|
+
// MySQL/MariaDB MATCH AGAINST
|
|
395
|
+
const searchColumns = columns.join(', ');
|
|
396
|
+
sql = `SELECT * FROM ${escapedTable}
|
|
397
|
+
WHERE MATCH(${searchColumns}) AGAINST(? IN NATURAL LANGUAGE MODE)
|
|
398
|
+
LIMIT ?`;
|
|
399
|
+
params.push(input.query, limit);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// SQLite LIKE fallback
|
|
403
|
+
const likeConditions = columns.map((col) => `${provider.escapeIdentifier(col)} LIKE ?`);
|
|
404
|
+
sql = `SELECT * FROM ${escapedTable}
|
|
405
|
+
WHERE ${likeConditions.join(' OR ')}
|
|
406
|
+
LIMIT ?`;
|
|
407
|
+
for (const _ of columns) {
|
|
408
|
+
params.push(`%${input.query}%`);
|
|
409
|
+
}
|
|
410
|
+
params.push(limit);
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const result = await provider.query(sql, params);
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
query: input.query,
|
|
417
|
+
results: result.rows,
|
|
418
|
+
count: result.rowCount,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return {
|
|
423
|
+
success: false,
|
|
424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
// ========================================================================
|
|
430
|
+
// Tool 9: sql_vector_search
|
|
431
|
+
// ========================================================================
|
|
432
|
+
api.mcp.registerTool('sql_vector_search', {
|
|
433
|
+
description: 'Perform semantic vector search',
|
|
434
|
+
inputSchema: z.object({
|
|
435
|
+
vector: z.array(z.number()).describe('Query vector'),
|
|
436
|
+
collection: z.string().optional().describe('Collection to search'),
|
|
437
|
+
limit: z.number().optional().describe('Maximum results'),
|
|
438
|
+
minSimilarity: z.number().optional().describe('Minimum similarity threshold (0-1)'),
|
|
439
|
+
}),
|
|
440
|
+
handler: async (input) => {
|
|
441
|
+
if (!vectorAdapter) {
|
|
442
|
+
return { error: 'Vector adapter not initialized' };
|
|
443
|
+
}
|
|
444
|
+
if (!vectorAdapter.isSupported()) {
|
|
445
|
+
return { error: 'Vector search not supported by current provider' };
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const results = await vectorAdapter.search(input.vector, {
|
|
449
|
+
collection: input.collection,
|
|
450
|
+
limit: input.limit ?? 10,
|
|
451
|
+
minSimilarity: input.minSimilarity ?? 0,
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
success: true,
|
|
455
|
+
count: results.length,
|
|
456
|
+
results: results.map((r) => ({
|
|
457
|
+
id: r.id,
|
|
458
|
+
entryId: r.entryId,
|
|
459
|
+
collection: r.collection,
|
|
460
|
+
similarity: r.similarity,
|
|
461
|
+
})),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
return {
|
|
466
|
+
success: false,
|
|
467
|
+
error: error instanceof Error ? error.message : String(error),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
// ========================================================================
|
|
473
|
+
// Tool 10: sql_vector_index
|
|
474
|
+
// ========================================================================
|
|
475
|
+
api.mcp.registerTool('sql_vector_index', {
|
|
476
|
+
description: 'Store or manage vector indexes',
|
|
477
|
+
inputSchema: z.object({
|
|
478
|
+
action: z.enum(['store', 'delete', 'create_index', 'status']).describe('Action to perform'),
|
|
479
|
+
id: z.string().optional().describe('Vector ID (for store/delete)'),
|
|
480
|
+
entryId: z.string().optional().describe('Entry ID (for store)'),
|
|
481
|
+
collection: z.string().optional().describe('Collection name'),
|
|
482
|
+
vector: z.array(z.number()).optional().describe('Vector data (for store)'),
|
|
483
|
+
indexType: z.string().optional().describe('Index type (hnsw, ivfflat)'),
|
|
484
|
+
}),
|
|
485
|
+
handler: async (input) => {
|
|
486
|
+
if (!vectorAdapter) {
|
|
487
|
+
return { error: 'Vector adapter not initialized' };
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
switch (input.action) {
|
|
491
|
+
case 'store':
|
|
492
|
+
if (!input.id || !input.entryId || !input.collection || !input.vector) {
|
|
493
|
+
return { error: 'Missing required fields for store: id, entryId, collection, vector' };
|
|
494
|
+
}
|
|
495
|
+
await vectorAdapter.store({
|
|
496
|
+
id: input.id,
|
|
497
|
+
entryId: input.entryId,
|
|
498
|
+
collection: input.collection,
|
|
499
|
+
vector: input.vector,
|
|
500
|
+
textHash: '',
|
|
501
|
+
});
|
|
502
|
+
return { success: true, action: 'stored', id: input.id };
|
|
503
|
+
case 'delete':
|
|
504
|
+
if (!input.id) {
|
|
505
|
+
return { error: 'Missing id for delete' };
|
|
506
|
+
}
|
|
507
|
+
const deleted = await vectorAdapter.delete(input.id);
|
|
508
|
+
return { success: deleted, action: 'deleted', id: input.id };
|
|
509
|
+
case 'create_index':
|
|
510
|
+
const indexed = await vectorAdapter.createIndex(input.indexType);
|
|
511
|
+
return { success: indexed, action: 'create_index', indexType: input.indexType ?? 'default' };
|
|
512
|
+
case 'status':
|
|
513
|
+
const hasIndex = await vectorAdapter.hasIndex();
|
|
514
|
+
const count = await vectorAdapter.count(input.collection);
|
|
515
|
+
return {
|
|
516
|
+
success: true,
|
|
517
|
+
supported: vectorAdapter.isSupported(),
|
|
518
|
+
type: vectorAdapter.type,
|
|
519
|
+
hasIndex,
|
|
520
|
+
vectorCount: count,
|
|
521
|
+
collection: input.collection,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
error: error instanceof Error ? error.message : String(error),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
// ========================================================================
|
|
534
|
+
// Tool 11: sql_cache_status
|
|
535
|
+
// ========================================================================
|
|
536
|
+
api.mcp.registerTool('sql_cache_status', {
|
|
537
|
+
description: 'Get cache statistics',
|
|
538
|
+
inputSchema: z.object({}),
|
|
539
|
+
handler: async () => {
|
|
540
|
+
if (!cacheLayer) {
|
|
541
|
+
return { error: 'Cache not initialized' };
|
|
542
|
+
}
|
|
543
|
+
const stats = cacheLayer.getStats();
|
|
544
|
+
return {
|
|
545
|
+
enabled: stats.enabled,
|
|
546
|
+
entries: stats.entries,
|
|
547
|
+
size: `${Math.round(stats.size / 1024)}KB`,
|
|
548
|
+
hits: stats.hits,
|
|
549
|
+
misses: stats.misses,
|
|
550
|
+
hitRate: stats.hits + stats.misses > 0
|
|
551
|
+
? `${Math.round((stats.hits / (stats.hits + stats.misses)) * 100)}%`
|
|
552
|
+
: 'N/A',
|
|
553
|
+
ttlMinutes: stats.ttlMinutes,
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
// ========================================================================
|
|
558
|
+
// Tool 12: sql_cache_clear
|
|
559
|
+
// ========================================================================
|
|
560
|
+
api.mcp.registerTool('sql_cache_clear', {
|
|
561
|
+
description: 'Clear cache entries',
|
|
562
|
+
inputSchema: z.object({
|
|
563
|
+
collection: z.string().optional().describe('Clear only specific collection'),
|
|
564
|
+
}),
|
|
565
|
+
handler: async (input) => {
|
|
566
|
+
if (!cacheLayer) {
|
|
567
|
+
return { error: 'Cache not initialized' };
|
|
568
|
+
}
|
|
569
|
+
if (input.collection) {
|
|
570
|
+
const cleared = cacheLayer.clearCollection(input.collection);
|
|
571
|
+
return { success: true, cleared, collection: input.collection };
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
cacheLayer.clear();
|
|
575
|
+
return { success: true, message: 'All cache cleared' };
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
// ========================================================================
|
|
580
|
+
// Tool 13: sql_schema
|
|
581
|
+
// ========================================================================
|
|
582
|
+
api.mcp.registerTool('sql_schema', {
|
|
583
|
+
description: 'View database schema',
|
|
584
|
+
inputSchema: z.object({
|
|
585
|
+
source: z.string().optional().describe('Source name (omit for primary)'),
|
|
586
|
+
table: z.string().optional().describe('Specific table for details'),
|
|
587
|
+
}),
|
|
588
|
+
handler: async (input) => {
|
|
589
|
+
if (!sourceManager) {
|
|
590
|
+
return { error: 'SQL plugin not initialized' };
|
|
591
|
+
}
|
|
592
|
+
const provider = input.source
|
|
593
|
+
? sourceManager.getProvider(input.source)
|
|
594
|
+
: sourceManager.getPrimary();
|
|
595
|
+
if (!provider || !provider.isConnected()) {
|
|
596
|
+
return { error: 'Provider not connected' };
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
if (input.table) {
|
|
600
|
+
const tableInfo = await provider.getTableInfo(input.table);
|
|
601
|
+
if (!tableInfo) {
|
|
602
|
+
return { error: `Table '${input.table}' not found` };
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
success: true,
|
|
606
|
+
table: tableInfo,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
const schema = await provider.getSchema();
|
|
611
|
+
return {
|
|
612
|
+
success: true,
|
|
613
|
+
tables: schema.tables.map((t) => ({
|
|
614
|
+
name: t.name,
|
|
615
|
+
schema: t.schema,
|
|
616
|
+
columns: t.columns.length,
|
|
617
|
+
rowCount: t.rowCount,
|
|
618
|
+
})),
|
|
619
|
+
views: schema.views,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
error: error instanceof Error ? error.message : String(error),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
// ========================================================================
|
|
632
|
+
// Tool 14: sql_map_table
|
|
633
|
+
// ========================================================================
|
|
634
|
+
api.mcp.registerTool('sql_map_table', {
|
|
635
|
+
description: 'Map a database table to a LokiJS collection',
|
|
636
|
+
inputSchema: z.object({
|
|
637
|
+
source: z.string().describe('Source name'),
|
|
638
|
+
table: z.string().describe('Table name'),
|
|
639
|
+
collection: z.string().describe('Target LokiJS collection name'),
|
|
640
|
+
primaryKey: z.string().optional().describe('Primary key column'),
|
|
641
|
+
columns: z.array(z.string()).optional().describe('Columns to include'),
|
|
642
|
+
where: z.string().optional().describe('WHERE clause for filtering'),
|
|
643
|
+
syncInterval: z.number().optional().describe('Auto-sync interval in minutes'),
|
|
644
|
+
}),
|
|
645
|
+
handler: async (input) => {
|
|
646
|
+
if (!sourceManager) {
|
|
647
|
+
return { error: 'SQL plugin not initialized' };
|
|
648
|
+
}
|
|
649
|
+
const source = sourceManager.getSource(input.source);
|
|
650
|
+
if (!source) {
|
|
651
|
+
return { error: `Source '${input.source}' not found` };
|
|
652
|
+
}
|
|
653
|
+
// Add table mapping to source config
|
|
654
|
+
source.config.tables[input.table] = {
|
|
655
|
+
collection: input.collection,
|
|
656
|
+
primaryKey: input.primaryKey,
|
|
657
|
+
columns: input.columns,
|
|
658
|
+
where: input.where,
|
|
659
|
+
syncInterval: input.syncInterval,
|
|
660
|
+
};
|
|
661
|
+
return {
|
|
662
|
+
success: true,
|
|
663
|
+
source: input.source,
|
|
664
|
+
table: input.table,
|
|
665
|
+
collection: input.collection,
|
|
666
|
+
mapping: source.config.tables[input.table],
|
|
667
|
+
message: `Mapped ${input.source}.${input.table} → ${input.collection}`,
|
|
668
|
+
};
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
// ========================================================================
|
|
672
|
+
// Tool 15: sql_configure
|
|
673
|
+
// ========================================================================
|
|
674
|
+
api.mcp.registerTool('sql_configure', {
|
|
675
|
+
description: 'Configure SQL plugin sources and settings',
|
|
676
|
+
inputSchema: z.object({
|
|
677
|
+
action: z.enum(['add_source', 'remove_source', 'set_primary', 'configure_cache', 'configure_vectors'])
|
|
678
|
+
.describe('Configuration action'),
|
|
679
|
+
// Source options
|
|
680
|
+
name: z.string().optional().describe('Source name'),
|
|
681
|
+
provider: z.enum(['postgresql', 'mysql', 'mariadb', 'sqlite']).optional().describe('Provider type'),
|
|
682
|
+
mode: z.enum(['readwrite', 'readonly']).optional().describe('Connection mode'),
|
|
683
|
+
connectionString: z.string().optional().describe('Connection string'),
|
|
684
|
+
host: z.string().optional().describe('Database host'),
|
|
685
|
+
port: z.number().optional().describe('Database port'),
|
|
686
|
+
database: z.string().optional().describe('Database name'),
|
|
687
|
+
user: z.string().optional().describe('Database user'),
|
|
688
|
+
password: z.string().optional().describe('Database password'),
|
|
689
|
+
// Cache options
|
|
690
|
+
cacheEnabled: z.boolean().optional().describe('Enable cache'),
|
|
691
|
+
cacheTtl: z.number().optional().describe('Cache TTL in minutes'),
|
|
692
|
+
// Vector options
|
|
693
|
+
vectorDimensions: z.number().optional().describe('Vector dimensions'),
|
|
694
|
+
vectorIndex: z.boolean().optional().describe('Enable vector indexing'),
|
|
695
|
+
}),
|
|
696
|
+
handler: async (input) => {
|
|
697
|
+
if (!sourceManager) {
|
|
698
|
+
return { error: 'SQL plugin not initialized' };
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
switch (input.action) {
|
|
702
|
+
case 'add_source':
|
|
703
|
+
if (!input.name || !input.provider) {
|
|
704
|
+
return { error: 'Missing required fields: name, provider' };
|
|
705
|
+
}
|
|
706
|
+
const sourceConfig = {
|
|
707
|
+
name: input.name,
|
|
708
|
+
provider: input.provider,
|
|
709
|
+
mode: input.mode ?? 'readonly',
|
|
710
|
+
connection: input.connectionString
|
|
711
|
+
? { connectionString: input.connectionString }
|
|
712
|
+
: {
|
|
713
|
+
host: input.host ?? 'localhost',
|
|
714
|
+
port: input.port,
|
|
715
|
+
database: input.database ?? '',
|
|
716
|
+
user: input.user ?? '',
|
|
717
|
+
password: input.password ?? '',
|
|
718
|
+
},
|
|
719
|
+
tables: {},
|
|
720
|
+
autoSync: false,
|
|
721
|
+
};
|
|
722
|
+
await sourceManager.addSource(sourceConfig);
|
|
723
|
+
await sourceManager.connect(input.name);
|
|
724
|
+
return {
|
|
725
|
+
success: true,
|
|
726
|
+
action: 'add_source',
|
|
727
|
+
name: input.name,
|
|
728
|
+
status: await sourceManager.getStatus(input.name),
|
|
729
|
+
};
|
|
730
|
+
case 'remove_source':
|
|
731
|
+
if (!input.name) {
|
|
732
|
+
return { error: 'Missing source name' };
|
|
733
|
+
}
|
|
734
|
+
await sourceManager.removeSource(input.name);
|
|
735
|
+
return { success: true, action: 'remove_source', name: input.name };
|
|
736
|
+
case 'set_primary':
|
|
737
|
+
if (!input.provider) {
|
|
738
|
+
return { error: 'Missing provider' };
|
|
739
|
+
}
|
|
740
|
+
await sourceManager.setPrimary(input.provider, input.mode ?? 'readwrite', input.connectionString
|
|
741
|
+
? { connectionString: input.connectionString }
|
|
742
|
+
: {
|
|
743
|
+
host: input.host ?? 'localhost',
|
|
744
|
+
port: input.port,
|
|
745
|
+
database: input.database ?? '',
|
|
746
|
+
user: input.user ?? '',
|
|
747
|
+
password: input.password ?? '',
|
|
748
|
+
});
|
|
749
|
+
const primary = sourceManager.getPrimary();
|
|
750
|
+
return {
|
|
751
|
+
success: true,
|
|
752
|
+
action: 'set_primary',
|
|
753
|
+
provider: input.provider,
|
|
754
|
+
status: primary ? await primary.getStatus() : null,
|
|
755
|
+
};
|
|
756
|
+
case 'configure_cache':
|
|
757
|
+
if (!cacheLayer) {
|
|
758
|
+
return { error: 'Cache not initialized' };
|
|
759
|
+
}
|
|
760
|
+
if (input.cacheEnabled !== undefined) {
|
|
761
|
+
cacheLayer.setEnabled(input.cacheEnabled);
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
success: true,
|
|
765
|
+
action: 'configure_cache',
|
|
766
|
+
stats: cacheLayer.getStats(),
|
|
767
|
+
};
|
|
768
|
+
case 'configure_vectors':
|
|
769
|
+
if (!vectorAdapter) {
|
|
770
|
+
return { error: 'Vector adapter not initialized' };
|
|
771
|
+
}
|
|
772
|
+
if (input.vectorIndex) {
|
|
773
|
+
await vectorAdapter.createIndex();
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
success: true,
|
|
777
|
+
action: 'configure_vectors',
|
|
778
|
+
supported: vectorAdapter.isSupported(),
|
|
779
|
+
hasIndex: await vectorAdapter.hasIndex(),
|
|
780
|
+
};
|
|
781
|
+
default:
|
|
782
|
+
return { error: `Unknown action: ${input.action}` };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
return {
|
|
787
|
+
success: false,
|
|
788
|
+
error: error instanceof Error ? error.message : String(error),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
api.logger.info('SQL plugin registered 15 MCP tools: sql_status, sql_connect, sql_disconnect, sql_sources, ' +
|
|
794
|
+
'sql_import, sql_sync, sql_query, sql_search, sql_vector_search, sql_vector_index, ' +
|
|
795
|
+
'sql_cache_status, sql_cache_clear, sql_schema, sql_map_table, sql_configure');
|
|
796
|
+
}
|
|
797
|
+
export default plugin;
|
|
798
|
+
//# sourceMappingURL=plugin.js.map
|