@bytebase/dbhub 0.1.0 → 0.1.1

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.
Files changed (33) hide show
  1. package/README.md +34 -33
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1489 -13
  4. package/dist/resources/employee-sqlite/employee.sql +117 -0
  5. package/dist/resources/employee-sqlite/load_department.sql +10 -0
  6. package/dist/resources/employee-sqlite/load_dept_emp.sql +1103 -0
  7. package/dist/resources/employee-sqlite/load_dept_manager.sql +17 -0
  8. package/dist/resources/employee-sqlite/load_employee.sql +1000 -0
  9. package/dist/resources/employee-sqlite/load_salary1.sql +9488 -0
  10. package/dist/resources/employee-sqlite/load_title.sql +1470 -0
  11. package/dist/resources/employee-sqlite/object.sql +74 -0
  12. package/dist/resources/employee-sqlite/show_elapsed.sql +4 -0
  13. package/dist/resources/employee-sqlite/test_employee_md5.sql +119 -0
  14. package/package.json +5 -4
  15. package/dist/config/demo-loader.js +0 -45
  16. package/dist/config/env.js +0 -146
  17. package/dist/connectors/interface.js +0 -55
  18. package/dist/connectors/manager.js +0 -91
  19. package/dist/connectors/mysql/index.js +0 -169
  20. package/dist/connectors/postgres/index.js +0 -172
  21. package/dist/connectors/sqlite/index.js +0 -208
  22. package/dist/connectors/sqlserver/index.js +0 -186
  23. package/dist/prompts/db-explainer.js +0 -201
  24. package/dist/prompts/index.js +0 -11
  25. package/dist/prompts/sql-generator.js +0 -114
  26. package/dist/resources/index.js +0 -12
  27. package/dist/resources/schema.js +0 -36
  28. package/dist/resources/tables.js +0 -17
  29. package/dist/server.js +0 -140
  30. package/dist/tools/index.js +0 -11
  31. package/dist/tools/list-connectors.js +0 -43
  32. package/dist/tools/run-query.js +0 -32
  33. package/dist/utils/response-formatter.js +0 -109
package/dist/index.js CHANGED
@@ -1,16 +1,1492 @@
1
1
  #!/usr/bin/env node
2
- // Import connector modules to register them
3
- import './connectors/postgres/index.js'; // Register PostgreSQL connector
4
- import './connectors/sqlserver/index.js'; // Register SQL Server connector
5
- import './connectors/sqlite/index.js'; // SQLite connector
6
- import './connectors/mysql/index.js'; // MySQL connector
7
- // Import main function from server.ts
8
- import { main } from './server.js';
9
- /**
10
- * Entry point for the DBHub MCP Server
11
- * Handles top-level exceptions and starts the server
12
- */
13
- main().catch(error => {
14
- console.error("Fatal error:", error);
2
+
3
+ // src/connectors/postgres/index.ts
4
+ import pg from "pg";
5
+
6
+ // src/connectors/interface.ts
7
+ var _ConnectorRegistry = class _ConnectorRegistry {
8
+ /**
9
+ * Register a new connector
10
+ */
11
+ static register(connector) {
12
+ _ConnectorRegistry.connectors.set(connector.id, connector);
13
+ }
14
+ /**
15
+ * Get a connector by ID
16
+ */
17
+ static getConnector(id) {
18
+ return _ConnectorRegistry.connectors.get(id) || null;
19
+ }
20
+ /**
21
+ * Get connector for a DSN string
22
+ * Tries to find a connector that can handle the given DSN format
23
+ */
24
+ static getConnectorForDSN(dsn) {
25
+ for (const connector of _ConnectorRegistry.connectors.values()) {
26
+ if (connector.dsnParser.isValidDSN(dsn)) {
27
+ return connector;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Get all available connector IDs
34
+ */
35
+ static getAvailableConnectors() {
36
+ return Array.from(_ConnectorRegistry.connectors.keys());
37
+ }
38
+ /**
39
+ * Get sample DSN for a specific connector
40
+ */
41
+ static getSampleDSN(connectorId) {
42
+ const connector = _ConnectorRegistry.getConnector(connectorId);
43
+ if (!connector) return null;
44
+ return connector.dsnParser.getSampleDSN();
45
+ }
46
+ /**
47
+ * Get all available sample DSNs
48
+ */
49
+ static getAllSampleDSNs() {
50
+ const samples = {};
51
+ for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
52
+ samples[id] = connector.dsnParser.getSampleDSN();
53
+ }
54
+ return samples;
55
+ }
56
+ };
57
+ _ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
58
+ var ConnectorRegistry = _ConnectorRegistry;
59
+
60
+ // src/connectors/postgres/index.ts
61
+ var { Pool } = pg;
62
+ var PostgresDSNParser = class {
63
+ parse(dsn) {
64
+ if (!this.isValidDSN(dsn)) {
65
+ throw new Error(`Invalid PostgreSQL DSN: ${dsn}`);
66
+ }
67
+ try {
68
+ const url = new URL(dsn);
69
+ const config = {
70
+ host: url.hostname,
71
+ port: url.port ? parseInt(url.port) : 5432,
72
+ database: url.pathname.substring(1),
73
+ // Remove leading '/'
74
+ user: url.username,
75
+ password: url.password
76
+ };
77
+ url.searchParams.forEach((value, key) => {
78
+ if (key === "sslmode") {
79
+ config.ssl = value !== "disable";
80
+ }
81
+ });
82
+ return config;
83
+ } catch (error) {
84
+ throw new Error(`Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`);
85
+ }
86
+ }
87
+ getSampleDSN() {
88
+ return "postgres://postgres:password@localhost:5432/postgres?sslmode=disable";
89
+ }
90
+ isValidDSN(dsn) {
91
+ try {
92
+ const url = new URL(dsn);
93
+ return url.protocol === "postgres:" || url.protocol === "postgresql:";
94
+ } catch (error) {
95
+ return false;
96
+ }
97
+ }
98
+ };
99
+ var PostgresConnector = class {
100
+ constructor() {
101
+ this.id = "postgres";
102
+ this.name = "PostgreSQL";
103
+ this.dsnParser = new PostgresDSNParser();
104
+ this.pool = null;
105
+ }
106
+ async connect(dsn) {
107
+ try {
108
+ const config = this.dsnParser.parse(dsn);
109
+ this.pool = new Pool(config);
110
+ const client = await this.pool.connect();
111
+ console.error("Successfully connected to PostgreSQL database");
112
+ client.release();
113
+ } catch (err) {
114
+ console.error("Failed to connect to PostgreSQL database:", err);
115
+ throw err;
116
+ }
117
+ }
118
+ async disconnect() {
119
+ if (this.pool) {
120
+ await this.pool.end();
121
+ this.pool = null;
122
+ }
123
+ }
124
+ async getTables() {
125
+ if (!this.pool) {
126
+ throw new Error("Not connected to database");
127
+ }
128
+ const client = await this.pool.connect();
129
+ try {
130
+ const result = await client.query(`
131
+ SELECT table_name
132
+ FROM information_schema.tables
133
+ WHERE table_schema = 'public'
134
+ ORDER BY table_name
135
+ `);
136
+ return result.rows.map((row) => row.table_name);
137
+ } finally {
138
+ client.release();
139
+ }
140
+ }
141
+ async tableExists(tableName) {
142
+ if (!this.pool) {
143
+ throw new Error("Not connected to database");
144
+ }
145
+ const client = await this.pool.connect();
146
+ try {
147
+ const result = await client.query(`
148
+ SELECT EXISTS (
149
+ SELECT FROM information_schema.tables
150
+ WHERE table_schema = 'public'
151
+ AND table_name = $1
152
+ )
153
+ `, [tableName]);
154
+ return result.rows[0].exists;
155
+ } finally {
156
+ client.release();
157
+ }
158
+ }
159
+ async getTableSchema(tableName) {
160
+ if (!this.pool) {
161
+ throw new Error("Not connected to database");
162
+ }
163
+ const client = await this.pool.connect();
164
+ try {
165
+ const result = await client.query(`
166
+ SELECT
167
+ column_name,
168
+ data_type,
169
+ is_nullable,
170
+ column_default
171
+ FROM information_schema.columns
172
+ WHERE table_schema = 'public'
173
+ AND table_name = $1
174
+ ORDER BY ordinal_position
175
+ `, [tableName]);
176
+ return result.rows;
177
+ } finally {
178
+ client.release();
179
+ }
180
+ }
181
+ async executeQuery(query) {
182
+ if (!this.pool) {
183
+ throw new Error("Not connected to database");
184
+ }
185
+ const safetyCheck = this.validateQuery(query);
186
+ if (!safetyCheck.isValid) {
187
+ throw new Error(safetyCheck.message || "Query validation failed");
188
+ }
189
+ const client = await this.pool.connect();
190
+ try {
191
+ return await client.query(query);
192
+ } finally {
193
+ client.release();
194
+ }
195
+ }
196
+ validateQuery(query) {
197
+ const normalizedQuery = query.trim().toLowerCase();
198
+ if (!normalizedQuery.startsWith("select")) {
199
+ return {
200
+ isValid: false,
201
+ message: "Only SELECT queries are allowed for security reasons."
202
+ };
203
+ }
204
+ return { isValid: true };
205
+ }
206
+ };
207
+ var postgresConnector = new PostgresConnector();
208
+ ConnectorRegistry.register(postgresConnector);
209
+
210
+ // src/connectors/sqlserver/index.ts
211
+ import sql from "mssql";
212
+ var SQLServerDSNParser = class {
213
+ parse(dsn) {
214
+ if (!this.isValidDSN(dsn)) {
215
+ throw new Error("Invalid SQL Server DSN format. Expected: sqlserver://username:password@host:port/database");
216
+ }
217
+ const url = new URL(dsn);
218
+ const host = url.hostname;
219
+ const port = url.port ? parseInt(url.port, 10) : 1433;
220
+ const database = url.pathname.substring(1);
221
+ const user = url.username;
222
+ const password = url.password;
223
+ const options = {};
224
+ for (const [key, value] of url.searchParams.entries()) {
225
+ if (key === "encrypt") {
226
+ options.encrypt = value;
227
+ } else if (key === "trustServerCertificate") {
228
+ options.trustServerCertificate = value === "true";
229
+ } else if (key === "connectTimeout") {
230
+ options.connectTimeout = parseInt(value, 10);
231
+ } else if (key === "requestTimeout") {
232
+ options.requestTimeout = parseInt(value, 10);
233
+ }
234
+ }
235
+ return {
236
+ user,
237
+ password,
238
+ server: host,
239
+ port,
240
+ database,
241
+ options: {
242
+ encrypt: options.encrypt ?? true,
243
+ // Default to encrypted connection
244
+ trustServerCertificate: options.trustServerCertificate === true,
245
+ // Need explicit conversion to boolean
246
+ connectTimeout: options.connectTimeout ?? 15e3,
247
+ requestTimeout: options.requestTimeout ?? 15e3
248
+ }
249
+ };
250
+ }
251
+ getSampleDSN() {
252
+ return "sqlserver://username:password@localhost:1433/database?encrypt=true";
253
+ }
254
+ isValidDSN(dsn) {
255
+ try {
256
+ const url = new URL(dsn);
257
+ return url.protocol === "sqlserver:";
258
+ } catch (e) {
259
+ return false;
260
+ }
261
+ }
262
+ };
263
+ var SQLServerConnector = class {
264
+ constructor() {
265
+ this.id = "sqlserver";
266
+ this.name = "SQL Server";
267
+ this.dsnParser = new SQLServerDSNParser();
268
+ }
269
+ async connect(dsn) {
270
+ try {
271
+ this.config = this.dsnParser.parse(dsn);
272
+ if (!this.config.options) {
273
+ this.config.options = {};
274
+ }
275
+ this.connection = await new sql.ConnectionPool(this.config).connect();
276
+ } catch (error) {
277
+ throw error;
278
+ }
279
+ }
280
+ async disconnect() {
281
+ if (this.connection) {
282
+ await this.connection.close();
283
+ this.connection = void 0;
284
+ }
285
+ }
286
+ async getTables() {
287
+ if (!this.connection) {
288
+ throw new Error("Not connected to SQL Server database");
289
+ }
290
+ try {
291
+ const result = await this.connection.request().query(`
292
+ SELECT TABLE_NAME
293
+ FROM INFORMATION_SCHEMA.TABLES
294
+ ORDER BY TABLE_NAME
295
+ `);
296
+ return result.recordset.map((row) => row.TABLE_NAME);
297
+ } catch (error) {
298
+ throw new Error(`Failed to get tables: ${error.message}`);
299
+ }
300
+ }
301
+ async tableExists(tableName) {
302
+ if (!this.connection) {
303
+ throw new Error("Not connected to SQL Server database");
304
+ }
305
+ try {
306
+ const result = await this.connection.request().input("tableName", sql.VarChar, tableName).query(`
307
+ SELECT COUNT(*) as count
308
+ FROM INFORMATION_SCHEMA.TABLES
309
+ WHERE TABLE_NAME = @tableName
310
+ `);
311
+ return result.recordset[0].count > 0;
312
+ } catch (error) {
313
+ throw new Error(`Failed to check if table exists: ${error.message}`);
314
+ }
315
+ }
316
+ async getTableSchema(tableName) {
317
+ if (!this.connection) {
318
+ throw new Error("Not connected to SQL Server database");
319
+ }
320
+ try {
321
+ const result = await this.connection.request().input("tableName", sql.VarChar, tableName).query(`
322
+ SELECT
323
+ COLUMN_NAME as column_name,
324
+ DATA_TYPE as data_type,
325
+ IS_NULLABLE as is_nullable,
326
+ COLUMN_DEFAULT as column_default
327
+ FROM INFORMATION_SCHEMA.COLUMNS
328
+ WHERE TABLE_NAME = @tableName
329
+ ORDER BY ORDINAL_POSITION
330
+ `);
331
+ return result.recordset;
332
+ } catch (error) {
333
+ throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
334
+ }
335
+ }
336
+ async executeQuery(query) {
337
+ if (!this.connection) {
338
+ throw new Error("Not connected to SQL Server database");
339
+ }
340
+ const safetyCheck = this.validateQuery(query);
341
+ if (!safetyCheck.isValid) {
342
+ throw new Error(safetyCheck.message || "Query validation failed");
343
+ }
344
+ try {
345
+ const result = await this.connection.request().query(query);
346
+ return {
347
+ rows: result.recordset || [],
348
+ fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
349
+ name: key
350
+ })) : [],
351
+ rowCount: result.rowsAffected[0] || 0
352
+ };
353
+ } catch (error) {
354
+ throw new Error(`Failed to execute query: ${error.message}`);
355
+ }
356
+ }
357
+ validateQuery(query) {
358
+ const normalizedQuery = query.trim().toLowerCase();
359
+ if (!normalizedQuery.startsWith("select")) {
360
+ return {
361
+ isValid: false,
362
+ message: "Only SELECT queries are allowed for security reasons."
363
+ };
364
+ }
365
+ return { isValid: true };
366
+ }
367
+ };
368
+ var sqlServerConnector = new SQLServerConnector();
369
+ ConnectorRegistry.register(sqlServerConnector);
370
+
371
+ // src/connectors/sqlite/index.ts
372
+ import Database from "better-sqlite3";
373
+ var SQLiteDSNParser = class {
374
+ parse(dsn) {
375
+ if (!this.isValidDSN(dsn)) {
376
+ throw new Error(`Invalid SQLite DSN: ${dsn}`);
377
+ }
378
+ try {
379
+ const url = new URL(dsn);
380
+ let dbPath;
381
+ if (url.hostname === "" && url.pathname === ":memory:") {
382
+ dbPath = ":memory:";
383
+ } else {
384
+ if (url.pathname.startsWith("//")) {
385
+ dbPath = url.pathname.substring(2);
386
+ } else {
387
+ dbPath = url.pathname;
388
+ }
389
+ }
390
+ return { dbPath };
391
+ } catch (error) {
392
+ throw new Error(`Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`);
393
+ }
394
+ }
395
+ getSampleDSN() {
396
+ return "sqlite:///path/to/database.db";
397
+ }
398
+ isValidDSN(dsn) {
399
+ try {
400
+ const url = new URL(dsn);
401
+ return url.protocol === "sqlite:";
402
+ } catch (error) {
403
+ return false;
404
+ }
405
+ }
406
+ };
407
+ var SQLiteConnector = class {
408
+ constructor() {
409
+ this.id = "sqlite";
410
+ this.name = "SQLite";
411
+ this.dsnParser = new SQLiteDSNParser();
412
+ this.db = null;
413
+ this.dbPath = ":memory:";
414
+ }
415
+ // Default to in-memory database
416
+ async connect(dsn, initScript) {
417
+ const config = this.dsnParser.parse(dsn);
418
+ this.dbPath = config.dbPath;
419
+ try {
420
+ this.db = new Database(this.dbPath);
421
+ console.error("Successfully connected to SQLite database");
422
+ if (initScript) {
423
+ this.db.exec(initScript);
424
+ console.error("Successfully initialized database with script");
425
+ }
426
+ } catch (error) {
427
+ console.error("Failed to connect to SQLite database:", error);
428
+ throw error;
429
+ }
430
+ }
431
+ async disconnect() {
432
+ if (this.db) {
433
+ try {
434
+ this.db.close();
435
+ this.db = null;
436
+ } catch (error) {
437
+ throw error;
438
+ }
439
+ }
440
+ return Promise.resolve();
441
+ }
442
+ async getTables() {
443
+ if (!this.db) {
444
+ throw new Error("Not connected to SQLite database");
445
+ }
446
+ try {
447
+ const rows = this.db.prepare(`
448
+ SELECT name FROM sqlite_master
449
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
450
+ ORDER BY name
451
+ `).all();
452
+ return rows.map((row) => row.name);
453
+ } catch (error) {
454
+ throw error;
455
+ }
456
+ }
457
+ async tableExists(tableName) {
458
+ if (!this.db) {
459
+ throw new Error("Not connected to SQLite database");
460
+ }
461
+ try {
462
+ const row = this.db.prepare(`
463
+ SELECT name FROM sqlite_master
464
+ WHERE type='table' AND name = ?
465
+ `).get(tableName);
466
+ return !!row;
467
+ } catch (error) {
468
+ throw error;
469
+ }
470
+ }
471
+ async getTableSchema(tableName) {
472
+ if (!this.db) {
473
+ throw new Error("Not connected to SQLite database");
474
+ }
475
+ try {
476
+ const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
477
+ const columns = rows.map((row) => ({
478
+ column_name: row.name,
479
+ data_type: row.type,
480
+ is_nullable: row.notnull === 0 ? "YES" : "NO",
481
+ // In SQLite, 0 means nullable
482
+ column_default: row.dflt_value
483
+ }));
484
+ return columns;
485
+ } catch (error) {
486
+ throw error;
487
+ }
488
+ }
489
+ async executeQuery(query) {
490
+ if (!this.db) {
491
+ throw new Error("Not connected to SQLite database");
492
+ }
493
+ const safetyCheck = this.validateQuery(query);
494
+ if (!safetyCheck.isValid) {
495
+ throw new Error(safetyCheck.message || "Query validation failed");
496
+ }
497
+ try {
498
+ const rows = this.db.prepare(query).all();
499
+ return { rows };
500
+ } catch (error) {
501
+ throw error;
502
+ }
503
+ }
504
+ validateQuery(query) {
505
+ const normalizedQuery = query.trim().toLowerCase();
506
+ if (!normalizedQuery.startsWith("select")) {
507
+ return {
508
+ isValid: false,
509
+ message: "Only SELECT queries are allowed for security reasons."
510
+ };
511
+ }
512
+ return { isValid: true };
513
+ }
514
+ };
515
+ var sqliteConnector = new SQLiteConnector();
516
+ ConnectorRegistry.register(sqliteConnector);
517
+
518
+ // src/connectors/mysql/index.ts
519
+ import mysql from "mysql2/promise";
520
+ var MySQLDSNParser = class {
521
+ parse(dsn) {
522
+ if (!this.isValidDSN(dsn)) {
523
+ throw new Error(`Invalid MySQL DSN: ${dsn}`);
524
+ }
525
+ try {
526
+ const url = new URL(dsn);
527
+ const config = {
528
+ host: url.hostname,
529
+ port: url.port ? parseInt(url.port) : 3306,
530
+ database: url.pathname.substring(1),
531
+ // Remove leading '/'
532
+ user: url.username,
533
+ password: url.password
534
+ };
535
+ url.searchParams.forEach((value, key) => {
536
+ if (key === "ssl") {
537
+ config.ssl = value === "true" ? {} : void 0;
538
+ }
539
+ });
540
+ return config;
541
+ } catch (error) {
542
+ throw new Error(`Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`);
543
+ }
544
+ }
545
+ getSampleDSN() {
546
+ return "mysql://root:password@localhost:3306/mysql";
547
+ }
548
+ isValidDSN(dsn) {
549
+ try {
550
+ const url = new URL(dsn);
551
+ return url.protocol === "mysql:";
552
+ } catch (error) {
553
+ return false;
554
+ }
555
+ }
556
+ };
557
+ var MySQLConnector = class {
558
+ constructor() {
559
+ this.id = "mysql";
560
+ this.name = "MySQL";
561
+ this.dsnParser = new MySQLDSNParser();
562
+ this.pool = null;
563
+ }
564
+ async connect(dsn) {
565
+ try {
566
+ const config = this.dsnParser.parse(dsn);
567
+ this.pool = mysql.createPool(config);
568
+ const [rows] = await this.pool.query("SELECT 1");
569
+ console.error("Successfully connected to MySQL database");
570
+ } catch (err) {
571
+ console.error("Failed to connect to MySQL database:", err);
572
+ throw err;
573
+ }
574
+ }
575
+ async disconnect() {
576
+ if (this.pool) {
577
+ await this.pool.end();
578
+ this.pool = null;
579
+ }
580
+ }
581
+ async getTables() {
582
+ if (!this.pool) {
583
+ throw new Error("Not connected to database");
584
+ }
585
+ try {
586
+ const [rows] = await this.pool.query(`
587
+ SELECT table_name
588
+ FROM information_schema.tables
589
+ WHERE table_schema = DATABASE()
590
+ ORDER BY table_name
591
+ `);
592
+ return rows.map((row) => row.table_name);
593
+ } catch (error) {
594
+ console.error("Error getting tables:", error);
595
+ throw error;
596
+ }
597
+ }
598
+ async tableExists(tableName) {
599
+ if (!this.pool) {
600
+ throw new Error("Not connected to database");
601
+ }
602
+ try {
603
+ const [rows] = await this.pool.query(`
604
+ SELECT COUNT(*) as count
605
+ FROM information_schema.tables
606
+ WHERE table_schema = DATABASE()
607
+ AND table_name = ?
608
+ `, [tableName]);
609
+ return rows[0].count > 0;
610
+ } catch (error) {
611
+ console.error("Error checking if table exists:", error);
612
+ throw error;
613
+ }
614
+ }
615
+ async getTableSchema(tableName) {
616
+ if (!this.pool) {
617
+ throw new Error("Not connected to database");
618
+ }
619
+ try {
620
+ const [rows] = await this.pool.query(`
621
+ SELECT
622
+ column_name,
623
+ data_type,
624
+ is_nullable,
625
+ column_default
626
+ FROM information_schema.columns
627
+ WHERE table_schema = DATABASE()
628
+ AND table_name = ?
629
+ ORDER BY ordinal_position
630
+ `, [tableName]);
631
+ return rows;
632
+ } catch (error) {
633
+ console.error("Error getting table schema:", error);
634
+ throw error;
635
+ }
636
+ }
637
+ async executeQuery(query) {
638
+ if (!this.pool) {
639
+ throw new Error("Not connected to database");
640
+ }
641
+ const safetyCheck = this.validateQuery(query);
642
+ if (!safetyCheck.isValid) {
643
+ throw new Error(safetyCheck.message || "Query validation failed");
644
+ }
645
+ try {
646
+ const [rows, fields] = await this.pool.query(query);
647
+ return { rows, fields };
648
+ } catch (error) {
649
+ console.error("Error executing query:", error);
650
+ throw error;
651
+ }
652
+ }
653
+ validateQuery(query) {
654
+ const normalizedQuery = query.trim().toLowerCase();
655
+ if (!normalizedQuery.startsWith("select")) {
656
+ return {
657
+ isValid: false,
658
+ message: "Only SELECT queries are allowed for security reasons."
659
+ };
660
+ }
661
+ return { isValid: true };
662
+ }
663
+ };
664
+ var mysqlConnector = new MySQLConnector();
665
+ ConnectorRegistry.register(mysqlConnector);
666
+
667
+ // src/server.ts
668
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
669
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
670
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
671
+ import express from "express";
672
+ import path3 from "path";
673
+ import { readFileSync } from "fs";
674
+ import { fileURLToPath as fileURLToPath3 } from "url";
675
+
676
+ // src/connectors/manager.ts
677
+ var managerInstance = null;
678
+ var ConnectorManager = class {
679
+ constructor() {
680
+ this.activeConnector = null;
681
+ this.connected = false;
682
+ if (!managerInstance) {
683
+ managerInstance = this;
684
+ }
685
+ }
686
+ /**
687
+ * Initialize and connect to the database using a DSN
688
+ */
689
+ async connectWithDSN(dsn, initScript) {
690
+ let connector = ConnectorRegistry.getConnectorForDSN(dsn);
691
+ if (!connector) {
692
+ throw new Error(`No connector found that can handle the DSN: ${dsn}`);
693
+ }
694
+ this.activeConnector = connector;
695
+ await this.activeConnector.connect(dsn, initScript);
696
+ this.connected = true;
697
+ }
698
+ /**
699
+ * Initialize and connect to the database using a specific connector type
700
+ */
701
+ async connectWithType(connectorType, dsn) {
702
+ const connector = ConnectorRegistry.getConnector(connectorType);
703
+ if (!connector) {
704
+ throw new Error(`Connector "${connectorType}" not found`);
705
+ }
706
+ this.activeConnector = connector;
707
+ const connectionString = dsn || connector.dsnParser.getSampleDSN();
708
+ await this.activeConnector.connect(connectionString);
709
+ this.connected = true;
710
+ }
711
+ /**
712
+ * Close the database connection
713
+ */
714
+ async disconnect() {
715
+ if (this.activeConnector && this.connected) {
716
+ await this.activeConnector.disconnect();
717
+ this.connected = false;
718
+ }
719
+ }
720
+ /**
721
+ * Get the active connector
722
+ */
723
+ getConnector() {
724
+ if (!this.activeConnector) {
725
+ throw new Error("No active connector. Call connectWithDSN() or connectWithType() first.");
726
+ }
727
+ return this.activeConnector;
728
+ }
729
+ /**
730
+ * Check if there's an active connection
731
+ */
732
+ isConnected() {
733
+ return this.connected;
734
+ }
735
+ /**
736
+ * Get all available connector types
737
+ */
738
+ static getAvailableConnectors() {
739
+ return ConnectorRegistry.getAvailableConnectors();
740
+ }
741
+ /**
742
+ * Get sample DSNs for all available connectors
743
+ */
744
+ static getAllSampleDSNs() {
745
+ return ConnectorRegistry.getAllSampleDSNs();
746
+ }
747
+ /**
748
+ * Get the current active connector instance
749
+ * This is used by resource and tool handlers
750
+ */
751
+ static getCurrentConnector() {
752
+ if (!managerInstance) {
753
+ throw new Error("ConnectorManager not initialized");
754
+ }
755
+ return managerInstance.getConnector();
756
+ }
757
+ };
758
+
759
+ // src/config/env.ts
760
+ import dotenv from "dotenv";
761
+ import path from "path";
762
+ import fs from "fs";
763
+ import { fileURLToPath } from "url";
764
+ var __filename = fileURLToPath(import.meta.url);
765
+ var __dirname = path.dirname(__filename);
766
+ function parseCommandLineArgs() {
767
+ const args = process.argv.slice(2);
768
+ const parsedManually = {};
769
+ for (let i = 0; i < args.length; i++) {
770
+ const arg = args[i];
771
+ if (arg.startsWith("--")) {
772
+ const [key, value] = arg.substring(2).split("=");
773
+ if (value) {
774
+ parsedManually[key] = value;
775
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
776
+ parsedManually[key] = args[i + 1];
777
+ i++;
778
+ } else {
779
+ parsedManually[key] = "true";
780
+ }
781
+ }
782
+ }
783
+ return parsedManually;
784
+ }
785
+ function loadEnvFiles() {
786
+ const isDevelopment = process.env.NODE_ENV === "development" || process.argv[1]?.includes("tsx");
787
+ const envFileNames = isDevelopment ? [".env.local", ".env"] : [".env"];
788
+ const envPaths = [];
789
+ for (const fileName of envFileNames) {
790
+ envPaths.push(
791
+ fileName,
792
+ // Current working directory
793
+ path.join(__dirname, "..", "..", fileName),
794
+ // Two levels up (src/config -> src -> root)
795
+ path.join(process.cwd(), fileName)
796
+ // Explicit current working directory
797
+ );
798
+ }
799
+ for (const envPath of envPaths) {
800
+ console.error(`Checking for env file: ${envPath}`);
801
+ if (fs.existsSync(envPath)) {
802
+ dotenv.config({ path: envPath });
803
+ return path.basename(envPath);
804
+ }
805
+ }
806
+ return null;
807
+ }
808
+ function isDemoMode() {
809
+ const args = parseCommandLineArgs();
810
+ return args.demo === "true";
811
+ }
812
+ function resolveDSN() {
813
+ const args = parseCommandLineArgs();
814
+ if (isDemoMode()) {
815
+ return {
816
+ dsn: "sqlite::memory:",
817
+ source: "demo mode",
818
+ isDemo: true
819
+ };
820
+ }
821
+ if (args.dsn) {
822
+ return { dsn: args.dsn, source: "command line argument" };
823
+ }
824
+ if (process.env.DSN) {
825
+ return { dsn: process.env.DSN, source: "environment variable" };
826
+ }
827
+ const loadedEnvFile = loadEnvFiles();
828
+ if (loadedEnvFile && process.env.DSN) {
829
+ return { dsn: process.env.DSN, source: `${loadedEnvFile} file` };
830
+ }
831
+ return null;
832
+ }
833
+ function resolveTransport() {
834
+ const args = parseCommandLineArgs();
835
+ if (args.transport) {
836
+ const type = args.transport === "sse" ? "sse" : "stdio";
837
+ return { type, source: "command line argument" };
838
+ }
839
+ if (process.env.TRANSPORT) {
840
+ const type = process.env.TRANSPORT === "sse" ? "sse" : "stdio";
841
+ return { type, source: "environment variable" };
842
+ }
843
+ return { type: "stdio", source: "default" };
844
+ }
845
+ function resolvePort() {
846
+ const args = parseCommandLineArgs();
847
+ if (args.port) {
848
+ const port = parseInt(args.port, 10);
849
+ return { port, source: "command line argument" };
850
+ }
851
+ if (process.env.PORT) {
852
+ const port = parseInt(process.env.PORT, 10);
853
+ return { port, source: "environment variable" };
854
+ }
855
+ return { port: 8080, source: "default" };
856
+ }
857
+
858
+ // src/config/demo-loader.ts
859
+ import fs2 from "fs";
860
+ import path2 from "path";
861
+ import { fileURLToPath as fileURLToPath2 } from "url";
862
+ var __filename2 = fileURLToPath2(import.meta.url);
863
+ var __dirname2 = path2.dirname(__filename2);
864
+ var DEMO_DATA_DIR;
865
+ var projectRootPath = path2.join(__dirname2, "..", "..", "..");
866
+ var projectResourcesPath = path2.join(projectRootPath, "resources", "employee-sqlite");
867
+ var distPath = path2.join(__dirname2, "..", "resources", "employee-sqlite");
868
+ if (fs2.existsSync(projectResourcesPath)) {
869
+ DEMO_DATA_DIR = projectResourcesPath;
870
+ } else if (fs2.existsSync(distPath)) {
871
+ DEMO_DATA_DIR = distPath;
872
+ } else {
873
+ DEMO_DATA_DIR = path2.join(process.cwd(), "resources", "employee-sqlite");
874
+ if (!fs2.existsSync(DEMO_DATA_DIR)) {
875
+ throw new Error(`Could not find employee-sqlite resources in any of the expected locations:
876
+ - ${projectResourcesPath}
877
+ - ${distPath}
878
+ - ${DEMO_DATA_DIR}`);
879
+ }
880
+ }
881
+ function loadSqlFile(fileName) {
882
+ const filePath = path2.join(DEMO_DATA_DIR, fileName);
883
+ return fs2.readFileSync(filePath, "utf8");
884
+ }
885
+ function getSqliteInMemorySetupSql() {
886
+ let sql2 = loadSqlFile("employee.sql");
887
+ const readRegex = /\.read\s+([a-zA-Z0-9_]+\.sql)/g;
888
+ let match;
889
+ while ((match = readRegex.exec(sql2)) !== null) {
890
+ const includePath = match[1];
891
+ const includeContent = loadSqlFile(includePath);
892
+ sql2 = sql2.replace(match[0], includeContent);
893
+ }
894
+ return sql2;
895
+ }
896
+
897
+ // src/resources/index.ts
898
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
899
+
900
+ // src/utils/response-formatter.ts
901
+ function formatSuccessResponse(data, meta = {}) {
902
+ return {
903
+ success: true,
904
+ data,
905
+ ...Object.keys(meta).length > 0 ? { meta } : {}
906
+ };
907
+ }
908
+ function formatErrorResponse(error, code = "ERROR", details) {
909
+ return {
910
+ success: false,
911
+ error,
912
+ code,
913
+ ...details ? { details } : {}
914
+ };
915
+ }
916
+ function createToolErrorResponse(error, code = "ERROR", details) {
917
+ return {
918
+ content: [{
919
+ type: "text",
920
+ text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
921
+ mimeType: "application/json"
922
+ }],
923
+ isError: true
924
+ };
925
+ }
926
+ function createToolSuccessResponse(data, meta = {}) {
927
+ return {
928
+ content: [{
929
+ type: "text",
930
+ text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
931
+ mimeType: "application/json"
932
+ }]
933
+ };
934
+ }
935
+ function createResourceErrorResponse(uri, error, code = "ERROR", details) {
936
+ return {
937
+ contents: [{
938
+ uri,
939
+ text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
940
+ mimeType: "application/json"
941
+ }]
942
+ };
943
+ }
944
+ function createResourceSuccessResponse(uri, data, meta = {}) {
945
+ return {
946
+ contents: [{
947
+ uri,
948
+ text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
949
+ mimeType: "application/json"
950
+ }]
951
+ };
952
+ }
953
+ function formatPromptSuccessResponse(text, references = []) {
954
+ return {
955
+ messages: [
956
+ {
957
+ role: "assistant",
958
+ content: {
959
+ type: "text",
960
+ text
961
+ }
962
+ }
963
+ ],
964
+ ...references.length > 0 ? { references } : {}
965
+ };
966
+ }
967
+ function formatPromptErrorResponse(error, code = "ERROR") {
968
+ return {
969
+ messages: [
970
+ {
971
+ role: "assistant",
972
+ content: {
973
+ type: "text",
974
+ text: `Error: ${error}`
975
+ }
976
+ }
977
+ ],
978
+ error,
979
+ code
980
+ };
981
+ }
982
+
983
+ // src/resources/tables.ts
984
+ async function tablesResourceHandler(uri, _extra) {
985
+ const connector = ConnectorManager.getCurrentConnector();
986
+ const tableNames = await connector.getTables();
987
+ const responseData = {
988
+ tables: tableNames,
989
+ count: tableNames.length
990
+ };
991
+ return createResourceSuccessResponse(uri.href, responseData);
992
+ }
993
+
994
+ // src/resources/schema.ts
995
+ async function schemaResourceHandler(uri, variables, _extra) {
996
+ const connector = ConnectorManager.getCurrentConnector();
997
+ const tableName = Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName;
998
+ try {
999
+ const columns = await connector.getTableSchema(tableName);
1000
+ const formattedColumns = columns.map((col) => ({
1001
+ name: col.column_name,
1002
+ type: col.data_type,
1003
+ nullable: col.is_nullable === "YES",
1004
+ default: col.column_default
1005
+ }));
1006
+ const responseData = {
1007
+ table: tableName,
1008
+ columns: formattedColumns,
1009
+ count: formattedColumns.length
1010
+ };
1011
+ return createResourceSuccessResponse(uri.href, responseData);
1012
+ } catch (error) {
1013
+ return createResourceErrorResponse(
1014
+ uri.href,
1015
+ `Table '${tableName}' does not exist or cannot be accessed`,
1016
+ "TABLE_NOT_FOUND"
1017
+ );
1018
+ }
1019
+ }
1020
+
1021
+ // src/resources/index.ts
1022
+ function registerResources(server) {
1023
+ server.resource(
1024
+ "tables",
1025
+ "db://tables",
1026
+ tablesResourceHandler
1027
+ );
1028
+ server.resource(
1029
+ "schema",
1030
+ new ResourceTemplate("db://schema/{tableName}", { list: void 0 }),
1031
+ schemaResourceHandler
1032
+ );
1033
+ }
1034
+
1035
+ // src/tools/run-query.ts
1036
+ import { z } from "zod";
1037
+ var runQuerySchema = {
1038
+ query: z.string().describe("SQL query to execute (SELECT only)")
1039
+ };
1040
+ async function runQueryToolHandler({ query }, _extra) {
1041
+ const connector = ConnectorManager.getCurrentConnector();
1042
+ try {
1043
+ const validationResult = connector.validateQuery(query);
1044
+ if (!validationResult.isValid) {
1045
+ return createToolErrorResponse(
1046
+ validationResult.message ?? "Unknown validation error",
1047
+ "VALIDATION_ERROR"
1048
+ );
1049
+ }
1050
+ const result = await connector.executeQuery(query);
1051
+ const responseData = {
1052
+ rows: result.rows,
1053
+ count: result.rows.length
1054
+ };
1055
+ return createToolSuccessResponse(responseData);
1056
+ } catch (error) {
1057
+ return createToolErrorResponse(
1058
+ error.message,
1059
+ "EXECUTION_ERROR"
1060
+ );
1061
+ }
1062
+ }
1063
+
1064
+ // src/tools/list-connectors.ts
1065
+ async function listConnectorsToolHandler(_args, _extra) {
1066
+ const connectors = ConnectorManager.getAvailableConnectors();
1067
+ const samples = ConnectorRegistry.getAllSampleDSNs();
1068
+ let activeConnectorId = null;
1069
+ try {
1070
+ const activeConnector = ConnectorManager.getCurrentConnector();
1071
+ activeConnectorId = activeConnector.id;
1072
+ } catch (error) {
1073
+ }
1074
+ const isDemo = isDemoMode();
1075
+ if (isDemo && !activeConnectorId) {
1076
+ activeConnectorId = "sqlite";
1077
+ }
1078
+ const sampleObjects = Object.entries(samples).map(([id, dsn]) => ({
1079
+ id,
1080
+ dsn,
1081
+ active: id === activeConnectorId
1082
+ }));
1083
+ const responseData = {
1084
+ connectors: sampleObjects,
1085
+ count: sampleObjects.length,
1086
+ activeConnector: activeConnectorId,
1087
+ demoMode: isDemo
1088
+ };
1089
+ return createToolSuccessResponse(responseData);
1090
+ }
1091
+
1092
+ // src/tools/index.ts
1093
+ function registerTools(server) {
1094
+ server.tool(
1095
+ "run_query",
1096
+ runQuerySchema,
1097
+ runQueryToolHandler
1098
+ );
1099
+ server.tool(
1100
+ "list_connectors",
1101
+ {},
1102
+ listConnectorsToolHandler
1103
+ );
1104
+ }
1105
+
1106
+ // src/prompts/sql-generator.ts
1107
+ import { z as z2 } from "zod";
1108
+ var sqlGeneratorSchema = {
1109
+ description: z2.string().describe("Natural language description of the SQL query to generate"),
1110
+ dialect: z2.enum(["postgres", "sqlite"]).optional().describe("SQL dialect to use (optional)")
1111
+ };
1112
+ async function sqlGeneratorPromptHandler({ description, dialect }, _extra) {
1113
+ try {
1114
+ const connector = ConnectorManager.getCurrentConnector();
1115
+ const sqlDialect = dialect || (connector.id === "postgres" ? "postgres" : connector.id === "sqlite" ? "sqlite" : "postgres");
1116
+ const tables = await connector.getTables();
1117
+ const tableSchemas = await Promise.all(
1118
+ tables.map(async (table) => {
1119
+ try {
1120
+ const columns = await connector.getTableSchema(table);
1121
+ return {
1122
+ table,
1123
+ columns: columns.map((col) => ({
1124
+ name: col.column_name,
1125
+ type: col.data_type
1126
+ }))
1127
+ };
1128
+ } catch (error) {
1129
+ return null;
1130
+ }
1131
+ })
1132
+ );
1133
+ const accessibleSchemas = tableSchemas.filter((schema) => schema !== null);
1134
+ const schemaContext = accessibleSchemas.length > 0 ? `Available tables and their columns:
1135
+ ${accessibleSchemas.map(
1136
+ (schema) => `- ${schema.table}: ${schema.columns.map(
1137
+ (col) => `${col.name} (${col.type})`
1138
+ ).join(", ")}`
1139
+ ).join("\n")}` : "No schema information available.";
1140
+ const dialectExamples = {
1141
+ postgres: [
1142
+ "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day'",
1143
+ "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
1144
+ "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
1145
+ ],
1146
+ sqlite: [
1147
+ "SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
1148
+ "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
1149
+ "SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
1150
+ ]
1151
+ };
1152
+ const prompt = `
1153
+ Generate a ${sqlDialect} SQL query based on this description: "${description}"
1154
+
1155
+ ${schemaContext}
1156
+
1157
+ The query should:
1158
+ 1. Be written for ${sqlDialect} dialect
1159
+ 2. Use only the available tables and columns
1160
+ 3. Prioritize readability
1161
+ 4. Include appropriate comments
1162
+ 5. Be compatible with ${sqlDialect} syntax
1163
+ `;
1164
+ let generatedSQL;
1165
+ if (description.toLowerCase().includes("count")) {
1166
+ generatedSQL = `-- Count query generated from: "${description}"
1167
+ SELECT COUNT(*) AS count
1168
+ FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name"};`;
1169
+ } else if (description.toLowerCase().includes("average") || description.toLowerCase().includes("avg")) {
1170
+ const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
1171
+ const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find((col) => ["int", "numeric", "decimal", "float", "real", "double"].some((t) => col.type.includes(t)))?.name || "numeric_column" : "numeric_column";
1172
+ generatedSQL = `-- Average query generated from: "${description}"
1173
+ SELECT AVG(${numericColumn}) AS average
1174
+ FROM ${table};`;
1175
+ } else if (description.toLowerCase().includes("join")) {
1176
+ generatedSQL = `-- Join query generated from: "${description}"
1177
+ SELECT t1.*, t2.*
1178
+ FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"} t1
1179
+ JOIN ${accessibleSchemas.length > 1 ? accessibleSchemas[1]?.table : "table2"} t2
1180
+ ON t1.id = t2.${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"}_id;`;
1181
+ } else {
1182
+ const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
1183
+ generatedSQL = `-- Query generated from: "${description}"
1184
+ SELECT *
1185
+ FROM ${table}
1186
+ LIMIT 10;`;
1187
+ }
1188
+ return formatPromptSuccessResponse(
1189
+ generatedSQL,
1190
+ // Add references to example queries that could help
1191
+ dialectExamples[sqlDialect]
1192
+ );
1193
+ } catch (error) {
1194
+ return formatPromptErrorResponse(
1195
+ `Failed to generate SQL: ${error.message}`,
1196
+ "SQL_GENERATION_ERROR"
1197
+ );
1198
+ }
1199
+ }
1200
+
1201
+ // src/prompts/db-explainer.ts
1202
+ import { z as z3 } from "zod";
1203
+ var dbExplainerSchema = {
1204
+ target: z3.string().describe("Name of the table, column, or database to explain")
1205
+ };
1206
+ async function dbExplainerPromptHandler({ target }, _extra) {
1207
+ try {
1208
+ const connector = ConnectorManager.getCurrentConnector();
1209
+ const tables = await connector.getTables();
1210
+ const normalizedTarget = target.toLowerCase();
1211
+ const matchingTable = tables.find((t) => t.toLowerCase() === normalizedTarget);
1212
+ if (matchingTable) {
1213
+ const columns = await connector.getTableSchema(matchingTable);
1214
+ const tableDescription = `Table: ${matchingTable}
1215
+
1216
+ Structure:
1217
+ ${columns.map((col) => `- ${col.column_name} (${col.data_type})${col.is_nullable === "YES" ? ", nullable" : ""}${col.column_default ? `, default: ${col.column_default}` : ""}`).join("\n")}
1218
+
1219
+ Purpose:
1220
+ This table appears to store ${determineTablePurpose(matchingTable, columns)}
1221
+
1222
+ Relationships:
1223
+ ${determineRelationships(matchingTable, columns)}`;
1224
+ return formatPromptSuccessResponse(tableDescription);
1225
+ }
1226
+ if (target.includes(".")) {
1227
+ const [tableName, columnName] = target.split(".");
1228
+ if (tables.find((t) => t.toLowerCase() === tableName.toLowerCase())) {
1229
+ const columns = await connector.getTableSchema(tableName);
1230
+ const column = columns.find((c) => c.column_name.toLowerCase() === columnName.toLowerCase());
1231
+ if (column) {
1232
+ const columnDescription = `Column: ${tableName}.${column.column_name}
1233
+
1234
+ Type: ${column.data_type}
1235
+ Nullable: ${column.is_nullable === "YES" ? "Yes" : "No"}
1236
+ Default: ${column.column_default || "None"}
1237
+
1238
+ Purpose:
1239
+ ${determineColumnPurpose(column.column_name, column.data_type)}`;
1240
+ return formatPromptSuccessResponse(columnDescription);
1241
+ }
1242
+ }
1243
+ }
1244
+ if (["database", "db", "schema", "overview", "all"].includes(normalizedTarget)) {
1245
+ let dbOverview = `Database Overview
1246
+
1247
+ Tables: ${tables.length}
1248
+ ${tables.map((t) => `- ${t}`).join("\n")}
1249
+
1250
+ This database ${describeDatabasePurpose(tables)}`;
1251
+ return formatPromptSuccessResponse(dbOverview);
1252
+ }
1253
+ const possibleTableMatches = tables.filter(
1254
+ (t) => t.toLowerCase().includes(normalizedTarget) || normalizedTarget.includes(t.toLowerCase())
1255
+ );
1256
+ if (possibleTableMatches.length > 0) {
1257
+ return formatPromptSuccessResponse(
1258
+ `Could not find exact match for "${target}". Did you mean one of these tables?
1259
+
1260
+ ${possibleTableMatches.join("\n")}`
1261
+ );
1262
+ }
1263
+ return formatPromptErrorResponse(
1264
+ `Could not find a table, column, or database feature matching "${target}"`,
1265
+ "NOT_FOUND"
1266
+ );
1267
+ } catch (error) {
1268
+ return formatPromptErrorResponse(
1269
+ `Error explaining database: ${error.message}`,
1270
+ "EXPLANATION_ERROR"
1271
+ );
1272
+ }
1273
+ }
1274
+ function determineTablePurpose(tableName, columns) {
1275
+ const lowerTableName = tableName.toLowerCase();
1276
+ const columnNames = columns.map((c) => c.column_name.toLowerCase());
1277
+ if (lowerTableName.includes("user") || columnNames.includes("username") || columnNames.includes("email")) {
1278
+ return "user information and profiles";
1279
+ }
1280
+ if (lowerTableName.includes("order") || lowerTableName.includes("purchase")) {
1281
+ return "order or purchase transactions";
1282
+ }
1283
+ if (lowerTableName.includes("product") || lowerTableName.includes("item")) {
1284
+ return "product or item information";
1285
+ }
1286
+ if (lowerTableName.includes("log") || columnNames.includes("timestamp")) {
1287
+ return "event or activity logs";
1288
+ }
1289
+ if (columnNames.includes("created_at") && columnNames.includes("updated_at")) {
1290
+ return "tracking timestamped data records";
1291
+ }
1292
+ return "data related to " + tableName;
1293
+ }
1294
+ function determineRelationships(tableName, columns) {
1295
+ const potentialRelationships = [];
1296
+ const idColumns = columns.filter(
1297
+ (c) => c.column_name.toLowerCase().endsWith("_id") && !c.column_name.toLowerCase().startsWith(tableName.toLowerCase())
1298
+ );
1299
+ if (idColumns.length > 0) {
1300
+ idColumns.forEach((col) => {
1301
+ const referencedTable = col.column_name.toLowerCase().replace("_id", "");
1302
+ potentialRelationships.push(`May have a relationship with the "${referencedTable}" table (via ${col.column_name})`);
1303
+ });
1304
+ }
1305
+ if (columns.some((c) => c.column_name.toLowerCase() === "id")) {
1306
+ potentialRelationships.push(`May be referenced by other tables as "${tableName.toLowerCase()}_id"`);
1307
+ }
1308
+ return potentialRelationships.length > 0 ? potentialRelationships.join("\n") : "No obvious relationships identified based on column names";
1309
+ }
1310
+ function determineColumnPurpose(columnName, dataType) {
1311
+ const lowerColumnName = columnName.toLowerCase();
1312
+ if (lowerColumnName === "id") {
1313
+ return "Primary identifier for records in this table";
1314
+ }
1315
+ if (lowerColumnName.endsWith("_id")) {
1316
+ const referencedTable = lowerColumnName.replace("_id", "");
1317
+ return `Foreign key reference to the "${referencedTable}" table`;
1318
+ }
1319
+ if (lowerColumnName.includes("name")) {
1320
+ return "Stores name information";
1321
+ }
1322
+ if (lowerColumnName.includes("email")) {
1323
+ return "Stores email address information";
1324
+ }
1325
+ if (lowerColumnName.includes("password") || lowerColumnName.includes("hash")) {
1326
+ return "Stores security credential information (likely hashed)";
1327
+ }
1328
+ if (lowerColumnName === "created_at" || lowerColumnName === "created_on") {
1329
+ return "Timestamp for when the record was created";
1330
+ }
1331
+ if (lowerColumnName === "updated_at" || lowerColumnName === "modified_at") {
1332
+ return "Timestamp for when the record was last updated";
1333
+ }
1334
+ if (lowerColumnName.includes("date") || lowerColumnName.includes("time")) {
1335
+ return "Stores date or time information";
1336
+ }
1337
+ if (lowerColumnName.includes("price") || lowerColumnName.includes("cost") || lowerColumnName.includes("amount")) {
1338
+ return "Stores monetary value information";
1339
+ }
1340
+ if (dataType.includes("boolean")) {
1341
+ return "Stores a true/false flag";
1342
+ }
1343
+ if (dataType.includes("json")) {
1344
+ return "Stores structured JSON data";
1345
+ }
1346
+ if (dataType.includes("text") || dataType.includes("varchar") || dataType.includes("char")) {
1347
+ return "Stores text information";
1348
+ }
1349
+ return `Stores ${dataType} data`;
1350
+ }
1351
+ function describeDatabasePurpose(tables) {
1352
+ const tableNames = tables.map((t) => t.toLowerCase());
1353
+ if (tableNames.some((t) => t.includes("user")) && tableNames.some((t) => t.includes("order"))) {
1354
+ return "appears to be an e-commerce or customer order management system";
1355
+ }
1356
+ if (tableNames.some((t) => t.includes("patient")) || tableNames.some((t) => t.includes("medical"))) {
1357
+ return "appears to be related to healthcare or medical record management";
1358
+ }
1359
+ if (tableNames.some((t) => t.includes("student")) || tableNames.some((t) => t.includes("course"))) {
1360
+ return "appears to be related to education or student management";
1361
+ }
1362
+ if (tableNames.some((t) => t.includes("employee")) || tableNames.some((t) => t.includes("payroll"))) {
1363
+ return "appears to be related to HR or employee management";
1364
+ }
1365
+ if (tableNames.some((t) => t.includes("inventory")) || tableNames.some((t) => t.includes("stock"))) {
1366
+ return "appears to be related to inventory or stock management";
1367
+ }
1368
+ return "contains multiple tables that store related information";
1369
+ }
1370
+
1371
+ // src/prompts/index.ts
1372
+ function registerPrompts(server) {
1373
+ server.prompt(
1374
+ "generate_sql",
1375
+ "Generate SQL queries from natural language descriptions",
1376
+ sqlGeneratorSchema,
1377
+ sqlGeneratorPromptHandler
1378
+ );
1379
+ server.prompt(
1380
+ "explain_db",
1381
+ "Get explanations about database tables, columns, and structures",
1382
+ dbExplainerSchema,
1383
+ dbExplainerPromptHandler
1384
+ );
1385
+ }
1386
+
1387
+ // src/server.ts
1388
+ var __filename3 = fileURLToPath3(import.meta.url);
1389
+ var __dirname3 = path3.dirname(__filename3);
1390
+ var packageJsonPath = path3.join(__dirname3, "..", "package.json");
1391
+ var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1392
+ var SERVER_NAME = "DBHub MCP Server";
1393
+ var SERVER_VERSION = packageJson.version;
1394
+ function generateBanner(version, isDemo = false) {
1395
+ const demoText = isDemo ? " [DEMO MODE]" : "";
1396
+ return `
1397
+ _____ ____ _ _ _
1398
+ | __ \\| _ \\| | | | | |
1399
+ | | | | |_) | |_| |_ _| |__
1400
+ | | | | _ <| _ | | | | '_ \\
1401
+ | |__| | |_) | | | | |_| | |_) |
1402
+ |_____/|____/|_| |_|\\__,_|_.__/
1403
+
1404
+ v${version}${demoText} - Universal Database MCP Server
1405
+ `;
1406
+ }
1407
+ async function main() {
1408
+ try {
1409
+ const dsnData = resolveDSN();
1410
+ if (!dsnData) {
1411
+ const samples = ConnectorRegistry.getAllSampleDSNs();
1412
+ const sampleFormats = Object.entries(samples).map(([id, dsn]) => ` - ${id}: ${dsn}`).join("\n");
1413
+ console.error(`
1414
+ ERROR: Database connection string (DSN) is required.
1415
+ Please provide the DSN in one of these ways (in order of priority):
1416
+
1417
+ 1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
1418
+ 2. Command line argument: --dsn="your-connection-string"
1419
+ 3. Environment variable: export DSN="your-connection-string"
1420
+ 4. .env file: DSN=your-connection-string
1421
+
1422
+ Example formats:
1423
+ ${sampleFormats}
1424
+
1425
+ See documentation for more details on configuring database connections.
1426
+ `);
1427
+ process.exit(1);
1428
+ }
1429
+ const server = new McpServer2({
1430
+ name: SERVER_NAME,
1431
+ version: SERVER_VERSION
1432
+ });
1433
+ registerResources(server);
1434
+ registerTools(server);
1435
+ registerPrompts(server);
1436
+ const connectorManager = new ConnectorManager();
1437
+ console.error(`Connecting with DSN: ${dsnData.dsn}`);
1438
+ console.error(`DSN source: ${dsnData.source}`);
1439
+ if (dsnData.isDemo) {
1440
+ console.error("Running in demo mode with sample employee database");
1441
+ const initScript = getSqliteInMemorySetupSql();
1442
+ await connectorManager.connectWithDSN(dsnData.dsn, initScript);
1443
+ } else {
1444
+ await connectorManager.connectWithDSN(dsnData.dsn);
1445
+ }
1446
+ const transportData = resolveTransport();
1447
+ console.error(`Using transport: ${transportData.type}`);
1448
+ console.error(`Transport source: ${transportData.source}`);
1449
+ console.error(generateBanner(SERVER_VERSION, dsnData.isDemo));
1450
+ if (transportData.type === "sse") {
1451
+ const app = express();
1452
+ let transport;
1453
+ app.get("/sse", async (req, res) => {
1454
+ transport = new SSEServerTransport("/message", res);
1455
+ console.error("Client connected", transport?.["_sessionId"]);
1456
+ await server.connect(transport);
1457
+ res.on("close", () => {
1458
+ console.error("Client Disconnected", transport?.["_sessionId"]);
1459
+ });
1460
+ });
1461
+ app.post("/message", async (req, res) => {
1462
+ console.error("Client Message", transport?.["_sessionId"]);
1463
+ await transport.handlePostMessage(req, res, req.body);
1464
+ });
1465
+ const portData = resolvePort();
1466
+ const port = portData.port;
1467
+ console.error(`Port source: ${portData.source}`);
1468
+ app.listen(port, () => {
1469
+ console.error(`DBHub server listening at http://localhost:${port}`);
1470
+ console.error(`Connect to MCP server at http://localhost:${port}/sse`);
1471
+ });
1472
+ } else {
1473
+ const transport = new StdioServerTransport();
1474
+ console.error("Starting with STDIO transport");
1475
+ await server.connect(transport);
1476
+ process.on("SIGINT", async () => {
1477
+ console.error("Shutting down...");
1478
+ await transport.close();
1479
+ process.exit(0);
1480
+ });
1481
+ }
1482
+ } catch (err) {
1483
+ console.error("Fatal error:", err);
15
1484
  process.exit(1);
1485
+ }
1486
+ }
1487
+
1488
+ // src/index.ts
1489
+ main().catch((error) => {
1490
+ console.error("Fatal error:", error);
1491
+ process.exit(1);
16
1492
  });