outlet-orm 6.0.0 → 7.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.
@@ -0,0 +1,685 @@
1
+ /**
2
+ * Outlet ORM — MCP Server (Model Context Protocol)
3
+ * Exposes ORM capabilities to AI agents via JSON-RPC 2.0 over stdio.
4
+ *
5
+ * Protocol: MCP (https://modelcontextprotocol.io)
6
+ * Transport: stdio (newline-delimited JSON-RPC)
7
+ *
8
+ * @since 7.0.0
9
+ */
10
+
11
+ const { EventEmitter } = require('events');
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ // ─── Tool Definitions ────────────────────────────────────────────
16
+
17
+ const TOOL_DEFINITIONS = [
18
+ {
19
+ name: 'migrate_status',
20
+ description: 'Show migration status — lists pending and executed migrations.',
21
+ inputSchema: { type: 'object', properties: {}, required: [] }
22
+ },
23
+ {
24
+ name: 'migrate_run',
25
+ description: 'Run all pending migrations to bring the database schema up to date.',
26
+ inputSchema: { type: 'object', properties: {}, required: [] }
27
+ },
28
+ {
29
+ name: 'migrate_rollback',
30
+ description: 'Rollback the last batch of migrations.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: { steps: { type: 'number', description: 'Number of batches to rollback (default 1)' } },
34
+ required: []
35
+ }
36
+ },
37
+ {
38
+ name: 'migrate_reset',
39
+ description: 'Rollback ALL migrations. ⚠️ DESTRUCTIVE — requires AI safety consent.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: { consent: { type: 'string', description: 'User consent text for destructive action' } },
43
+ required: ['consent']
44
+ }
45
+ },
46
+ {
47
+ name: 'migrate_make',
48
+ description: 'Create a new migration file.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: { name: { type: 'string', description: 'Migration name, e.g. "create_users_table"' } },
52
+ required: ['name']
53
+ }
54
+ },
55
+ {
56
+ name: 'seed_run',
57
+ description: 'Run database seeders.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: { class: { type: 'string', description: 'Specific seeder class name (optional)' } },
61
+ required: []
62
+ }
63
+ },
64
+ {
65
+ name: 'schema_introspect',
66
+ description: 'Introspect database schema — returns all tables and their columns.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: { table: { type: 'string', description: 'Specific table to introspect (optional — omit for all tables)' } },
70
+ required: []
71
+ }
72
+ },
73
+ {
74
+ name: 'query_execute',
75
+ description: 'Execute a raw SQL query on the database. ⚠️ Write queries require AI safety consent.',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ sql: { type: 'string', description: 'The SQL query to execute' },
80
+ params: { type: 'array', description: 'Parameterised values (optional)', items: {} },
81
+ consent: { type: 'string', description: 'User consent text (required for write queries)' }
82
+ },
83
+ required: ['sql']
84
+ }
85
+ },
86
+ {
87
+ name: 'model_list',
88
+ description: 'List all Model files discovered in the project models/ directory.',
89
+ inputSchema: { type: 'object', properties: {}, required: [] }
90
+ },
91
+ {
92
+ name: 'backup_create',
93
+ description: 'Create a database backup.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ type: { type: 'string', enum: ['full', 'partial', 'journal'], description: 'Backup type (default: full)' },
98
+ tables: { type: 'array', items: { type: 'string' }, description: 'Tables for partial backup (optional)' },
99
+ format: { type: 'string', enum: ['sql', 'json'], description: 'Output format (default: sql)' }
100
+ },
101
+ required: []
102
+ }
103
+ },
104
+ {
105
+ name: 'backup_restore',
106
+ description: 'Restore a database from a backup file. ⚠️ DESTRUCTIVE — requires AI safety consent.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ filePath: { type: 'string', description: 'Path to the backup file' },
111
+ consent: { type: 'string', description: 'User consent text for destructive action' }
112
+ },
113
+ required: ['filePath', 'consent']
114
+ }
115
+ }
116
+ ];
117
+
118
+ // ─── AI Safety Guardrails ────────────────────────────────────────
119
+
120
+ const DESTRUCTIVE_TOOLS = new Set(['migrate_reset', 'backup_restore']);
121
+
122
+ function isWriteQuery(sql) {
123
+ const upper = sql.trim().toUpperCase();
124
+ return /^(INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE|CREATE|REPLACE)\b/.test(upper);
125
+ }
126
+
127
+ function validateConsent(consent) {
128
+ return typeof consent === 'string' && consent.trim().length > 0;
129
+ }
130
+
131
+ // ─── MCP Server ──────────────────────────────────────────────────
132
+
133
+ class MCPServer extends EventEmitter {
134
+ /**
135
+ * @param {object} options
136
+ * @param {object} options.connection - DatabaseConnection instance (optional, auto-loaded from project)
137
+ * @param {string} options.projectDir - Project root directory (default: process.cwd())
138
+ * @param {boolean} options.safetyGuardrails - Enable AI safety guardrails (default: true)
139
+ */
140
+ constructor(options = {}) {
141
+ super();
142
+ this.projectDir = options.projectDir || process.cwd();
143
+ this.connection = options.connection || null;
144
+ this.safetyGuardrails = options.safetyGuardrails !== false;
145
+ this._buffer = '';
146
+ this._initialized = false;
147
+ }
148
+
149
+ /**
150
+ * Start the MCP server on stdio
151
+ */
152
+ start() {
153
+ process.stdin.setEncoding('utf8');
154
+ process.stdin.on('data', (chunk) => this._onData(chunk));
155
+ process.stdin.on('end', () => this.emit('close'));
156
+ this.emit('started');
157
+ }
158
+
159
+ /**
160
+ * Start in programmatic mode (no stdio binding)
161
+ * Returns a handler function for processing messages
162
+ */
163
+ handler() {
164
+ return async (message) => {
165
+ return this._handleMessage(message);
166
+ };
167
+ }
168
+
169
+ // ─── Internal: stdio data handling ─────────────────────────────
170
+
171
+ _onData(chunk) {
172
+ this._buffer += chunk;
173
+ const lines = this._buffer.split('\n');
174
+ this._buffer = lines.pop(); // keep incomplete line in buffer
175
+
176
+ for (const line of lines) {
177
+ const trimmed = line.trim();
178
+ if (!trimmed) continue;
179
+
180
+ try {
181
+ const message = JSON.parse(trimmed);
182
+ this._handleMessage(message).then(response => {
183
+ if (response) {
184
+ this._send(response);
185
+ }
186
+ }).catch(err => {
187
+ this._send({
188
+ jsonrpc: '2.0',
189
+ id: message.id || null,
190
+ error: { code: -32603, message: err.message }
191
+ });
192
+ });
193
+ } catch {
194
+ this._send({
195
+ jsonrpc: '2.0',
196
+ id: null,
197
+ error: { code: -32700, message: 'Parse error' }
198
+ });
199
+ }
200
+ }
201
+ }
202
+
203
+ _send(obj) {
204
+ const json = JSON.stringify(obj);
205
+ process.stdout.write(json + '\n');
206
+ this.emit('response', obj);
207
+ }
208
+
209
+ // ─── Internal: JSON-RPC message routing ────────────────────────
210
+
211
+ async _handleMessage(message) {
212
+ const { method, id, params } = message;
213
+
214
+ // Notifications (no id) — no response
215
+ if (method === 'notifications/initialized') {
216
+ this._initialized = true;
217
+ this.emit('initialized');
218
+ return null;
219
+ }
220
+
221
+ switch (method) {
222
+ case 'initialize':
223
+ return {
224
+ jsonrpc: '2.0',
225
+ id,
226
+ result: {
227
+ protocolVersion: '2024-11-05',
228
+ capabilities: {
229
+ tools: { listChanged: false }
230
+ },
231
+ serverInfo: {
232
+ name: 'outlet-orm',
233
+ version: require('../../package.json').version
234
+ }
235
+ }
236
+ };
237
+
238
+ case 'tools/list':
239
+ return {
240
+ jsonrpc: '2.0',
241
+ id,
242
+ result: { tools: TOOL_DEFINITIONS }
243
+ };
244
+
245
+ case 'tools/call':
246
+ return this._handleToolCall(id, params);
247
+
248
+ case 'ping':
249
+ return { jsonrpc: '2.0', id, result: {} };
250
+
251
+ default:
252
+ return {
253
+ jsonrpc: '2.0',
254
+ id,
255
+ error: { code: -32601, message: `Method not found: ${method}` }
256
+ };
257
+ }
258
+ }
259
+
260
+ // ─── Internal: Tool execution ──────────────────────────────────
261
+
262
+ async _handleToolCall(id, params) {
263
+ const { name, arguments: args = {} } = params || {};
264
+
265
+ try {
266
+ // Safety guardrails for destructive tools
267
+ if (this.safetyGuardrails && DESTRUCTIVE_TOOLS.has(name)) {
268
+ if (!validateConsent(args.consent)) {
269
+ return this._toolError(id, name,
270
+ `⚠️ SAFETY GUARDRAIL: "${name}" is a destructive operation.\n` +
271
+ 'This action can irreversibly destroy data.\n' +
272
+ 'You MUST obtain explicit user consent before proceeding.\n' +
273
+ 'Pass the user\'s consent text in the "consent" parameter.');
274
+ }
275
+ }
276
+
277
+ const result = await this._executeTool(name, args);
278
+ return {
279
+ jsonrpc: '2.0',
280
+ id,
281
+ result: {
282
+ content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
283
+ }
284
+ };
285
+ } catch (error) {
286
+ return this._toolError(id, name, error.message);
287
+ }
288
+ }
289
+
290
+ _toolError(id, toolName, message) {
291
+ return {
292
+ jsonrpc: '2.0',
293
+ id,
294
+ result: {
295
+ content: [{ type: 'text', text: `Error [${toolName}]: ${message}` }],
296
+ isError: true
297
+ }
298
+ };
299
+ }
300
+
301
+ // ─── Tool Implementations ─────────────────────────────────────
302
+
303
+ async _executeTool(name, args) {
304
+ switch (name) {
305
+ case 'migrate_status': return this._toolMigrateStatus();
306
+ case 'migrate_run': return this._toolMigrateRun();
307
+ case 'migrate_rollback': return this._toolMigrateRollback(args);
308
+ case 'migrate_reset': return this._toolMigrateReset(args);
309
+ case 'migrate_make': return this._toolMigrateMake(args);
310
+ case 'seed_run': return this._toolSeedRun(args);
311
+ case 'schema_introspect': return this._toolSchemaIntrospect(args);
312
+ case 'query_execute': return this._toolQueryExecute(args);
313
+ case 'model_list': return this._toolModelList();
314
+ case 'backup_create': return this._toolBackupCreate(args);
315
+ case 'backup_restore': return this._toolBackupRestore(args);
316
+ default:
317
+ throw new Error(`Unknown tool: ${name}`);
318
+ }
319
+ }
320
+
321
+ // ─── Connection helper ─────────────────────────────────────────
322
+
323
+ async _getConnection() {
324
+ if (this.connection) return this.connection;
325
+
326
+ // Auto-load from project
327
+ const { DatabaseConnection } = require('../../src');
328
+ const dbConfigPath = path.join(this.projectDir, 'database', 'config.js');
329
+
330
+ let config;
331
+ try {
332
+ config = require(dbConfigPath);
333
+ if (config instanceof DatabaseConnection) {
334
+ this.connection = config;
335
+ if (!this.connection._connected) await this.connection.connect();
336
+ return this.connection;
337
+ }
338
+ } catch {
339
+ // Fallback to .env
340
+ try { require('dotenv').config({ path: path.join(this.projectDir, '.env') }); } catch { /* dotenv optional */ }
341
+ const env = process.env;
342
+ config = {
343
+ driver: env.DB_DRIVER || env.DATABASE_DRIVER,
344
+ host: env.DB_HOST,
345
+ port: env.DB_PORT ? Number(env.DB_PORT) : undefined,
346
+ user: env.DB_USER || env.DB_USERNAME,
347
+ password: env.DB_PASSWORD,
348
+ database: env.DB_DATABASE || env.DB_NAME || env.DB_FILE
349
+ };
350
+ if (!config.driver) throw new Error('No database configuration found. Run outlet-init first.');
351
+ }
352
+
353
+ this.connection = new DatabaseConnection(config);
354
+ await this.connection.connect();
355
+ return this.connection;
356
+ }
357
+
358
+ // ── migrate_status ─────────────────────────────────────────────
359
+
360
+ async _toolMigrateStatus() {
361
+ const conn = await this._getConnection();
362
+ const { MigrationManager } = require('../../src');
363
+ const manager = new MigrationManager(conn);
364
+
365
+ // Capture console output
366
+ const logs = [];
367
+ const origLog = console.log;
368
+ console.log = (...a) => logs.push(a.map(String).join(' '));
369
+ try {
370
+ await manager.status();
371
+ } finally {
372
+ console.log = origLog;
373
+ }
374
+ return logs.join('\n') || 'No migrations found.';
375
+ }
376
+
377
+ // ── migrate_run ────────────────────────────────────────────────
378
+
379
+ async _toolMigrateRun() {
380
+ const conn = await this._getConnection();
381
+ const { MigrationManager } = require('../../src');
382
+ const manager = new MigrationManager(conn);
383
+
384
+ const logs = [];
385
+ const origLog = console.log;
386
+ console.log = (...a) => logs.push(a.map(String).join(' '));
387
+ try {
388
+ await manager.run();
389
+ } finally {
390
+ console.log = origLog;
391
+ }
392
+ return logs.join('\n') || 'All migrations are up to date.';
393
+ }
394
+
395
+ // ── migrate_rollback ───────────────────────────────────────────
396
+
397
+ async _toolMigrateRollback(args) {
398
+ const conn = await this._getConnection();
399
+ const { MigrationManager } = require('../../src');
400
+ const manager = new MigrationManager(conn);
401
+ const steps = Number(args.steps) || 1;
402
+
403
+ const logs = [];
404
+ const origLog = console.log;
405
+ console.log = (...a) => logs.push(a.map(String).join(' '));
406
+ try {
407
+ await manager.rollback(steps);
408
+ } finally {
409
+ console.log = origLog;
410
+ }
411
+ return logs.join('\n') || `Rolled back ${steps} batch(es).`;
412
+ }
413
+
414
+ // ── migrate_reset ──────────────────────────────────────────────
415
+
416
+ async _toolMigrateReset(args) {
417
+ // Consent already validated in _handleToolCall
418
+ const conn = await this._getConnection();
419
+ const { MigrationManager } = require('../../src');
420
+ const manager = new MigrationManager(conn);
421
+
422
+ const logs = [];
423
+ const origLog = console.log;
424
+ console.log = (...a) => logs.push(a.map(String).join(' '));
425
+ try {
426
+ await manager.reset();
427
+ } finally {
428
+ console.log = origLog;
429
+ }
430
+ return logs.join('\n') || 'All migrations have been rolled back.';
431
+ }
432
+
433
+ // ── migrate_make ───────────────────────────────────────────────
434
+
435
+ async _toolMigrateMake(args) {
436
+ if (!args.name) throw new Error('Migration name is required');
437
+
438
+ const migrationsDir = path.join(this.projectDir, 'database', 'migrations');
439
+ try { fs.mkdirSync(migrationsDir, { recursive: true }); } catch { /* */ }
440
+
441
+ const timestamp = new Date().toISOString()
442
+ .replace(/[-:]/g, '')
443
+ .replace(/T/, '_')
444
+ .replace(/\..+/, '');
445
+
446
+ const fileName = `${timestamp}_${args.name}.js`;
447
+ const filePath = path.join(migrationsDir, fileName);
448
+
449
+ const isCreate = args.name.includes('create_');
450
+ const tableName = this._extractTableName(args.name);
451
+
452
+ const template = isCreate
453
+ ? this._createMigrationTemplate(tableName)
454
+ : this._alterMigrationTemplate(tableName);
455
+
456
+ fs.writeFileSync(filePath, template);
457
+ return `Migration created: ${fileName}\nLocation: ${filePath}`;
458
+ }
459
+
460
+ // ── seed_run ───────────────────────────────────────────────────
461
+
462
+ async _toolSeedRun(args) {
463
+ const conn = await this._getConnection();
464
+ const { SeederManager } = require('../../src');
465
+ const seederManager = new SeederManager(conn);
466
+
467
+ const logs = [];
468
+ const origLog = console.log;
469
+ console.log = (...a) => logs.push(a.map(String).join(' '));
470
+ try {
471
+ await seederManager.run(args.class || null);
472
+ } finally {
473
+ console.log = origLog;
474
+ }
475
+ return logs.join('\n') || 'Seeders executed successfully.';
476
+ }
477
+
478
+ // ── schema_introspect ──────────────────────────────────────────
479
+
480
+ async _toolSchemaIntrospect(args) {
481
+ const conn = await this._getConnection();
482
+ const driver = conn.config.driver;
483
+ let tables;
484
+
485
+ if (args.table) {
486
+ tables = [args.table];
487
+ } else {
488
+ // Get all tables
489
+ let query;
490
+ if (driver === 'sqlite') {
491
+ query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name";
492
+ } else if (driver === 'postgres') {
493
+ query = "SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename";
494
+ } else {
495
+ query = 'SHOW TABLES';
496
+ }
497
+ const rows = await conn.execute(query);
498
+ tables = rows.map(r => {
499
+ const vals = Object.values(r);
500
+ return vals[0];
501
+ });
502
+ }
503
+
504
+ const result = {};
505
+ for (const table of tables) {
506
+ let colQuery;
507
+ if (driver === 'sqlite') {
508
+ colQuery = `PRAGMA table_info("${table}")`;
509
+ } else if (driver === 'postgres') {
510
+ colQuery = `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '${table}' ORDER BY ordinal_position`;
511
+ } else {
512
+ colQuery = `DESCRIBE \`${table}\``;
513
+ }
514
+ const cols = await conn.execute(colQuery);
515
+ result[table] = cols;
516
+ }
517
+
518
+ return result;
519
+ }
520
+
521
+ // ── query_execute ──────────────────────────────────────────────
522
+
523
+ async _toolQueryExecute(args) {
524
+ if (!args.sql) throw new Error('SQL query is required');
525
+
526
+ // Safety guardrail for write queries
527
+ if (this.safetyGuardrails && isWriteQuery(args.sql)) {
528
+ if (!validateConsent(args.consent)) {
529
+ throw new Error(
530
+ '⚠️ SAFETY GUARDRAIL: This is a write query that modifies data.\n' +
531
+ 'You MUST obtain explicit user consent before proceeding.\n' +
532
+ 'Pass the user\'s consent text in the "consent" parameter.'
533
+ );
534
+ }
535
+ }
536
+
537
+ const conn = await this._getConnection();
538
+ const result = await conn.execute(args.sql, args.params || []);
539
+ return result;
540
+ }
541
+
542
+ // ── model_list ─────────────────────────────────────────────────
543
+
544
+ async _toolModelList() {
545
+ const modelsDir = path.join(this.projectDir, 'models');
546
+ if (!fs.existsSync(modelsDir)) {
547
+ return 'No models/ directory found. Models may be in src/ or another location.';
548
+ }
549
+
550
+ const files = fs.readdirSync(modelsDir)
551
+ .filter(f => f.endsWith('.js') || f.endsWith('.ts'))
552
+ .sort();
553
+
554
+ if (files.length === 0) {
555
+ return 'No model files found in models/ directory.';
556
+ }
557
+
558
+ const models = [];
559
+ for (const file of files) {
560
+ try {
561
+ const modelPath = path.join(modelsDir, file);
562
+ const ModelClass = require(modelPath);
563
+ models.push({
564
+ file,
565
+ table: ModelClass.table || '(not defined)',
566
+ fillable: ModelClass.fillable || [],
567
+ hidden: ModelClass.hidden || [],
568
+ timestamps: ModelClass.timestamps !== false
569
+ });
570
+ } catch {
571
+ models.push({ file, error: 'Could not load model' });
572
+ }
573
+ }
574
+ return models;
575
+ }
576
+
577
+ // ── backup_create ──────────────────────────────────────────────
578
+
579
+ async _toolBackupCreate(args) {
580
+ const conn = await this._getConnection();
581
+ const { BackupManager } = require('../../src');
582
+ const backupDir = path.join(this.projectDir, 'database', 'backups');
583
+ try { fs.mkdirSync(backupDir, { recursive: true }); } catch { /* */ }
584
+
585
+ const backupManager = new BackupManager(conn, { outputDir: backupDir, format: args.format || 'sql' });
586
+ const type = args.type || 'full';
587
+ let result;
588
+
589
+ if (type === 'full') {
590
+ result = await backupManager.full();
591
+ } else if (type === 'partial') {
592
+ result = await backupManager.partial(args.tables || []);
593
+ } else if (type === 'journal') {
594
+ result = await backupManager.journal();
595
+ } else {
596
+ throw new Error(`Unknown backup type: ${type}`);
597
+ }
598
+
599
+ return `Backup created: ${result || 'Success'}`;
600
+ }
601
+
602
+ // ── backup_restore ─────────────────────────────────────────────
603
+
604
+ async _toolBackupRestore(args) {
605
+ if (!args.filePath) throw new Error('Backup file path is required');
606
+ const conn = await this._getConnection();
607
+ const { BackupManager } = require('../../src');
608
+ const backupManager = new BackupManager(conn);
609
+ await backupManager.restore(args.filePath);
610
+ return `Backup restored from: ${args.filePath}`;
611
+ }
612
+
613
+ // ─── Template helpers ──────────────────────────────────────────
614
+
615
+ _extractTableName(migrationName) {
616
+ const patterns = [/create_(\w+)_table/, /to_(\w+)_table/, /alter_(\w+)_table/, /(\w+)_table/];
617
+ for (const p of patterns) {
618
+ const m = migrationName.match(p);
619
+ if (m) return m[1];
620
+ }
621
+ return 'table_name';
622
+ }
623
+
624
+ _capitalize(str) {
625
+ return str.charAt(0).toUpperCase() + str.slice(1);
626
+ }
627
+
628
+ _createMigrationTemplate(tableName) {
629
+ return `const { Migration } = require('outlet-orm');
630
+
631
+ class Create${this._capitalize(tableName)}Table extends Migration {
632
+ async up() {
633
+ const schema = this.getSchema();
634
+ await schema.create('${tableName}', (table) => {
635
+ table.id();
636
+ table.string('name');
637
+ table.timestamps();
638
+ });
639
+ }
640
+
641
+ async down() {
642
+ const schema = this.getSchema();
643
+ await schema.dropIfExists('${tableName}');
644
+ }
645
+ }
646
+
647
+ module.exports = Create${this._capitalize(tableName)}Table;
648
+ `;
649
+ }
650
+
651
+ _alterMigrationTemplate(tableName) {
652
+ return `const { Migration } = require('outlet-orm');
653
+
654
+ class Alter${this._capitalize(tableName)}Table extends Migration {
655
+ async up() {
656
+ const schema = this.getSchema();
657
+ await schema.table('${tableName}', (table) => {
658
+ // table.string('new_column');
659
+ });
660
+ }
661
+
662
+ async down() {
663
+ const schema = this.getSchema();
664
+ await schema.table('${tableName}', (table) => {
665
+ // table.dropColumn('new_column');
666
+ });
667
+ }
668
+ }
669
+
670
+ module.exports = Alter${this._capitalize(tableName)}Table;
671
+ `;
672
+ }
673
+
674
+ /**
675
+ * Graceful shutdown
676
+ */
677
+ async close() {
678
+ if (this.connection && typeof this.connection.disconnect === 'function') {
679
+ await this.connection.disconnect();
680
+ }
681
+ this.emit('close');
682
+ }
683
+ }
684
+
685
+ module.exports = MCPServer;