s3db.js 7.3.5 → 7.3.8

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/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 };