s3db.js 7.3.5 → 7.3.6
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/PLUGINS.md +1285 -157
- package/dist/s3db.cjs.js +296 -83
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.es.js +296 -83
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +296 -83
- package/dist/s3db.iife.min.js +1 -1
- package/mcp/server.js +1410 -0
- package/package.json +7 -1
- package/src/plugins/cache/filesystem-cache.class.js +9 -0
- package/src/plugins/replicator.plugin.js +130 -46
- package/src/plugins/replicators/bigquery-replicator.class.js +31 -5
- package/src/plugins/replicators/postgres-replicator.class.js +17 -2
- package/src/plugins/replicators/s3db-replicator.class.js +172 -68
- package/src/plugins/replicators/sqs-replicator.class.js +13 -1
package/mcp/server.js
ADDED
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { S3db, CachePlugin, CostsPlugin, FilesystemCache } from 's3db.js';
|
|
8
|
+
import { config } from 'dotenv';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
// Load environment variables
|
|
14
|
+
config();
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
// Global database instance
|
|
20
|
+
let database = null;
|
|
21
|
+
|
|
22
|
+
// Server configuration
|
|
23
|
+
const SERVER_NAME = 's3db-mcp';
|
|
24
|
+
const SERVER_VERSION = '1.0.0';
|
|
25
|
+
|
|
26
|
+
class S3dbMCPServer {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.server = new Server(
|
|
29
|
+
{
|
|
30
|
+
name: SERVER_NAME,
|
|
31
|
+
version: SERVER_VERSION,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
capabilities: {
|
|
35
|
+
tools: {},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this.setupToolHandlers();
|
|
41
|
+
this.setupTransport();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setupToolHandlers() {
|
|
45
|
+
// List available tools
|
|
46
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
47
|
+
return {
|
|
48
|
+
tools: [
|
|
49
|
+
{
|
|
50
|
+
name: 'dbConnect',
|
|
51
|
+
description: 'Connect to an S3DB database with automatic costs tracking and configurable cache (memory or filesystem)',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
connectionString: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'S3DB connection string (e.g., s3://key:secret@bucket/path)'
|
|
58
|
+
},
|
|
59
|
+
verbose: {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
description: 'Enable verbose logging',
|
|
62
|
+
default: false
|
|
63
|
+
},
|
|
64
|
+
parallelism: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
description: 'Number of parallel operations',
|
|
67
|
+
default: 10
|
|
68
|
+
},
|
|
69
|
+
passphrase: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Passphrase for encryption',
|
|
72
|
+
default: 'secret'
|
|
73
|
+
},
|
|
74
|
+
versioningEnabled: {
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
description: 'Enable resource versioning',
|
|
77
|
+
default: false
|
|
78
|
+
},
|
|
79
|
+
enableCache: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description: 'Enable cache for improved performance',
|
|
82
|
+
default: true
|
|
83
|
+
},
|
|
84
|
+
enableCosts: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
description: 'Enable costs tracking for S3 operations',
|
|
87
|
+
default: true
|
|
88
|
+
},
|
|
89
|
+
cacheDriver: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Cache driver type: "memory" or "filesystem"',
|
|
92
|
+
enum: ['memory', 'filesystem'],
|
|
93
|
+
default: 'memory'
|
|
94
|
+
},
|
|
95
|
+
cacheMaxSize: {
|
|
96
|
+
type: 'number',
|
|
97
|
+
description: 'Maximum number of items in memory cache (memory driver only)',
|
|
98
|
+
default: 1000
|
|
99
|
+
},
|
|
100
|
+
cacheTtl: {
|
|
101
|
+
type: 'number',
|
|
102
|
+
description: 'Cache time-to-live in milliseconds',
|
|
103
|
+
default: 300000
|
|
104
|
+
},
|
|
105
|
+
cacheDirectory: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Directory path for filesystem cache (filesystem driver only)',
|
|
108
|
+
default: './cache'
|
|
109
|
+
},
|
|
110
|
+
cachePrefix: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'Prefix for cache files (filesystem driver only)',
|
|
113
|
+
default: 'cache'
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
required: ['connectionString']
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'dbDisconnect',
|
|
121
|
+
description: 'Disconnect from the S3DB database',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {},
|
|
125
|
+
required: []
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'dbStatus',
|
|
130
|
+
description: 'Get the current database connection status',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {},
|
|
134
|
+
required: []
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'dbCreateResource',
|
|
139
|
+
description: 'Create a new resource (collection/table) in the database',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
name: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'Resource name'
|
|
146
|
+
},
|
|
147
|
+
attributes: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
description: 'Schema attributes definition (e.g., {"name": "string|required", "age": "number"})'
|
|
150
|
+
},
|
|
151
|
+
behavior: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'Resource behavior',
|
|
154
|
+
enum: ['user-managed', 'body-only', 'body-overflow', 'enforce-limits', 'truncate-data'],
|
|
155
|
+
default: 'user-managed'
|
|
156
|
+
},
|
|
157
|
+
timestamps: {
|
|
158
|
+
type: 'boolean',
|
|
159
|
+
description: 'Enable automatic timestamps',
|
|
160
|
+
default: false
|
|
161
|
+
},
|
|
162
|
+
partitions: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
description: 'Partition configuration'
|
|
165
|
+
},
|
|
166
|
+
paranoid: {
|
|
167
|
+
type: 'boolean',
|
|
168
|
+
description: 'Enable paranoid mode (soft deletes)',
|
|
169
|
+
default: true
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
required: ['name', 'attributes']
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'dbListResources',
|
|
177
|
+
description: 'List all resources in the database',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {},
|
|
181
|
+
required: []
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'resourceInsert',
|
|
186
|
+
description: 'Insert a new document into a resource',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
resourceName: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Name of the resource'
|
|
193
|
+
},
|
|
194
|
+
data: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
description: 'Data to insert'
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
required: ['resourceName', 'data']
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'resourceInsertMany',
|
|
204
|
+
description: 'Insert multiple documents into a resource',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
resourceName: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: 'Name of the resource'
|
|
211
|
+
},
|
|
212
|
+
data: {
|
|
213
|
+
type: 'array',
|
|
214
|
+
description: 'Array of documents to insert'
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
required: ['resourceName', 'data']
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'resourceGet',
|
|
222
|
+
description: 'Get a document by ID from a resource',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
resourceName: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
description: 'Name of the resource'
|
|
229
|
+
},
|
|
230
|
+
id: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
description: 'Document ID'
|
|
233
|
+
},
|
|
234
|
+
partition: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Partition name for optimized retrieval'
|
|
237
|
+
},
|
|
238
|
+
partitionValues: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
description: 'Partition values for targeted access'
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
required: ['resourceName', 'id']
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'resourceGetMany',
|
|
248
|
+
description: 'Get multiple documents by IDs from a resource',
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
resourceName: {
|
|
253
|
+
type: 'string',
|
|
254
|
+
description: 'Name of the resource'
|
|
255
|
+
},
|
|
256
|
+
ids: {
|
|
257
|
+
type: 'array',
|
|
258
|
+
items: { type: 'string' },
|
|
259
|
+
description: 'Array of document IDs'
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
required: ['resourceName', 'ids']
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'resourceUpdate',
|
|
267
|
+
description: 'Update a document in a resource',
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
resourceName: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: 'Name of the resource'
|
|
274
|
+
},
|
|
275
|
+
id: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
description: 'Document ID'
|
|
278
|
+
},
|
|
279
|
+
data: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
description: 'Data to update'
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
required: ['resourceName', 'id', 'data']
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: 'resourceUpsert',
|
|
289
|
+
description: 'Insert or update a document in a resource',
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: {
|
|
293
|
+
resourceName: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
description: 'Name of the resource'
|
|
296
|
+
},
|
|
297
|
+
data: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
description: 'Data to upsert (must include id if updating)'
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
required: ['resourceName', 'data']
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'resourceDelete',
|
|
307
|
+
description: 'Delete a document from a resource',
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
resourceName: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'Name of the resource'
|
|
314
|
+
},
|
|
315
|
+
id: {
|
|
316
|
+
type: 'string',
|
|
317
|
+
description: 'Document ID'
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
required: ['resourceName', 'id']
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'resourceDeleteMany',
|
|
325
|
+
description: 'Delete multiple documents from a resource',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {
|
|
329
|
+
resourceName: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'Name of the resource'
|
|
332
|
+
},
|
|
333
|
+
ids: {
|
|
334
|
+
type: 'array',
|
|
335
|
+
items: { type: 'string' },
|
|
336
|
+
description: 'Array of document IDs to delete'
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
required: ['resourceName', 'ids']
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'resourceExists',
|
|
344
|
+
description: 'Check if a document exists in a resource',
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: 'object',
|
|
347
|
+
properties: {
|
|
348
|
+
resourceName: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'Name of the resource'
|
|
351
|
+
},
|
|
352
|
+
id: {
|
|
353
|
+
type: 'string',
|
|
354
|
+
description: 'Document ID'
|
|
355
|
+
},
|
|
356
|
+
partition: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
description: 'Partition name for optimized check'
|
|
359
|
+
},
|
|
360
|
+
partitionValues: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
description: 'Partition values for targeted check'
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
required: ['resourceName', 'id']
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: 'resourceList',
|
|
370
|
+
description: 'List documents in a resource with pagination and filtering',
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
properties: {
|
|
374
|
+
resourceName: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
description: 'Name of the resource'
|
|
377
|
+
},
|
|
378
|
+
limit: {
|
|
379
|
+
type: 'number',
|
|
380
|
+
description: 'Maximum number of documents to return',
|
|
381
|
+
default: 100
|
|
382
|
+
},
|
|
383
|
+
offset: {
|
|
384
|
+
type: 'number',
|
|
385
|
+
description: 'Number of documents to skip',
|
|
386
|
+
default: 0
|
|
387
|
+
},
|
|
388
|
+
partition: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'Partition name to filter by'
|
|
391
|
+
},
|
|
392
|
+
partitionValues: {
|
|
393
|
+
type: 'object',
|
|
394
|
+
description: 'Partition values for filtering'
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
required: ['resourceName']
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: 'resourceListIds',
|
|
402
|
+
description: 'List document IDs in a resource',
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
resourceName: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'Name of the resource'
|
|
409
|
+
},
|
|
410
|
+
limit: {
|
|
411
|
+
type: 'number',
|
|
412
|
+
description: 'Maximum number of IDs to return',
|
|
413
|
+
default: 1000
|
|
414
|
+
},
|
|
415
|
+
offset: {
|
|
416
|
+
type: 'number',
|
|
417
|
+
description: 'Number of IDs to skip',
|
|
418
|
+
default: 0
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
required: ['resourceName']
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: 'resourceCount',
|
|
426
|
+
description: 'Count documents in a resource',
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: 'object',
|
|
429
|
+
properties: {
|
|
430
|
+
resourceName: {
|
|
431
|
+
type: 'string',
|
|
432
|
+
description: 'Name of the resource'
|
|
433
|
+
},
|
|
434
|
+
partition: {
|
|
435
|
+
type: 'string',
|
|
436
|
+
description: 'Partition name to filter by'
|
|
437
|
+
},
|
|
438
|
+
partitionValues: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
description: 'Partition values for filtering'
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
required: ['resourceName']
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'resourceGetAll',
|
|
448
|
+
description: 'Get all documents from a resource (use with caution on large datasets)',
|
|
449
|
+
inputSchema: {
|
|
450
|
+
type: 'object',
|
|
451
|
+
properties: {
|
|
452
|
+
resourceName: {
|
|
453
|
+
type: 'string',
|
|
454
|
+
description: 'Name of the resource'
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
required: ['resourceName']
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: 'resourceDeleteAll',
|
|
462
|
+
description: 'Delete all documents from a resource',
|
|
463
|
+
inputSchema: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
resourceName: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: 'Name of the resource'
|
|
469
|
+
},
|
|
470
|
+
confirm: {
|
|
471
|
+
type: 'boolean',
|
|
472
|
+
description: 'Confirmation flag - must be true to proceed'
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
required: ['resourceName', 'confirm']
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: 'dbGetStats',
|
|
480
|
+
description: 'Get database statistics including costs and cache performance',
|
|
481
|
+
inputSchema: {
|
|
482
|
+
type: 'object',
|
|
483
|
+
properties: {},
|
|
484
|
+
required: []
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'dbClearCache',
|
|
489
|
+
description: 'Clear all cached data or cache for specific resource',
|
|
490
|
+
inputSchema: {
|
|
491
|
+
type: 'object',
|
|
492
|
+
properties: {
|
|
493
|
+
resourceName: {
|
|
494
|
+
type: 'string',
|
|
495
|
+
description: 'Name of specific resource to clear cache (optional - if not provided, clears all cache)'
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
required: []
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
]
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Handle tool calls
|
|
506
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
507
|
+
const { name, arguments: args } = request.params;
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
let result;
|
|
511
|
+
|
|
512
|
+
switch (name) {
|
|
513
|
+
case 'dbConnect':
|
|
514
|
+
result = await this.handleDbConnect(args);
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'dbDisconnect':
|
|
518
|
+
result = await this.handleDbDisconnect(args);
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
case 'dbStatus':
|
|
522
|
+
result = await this.handleDbStatus(args);
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'dbCreateResource':
|
|
526
|
+
result = await this.handleDbCreateResource(args);
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'dbListResources':
|
|
530
|
+
result = await this.handleDbListResources(args);
|
|
531
|
+
break;
|
|
532
|
+
|
|
533
|
+
case 'resourceInsert':
|
|
534
|
+
result = await this.handleResourceInsert(args);
|
|
535
|
+
break;
|
|
536
|
+
|
|
537
|
+
case 'resourceInsertMany':
|
|
538
|
+
result = await this.handleResourceInsertMany(args);
|
|
539
|
+
break;
|
|
540
|
+
|
|
541
|
+
case 'resourceGet':
|
|
542
|
+
result = await this.handleResourceGet(args);
|
|
543
|
+
break;
|
|
544
|
+
|
|
545
|
+
case 'resourceGetMany':
|
|
546
|
+
result = await this.handleResourceGetMany(args);
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case 'resourceUpdate':
|
|
550
|
+
result = await this.handleResourceUpdate(args);
|
|
551
|
+
break;
|
|
552
|
+
|
|
553
|
+
case 'resourceUpsert':
|
|
554
|
+
result = await this.handleResourceUpsert(args);
|
|
555
|
+
break;
|
|
556
|
+
|
|
557
|
+
case 'resourceDelete':
|
|
558
|
+
result = await this.handleResourceDelete(args);
|
|
559
|
+
break;
|
|
560
|
+
|
|
561
|
+
case 'resourceDeleteMany':
|
|
562
|
+
result = await this.handleResourceDeleteMany(args);
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
case 'resourceExists':
|
|
566
|
+
result = await this.handleResourceExists(args);
|
|
567
|
+
break;
|
|
568
|
+
|
|
569
|
+
case 'resourceList':
|
|
570
|
+
result = await this.handleResourceList(args);
|
|
571
|
+
break;
|
|
572
|
+
|
|
573
|
+
case 'resourceListIds':
|
|
574
|
+
result = await this.handleResourceListIds(args);
|
|
575
|
+
break;
|
|
576
|
+
|
|
577
|
+
case 'resourceCount':
|
|
578
|
+
result = await this.handleResourceCount(args);
|
|
579
|
+
break;
|
|
580
|
+
|
|
581
|
+
case 'resourceGetAll':
|
|
582
|
+
result = await this.handleResourceGetAll(args);
|
|
583
|
+
break;
|
|
584
|
+
|
|
585
|
+
case 'resourceDeleteAll':
|
|
586
|
+
result = await this.handleResourceDeleteAll(args);
|
|
587
|
+
break;
|
|
588
|
+
|
|
589
|
+
case 'dbGetStats':
|
|
590
|
+
result = await this.handleDbGetStats(args);
|
|
591
|
+
break;
|
|
592
|
+
|
|
593
|
+
case 'dbClearCache':
|
|
594
|
+
result = await this.handleDbClearCache(args);
|
|
595
|
+
break;
|
|
596
|
+
|
|
597
|
+
default:
|
|
598
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
content: [
|
|
603
|
+
{
|
|
604
|
+
type: 'text',
|
|
605
|
+
text: JSON.stringify(result, null, 2)
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: 'text',
|
|
615
|
+
text: JSON.stringify({
|
|
616
|
+
error: error.message,
|
|
617
|
+
type: error.constructor.name,
|
|
618
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
619
|
+
}, null, 2)
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
isError: true
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
setupTransport() {
|
|
629
|
+
const transport = process.argv.includes('--transport=sse') || process.env.MCP_TRANSPORT === 'sse'
|
|
630
|
+
? new SSEServerTransport('/sse', process.env.MCP_SERVER_HOST || '0.0.0.0', parseInt(process.env.MCP_SERVER_PORT || '8000'))
|
|
631
|
+
: new StdioServerTransport();
|
|
632
|
+
|
|
633
|
+
this.server.connect(transport);
|
|
634
|
+
|
|
635
|
+
// SSE specific setup
|
|
636
|
+
if (transport instanceof SSEServerTransport) {
|
|
637
|
+
const host = process.env.MCP_SERVER_HOST || '0.0.0.0';
|
|
638
|
+
const port = process.env.MCP_SERVER_PORT || '8000';
|
|
639
|
+
|
|
640
|
+
console.log(`S3DB MCP Server running on http://${host}:${port}/sse`);
|
|
641
|
+
|
|
642
|
+
// Add health check endpoint for SSE transport
|
|
643
|
+
this.setupHealthCheck(host, port);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
setupHealthCheck(host, port) {
|
|
648
|
+
import('http').then(({ createServer }) => {
|
|
649
|
+
const healthServer = createServer((req, res) => {
|
|
650
|
+
if (req.url === '/health') {
|
|
651
|
+
const healthStatus = {
|
|
652
|
+
status: 'healthy',
|
|
653
|
+
timestamp: new Date().toISOString(),
|
|
654
|
+
uptime: process.uptime(),
|
|
655
|
+
version: SERVER_VERSION,
|
|
656
|
+
database: {
|
|
657
|
+
connected: database ? database.isConnected() : false,
|
|
658
|
+
bucket: database?.bucket || null,
|
|
659
|
+
keyPrefix: database?.keyPrefix || null,
|
|
660
|
+
resourceCount: database ? Object.keys(database.resources || {}).length : 0
|
|
661
|
+
},
|
|
662
|
+
memory: process.memoryUsage(),
|
|
663
|
+
environment: {
|
|
664
|
+
nodeVersion: process.version,
|
|
665
|
+
platform: process.platform,
|
|
666
|
+
transport: 'sse'
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
res.writeHead(200, {
|
|
671
|
+
'Content-Type': 'application/json',
|
|
672
|
+
'Access-Control-Allow-Origin': '*',
|
|
673
|
+
'Access-Control-Allow-Methods': 'GET',
|
|
674
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
675
|
+
});
|
|
676
|
+
res.end(JSON.stringify(healthStatus, null, 2));
|
|
677
|
+
} else {
|
|
678
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
679
|
+
res.end('Not Found');
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Listen on a different port for health checks to avoid conflicts
|
|
684
|
+
const healthPort = parseInt(port) + 1;
|
|
685
|
+
healthServer.listen(healthPort, host, () => {
|
|
686
|
+
console.log(`Health check endpoint: http://${host}:${healthPort}/health`);
|
|
687
|
+
});
|
|
688
|
+
}).catch(err => {
|
|
689
|
+
console.warn('Could not setup health check endpoint:', err.message);
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Database connection handlers
|
|
694
|
+
async handleDbConnect(args) {
|
|
695
|
+
const {
|
|
696
|
+
connectionString,
|
|
697
|
+
verbose = false,
|
|
698
|
+
parallelism = 10,
|
|
699
|
+
passphrase = 'secret',
|
|
700
|
+
versioningEnabled = false,
|
|
701
|
+
enableCache = true,
|
|
702
|
+
enableCosts = true,
|
|
703
|
+
cacheDriver = 'memory', // 'memory', 'filesystem', or 'custom'
|
|
704
|
+
cacheMaxSize = 1000,
|
|
705
|
+
cacheTtl = 300000, // 5 minutes
|
|
706
|
+
cacheDirectory = './cache', // For filesystem cache
|
|
707
|
+
cachePrefix = 'cache'
|
|
708
|
+
} = args;
|
|
709
|
+
|
|
710
|
+
if (database && database.isConnected()) {
|
|
711
|
+
return { success: false, message: 'Database is already connected' };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Setup plugins array
|
|
715
|
+
const plugins = [];
|
|
716
|
+
|
|
717
|
+
// Always add CostsPlugin (unless explicitly disabled)
|
|
718
|
+
const costsEnabled = enableCosts !== false && process.env.S3DB_COSTS_ENABLED !== 'false';
|
|
719
|
+
if (costsEnabled) {
|
|
720
|
+
plugins.push(CostsPlugin);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Add CachePlugin (enabled by default, configurable)
|
|
724
|
+
const cacheEnabled = enableCache !== false && process.env.S3DB_CACHE_ENABLED !== 'false';
|
|
725
|
+
if (cacheEnabled) {
|
|
726
|
+
const cacheMaxSizeEnv = process.env.S3DB_CACHE_MAX_SIZE ? parseInt(process.env.S3DB_CACHE_MAX_SIZE) : cacheMaxSize;
|
|
727
|
+
const cacheTtlEnv = process.env.S3DB_CACHE_TTL ? parseInt(process.env.S3DB_CACHE_TTL) : cacheTtl;
|
|
728
|
+
const cacheDriverEnv = process.env.S3DB_CACHE_DRIVER || cacheDriver;
|
|
729
|
+
const cacheDirectoryEnv = process.env.S3DB_CACHE_DIRECTORY || cacheDirectory;
|
|
730
|
+
const cachePrefixEnv = process.env.S3DB_CACHE_PREFIX || cachePrefix;
|
|
731
|
+
|
|
732
|
+
let cacheConfig = {
|
|
733
|
+
includePartitions: true
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
if (cacheDriverEnv === 'filesystem') {
|
|
737
|
+
// Filesystem cache configuration
|
|
738
|
+
cacheConfig.driver = new FilesystemCache({
|
|
739
|
+
directory: cacheDirectoryEnv,
|
|
740
|
+
prefix: cachePrefixEnv,
|
|
741
|
+
ttl: cacheTtlEnv,
|
|
742
|
+
enableCompression: true,
|
|
743
|
+
enableStats: verbose,
|
|
744
|
+
enableCleanup: true,
|
|
745
|
+
cleanupInterval: 300000, // 5 minutes
|
|
746
|
+
createDirectory: true
|
|
747
|
+
});
|
|
748
|
+
} else {
|
|
749
|
+
// Memory cache configuration (default)
|
|
750
|
+
cacheConfig.driverType = 'memory';
|
|
751
|
+
cacheConfig.memoryOptions = {
|
|
752
|
+
maxSize: cacheMaxSizeEnv,
|
|
753
|
+
ttl: cacheTtlEnv,
|
|
754
|
+
enableStats: verbose
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
plugins.push(new CachePlugin(cacheConfig));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
database = new S3db({
|
|
762
|
+
connectionString,
|
|
763
|
+
verbose,
|
|
764
|
+
parallelism,
|
|
765
|
+
passphrase,
|
|
766
|
+
versioningEnabled,
|
|
767
|
+
plugins
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
await database.connect();
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
success: true,
|
|
774
|
+
message: 'Connected to S3DB database',
|
|
775
|
+
status: {
|
|
776
|
+
connected: database.isConnected(),
|
|
777
|
+
bucket: database.bucket,
|
|
778
|
+
keyPrefix: database.keyPrefix,
|
|
779
|
+
version: database.s3dbVersion,
|
|
780
|
+
plugins: {
|
|
781
|
+
costs: costsEnabled,
|
|
782
|
+
cache: cacheEnabled,
|
|
783
|
+
cacheDriver: cacheEnabled ? cacheDriverEnv : null,
|
|
784
|
+
cacheDirectory: cacheEnabled && cacheDriverEnv === 'filesystem' ? cacheDirectoryEnv : null,
|
|
785
|
+
cacheMaxSize: cacheEnabled && cacheDriverEnv === 'memory' ? cacheMaxSizeEnv : null,
|
|
786
|
+
cacheTtl: cacheEnabled ? cacheTtlEnv : null
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async handleDbDisconnect(args) {
|
|
793
|
+
if (!database || !database.isConnected()) {
|
|
794
|
+
return { success: false, message: 'No database connection to disconnect' };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
await database.disconnect();
|
|
798
|
+
database = null;
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
success: true,
|
|
802
|
+
message: 'Disconnected from S3DB database'
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async handleDbStatus(args) {
|
|
807
|
+
if (!database) {
|
|
808
|
+
return {
|
|
809
|
+
connected: false,
|
|
810
|
+
message: 'No database instance created'
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
connected: database.isConnected(),
|
|
816
|
+
bucket: database.bucket,
|
|
817
|
+
keyPrefix: database.keyPrefix,
|
|
818
|
+
version: database.s3dbVersion,
|
|
819
|
+
resourceCount: Object.keys(database.resources || {}).length,
|
|
820
|
+
resources: Object.keys(database.resources || {})
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async handleDbCreateResource(args) {
|
|
825
|
+
this.ensureConnected();
|
|
826
|
+
|
|
827
|
+
const { name, attributes, behavior = 'user-managed', timestamps = false, partitions, paranoid = true } = args;
|
|
828
|
+
|
|
829
|
+
const resource = await database.createResource({
|
|
830
|
+
name,
|
|
831
|
+
attributes,
|
|
832
|
+
behavior,
|
|
833
|
+
timestamps,
|
|
834
|
+
partitions,
|
|
835
|
+
paranoid
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
return {
|
|
839
|
+
success: true,
|
|
840
|
+
resource: {
|
|
841
|
+
name: resource.name,
|
|
842
|
+
behavior: resource.behavior,
|
|
843
|
+
attributes: resource.attributes,
|
|
844
|
+
partitions: resource.config.partitions,
|
|
845
|
+
timestamps: resource.config.timestamps
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async handleDbListResources(args) {
|
|
851
|
+
this.ensureConnected();
|
|
852
|
+
|
|
853
|
+
const resourceList = await database.listResources();
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
success: true,
|
|
857
|
+
resources: resourceList,
|
|
858
|
+
count: resourceList.length
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Resource operation handlers
|
|
863
|
+
async handleResourceInsert(args) {
|
|
864
|
+
this.ensureConnected();
|
|
865
|
+
const { resourceName, data } = args;
|
|
866
|
+
|
|
867
|
+
const resource = this.getResource(resourceName);
|
|
868
|
+
const result = await resource.insert(data);
|
|
869
|
+
|
|
870
|
+
// Extract partition information for cache invalidation
|
|
871
|
+
const partitionInfo = this._extractPartitionInfo(resource, result);
|
|
872
|
+
|
|
873
|
+
// Generate cache invalidation patterns
|
|
874
|
+
const cacheInvalidationPatterns = this._generateCacheInvalidationPatterns(resource, result, 'insert');
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
success: true,
|
|
878
|
+
data: result,
|
|
879
|
+
...(partitionInfo && { partitionInfo }),
|
|
880
|
+
cacheInvalidationPatterns
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async handleResourceInsertMany(args) {
|
|
885
|
+
this.ensureConnected();
|
|
886
|
+
const { resourceName, data } = args;
|
|
887
|
+
|
|
888
|
+
const resource = this.getResource(resourceName);
|
|
889
|
+
const result = await resource.insertMany(data);
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
success: true,
|
|
893
|
+
data: result,
|
|
894
|
+
count: result.length
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async handleResourceGet(args) {
|
|
899
|
+
this.ensureConnected();
|
|
900
|
+
const { resourceName, id, partition, partitionValues } = args;
|
|
901
|
+
|
|
902
|
+
const resource = this.getResource(resourceName);
|
|
903
|
+
|
|
904
|
+
// Use partition information for optimized retrieval if provided
|
|
905
|
+
let options = {};
|
|
906
|
+
if (partition && partitionValues) {
|
|
907
|
+
options.partition = partition;
|
|
908
|
+
options.partitionValues = partitionValues;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const result = await resource.get(id, options);
|
|
912
|
+
|
|
913
|
+
// Extract partition information from result
|
|
914
|
+
const partitionInfo = this._extractPartitionInfo(resource, result);
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
success: true,
|
|
918
|
+
data: result,
|
|
919
|
+
...(partitionInfo && { partitionInfo })
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async handleResourceGetMany(args) {
|
|
924
|
+
this.ensureConnected();
|
|
925
|
+
const { resourceName, ids } = args;
|
|
926
|
+
|
|
927
|
+
const resource = this.getResource(resourceName);
|
|
928
|
+
const result = await resource.getMany(ids);
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
success: true,
|
|
932
|
+
data: result,
|
|
933
|
+
count: result.length
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async handleResourceUpdate(args) {
|
|
938
|
+
this.ensureConnected();
|
|
939
|
+
const { resourceName, id, data } = args;
|
|
940
|
+
|
|
941
|
+
const resource = this.getResource(resourceName);
|
|
942
|
+
const result = await resource.update(id, data);
|
|
943
|
+
|
|
944
|
+
// Extract partition information for cache invalidation
|
|
945
|
+
const partitionInfo = this._extractPartitionInfo(resource, result);
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
success: true,
|
|
949
|
+
data: result,
|
|
950
|
+
...(partitionInfo && { partitionInfo })
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async handleResourceUpsert(args) {
|
|
955
|
+
this.ensureConnected();
|
|
956
|
+
const { resourceName, data } = args;
|
|
957
|
+
|
|
958
|
+
const resource = this.getResource(resourceName);
|
|
959
|
+
const result = await resource.upsert(data);
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
success: true,
|
|
963
|
+
data: result
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async handleResourceDelete(args) {
|
|
968
|
+
this.ensureConnected();
|
|
969
|
+
const { resourceName, id } = args;
|
|
970
|
+
|
|
971
|
+
const resource = this.getResource(resourceName);
|
|
972
|
+
await resource.delete(id);
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
success: true,
|
|
976
|
+
message: `Document ${id} deleted from ${resourceName}`
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async handleResourceDeleteMany(args) {
|
|
981
|
+
this.ensureConnected();
|
|
982
|
+
const { resourceName, ids } = args;
|
|
983
|
+
|
|
984
|
+
const resource = this.getResource(resourceName);
|
|
985
|
+
await resource.deleteMany(ids);
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
success: true,
|
|
989
|
+
message: `${ids.length} documents deleted from ${resourceName}`,
|
|
990
|
+
deletedIds: ids
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async handleResourceExists(args) {
|
|
995
|
+
this.ensureConnected();
|
|
996
|
+
const { resourceName, id, partition, partitionValues } = args;
|
|
997
|
+
|
|
998
|
+
const resource = this.getResource(resourceName);
|
|
999
|
+
|
|
1000
|
+
// Use partition information for optimized existence check if provided
|
|
1001
|
+
let options = {};
|
|
1002
|
+
if (partition && partitionValues) {
|
|
1003
|
+
options.partition = partition;
|
|
1004
|
+
options.partitionValues = partitionValues;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const exists = await resource.exists(id, options);
|
|
1008
|
+
|
|
1009
|
+
return {
|
|
1010
|
+
success: true,
|
|
1011
|
+
exists,
|
|
1012
|
+
id,
|
|
1013
|
+
resource: resourceName,
|
|
1014
|
+
...(partition && { partition }),
|
|
1015
|
+
...(partitionValues && { partitionValues })
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async handleResourceList(args) {
|
|
1020
|
+
this.ensureConnected();
|
|
1021
|
+
const { resourceName, limit = 100, offset = 0, partition, partitionValues } = args;
|
|
1022
|
+
|
|
1023
|
+
const resource = this.getResource(resourceName);
|
|
1024
|
+
const options = { limit, offset };
|
|
1025
|
+
|
|
1026
|
+
if (partition && partitionValues) {
|
|
1027
|
+
options.partition = partition;
|
|
1028
|
+
options.partitionValues = partitionValues;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const result = await resource.list(options);
|
|
1032
|
+
|
|
1033
|
+
// Generate cache key hint for intelligent caching
|
|
1034
|
+
const cacheKeyHint = this._generateCacheKeyHint(resourceName, 'list', {
|
|
1035
|
+
limit,
|
|
1036
|
+
offset,
|
|
1037
|
+
partition,
|
|
1038
|
+
partitionValues
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
success: true,
|
|
1043
|
+
data: result,
|
|
1044
|
+
count: result.length,
|
|
1045
|
+
pagination: {
|
|
1046
|
+
limit,
|
|
1047
|
+
offset,
|
|
1048
|
+
hasMore: result.length === limit
|
|
1049
|
+
},
|
|
1050
|
+
cacheKeyHint,
|
|
1051
|
+
...(partition && { partition }),
|
|
1052
|
+
...(partitionValues && { partitionValues })
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async handleResourceListIds(args) {
|
|
1057
|
+
this.ensureConnected();
|
|
1058
|
+
const { resourceName, limit = 1000, offset = 0 } = args;
|
|
1059
|
+
|
|
1060
|
+
const resource = this.getResource(resourceName);
|
|
1061
|
+
const result = await resource.listIds({ limit, offset });
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
success: true,
|
|
1065
|
+
ids: result,
|
|
1066
|
+
count: result.length,
|
|
1067
|
+
pagination: {
|
|
1068
|
+
limit,
|
|
1069
|
+
offset,
|
|
1070
|
+
hasMore: result.length === limit
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async handleResourceCount(args) {
|
|
1076
|
+
this.ensureConnected();
|
|
1077
|
+
const { resourceName, partition, partitionValues } = args;
|
|
1078
|
+
|
|
1079
|
+
const resource = this.getResource(resourceName);
|
|
1080
|
+
const options = {};
|
|
1081
|
+
|
|
1082
|
+
if (partition && partitionValues) {
|
|
1083
|
+
options.partition = partition;
|
|
1084
|
+
options.partitionValues = partitionValues;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const count = await resource.count(options);
|
|
1088
|
+
|
|
1089
|
+
// Generate cache key hint for intelligent caching
|
|
1090
|
+
const cacheKeyHint = this._generateCacheKeyHint(resourceName, 'count', {
|
|
1091
|
+
partition,
|
|
1092
|
+
partitionValues
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
success: true,
|
|
1097
|
+
count,
|
|
1098
|
+
resource: resourceName,
|
|
1099
|
+
cacheKeyHint,
|
|
1100
|
+
...(partition && { partition }),
|
|
1101
|
+
...(partitionValues && { partitionValues })
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async handleResourceGetAll(args) {
|
|
1106
|
+
this.ensureConnected();
|
|
1107
|
+
const { resourceName } = args;
|
|
1108
|
+
|
|
1109
|
+
const resource = this.getResource(resourceName);
|
|
1110
|
+
const result = await resource.getAll();
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
success: true,
|
|
1114
|
+
data: result,
|
|
1115
|
+
count: result.length,
|
|
1116
|
+
warning: result.length > 1000 ? 'Large dataset returned. Consider using resourceList with pagination.' : undefined
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async handleResourceDeleteAll(args) {
|
|
1121
|
+
this.ensureConnected();
|
|
1122
|
+
const { resourceName, confirm } = args;
|
|
1123
|
+
|
|
1124
|
+
if (!confirm) {
|
|
1125
|
+
throw new Error('Confirmation required. Set confirm: true to proceed with deleting all data.');
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const resource = this.getResource(resourceName);
|
|
1129
|
+
await resource.deleteAll();
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
success: true,
|
|
1133
|
+
message: `All documents deleted from ${resourceName}`
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async handleDbGetStats(args) {
|
|
1138
|
+
this.ensureConnected();
|
|
1139
|
+
|
|
1140
|
+
const stats = {
|
|
1141
|
+
database: {
|
|
1142
|
+
connected: database.isConnected(),
|
|
1143
|
+
bucket: database.bucket,
|
|
1144
|
+
keyPrefix: database.keyPrefix,
|
|
1145
|
+
version: database.s3dbVersion,
|
|
1146
|
+
resourceCount: Object.keys(database.resources || {}).length,
|
|
1147
|
+
resources: Object.keys(database.resources || {})
|
|
1148
|
+
},
|
|
1149
|
+
costs: null,
|
|
1150
|
+
cache: null
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// Get costs from client if available
|
|
1154
|
+
if (database.client && database.client.costs) {
|
|
1155
|
+
stats.costs = {
|
|
1156
|
+
total: database.client.costs.total,
|
|
1157
|
+
totalRequests: database.client.costs.requests.total,
|
|
1158
|
+
requestsByType: { ...database.client.costs.requests },
|
|
1159
|
+
eventsByType: { ...database.client.costs.events },
|
|
1160
|
+
estimatedCostUSD: database.client.costs.total
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Get cache stats from plugins if available
|
|
1165
|
+
try {
|
|
1166
|
+
const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
|
|
1167
|
+
if (cachePlugin && cachePlugin.driver) {
|
|
1168
|
+
const cacheSize = await cachePlugin.driver.size();
|
|
1169
|
+
const cacheKeys = await cachePlugin.driver.keys();
|
|
1170
|
+
|
|
1171
|
+
stats.cache = {
|
|
1172
|
+
enabled: true,
|
|
1173
|
+
driver: cachePlugin.driver.constructor.name,
|
|
1174
|
+
size: cacheSize,
|
|
1175
|
+
maxSize: cachePlugin.driver.maxSize || 'unlimited',
|
|
1176
|
+
ttl: cachePlugin.driver.ttl || 'no expiration',
|
|
1177
|
+
keyCount: cacheKeys.length,
|
|
1178
|
+
sampleKeys: cacheKeys.slice(0, 5) // First 5 keys as sample
|
|
1179
|
+
};
|
|
1180
|
+
} else {
|
|
1181
|
+
stats.cache = { enabled: false };
|
|
1182
|
+
}
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
stats.cache = { enabled: false, error: error.message };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return {
|
|
1188
|
+
success: true,
|
|
1189
|
+
stats
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async handleDbClearCache(args) {
|
|
1194
|
+
this.ensureConnected();
|
|
1195
|
+
const { resourceName } = args;
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
|
|
1199
|
+
if (!cachePlugin || !cachePlugin.driver) {
|
|
1200
|
+
return {
|
|
1201
|
+
success: false,
|
|
1202
|
+
message: 'Cache is not enabled or available'
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (resourceName) {
|
|
1207
|
+
// Clear cache for specific resource
|
|
1208
|
+
const resource = this.getResource(resourceName);
|
|
1209
|
+
await cachePlugin.clearCacheForResource(resource);
|
|
1210
|
+
|
|
1211
|
+
return {
|
|
1212
|
+
success: true,
|
|
1213
|
+
message: `Cache cleared for resource: ${resourceName}`
|
|
1214
|
+
};
|
|
1215
|
+
} else {
|
|
1216
|
+
// Clear all cache
|
|
1217
|
+
await cachePlugin.driver.clear();
|
|
1218
|
+
|
|
1219
|
+
return {
|
|
1220
|
+
success: true,
|
|
1221
|
+
message: 'All cache cleared'
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
return {
|
|
1226
|
+
success: false,
|
|
1227
|
+
message: `Failed to clear cache: ${error.message}`
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Helper methods
|
|
1233
|
+
ensureConnected() {
|
|
1234
|
+
if (!database || !database.isConnected()) {
|
|
1235
|
+
throw new Error('Database not connected. Use dbConnect tool first.');
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
getResource(resourceName) {
|
|
1240
|
+
this.ensureConnected();
|
|
1241
|
+
|
|
1242
|
+
if (!database.resources[resourceName]) {
|
|
1243
|
+
throw new Error(`Resource '${resourceName}' not found. Available resources: ${Object.keys(database.resources).join(', ')}`);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return database.resources[resourceName];
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Helper method to extract partition information from data for cache optimization
|
|
1250
|
+
_extractPartitionInfo(resource, data) {
|
|
1251
|
+
if (!resource || !data || !resource.config?.partitions) {
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const partitionInfo = {};
|
|
1256
|
+
const partitions = resource.config.partitions;
|
|
1257
|
+
|
|
1258
|
+
for (const [partitionName, partitionConfig] of Object.entries(partitions)) {
|
|
1259
|
+
if (partitionConfig.fields) {
|
|
1260
|
+
const partitionValues = {};
|
|
1261
|
+
let hasValues = false;
|
|
1262
|
+
|
|
1263
|
+
for (const fieldName of Object.keys(partitionConfig.fields)) {
|
|
1264
|
+
if (data[fieldName] !== undefined && data[fieldName] !== null) {
|
|
1265
|
+
partitionValues[fieldName] = data[fieldName];
|
|
1266
|
+
hasValues = true;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (hasValues) {
|
|
1271
|
+
partitionInfo[partitionName] = partitionValues;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return Object.keys(partitionInfo).length > 0 ? partitionInfo : null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Helper method to generate intelligent cache keys including partition information
|
|
1280
|
+
_generateCacheKeyHint(resourceName, action, params = {}) {
|
|
1281
|
+
const keyParts = [`resource=${resourceName}`, `action=${action}`];
|
|
1282
|
+
|
|
1283
|
+
// Add partition information if present
|
|
1284
|
+
if (params.partition && params.partitionValues) {
|
|
1285
|
+
keyParts.push(`partition=${params.partition}`);
|
|
1286
|
+
|
|
1287
|
+
// Sort partition values for consistent cache keys
|
|
1288
|
+
const sortedValues = Object.entries(params.partitionValues)
|
|
1289
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1290
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
1291
|
+
.join('&');
|
|
1292
|
+
|
|
1293
|
+
if (sortedValues) {
|
|
1294
|
+
keyParts.push(`values=${sortedValues}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Add other parameters (excluding partition info to avoid duplication)
|
|
1299
|
+
const otherParams = { ...params };
|
|
1300
|
+
delete otherParams.partition;
|
|
1301
|
+
delete otherParams.partitionValues;
|
|
1302
|
+
|
|
1303
|
+
if (Object.keys(otherParams).length > 0) {
|
|
1304
|
+
const sortedParams = Object.entries(otherParams)
|
|
1305
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1306
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
1307
|
+
.join('&');
|
|
1308
|
+
|
|
1309
|
+
if (sortedParams) {
|
|
1310
|
+
keyParts.push(`params=${sortedParams}`);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return keyParts.join('/') + '.json.gz';
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Helper method to generate cache invalidation patterns based on data changes
|
|
1318
|
+
_generateCacheInvalidationPatterns(resource, data, action = 'write') {
|
|
1319
|
+
const patterns = [];
|
|
1320
|
+
const resourceName = resource.name;
|
|
1321
|
+
|
|
1322
|
+
// Always invalidate general resource cache
|
|
1323
|
+
patterns.push(`resource=${resourceName}/action=list`);
|
|
1324
|
+
patterns.push(`resource=${resourceName}/action=count`);
|
|
1325
|
+
patterns.push(`resource=${resourceName}/action=getAll`);
|
|
1326
|
+
|
|
1327
|
+
// Extract partition info and invalidate partition-specific cache
|
|
1328
|
+
const partitionInfo = this._extractPartitionInfo(resource, data);
|
|
1329
|
+
if (partitionInfo) {
|
|
1330
|
+
for (const [partitionName, partitionValues] of Object.entries(partitionInfo)) {
|
|
1331
|
+
const sortedValues = Object.entries(partitionValues)
|
|
1332
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1333
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
1334
|
+
.join('&');
|
|
1335
|
+
|
|
1336
|
+
if (sortedValues) {
|
|
1337
|
+
// Invalidate specific partition caches
|
|
1338
|
+
patterns.push(`resource=${resourceName}/action=list/partition=${partitionName}/values=${sortedValues}`);
|
|
1339
|
+
patterns.push(`resource=${resourceName}/action=count/partition=${partitionName}/values=${sortedValues}`);
|
|
1340
|
+
patterns.push(`resource=${resourceName}/action=listIds/partition=${partitionName}/values=${sortedValues}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// For specific document operations, invalidate document cache
|
|
1346
|
+
if (data.id) {
|
|
1347
|
+
patterns.push(`resource=${resourceName}/action=get/params=id=${data.id}`);
|
|
1348
|
+
patterns.push(`resource=${resourceName}/action=exists/params=id=${data.id}`);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return patterns;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Handle command line arguments
|
|
1356
|
+
function parseArgs() {
|
|
1357
|
+
const args = {
|
|
1358
|
+
transport: 'stdio',
|
|
1359
|
+
host: '0.0.0.0',
|
|
1360
|
+
port: 8000
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
process.argv.forEach((arg, index) => {
|
|
1364
|
+
if (arg.startsWith('--transport=')) {
|
|
1365
|
+
args.transport = arg.split('=')[1];
|
|
1366
|
+
} else if (arg === '--transport' && process.argv[index + 1]) {
|
|
1367
|
+
args.transport = process.argv[index + 1];
|
|
1368
|
+
} else if (arg.startsWith('--host=')) {
|
|
1369
|
+
args.host = arg.split('=')[1];
|
|
1370
|
+
} else if (arg.startsWith('--port=')) {
|
|
1371
|
+
args.port = parseInt(arg.split('=')[1]);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
return args;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Main execution
|
|
1379
|
+
async function main() {
|
|
1380
|
+
const args = parseArgs();
|
|
1381
|
+
|
|
1382
|
+
// Set environment variables from command line args
|
|
1383
|
+
process.env.MCP_TRANSPORT = args.transport;
|
|
1384
|
+
process.env.MCP_SERVER_HOST = args.host;
|
|
1385
|
+
process.env.MCP_SERVER_PORT = args.port.toString();
|
|
1386
|
+
|
|
1387
|
+
const server = new S3dbMCPServer();
|
|
1388
|
+
|
|
1389
|
+
// Handle graceful shutdown
|
|
1390
|
+
process.on('SIGINT', async () => {
|
|
1391
|
+
console.log('\nShutting down S3DB MCP Server...');
|
|
1392
|
+
if (database && database.isConnected()) {
|
|
1393
|
+
await database.disconnect();
|
|
1394
|
+
}
|
|
1395
|
+
process.exit(0);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
console.log(`S3DB MCP Server v${SERVER_VERSION} started`);
|
|
1399
|
+
console.log(`Transport: ${args.transport}`);
|
|
1400
|
+
if (args.transport === 'sse') {
|
|
1401
|
+
console.log(`URL: http://${args.host}:${args.port}/sse`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Start the server
|
|
1406
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1407
|
+
main().catch(console.error);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
export { S3dbMCPServer };
|