@adevguide/mcp-database-server 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2406 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import dotenv from "dotenv";
5
+ import { parseArgs } from "util";
6
+ import { readFileSync } from "fs";
7
+ import { fileURLToPath } from "url";
8
+ import { dirname as dirname2, join as join2 } from "path";
9
+
10
+ // src/config.ts
11
+ import fs from "fs/promises";
12
+ import { existsSync } from "fs";
13
+ import { dirname, resolve, join } from "path";
14
+
15
+ // src/types.ts
16
+ import { z } from "zod";
17
+ var ConnectionPoolSchema = z.object({
18
+ min: z.number().min(0).optional(),
19
+ max: z.number().min(1).optional(),
20
+ idleTimeoutMillis: z.number().min(0).optional(),
21
+ connectionTimeoutMillis: z.number().min(0).optional()
22
+ });
23
+ var IntrospectionOptionsSchema = z.object({
24
+ includeViews: z.boolean().optional().default(true),
25
+ includeRoutines: z.boolean().optional().default(false),
26
+ maxTables: z.number().min(1).optional(),
27
+ excludeSchemas: z.array(z.string()).optional(),
28
+ includeSchemas: z.array(z.string()).optional()
29
+ });
30
+ var DatabaseConfigSchema = z.object({
31
+ id: z.string().min(1),
32
+ type: z.enum(["postgres", "mysql", "mssql", "sqlite", "oracle"]),
33
+ url: z.string().optional(),
34
+ path: z.string().optional(),
35
+ readOnly: z.boolean().optional().default(true),
36
+ pool: ConnectionPoolSchema.optional(),
37
+ introspection: IntrospectionOptionsSchema.optional(),
38
+ eagerConnect: z.boolean().optional().default(false)
39
+ });
40
+ var ServerConfigSchema = z.object({
41
+ databases: z.array(DatabaseConfigSchema).min(1),
42
+ cache: z.object({
43
+ directory: z.string().optional().default(".sql-mcp-cache"),
44
+ ttlMinutes: z.number().min(0).optional().default(10)
45
+ }).optional().default({ directory: ".sql-mcp-cache", ttlMinutes: 10 }),
46
+ security: z.object({
47
+ allowWrite: z.boolean().optional().default(false),
48
+ allowedWriteOperations: z.array(z.string()).optional(),
49
+ disableDangerousOperations: z.boolean().optional().default(true),
50
+ redactSecrets: z.boolean().optional().default(true)
51
+ }).optional().default({ allowWrite: false, disableDangerousOperations: true, redactSecrets: true }),
52
+ logging: z.object({
53
+ level: z.enum(["trace", "debug", "info", "warn", "error"]).optional().default("info"),
54
+ pretty: z.boolean().optional().default(false)
55
+ }).optional().default({ level: "info", pretty: false })
56
+ });
57
+ var DatabaseError = class extends Error {
58
+ constructor(message, _code, _dbId, _originalError) {
59
+ super(message);
60
+ this._code = _code;
61
+ this._dbId = _dbId;
62
+ this._originalError = _originalError;
63
+ this.name = "DatabaseError";
64
+ }
65
+ };
66
+ var ConfigError = class extends Error {
67
+ constructor(message, _details) {
68
+ super(message);
69
+ this._details = _details;
70
+ this.name = "ConfigError";
71
+ }
72
+ };
73
+ var CacheError = class extends Error {
74
+ constructor(message, _originalError) {
75
+ super(message);
76
+ this._originalError = _originalError;
77
+ this.name = "CacheError";
78
+ }
79
+ };
80
+
81
+ // src/utils.ts
82
+ import crypto from "crypto";
83
+ import { URL } from "url";
84
+ function redactUrl(url) {
85
+ try {
86
+ if (url.includes("://")) {
87
+ const urlObj = new URL(url);
88
+ if (urlObj.password) {
89
+ urlObj.password = "***";
90
+ }
91
+ if (urlObj.username && urlObj.password) {
92
+ return urlObj.toString();
93
+ }
94
+ }
95
+ if (url.includes("Password=")) {
96
+ return url.replace(/(Password=)[^;]+/gi, "$1***");
97
+ }
98
+ if (url.includes("/") && url.includes("@")) {
99
+ return url.replace(/\/[^@]+@/, "/***@");
100
+ }
101
+ return url;
102
+ } catch {
103
+ return url.replace(/:\/\/[^@]*@/, "://***@");
104
+ }
105
+ }
106
+ function interpolateEnv(value) {
107
+ return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
108
+ return process.env[varName] || "";
109
+ });
110
+ }
111
+ function generateSchemaVersion(schema) {
112
+ const hash = crypto.createHash("sha256");
113
+ const schemaData = {
114
+ dbType: schema.dbType,
115
+ schemas: schema.schemas.map((s) => ({
116
+ name: s.name,
117
+ tables: s.tables.sort((a, b) => a.name.localeCompare(b.name)).map((t) => ({
118
+ name: t.name,
119
+ type: t.type,
120
+ columns: t.columns.sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
121
+ name: c.name,
122
+ dataType: c.dataType,
123
+ nullable: c.nullable
124
+ })),
125
+ foreignKeys: t.foreignKeys.sort((a, b) => a.name.localeCompare(b.name))
126
+ }))
127
+ }))
128
+ };
129
+ hash.update(JSON.stringify(schemaData));
130
+ return hash.digest("hex").substring(0, 16);
131
+ }
132
+ function inferRelationships(schema) {
133
+ const relationships = [];
134
+ const tableLookup = /* @__PURE__ */ new Map();
135
+ for (const schemaObj of schema.schemas) {
136
+ for (const table of schemaObj.tables) {
137
+ const fullName = `${schemaObj.name}.${table.name}`;
138
+ const pk = table.primaryKey?.columns || [];
139
+ tableLookup.set(table.name.toLowerCase(), { schema: schemaObj.name, pk });
140
+ tableLookup.set(fullName.toLowerCase(), { schema: schemaObj.name, pk });
141
+ }
142
+ }
143
+ for (const schemaObj of schema.schemas) {
144
+ for (const table of schemaObj.tables) {
145
+ for (const column of table.columns) {
146
+ const columnName = column.name.toLowerCase();
147
+ const patterns = [
148
+ /^(.+?)_id$/,
149
+ /^(.+?)id$/i
150
+ ];
151
+ for (const pattern of patterns) {
152
+ const match = columnName.match(pattern);
153
+ if (match) {
154
+ const referencedTableName = match[1].toLowerCase();
155
+ const referencedTable = tableLookup.get(referencedTableName);
156
+ if (referencedTable && referencedTable.pk.length > 0) {
157
+ relationships.push({
158
+ fromSchema: schemaObj.name,
159
+ fromTable: table.name,
160
+ fromColumns: [column.name],
161
+ toSchema: referencedTable.schema,
162
+ toTable: referencedTableName,
163
+ toColumns: referencedTable.pk,
164
+ type: "inferred",
165
+ confidence: 0.7
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ return relationships;
174
+ }
175
+ function extractTableNames(sql) {
176
+ const tables = /* @__PURE__ */ new Set();
177
+ const patterns = [
178
+ /(?:FROM|JOIN|INTO|UPDATE)\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)/gi,
179
+ /DELETE\s+FROM\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)/gi
180
+ ];
181
+ for (const pattern of patterns) {
182
+ const matches = sql.matchAll(pattern);
183
+ for (const match of matches) {
184
+ if (match[1]) {
185
+ tables.add(match[1].toLowerCase());
186
+ }
187
+ }
188
+ }
189
+ return Array.from(tables);
190
+ }
191
+ function isWriteOperation(sql) {
192
+ const upperSql = sql.trim().toUpperCase();
193
+ const writeKeywords = [
194
+ "INSERT",
195
+ "UPDATE",
196
+ "DELETE",
197
+ "CREATE",
198
+ "ALTER",
199
+ "DROP",
200
+ "TRUNCATE",
201
+ "REPLACE",
202
+ "MERGE"
203
+ ];
204
+ for (const keyword of writeKeywords) {
205
+ if (upperSql.startsWith(keyword)) {
206
+ return true;
207
+ }
208
+ }
209
+ return false;
210
+ }
211
+ function findJoinPaths(tables, relationships, maxDepth = 3) {
212
+ if (tables.length < 2) {
213
+ return [];
214
+ }
215
+ const paths = [];
216
+ const graph = /* @__PURE__ */ new Map();
217
+ for (const rel of relationships) {
218
+ const fromKey = `${rel.fromSchema}.${rel.fromTable}`.toLowerCase();
219
+ const toKey = `${rel.toSchema}.${rel.toTable}`.toLowerCase();
220
+ if (!graph.has(fromKey)) {
221
+ graph.set(fromKey, []);
222
+ }
223
+ graph.get(fromKey).push(rel);
224
+ const reverseRel = {
225
+ ...rel,
226
+ fromSchema: rel.toSchema,
227
+ fromTable: rel.toTable,
228
+ fromColumns: rel.toColumns,
229
+ toSchema: rel.fromSchema,
230
+ toTable: rel.fromTable,
231
+ toColumns: rel.fromColumns
232
+ };
233
+ if (!graph.has(toKey)) {
234
+ graph.set(toKey, []);
235
+ }
236
+ graph.get(toKey).push(reverseRel);
237
+ }
238
+ const start = tables[0].toLowerCase();
239
+ const end = tables[1].toLowerCase();
240
+ const queue = [{ current: start, path: [] }];
241
+ const visited = /* @__PURE__ */ new Set([start]);
242
+ while (queue.length > 0) {
243
+ const { current, path: path2 } = queue.shift();
244
+ if (path2.length >= maxDepth) {
245
+ continue;
246
+ }
247
+ const neighbors = graph.get(current) || [];
248
+ for (const rel of neighbors) {
249
+ const next = `${rel.toSchema}.${rel.toTable}`.toLowerCase();
250
+ if (next === end) {
251
+ paths.push({
252
+ tables: [start, ...path2.map((r) => `${r.toSchema}.${r.toTable}`), end],
253
+ joins: [...path2, rel]
254
+ });
255
+ continue;
256
+ }
257
+ if (!visited.has(next)) {
258
+ visited.add(next);
259
+ queue.push({ current: next, path: [...path2, rel] });
260
+ }
261
+ }
262
+ }
263
+ return paths;
264
+ }
265
+
266
+ // src/config.ts
267
+ function findConfigFile(fileName, startDir = process.cwd()) {
268
+ let currentDir = resolve(startDir);
269
+ while (true) {
270
+ const configPath = join(currentDir, fileName);
271
+ if (existsSync(configPath)) {
272
+ return configPath;
273
+ }
274
+ const parentDir = dirname(currentDir);
275
+ if (parentDir === currentDir) {
276
+ break;
277
+ }
278
+ currentDir = parentDir;
279
+ }
280
+ return null;
281
+ }
282
+ async function loadConfig(configPath) {
283
+ try {
284
+ const content = await fs.readFile(configPath, "utf-8");
285
+ const rawConfig = JSON.parse(content);
286
+ const interpolatedConfig = interpolateConfigValues(rawConfig);
287
+ const config = ServerConfigSchema.parse(interpolatedConfig);
288
+ validateDatabaseConfigs(config);
289
+ return config;
290
+ } catch (error) {
291
+ if (error.name === "ZodError") {
292
+ throw new ConfigError("Configuration validation failed", error.errors);
293
+ }
294
+ throw new ConfigError(`Failed to load config from ${configPath}: ${error.message}`);
295
+ }
296
+ }
297
+ function interpolateConfigValues(obj) {
298
+ if (typeof obj === "string") {
299
+ return interpolateEnv(obj);
300
+ }
301
+ if (Array.isArray(obj)) {
302
+ return obj.map(interpolateConfigValues);
303
+ }
304
+ if (obj && typeof obj === "object") {
305
+ const result = {};
306
+ for (const [key, value] of Object.entries(obj)) {
307
+ result[key] = interpolateConfigValues(value);
308
+ }
309
+ return result;
310
+ }
311
+ return obj;
312
+ }
313
+ function validateDatabaseConfigs(config) {
314
+ const ids = /* @__PURE__ */ new Set();
315
+ for (const db of config.databases) {
316
+ if (ids.has(db.id)) {
317
+ throw new ConfigError(`Duplicate database ID: ${db.id}`);
318
+ }
319
+ ids.add(db.id);
320
+ if (db.type === "sqlite") {
321
+ if (!db.path && !db.url) {
322
+ throw new ConfigError(`SQLite database ${db.id} requires 'path' or 'url'`);
323
+ }
324
+ } else {
325
+ if (!db.url) {
326
+ throw new ConfigError(`Database ${db.id} requires 'url'`);
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ // src/adapters/postgres.ts
333
+ import pg from "pg";
334
+
335
+ // src/logger.ts
336
+ import pino from "pino";
337
+ var logger;
338
+ function initLogger(level = "info", pretty = false) {
339
+ logger = pino({
340
+ level,
341
+ transport: pretty ? {
342
+ target: "pino-pretty",
343
+ options: {
344
+ colorize: true,
345
+ translateTime: "SYS:standard",
346
+ ignore: "pid,hostname"
347
+ }
348
+ } : void 0
349
+ });
350
+ return logger;
351
+ }
352
+ function getLogger() {
353
+ if (!logger) {
354
+ logger = initLogger();
355
+ }
356
+ return logger;
357
+ }
358
+
359
+ // src/adapters/base.ts
360
+ var BaseAdapter = class {
361
+ constructor(_config) {
362
+ this._config = _config;
363
+ }
364
+ logger = getLogger();
365
+ connected = false;
366
+ ensureConnected() {
367
+ if (!this.connected) {
368
+ throw new DatabaseError(
369
+ "Database not connected",
370
+ "NOT_CONNECTED",
371
+ this._config.id
372
+ );
373
+ }
374
+ }
375
+ handleError(error, operation) {
376
+ this.logger.error({ error, dbId: this._config.id, operation }, "Database operation failed");
377
+ throw new DatabaseError(
378
+ `${operation} failed: ${error.message}`,
379
+ error.code || "UNKNOWN_ERROR",
380
+ this._config.id,
381
+ error
382
+ );
383
+ }
384
+ };
385
+
386
+ // src/adapters/postgres.ts
387
+ var { Pool } = pg;
388
+ var PostgresAdapter = class extends BaseAdapter {
389
+ pool;
390
+ async connect() {
391
+ try {
392
+ this.pool = new Pool({
393
+ connectionString: this._config.url,
394
+ min: this._config.pool?.min || 2,
395
+ max: this._config.pool?.max || 10,
396
+ idleTimeoutMillis: this._config.pool?.idleTimeoutMillis || 3e4,
397
+ connectionTimeoutMillis: this._config.pool?.connectionTimeoutMillis || 1e4
398
+ });
399
+ const client = await this.pool.connect();
400
+ client.release();
401
+ this.connected = true;
402
+ this.logger.info({ dbId: this._config.id }, "PostgreSQL connected");
403
+ } catch (error) {
404
+ this.handleError(error, "connect");
405
+ }
406
+ }
407
+ async disconnect() {
408
+ if (this.pool) {
409
+ await this.pool.end();
410
+ this.pool = void 0;
411
+ this.connected = false;
412
+ this.logger.info({ dbId: this._config.id }, "PostgreSQL disconnected");
413
+ }
414
+ }
415
+ async introspect(options) {
416
+ this.ensureConnected();
417
+ try {
418
+ const schemas = await this.getSchemas(options);
419
+ const dbSchema = {
420
+ dbId: this._config.id,
421
+ dbType: "postgres",
422
+ schemas,
423
+ introspectedAt: /* @__PURE__ */ new Date(),
424
+ version: ""
425
+ };
426
+ dbSchema.version = generateSchemaVersion(dbSchema);
427
+ return dbSchema;
428
+ } catch (error) {
429
+ this.handleError(error, "introspect");
430
+ }
431
+ }
432
+ async getSchemas(options) {
433
+ const result = [];
434
+ const schemasQuery = `
435
+ SELECT schema_name
436
+ FROM information_schema.schemata
437
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
438
+ ORDER BY schema_name
439
+ `;
440
+ const schemasResult = await this.pool.query(schemasQuery);
441
+ let schemaNames = schemasResult.rows.map((r) => r.schema_name);
442
+ if (options?.includeSchemas && options.includeSchemas.length > 0) {
443
+ schemaNames = schemaNames.filter((s) => options.includeSchemas.includes(s));
444
+ }
445
+ if (options?.excludeSchemas && options.excludeSchemas.length > 0) {
446
+ schemaNames = schemaNames.filter((s) => !options.excludeSchemas.includes(s));
447
+ }
448
+ for (const schemaName of schemaNames) {
449
+ const tables = await this.getTables(schemaName, options);
450
+ result.push({
451
+ name: schemaName,
452
+ tables
453
+ });
454
+ }
455
+ return result;
456
+ }
457
+ async getTables(schemaName, options) {
458
+ const result = [];
459
+ let tableTypes = "'BASE TABLE'";
460
+ if (options?.includeViews) {
461
+ tableTypes += ",'VIEW'";
462
+ }
463
+ const tablesQuery = `
464
+ SELECT table_name, table_type
465
+ FROM information_schema.tables
466
+ WHERE table_schema = $1 AND table_type IN (${tableTypes})
467
+ ORDER BY table_name
468
+ ${options?.maxTables ? `LIMIT ${options.maxTables}` : ""}
469
+ `;
470
+ const tablesResult = await this.pool.query(tablesQuery, [schemaName]);
471
+ for (const row of tablesResult.rows) {
472
+ const columns = await this.getColumns(schemaName, row.table_name);
473
+ const indexes = await this.getIndexes(schemaName, row.table_name);
474
+ const foreignKeys = await this.getForeignKeys(schemaName, row.table_name);
475
+ const primaryKey = indexes.find((idx) => idx.isPrimary);
476
+ result.push({
477
+ schema: schemaName,
478
+ name: row.table_name,
479
+ type: row.table_type === "VIEW" ? "view" : "table",
480
+ columns,
481
+ primaryKey,
482
+ indexes: indexes.filter((idx) => !idx.isPrimary),
483
+ foreignKeys
484
+ });
485
+ }
486
+ return result;
487
+ }
488
+ async getColumns(schemaName, tableName) {
489
+ const query = `
490
+ SELECT
491
+ column_name,
492
+ data_type,
493
+ is_nullable,
494
+ column_default,
495
+ character_maximum_length,
496
+ numeric_precision,
497
+ numeric_scale,
498
+ is_identity
499
+ FROM information_schema.columns
500
+ WHERE table_schema = $1 AND table_name = $2
501
+ ORDER BY ordinal_position
502
+ `;
503
+ const result = await this.pool.query(query, [schemaName, tableName]);
504
+ return result.rows.map((row) => ({
505
+ name: row.column_name,
506
+ dataType: row.data_type,
507
+ nullable: row.is_nullable === "YES",
508
+ defaultValue: row.column_default,
509
+ maxLength: row.character_maximum_length,
510
+ precision: row.numeric_precision,
511
+ scale: row.numeric_scale,
512
+ isAutoIncrement: row.is_identity === "YES"
513
+ }));
514
+ }
515
+ async getIndexes(schemaName, tableName) {
516
+ const query = `
517
+ SELECT
518
+ i.relname AS index_name,
519
+ ix.indisunique AS is_unique,
520
+ ix.indisprimary AS is_primary,
521
+ array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) AS column_names
522
+ FROM pg_class t
523
+ JOIN pg_index ix ON t.oid = ix.indrelid
524
+ JOIN pg_class i ON i.oid = ix.indexrelid
525
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
526
+ JOIN pg_namespace n ON n.oid = t.relnamespace
527
+ WHERE n.nspname = $1 AND t.relname = $2
528
+ GROUP BY i.relname, ix.indisunique, ix.indisprimary
529
+ `;
530
+ const result = await this.pool.query(query, [schemaName, tableName]);
531
+ return result.rows.map((row) => ({
532
+ name: row.index_name,
533
+ columns: row.column_names,
534
+ isUnique: row.is_unique,
535
+ isPrimary: row.is_primary
536
+ }));
537
+ }
538
+ async getForeignKeys(schemaName, tableName) {
539
+ const query = `
540
+ SELECT
541
+ tc.constraint_name,
542
+ array_agg(kcu.column_name ORDER BY kcu.ordinal_position) AS column_names,
543
+ ccu.table_schema AS referenced_schema,
544
+ ccu.table_name AS referenced_table,
545
+ array_agg(ccu.column_name ORDER BY kcu.ordinal_position) AS referenced_columns,
546
+ rc.update_rule,
547
+ rc.delete_rule
548
+ FROM information_schema.table_constraints AS tc
549
+ JOIN information_schema.key_column_usage AS kcu
550
+ ON tc.constraint_name = kcu.constraint_name
551
+ AND tc.table_schema = kcu.table_schema
552
+ JOIN information_schema.constraint_column_usage AS ccu
553
+ ON ccu.constraint_name = tc.constraint_name
554
+ AND ccu.table_schema = tc.table_schema
555
+ JOIN information_schema.referential_constraints AS rc
556
+ ON rc.constraint_name = tc.constraint_name
557
+ AND rc.constraint_schema = tc.table_schema
558
+ WHERE tc.constraint_type = 'FOREIGN KEY'
559
+ AND tc.table_schema = $1
560
+ AND tc.table_name = $2
561
+ GROUP BY tc.constraint_name, ccu.table_schema, ccu.table_name, rc.update_rule, rc.delete_rule
562
+ `;
563
+ const result = await this.pool.query(query, [schemaName, tableName]);
564
+ return result.rows.map((row) => ({
565
+ name: row.constraint_name,
566
+ columns: row.column_names,
567
+ referencedSchema: row.referenced_schema,
568
+ referencedTable: row.referenced_table,
569
+ referencedColumns: row.referenced_columns,
570
+ onUpdate: row.update_rule,
571
+ onDelete: row.delete_rule
572
+ }));
573
+ }
574
+ async query(sql, params = [], timeoutMs) {
575
+ this.ensureConnected();
576
+ const startTime = Date.now();
577
+ try {
578
+ const queryConfig = {
579
+ text: sql,
580
+ values: params
581
+ };
582
+ if (timeoutMs) {
583
+ queryConfig.statement_timeout = timeoutMs;
584
+ }
585
+ const result = await this.pool.query(queryConfig);
586
+ const executionTimeMs = Date.now() - startTime;
587
+ return {
588
+ rows: result.rows,
589
+ columns: result.fields.map((f) => f.name),
590
+ rowCount: result.rowCount || 0,
591
+ executionTimeMs,
592
+ affectedRows: result.rowCount || 0
593
+ };
594
+ } catch (error) {
595
+ this.handleError(error, "query");
596
+ }
597
+ }
598
+ async explain(sql, params = []) {
599
+ this.ensureConnected();
600
+ try {
601
+ const explainSql = `EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS) ${sql}`;
602
+ const result = await this.pool.query(explainSql, params);
603
+ return {
604
+ plan: result.rows[0]["QUERY PLAN"],
605
+ formattedPlan: JSON.stringify(result.rows[0]["QUERY PLAN"], null, 2)
606
+ };
607
+ } catch (error) {
608
+ this.handleError(error, "explain");
609
+ }
610
+ }
611
+ async testConnection() {
612
+ try {
613
+ if (!this.pool) return false;
614
+ const client = await this.pool.connect();
615
+ client.release();
616
+ return true;
617
+ } catch {
618
+ return false;
619
+ }
620
+ }
621
+ async getVersion() {
622
+ this.ensureConnected();
623
+ try {
624
+ const result = await this.pool.query("SELECT version()");
625
+ return result.rows[0].version;
626
+ } catch (error) {
627
+ this.handleError(error, "getVersion");
628
+ }
629
+ }
630
+ };
631
+
632
+ // src/adapters/mysql.ts
633
+ import mysql from "mysql2/promise";
634
+ var MySQLAdapter = class extends BaseAdapter {
635
+ pool;
636
+ database;
637
+ async connect() {
638
+ try {
639
+ this.pool = mysql.createPool({
640
+ uri: this._config.url,
641
+ waitForConnections: true,
642
+ connectionLimit: this._config.pool?.max || 10,
643
+ queueLimit: 0,
644
+ connectTimeout: this._config.pool?.connectionTimeoutMillis || 1e4
645
+ });
646
+ const connection = await this.pool.getConnection();
647
+ const [rows] = await connection.query("SELECT DATABASE() as db");
648
+ this.database = rows[0].db;
649
+ connection.release();
650
+ this.connected = true;
651
+ this.logger.info({ dbId: this._config.id }, "MySQL connected");
652
+ } catch (error) {
653
+ this.handleError(error, "connect");
654
+ }
655
+ }
656
+ async disconnect() {
657
+ if (this.pool) {
658
+ await this.pool.end();
659
+ this.pool = void 0;
660
+ this.connected = false;
661
+ this.logger.info({ dbId: this._config.id }, "MySQL disconnected");
662
+ }
663
+ }
664
+ async introspect(options) {
665
+ this.ensureConnected();
666
+ try {
667
+ const schemas = await this.getSchemas(options);
668
+ const dbSchema = {
669
+ dbId: this._config.id,
670
+ dbType: "mysql",
671
+ schemas,
672
+ introspectedAt: /* @__PURE__ */ new Date(),
673
+ version: ""
674
+ };
675
+ dbSchema.version = generateSchemaVersion(dbSchema);
676
+ return dbSchema;
677
+ } catch (error) {
678
+ this.handleError(error, "introspect");
679
+ }
680
+ }
681
+ async getSchemas(options) {
682
+ const tables = await this.getTables(this.database, options);
683
+ return [
684
+ {
685
+ name: this.database,
686
+ tables
687
+ }
688
+ ];
689
+ }
690
+ async getTables(schemaName, options) {
691
+ const result = [];
692
+ let tableTypes = "'BASE TABLE'";
693
+ if (options?.includeViews) {
694
+ tableTypes += ",'VIEW'";
695
+ }
696
+ const tablesQuery = `
697
+ SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT
698
+ FROM information_schema.TABLES
699
+ WHERE TABLE_SCHEMA = ? AND TABLE_TYPE IN (${tableTypes})
700
+ ORDER BY TABLE_NAME
701
+ ${options?.maxTables ? `LIMIT ${options.maxTables}` : ""}
702
+ `;
703
+ const [rows] = await this.pool.query(tablesQuery, [schemaName]);
704
+ for (const row of rows) {
705
+ const columns = await this.getColumns(schemaName, row.TABLE_NAME);
706
+ const indexes = await this.getIndexes(schemaName, row.TABLE_NAME);
707
+ const foreignKeys = await this.getForeignKeys(schemaName, row.TABLE_NAME);
708
+ const primaryKey = indexes.find((idx) => idx.isPrimary);
709
+ result.push({
710
+ schema: schemaName,
711
+ name: row.TABLE_NAME,
712
+ type: row.TABLE_TYPE === "VIEW" ? "view" : "table",
713
+ columns,
714
+ primaryKey,
715
+ indexes: indexes.filter((idx) => !idx.isPrimary),
716
+ foreignKeys,
717
+ comment: row.TABLE_COMMENT
718
+ });
719
+ }
720
+ return result;
721
+ }
722
+ async getColumns(schemaName, tableName) {
723
+ const query = `
724
+ SELECT
725
+ COLUMN_NAME,
726
+ DATA_TYPE,
727
+ IS_NULLABLE,
728
+ COLUMN_DEFAULT,
729
+ CHARACTER_MAXIMUM_LENGTH,
730
+ NUMERIC_PRECISION,
731
+ NUMERIC_SCALE,
732
+ EXTRA,
733
+ COLUMN_COMMENT
734
+ FROM information_schema.COLUMNS
735
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
736
+ ORDER BY ORDINAL_POSITION
737
+ `;
738
+ const [rows] = await this.pool.query(query, [schemaName, tableName]);
739
+ return rows.map((row) => ({
740
+ name: row.COLUMN_NAME,
741
+ dataType: row.DATA_TYPE,
742
+ nullable: row.IS_NULLABLE === "YES",
743
+ defaultValue: row.COLUMN_DEFAULT,
744
+ maxLength: row.CHARACTER_MAXIMUM_LENGTH,
745
+ precision: row.NUMERIC_PRECISION,
746
+ scale: row.NUMERIC_SCALE,
747
+ isAutoIncrement: row.EXTRA.includes("auto_increment"),
748
+ comment: row.COLUMN_COMMENT
749
+ }));
750
+ }
751
+ async getIndexes(schemaName, tableName) {
752
+ const query = `
753
+ SELECT
754
+ INDEX_NAME,
755
+ NON_UNIQUE,
756
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS column_names
757
+ FROM information_schema.STATISTICS
758
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
759
+ GROUP BY INDEX_NAME, NON_UNIQUE
760
+ `;
761
+ const [rows] = await this.pool.query(query, [schemaName, tableName]);
762
+ return rows.map((row) => ({
763
+ name: row.INDEX_NAME,
764
+ columns: row.column_names.split(","),
765
+ isUnique: row.NON_UNIQUE === 0,
766
+ isPrimary: row.INDEX_NAME === "PRIMARY"
767
+ }));
768
+ }
769
+ async getForeignKeys(schemaName, tableName) {
770
+ const query = `
771
+ SELECT
772
+ kcu.CONSTRAINT_NAME,
773
+ GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) AS column_names,
774
+ kcu.REFERENCED_TABLE_SCHEMA,
775
+ kcu.REFERENCED_TABLE_NAME,
776
+ GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) AS referenced_columns,
777
+ rc.UPDATE_RULE,
778
+ rc.DELETE_RULE
779
+ FROM information_schema.KEY_COLUMN_USAGE AS kcu
780
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc
781
+ ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
782
+ AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
783
+ WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ?
784
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
785
+ GROUP BY kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_SCHEMA,
786
+ kcu.REFERENCED_TABLE_NAME, rc.UPDATE_RULE, rc.DELETE_RULE
787
+ `;
788
+ const [rows] = await this.pool.query(query, [schemaName, tableName]);
789
+ return rows.map((row) => ({
790
+ name: row.CONSTRAINT_NAME,
791
+ columns: row.column_names.split(","),
792
+ referencedSchema: row.REFERENCED_TABLE_SCHEMA,
793
+ referencedTable: row.REFERENCED_TABLE_NAME,
794
+ referencedColumns: row.referenced_columns.split(","),
795
+ onUpdate: row.UPDATE_RULE,
796
+ onDelete: row.DELETE_RULE
797
+ }));
798
+ }
799
+ async query(sql, params = [], timeoutMs) {
800
+ this.ensureConnected();
801
+ const startTime = Date.now();
802
+ try {
803
+ const connection = await this.pool.getConnection();
804
+ if (timeoutMs) {
805
+ await connection.query(`SET SESSION max_execution_time=${timeoutMs}`);
806
+ }
807
+ const [rows, fields] = await connection.query(sql, params);
808
+ connection.release();
809
+ const executionTimeMs = Date.now() - startTime;
810
+ return {
811
+ rows: Array.isArray(rows) ? rows : [],
812
+ columns: Array.isArray(fields) ? fields.map((f) => f.name) : [],
813
+ rowCount: Array.isArray(rows) ? rows.length : 0,
814
+ executionTimeMs,
815
+ affectedRows: rows.affectedRows
816
+ };
817
+ } catch (error) {
818
+ this.handleError(error, "query");
819
+ }
820
+ }
821
+ async explain(sql, params = []) {
822
+ this.ensureConnected();
823
+ try {
824
+ const explainSql = `EXPLAIN FORMAT=JSON ${sql}`;
825
+ const [rows] = await this.pool.query(explainSql, params);
826
+ return {
827
+ plan: rows[0].EXPLAIN,
828
+ formattedPlan: JSON.stringify(rows[0].EXPLAIN, null, 2)
829
+ };
830
+ } catch (error) {
831
+ this.handleError(error, "explain");
832
+ }
833
+ }
834
+ async testConnection() {
835
+ try {
836
+ if (!this.pool) return false;
837
+ const connection = await this.pool.getConnection();
838
+ connection.release();
839
+ return true;
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+ async getVersion() {
845
+ this.ensureConnected();
846
+ try {
847
+ const [rows] = await this.pool.query("SELECT VERSION() as version");
848
+ return rows[0].version;
849
+ } catch (error) {
850
+ this.handleError(error, "getVersion");
851
+ }
852
+ }
853
+ };
854
+
855
+ // src/adapters/sqlite.ts
856
+ import Database from "better-sqlite3";
857
+ var SQLiteAdapter = class extends BaseAdapter {
858
+ db;
859
+ async connect() {
860
+ try {
861
+ const dbPath = this._config.path || this._config.url;
862
+ if (!dbPath) {
863
+ throw new Error("SQLite requires path or url configuration");
864
+ }
865
+ this.db = new Database(dbPath, {
866
+ readonly: this._config.readOnly,
867
+ fileMustExist: false
868
+ });
869
+ this.db.pragma("foreign_keys = ON");
870
+ this.connected = true;
871
+ this.logger.info({ dbId: this._config.id, path: dbPath }, "SQLite connected");
872
+ } catch (error) {
873
+ this.handleError(error, "connect");
874
+ }
875
+ }
876
+ async disconnect() {
877
+ if (this.db) {
878
+ this.db.close();
879
+ this.db = void 0;
880
+ this.connected = false;
881
+ this.logger.info({ dbId: this._config.id }, "SQLite disconnected");
882
+ }
883
+ }
884
+ async introspect(options) {
885
+ this.ensureConnected();
886
+ try {
887
+ const schemas = await this.getSchemas(options);
888
+ const dbSchema = {
889
+ dbId: this._config.id,
890
+ dbType: "sqlite",
891
+ schemas,
892
+ introspectedAt: /* @__PURE__ */ new Date(),
893
+ version: ""
894
+ };
895
+ dbSchema.version = generateSchemaVersion(dbSchema);
896
+ return dbSchema;
897
+ } catch (error) {
898
+ this.handleError(error, "introspect");
899
+ }
900
+ }
901
+ async getSchemas(options) {
902
+ const tables = await this.getTables("main", options);
903
+ return [
904
+ {
905
+ name: "main",
906
+ tables
907
+ }
908
+ ];
909
+ }
910
+ async getTables(schemaName, options) {
911
+ const result = [];
912
+ let query = `
913
+ SELECT name, type
914
+ FROM sqlite_master
915
+ WHERE type IN ('table', 'view')
916
+ AND name NOT LIKE 'sqlite_%'
917
+ ORDER BY name
918
+ `;
919
+ if (options?.maxTables) {
920
+ query += ` LIMIT ${options.maxTables}`;
921
+ }
922
+ const tables = this.db.prepare(query).all();
923
+ for (const table of tables) {
924
+ if (table.type === "view" && !options?.includeViews) {
925
+ continue;
926
+ }
927
+ const columns = await this.getColumns(table.name);
928
+ const indexes = await this.getIndexes(table.name);
929
+ const foreignKeys = await this.getForeignKeys(table.name);
930
+ const primaryKey = indexes.find((idx) => idx.isPrimary);
931
+ result.push({
932
+ schema: schemaName,
933
+ name: table.name,
934
+ type: table.type === "view" ? "view" : "table",
935
+ columns,
936
+ primaryKey,
937
+ indexes: indexes.filter((idx) => !idx.isPrimary),
938
+ foreignKeys
939
+ });
940
+ }
941
+ return result;
942
+ }
943
+ async getColumns(tableName) {
944
+ const pragma = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
945
+ return pragma.map((col) => ({
946
+ name: col.name,
947
+ dataType: col.type || "TEXT",
948
+ nullable: col.notnull === 0,
949
+ defaultValue: col.dflt_value || void 0,
950
+ isAutoIncrement: col.pk === 1 && col.type.toUpperCase() === "INTEGER"
951
+ }));
952
+ }
953
+ async getIndexes(tableName) {
954
+ const result = [];
955
+ const indexes = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
956
+ for (const index of indexes) {
957
+ const indexInfo = this.db.prepare(`PRAGMA index_info(${index.name})`).all();
958
+ result.push({
959
+ name: index.name,
960
+ columns: indexInfo.map((info) => info.name),
961
+ isUnique: index.unique === 1,
962
+ isPrimary: index.origin === "pk"
963
+ });
964
+ }
965
+ return result;
966
+ }
967
+ async getForeignKeys(tableName) {
968
+ const foreignKeys = this.db.prepare(`PRAGMA foreign_key_list(${tableName})`).all();
969
+ const grouped = /* @__PURE__ */ new Map();
970
+ for (const fk of foreignKeys) {
971
+ if (!grouped.has(fk.id)) {
972
+ grouped.set(fk.id, []);
973
+ }
974
+ grouped.get(fk.id).push(fk);
975
+ }
976
+ return Array.from(grouped.values()).map((fks) => ({
977
+ name: `fk_${tableName}_${fks[0].id}`,
978
+ columns: fks.map((fk) => fk.from),
979
+ referencedSchema: "main",
980
+ referencedTable: fks[0].table,
981
+ referencedColumns: fks.map((fk) => fk.to),
982
+ onUpdate: fks[0].on_update,
983
+ onDelete: fks[0].on_delete
984
+ }));
985
+ }
986
+ async query(sql, params = [], timeoutMs) {
987
+ this.ensureConnected();
988
+ const startTime = Date.now();
989
+ try {
990
+ if (timeoutMs) {
991
+ this.db.pragma(`busy_timeout = ${timeoutMs}`);
992
+ }
993
+ const stmt = this.db.prepare(sql);
994
+ const isSelect = sql.trim().toUpperCase().startsWith("SELECT");
995
+ let rows;
996
+ let affectedRows = 0;
997
+ if (isSelect) {
998
+ rows = stmt.all(...params);
999
+ } else {
1000
+ const result = stmt.run(...params);
1001
+ rows = [];
1002
+ affectedRows = result.changes;
1003
+ }
1004
+ const executionTimeMs = Date.now() - startTime;
1005
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
1006
+ return {
1007
+ rows,
1008
+ columns,
1009
+ rowCount: rows.length,
1010
+ executionTimeMs,
1011
+ affectedRows
1012
+ };
1013
+ } catch (error) {
1014
+ this.handleError(error, "query");
1015
+ }
1016
+ }
1017
+ async explain(sql, params = []) {
1018
+ this.ensureConnected();
1019
+ try {
1020
+ const explainSql = `EXPLAIN QUERY PLAN ${sql}`;
1021
+ const stmt = this.db.prepare(explainSql);
1022
+ const plan = stmt.all(...params);
1023
+ return {
1024
+ plan,
1025
+ formattedPlan: JSON.stringify(plan, null, 2)
1026
+ };
1027
+ } catch (error) {
1028
+ this.handleError(error, "explain");
1029
+ }
1030
+ }
1031
+ async testConnection() {
1032
+ try {
1033
+ if (!this.db) return false;
1034
+ this.db.prepare("SELECT 1").get();
1035
+ return true;
1036
+ } catch {
1037
+ return false;
1038
+ }
1039
+ }
1040
+ async getVersion() {
1041
+ this.ensureConnected();
1042
+ try {
1043
+ const result = this.db.prepare("SELECT sqlite_version() as version").get();
1044
+ return `SQLite ${result.version}`;
1045
+ } catch (error) {
1046
+ this.handleError(error, "getVersion");
1047
+ }
1048
+ }
1049
+ };
1050
+
1051
+ // src/adapters/mssql.ts
1052
+ import { Connection, Request } from "tedious";
1053
+ var MSSQLAdapter = class extends BaseAdapter {
1054
+ connection;
1055
+ async connect() {
1056
+ return new Promise((resolve2, reject) => {
1057
+ try {
1058
+ const config = this.parseConnectionString(this._config.url);
1059
+ this.connection = new Connection(config);
1060
+ this.connection.on("connect", (err) => {
1061
+ if (err) {
1062
+ reject(err);
1063
+ } else {
1064
+ this.connected = true;
1065
+ this.logger.info({ dbId: this._config.id }, "SQL Server connected");
1066
+ resolve2();
1067
+ }
1068
+ });
1069
+ this.connection.connect();
1070
+ } catch (error) {
1071
+ this.handleError(error, "connect");
1072
+ }
1073
+ });
1074
+ }
1075
+ parseConnectionString(connStr) {
1076
+ const config = {
1077
+ options: {
1078
+ encrypt: true,
1079
+ trustServerCertificate: true,
1080
+ enableArithAbort: true
1081
+ }
1082
+ };
1083
+ const parts = connStr.split(";").filter((p) => p.trim());
1084
+ for (const part of parts) {
1085
+ const [key, value] = part.split("=").map((s) => s.trim());
1086
+ const lowerKey = key.toLowerCase();
1087
+ if (lowerKey === "server") {
1088
+ const [host, port] = value.split(",");
1089
+ config.server = host;
1090
+ if (port) config.options.port = parseInt(port);
1091
+ } else if (lowerKey === "database") {
1092
+ config.options.database = value;
1093
+ } else if (lowerKey === "user id") {
1094
+ config.authentication = {
1095
+ type: "default",
1096
+ options: { userName: value, password: "" }
1097
+ };
1098
+ } else if (lowerKey === "password") {
1099
+ if (config.authentication) {
1100
+ config.authentication.options.password = value;
1101
+ }
1102
+ } else if (lowerKey === "encrypt") {
1103
+ config.options.encrypt = value.toLowerCase() === "true";
1104
+ } else if (lowerKey === "trustservercertificate") {
1105
+ config.options.trustServerCertificate = value.toLowerCase() === "true";
1106
+ }
1107
+ }
1108
+ return config;
1109
+ }
1110
+ async disconnect() {
1111
+ if (this.connection) {
1112
+ this.connection.close();
1113
+ this.connection = void 0;
1114
+ this.connected = false;
1115
+ this.logger.info({ dbId: this._config.id }, "SQL Server disconnected");
1116
+ }
1117
+ }
1118
+ executeQuery(sql, _params = []) {
1119
+ return new Promise((resolve2, reject) => {
1120
+ const rows = [];
1121
+ const request = new Request(sql, (err) => {
1122
+ if (err) {
1123
+ reject(err);
1124
+ } else {
1125
+ resolve2(rows);
1126
+ }
1127
+ });
1128
+ request.on("row", (columns) => {
1129
+ const row = {};
1130
+ columns.forEach((col) => {
1131
+ row[col.metadata.colName] = col.value;
1132
+ });
1133
+ rows.push(row);
1134
+ });
1135
+ this.connection.execSql(request);
1136
+ });
1137
+ }
1138
+ async introspect(options) {
1139
+ this.ensureConnected();
1140
+ try {
1141
+ const schemas = await this.getSchemas(options);
1142
+ const dbSchema = {
1143
+ dbId: this._config.id,
1144
+ dbType: "mssql",
1145
+ schemas,
1146
+ introspectedAt: /* @__PURE__ */ new Date(),
1147
+ version: ""
1148
+ };
1149
+ dbSchema.version = generateSchemaVersion(dbSchema);
1150
+ return dbSchema;
1151
+ } catch (error) {
1152
+ this.handleError(error, "introspect");
1153
+ }
1154
+ }
1155
+ async getSchemas(options) {
1156
+ const result = [];
1157
+ const schemasQuery = `
1158
+ SELECT SCHEMA_NAME
1159
+ FROM INFORMATION_SCHEMA.SCHEMATA
1160
+ WHERE SCHEMA_NAME NOT IN ('sys', 'INFORMATION_SCHEMA', 'guest')
1161
+ ORDER BY SCHEMA_NAME
1162
+ `;
1163
+ const schemasResult = await this.executeQuery(schemasQuery);
1164
+ let schemaNames = schemasResult.map((r) => r.SCHEMA_NAME);
1165
+ if (options?.includeSchemas && options.includeSchemas.length > 0) {
1166
+ schemaNames = schemaNames.filter((s) => options.includeSchemas.includes(s));
1167
+ }
1168
+ if (options?.excludeSchemas && options.excludeSchemas.length > 0) {
1169
+ schemaNames = schemaNames.filter((s) => !options.excludeSchemas.includes(s));
1170
+ }
1171
+ for (const schemaName of schemaNames) {
1172
+ const tables = await this.getTables(schemaName, options);
1173
+ result.push({
1174
+ name: schemaName,
1175
+ tables
1176
+ });
1177
+ }
1178
+ return result;
1179
+ }
1180
+ async getTables(schemaName, options) {
1181
+ const result = [];
1182
+ let tableTypes = "'BASE TABLE'";
1183
+ if (options?.includeViews) {
1184
+ tableTypes += ",'VIEW'";
1185
+ }
1186
+ const tablesQuery = `
1187
+ SELECT TABLE_NAME, TABLE_TYPE
1188
+ FROM INFORMATION_SCHEMA.TABLES
1189
+ WHERE TABLE_SCHEMA = '${schemaName}' AND TABLE_TYPE IN (${tableTypes})
1190
+ ORDER BY TABLE_NAME
1191
+ ${options?.maxTables ? `OFFSET 0 ROWS FETCH NEXT ${options.maxTables} ROWS ONLY` : ""}
1192
+ `;
1193
+ const tablesResult = await this.executeQuery(tablesQuery);
1194
+ for (const row of tablesResult) {
1195
+ const columns = await this.getColumns(schemaName, row.TABLE_NAME);
1196
+ const indexes = await this.getIndexes(schemaName, row.TABLE_NAME);
1197
+ const foreignKeys = await this.getForeignKeys(schemaName, row.TABLE_NAME);
1198
+ const primaryKey = indexes.find((idx) => idx.isPrimary);
1199
+ result.push({
1200
+ schema: schemaName,
1201
+ name: row.TABLE_NAME,
1202
+ type: row.TABLE_TYPE === "VIEW" ? "view" : "table",
1203
+ columns,
1204
+ primaryKey,
1205
+ indexes: indexes.filter((idx) => !idx.isPrimary),
1206
+ foreignKeys
1207
+ });
1208
+ }
1209
+ return result;
1210
+ }
1211
+ async getColumns(schemaName, tableName) {
1212
+ const query = `
1213
+ SELECT
1214
+ COLUMN_NAME,
1215
+ DATA_TYPE,
1216
+ IS_NULLABLE,
1217
+ COLUMN_DEFAULT,
1218
+ CHARACTER_MAXIMUM_LENGTH,
1219
+ NUMERIC_PRECISION,
1220
+ NUMERIC_SCALE,
1221
+ COLUMNPROPERTY(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') AS IS_IDENTITY
1222
+ FROM INFORMATION_SCHEMA.COLUMNS
1223
+ WHERE TABLE_SCHEMA = '${schemaName}' AND TABLE_NAME = '${tableName}'
1224
+ ORDER BY ORDINAL_POSITION
1225
+ `;
1226
+ const result = await this.executeQuery(query);
1227
+ return result.map((row) => ({
1228
+ name: row.COLUMN_NAME,
1229
+ dataType: row.DATA_TYPE,
1230
+ nullable: row.IS_NULLABLE === "YES",
1231
+ defaultValue: row.COLUMN_DEFAULT,
1232
+ maxLength: row.CHARACTER_MAXIMUM_LENGTH,
1233
+ precision: row.NUMERIC_PRECISION,
1234
+ scale: row.NUMERIC_SCALE,
1235
+ isAutoIncrement: row.IS_IDENTITY === 1
1236
+ }));
1237
+ }
1238
+ async getIndexes(schemaName, tableName) {
1239
+ const query = `
1240
+ SELECT
1241
+ i.name AS index_name,
1242
+ i.is_unique,
1243
+ i.is_primary_key,
1244
+ STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) AS column_names
1245
+ FROM sys.indexes i
1246
+ INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
1247
+ INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
1248
+ INNER JOIN sys.tables t ON i.object_id = t.object_id
1249
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
1250
+ WHERE s.name = '${schemaName}' AND t.name = '${tableName}'
1251
+ GROUP BY i.name, i.is_unique, i.is_primary_key
1252
+ `;
1253
+ const result = await this.executeQuery(query);
1254
+ return result.map((row) => ({
1255
+ name: row.index_name,
1256
+ columns: row.column_names.split(","),
1257
+ isUnique: row.is_unique,
1258
+ isPrimary: row.is_primary_key
1259
+ }));
1260
+ }
1261
+ async getForeignKeys(schemaName, tableName) {
1262
+ const query = `
1263
+ SELECT
1264
+ fk.name AS constraint_name,
1265
+ STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY fkc.constraint_column_id) AS column_names,
1266
+ rs.name AS referenced_schema,
1267
+ rt.name AS referenced_table,
1268
+ STRING_AGG(rc.name, ',') WITHIN GROUP (ORDER BY fkc.constraint_column_id) AS referenced_columns,
1269
+ fk.update_referential_action_desc,
1270
+ fk.delete_referential_action_desc
1271
+ FROM sys.foreign_keys fk
1272
+ INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
1273
+ INNER JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id
1274
+ INNER JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id
1275
+ INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id
1276
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
1277
+ INNER JOIN sys.tables rt ON fk.referenced_object_id = rt.object_id
1278
+ INNER JOIN sys.schemas rs ON rt.schema_id = rs.schema_id
1279
+ WHERE s.name = '${schemaName}' AND t.name = '${tableName}'
1280
+ GROUP BY fk.name, rs.name, rt.name, fk.update_referential_action_desc, fk.delete_referential_action_desc
1281
+ `;
1282
+ const result = await this.executeQuery(query);
1283
+ return result.map((row) => ({
1284
+ name: row.constraint_name,
1285
+ columns: row.column_names.split(","),
1286
+ referencedSchema: row.referenced_schema,
1287
+ referencedTable: row.referenced_table,
1288
+ referencedColumns: row.referenced_columns.split(","),
1289
+ onUpdate: row.update_referential_action_desc,
1290
+ onDelete: row.delete_referential_action_desc
1291
+ }));
1292
+ }
1293
+ async query(sql, params = [], _timeoutMs) {
1294
+ this.ensureConnected();
1295
+ const startTime = Date.now();
1296
+ try {
1297
+ const rows = await this.executeQuery(sql, params);
1298
+ const executionTimeMs = Date.now() - startTime;
1299
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
1300
+ return {
1301
+ rows,
1302
+ columns,
1303
+ rowCount: rows.length,
1304
+ executionTimeMs
1305
+ };
1306
+ } catch (error) {
1307
+ this.handleError(error, "query");
1308
+ }
1309
+ }
1310
+ async explain(sql, params = []) {
1311
+ this.ensureConnected();
1312
+ try {
1313
+ const explainSql = `SET SHOWPLAN_TEXT ON; ${sql}; SET SHOWPLAN_TEXT OFF;`;
1314
+ const plan = await this.executeQuery(explainSql, params);
1315
+ return {
1316
+ plan,
1317
+ formattedPlan: JSON.stringify(plan, null, 2)
1318
+ };
1319
+ } catch (error) {
1320
+ this.handleError(error, "explain");
1321
+ }
1322
+ }
1323
+ async testConnection() {
1324
+ try {
1325
+ if (!this.connection) return false;
1326
+ await this.executeQuery("SELECT 1");
1327
+ return true;
1328
+ } catch {
1329
+ return false;
1330
+ }
1331
+ }
1332
+ async getVersion() {
1333
+ this.ensureConnected();
1334
+ try {
1335
+ const result = await this.executeQuery("SELECT @@VERSION as version");
1336
+ return result[0].version;
1337
+ } catch (error) {
1338
+ this.handleError(error, "getVersion");
1339
+ }
1340
+ }
1341
+ };
1342
+
1343
+ // src/adapters/oracle.ts
1344
+ var OracleAdapter = class extends BaseAdapter {
1345
+ connection;
1346
+ async connect() {
1347
+ this.logger.warn(
1348
+ { dbId: this._config.id },
1349
+ "Oracle adapter is not fully implemented. Requires Oracle Instant Client and oracledb package."
1350
+ );
1351
+ throw new Error(
1352
+ "Oracle adapter not implemented. Please install Oracle Instant Client and implement OracleAdapter methods."
1353
+ );
1354
+ }
1355
+ async disconnect() {
1356
+ if (this.connection) {
1357
+ this.connection = void 0;
1358
+ this.connected = false;
1359
+ }
1360
+ }
1361
+ async introspect(_options) {
1362
+ this.ensureConnected();
1363
+ throw new Error("Oracle introspection not implemented");
1364
+ }
1365
+ async query(_sql, _params = [], _timeoutMs) {
1366
+ this.ensureConnected();
1367
+ throw new Error("Oracle query not implemented");
1368
+ }
1369
+ async explain(_sql, _params = []) {
1370
+ this.ensureConnected();
1371
+ throw new Error("Oracle explain not implemented");
1372
+ }
1373
+ async testConnection() {
1374
+ return false;
1375
+ }
1376
+ async getVersion() {
1377
+ return "Oracle (not implemented)";
1378
+ }
1379
+ };
1380
+
1381
+ // src/adapters/index.ts
1382
+ function createAdapter(config) {
1383
+ switch (config.type) {
1384
+ case "postgres":
1385
+ return new PostgresAdapter(config);
1386
+ case "mysql":
1387
+ return new MySQLAdapter(config);
1388
+ case "sqlite":
1389
+ return new SQLiteAdapter(config);
1390
+ case "mssql":
1391
+ return new MSSQLAdapter(config);
1392
+ case "oracle":
1393
+ return new OracleAdapter(config);
1394
+ default:
1395
+ throw new Error(`Unsupported database type: ${config.type}`);
1396
+ }
1397
+ }
1398
+
1399
+ // src/cache.ts
1400
+ import fs2 from "fs/promises";
1401
+ import path from "path";
1402
+ var SchemaCache = class {
1403
+ constructor(_cacheDir, _defaultTtlMinutes) {
1404
+ this._cacheDir = _cacheDir;
1405
+ this._defaultTtlMinutes = _defaultTtlMinutes;
1406
+ }
1407
+ logger = getLogger();
1408
+ cache = /* @__PURE__ */ new Map();
1409
+ introspectionLocks = /* @__PURE__ */ new Map();
1410
+ async init() {
1411
+ try {
1412
+ await fs2.mkdir(this._cacheDir, { recursive: true });
1413
+ this.logger.info({ cacheDir: this._cacheDir }, "Schema cache initialized");
1414
+ } catch (error) {
1415
+ throw new CacheError("Failed to initialize cache directory", error);
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Get cached schema if valid, otherwise return null
1420
+ */
1421
+ async get(dbId) {
1422
+ const memEntry = this.cache.get(dbId);
1423
+ if (memEntry && !this.isExpired(memEntry)) {
1424
+ return memEntry;
1425
+ }
1426
+ try {
1427
+ const diskEntry = await this.loadFromDisk(dbId);
1428
+ if (diskEntry && !this.isExpired(diskEntry)) {
1429
+ this.cache.set(dbId, diskEntry);
1430
+ return diskEntry;
1431
+ }
1432
+ } catch (error) {
1433
+ this.logger.warn({ dbId, error }, "Failed to load cache from disk");
1434
+ }
1435
+ return null;
1436
+ }
1437
+ /**
1438
+ * Set or update cache entry
1439
+ */
1440
+ async set(dbId, schema, ttlMinutes) {
1441
+ const entry = {
1442
+ schema,
1443
+ relationships: this.buildRelationships(schema),
1444
+ cachedAt: /* @__PURE__ */ new Date(),
1445
+ ttlMinutes: ttlMinutes || this._defaultTtlMinutes
1446
+ };
1447
+ this.cache.set(dbId, entry);
1448
+ this.saveToDisk(dbId, entry).catch((error) => {
1449
+ this.logger.error({ dbId, error }, "Failed to save cache to disk");
1450
+ });
1451
+ }
1452
+ /**
1453
+ * Clear cache for a specific database or all databases
1454
+ */
1455
+ async clear(dbId) {
1456
+ if (dbId) {
1457
+ this.cache.delete(dbId);
1458
+ try {
1459
+ const filePath = this.getCacheFilePath(dbId);
1460
+ await fs2.unlink(filePath);
1461
+ } catch (error) {
1462
+ if (error.code !== "ENOENT") {
1463
+ this.logger.warn({ dbId, error }, "Failed to delete cache file");
1464
+ }
1465
+ }
1466
+ this.logger.info({ dbId }, "Cache cleared");
1467
+ } else {
1468
+ this.cache.clear();
1469
+ try {
1470
+ const files = await fs2.readdir(this._cacheDir);
1471
+ await Promise.all(
1472
+ files.filter((f) => f.endsWith(".json")).map((f) => fs2.unlink(path.join(this._cacheDir, f)))
1473
+ );
1474
+ } catch (error) {
1475
+ this.logger.warn({ error }, "Failed to clear cache directory");
1476
+ }
1477
+ this.logger.info("All caches cleared");
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Get cache status
1482
+ */
1483
+ async getStatus(dbId) {
1484
+ const statuses = [];
1485
+ if (dbId) {
1486
+ const status = await this.getStatusForDb(dbId);
1487
+ statuses.push(status);
1488
+ } else {
1489
+ const dbIds = /* @__PURE__ */ new Set([
1490
+ ...this.cache.keys(),
1491
+ ...await this.getPersistedDbIds()
1492
+ ]);
1493
+ for (const id of dbIds) {
1494
+ const status = await this.getStatusForDb(id);
1495
+ statuses.push(status);
1496
+ }
1497
+ }
1498
+ return statuses;
1499
+ }
1500
+ async getStatusForDb(dbId) {
1501
+ const entry = await this.get(dbId);
1502
+ if (!entry) {
1503
+ return {
1504
+ dbId,
1505
+ exists: false
1506
+ };
1507
+ }
1508
+ const age = Date.now() - new Date(entry.cachedAt).getTime();
1509
+ const expired = this.isExpired(entry);
1510
+ return {
1511
+ dbId,
1512
+ exists: true,
1513
+ age,
1514
+ ttlMinutes: entry.ttlMinutes,
1515
+ expired,
1516
+ version: entry.schema.version,
1517
+ tableCount: entry.schema.schemas.reduce((sum, s) => sum + s.tables.length, 0),
1518
+ relationshipCount: entry.relationships.length
1519
+ };
1520
+ }
1521
+ /**
1522
+ * Acquire lock for introspection to prevent concurrent introspection
1523
+ */
1524
+ async acquireIntrospectionLock(dbId) {
1525
+ const existingLock = this.introspectionLocks.get(dbId);
1526
+ if (existingLock) {
1527
+ await existingLock;
1528
+ }
1529
+ let releaseLock;
1530
+ const lockPromise = new Promise((resolve2) => {
1531
+ releaseLock = resolve2;
1532
+ });
1533
+ this.introspectionLocks.set(dbId, lockPromise);
1534
+ return () => {
1535
+ releaseLock();
1536
+ this.introspectionLocks.delete(dbId);
1537
+ };
1538
+ }
1539
+ isExpired(entry) {
1540
+ const age = Date.now() - new Date(entry.cachedAt).getTime();
1541
+ const ttlMs = entry.ttlMinutes * 60 * 1e3;
1542
+ return age > ttlMs;
1543
+ }
1544
+ buildRelationships(schema) {
1545
+ const relationships = [];
1546
+ for (const schemaObj of schema.schemas) {
1547
+ for (const table of schemaObj.tables) {
1548
+ for (const fk of table.foreignKeys) {
1549
+ relationships.push({
1550
+ fromSchema: schemaObj.name,
1551
+ fromTable: table.name,
1552
+ fromColumns: fk.columns,
1553
+ toSchema: fk.referencedSchema,
1554
+ toTable: fk.referencedTable,
1555
+ toColumns: fk.referencedColumns,
1556
+ type: "foreign_key"
1557
+ });
1558
+ }
1559
+ }
1560
+ }
1561
+ const inferred = inferRelationships(schema);
1562
+ const relationshipKeys = new Set(
1563
+ relationships.map((r) => this.getRelationshipKey(r))
1564
+ );
1565
+ for (const rel of inferred) {
1566
+ const key = this.getRelationshipKey(rel);
1567
+ if (!relationshipKeys.has(key)) {
1568
+ relationships.push(rel);
1569
+ relationshipKeys.add(key);
1570
+ }
1571
+ }
1572
+ return relationships;
1573
+ }
1574
+ getRelationshipKey(rel) {
1575
+ return `${rel.fromSchema}.${rel.fromTable}.${rel.fromColumns.join(",")}\u2192${rel.toSchema}.${rel.toTable}.${rel.toColumns.join(",")}`;
1576
+ }
1577
+ getCacheFilePath(dbId) {
1578
+ return path.join(this._cacheDir, `${dbId}.json`);
1579
+ }
1580
+ async loadFromDisk(dbId) {
1581
+ try {
1582
+ const filePath = this.getCacheFilePath(dbId);
1583
+ const data = await fs2.readFile(filePath, "utf-8");
1584
+ const entry = JSON.parse(data);
1585
+ entry.cachedAt = new Date(entry.cachedAt);
1586
+ entry.schema.introspectedAt = new Date(entry.schema.introspectedAt);
1587
+ return entry;
1588
+ } catch (error) {
1589
+ if (error.code === "ENOENT") {
1590
+ return null;
1591
+ }
1592
+ throw error;
1593
+ }
1594
+ }
1595
+ async saveToDisk(dbId, entry) {
1596
+ const filePath = this.getCacheFilePath(dbId);
1597
+ const data = JSON.stringify(entry, null, 2);
1598
+ await fs2.writeFile(filePath, data, "utf-8");
1599
+ }
1600
+ async getPersistedDbIds() {
1601
+ try {
1602
+ const files = await fs2.readdir(this._cacheDir);
1603
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
1604
+ } catch {
1605
+ return [];
1606
+ }
1607
+ }
1608
+ };
1609
+
1610
+ // src/query-tracker.ts
1611
+ var QueryTracker = class {
1612
+ history = /* @__PURE__ */ new Map();
1613
+ maxHistoryPerDb = 100;
1614
+ track(dbId, sql, executionTimeMs, rowCount, error) {
1615
+ const entry = {
1616
+ timestamp: /* @__PURE__ */ new Date(),
1617
+ sql,
1618
+ tables: extractTableNames(sql),
1619
+ executionTimeMs,
1620
+ rowCount,
1621
+ error
1622
+ };
1623
+ if (!this.history.has(dbId)) {
1624
+ this.history.set(dbId, []);
1625
+ }
1626
+ const dbHistory = this.history.get(dbId);
1627
+ dbHistory.push(entry);
1628
+ if (dbHistory.length > this.maxHistoryPerDb) {
1629
+ dbHistory.shift();
1630
+ }
1631
+ }
1632
+ getHistory(dbId, limit) {
1633
+ const dbHistory = this.history.get(dbId) || [];
1634
+ if (limit) {
1635
+ return dbHistory.slice(-limit);
1636
+ }
1637
+ return [...dbHistory];
1638
+ }
1639
+ getStats(dbId) {
1640
+ const dbHistory = this.history.get(dbId) || [];
1641
+ const stats = {
1642
+ totalQueries: dbHistory.length,
1643
+ avgExecutionTime: 0,
1644
+ errorCount: 0,
1645
+ tableUsage: {}
1646
+ };
1647
+ if (dbHistory.length === 0) {
1648
+ return stats;
1649
+ }
1650
+ let totalTime = 0;
1651
+ for (const entry of dbHistory) {
1652
+ totalTime += entry.executionTimeMs;
1653
+ if (entry.error) {
1654
+ stats.errorCount++;
1655
+ }
1656
+ for (const table of entry.tables) {
1657
+ stats.tableUsage[table] = (stats.tableUsage[table] || 0) + 1;
1658
+ }
1659
+ }
1660
+ stats.avgExecutionTime = totalTime / dbHistory.length;
1661
+ return stats;
1662
+ }
1663
+ clear(dbId) {
1664
+ if (dbId) {
1665
+ this.history.delete(dbId);
1666
+ } else {
1667
+ this.history.clear();
1668
+ }
1669
+ }
1670
+ };
1671
+
1672
+ // src/database-manager.ts
1673
+ var DatabaseManager = class {
1674
+ constructor(_configs, options) {
1675
+ this._configs = _configs;
1676
+ this.options = options;
1677
+ this.cache = new SchemaCache(options.cacheDir, options.cacheTtlMinutes);
1678
+ }
1679
+ logger = getLogger();
1680
+ adapters = /* @__PURE__ */ new Map();
1681
+ cache;
1682
+ queryTracker = new QueryTracker();
1683
+ async init() {
1684
+ await this.cache.init();
1685
+ for (const config of this._configs) {
1686
+ const adapter = createAdapter(config);
1687
+ this.adapters.set(config.id, adapter);
1688
+ if (config.eagerConnect) {
1689
+ try {
1690
+ await this.connect(config.id);
1691
+ } catch (error) {
1692
+ this.logger.error({ dbId: config.id, error }, "Failed to eager connect");
1693
+ }
1694
+ }
1695
+ }
1696
+ this.logger.info({ databases: this._configs.length }, "Database manager initialized");
1697
+ }
1698
+ async shutdown() {
1699
+ for (const [dbId, adapter] of this.adapters) {
1700
+ try {
1701
+ await adapter.disconnect();
1702
+ } catch (error) {
1703
+ this.logger.error({ dbId, error }, "Failed to disconnect");
1704
+ }
1705
+ }
1706
+ this.logger.info("Database manager shut down");
1707
+ }
1708
+ getConfigs() {
1709
+ return this._configs;
1710
+ }
1711
+ getConfig(dbId) {
1712
+ return this._configs.find((c) => c.id === dbId);
1713
+ }
1714
+ getAdapter(dbId) {
1715
+ const adapter = this.adapters.get(dbId);
1716
+ if (!adapter) {
1717
+ throw new Error(`Database not found: ${dbId}`);
1718
+ }
1719
+ return adapter;
1720
+ }
1721
+ async connect(dbId) {
1722
+ const adapter = this.getAdapter(dbId);
1723
+ await adapter.connect();
1724
+ }
1725
+ async ensureConnected(dbId) {
1726
+ const adapter = this.getAdapter(dbId);
1727
+ const connected = await adapter.testConnection();
1728
+ if (!connected) {
1729
+ await this.connect(dbId);
1730
+ }
1731
+ }
1732
+ async testConnection(dbId) {
1733
+ const adapter = this.getAdapter(dbId);
1734
+ return adapter.testConnection();
1735
+ }
1736
+ async getVersion(dbId) {
1737
+ await this.ensureConnected(dbId);
1738
+ const adapter = this.getAdapter(dbId);
1739
+ return adapter.getVersion();
1740
+ }
1741
+ async introspectSchema(dbId, forceRefresh = false, options) {
1742
+ if (!forceRefresh) {
1743
+ const cached = await this.cache.get(dbId);
1744
+ if (cached) {
1745
+ this.logger.debug({ dbId }, "Using cached schema");
1746
+ return cached;
1747
+ }
1748
+ }
1749
+ const releaseLock = await this.cache.acquireIntrospectionLock(dbId);
1750
+ try {
1751
+ if (!forceRefresh) {
1752
+ const cached = await this.cache.get(dbId);
1753
+ if (cached) {
1754
+ return cached;
1755
+ }
1756
+ }
1757
+ this.logger.info({ dbId, forceRefresh }, "Introspecting schema");
1758
+ await this.ensureConnected(dbId);
1759
+ const adapter = this.getAdapter(dbId);
1760
+ const schema = await adapter.introspect(options);
1761
+ const config = this.getConfig(dbId);
1762
+ await this.cache.set(dbId, schema, config?.introspection?.maxTables);
1763
+ const entry = await this.cache.get(dbId);
1764
+ return entry;
1765
+ } finally {
1766
+ releaseLock();
1767
+ }
1768
+ }
1769
+ async getSchema(dbId) {
1770
+ return this.introspectSchema(dbId, false);
1771
+ }
1772
+ async runQuery(dbId, sql, params = [], timeoutMs) {
1773
+ const config = this.getConfig(dbId);
1774
+ if (isWriteOperation(sql)) {
1775
+ if (!this.options.allowWrite && !config?.readOnly === false) {
1776
+ throw new Error("Write operations are not allowed. Set allowWrite in config.");
1777
+ }
1778
+ if (this.options.disableDangerousOperations) {
1779
+ const operation = sql.trim().split(/\s+/)[0].toUpperCase();
1780
+ const dangerousOps = ["DELETE", "TRUNCATE", "DROP"];
1781
+ if (dangerousOps.includes(operation)) {
1782
+ throw new Error(`Dangerous operation ${operation} is disabled. Set disableDangerousOperations: false in security config to allow.`);
1783
+ }
1784
+ }
1785
+ if (this.options.allowedWriteOperations && this.options.allowedWriteOperations.length > 0) {
1786
+ const operation = sql.trim().split(/\s+/)[0].toUpperCase();
1787
+ if (!this.options.allowedWriteOperations.includes(operation)) {
1788
+ throw new Error(`Write operation ${operation} is not allowed.`);
1789
+ }
1790
+ }
1791
+ }
1792
+ await this.introspectSchema(dbId, false);
1793
+ await this.ensureConnected(dbId);
1794
+ const adapter = this.getAdapter(dbId);
1795
+ try {
1796
+ const result = await adapter.query(sql, params, timeoutMs);
1797
+ this.queryTracker.track(dbId, sql, result.executionTimeMs, result.rowCount);
1798
+ return result;
1799
+ } catch (error) {
1800
+ this.queryTracker.track(dbId, sql, 0, 0, error.message);
1801
+ throw error;
1802
+ }
1803
+ }
1804
+ async explainQuery(dbId, sql, params = []) {
1805
+ await this.ensureConnected(dbId);
1806
+ const adapter = this.getAdapter(dbId);
1807
+ return adapter.explain(sql, params);
1808
+ }
1809
+ async suggestJoins(dbId, tables) {
1810
+ const cacheEntry = await this.getSchema(dbId);
1811
+ return findJoinPaths(tables, cacheEntry.relationships);
1812
+ }
1813
+ async clearCache(dbId) {
1814
+ await this.cache.clear(dbId);
1815
+ this.queryTracker.clear(dbId);
1816
+ }
1817
+ async getCacheStatus(dbId) {
1818
+ return this.cache.getStatus(dbId);
1819
+ }
1820
+ getQueryStats(dbId) {
1821
+ return this.queryTracker.getStats(dbId);
1822
+ }
1823
+ getQueryHistory(dbId, limit) {
1824
+ return this.queryTracker.getHistory(dbId, limit);
1825
+ }
1826
+ };
1827
+
1828
+ // src/mcp-server.ts
1829
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1830
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1831
+ import {
1832
+ CallToolRequestSchema,
1833
+ ListToolsRequestSchema,
1834
+ ListResourcesRequestSchema,
1835
+ ReadResourceRequestSchema
1836
+ } from "@modelcontextprotocol/sdk/types.js";
1837
+ var MCPServer = class {
1838
+ constructor(_dbManager, _config) {
1839
+ this._dbManager = _dbManager;
1840
+ this._config = _config;
1841
+ this.server = new Server(
1842
+ {
1843
+ name: "mcp-database-server",
1844
+ version: "1.0.0"
1845
+ },
1846
+ {
1847
+ capabilities: {
1848
+ tools: {},
1849
+ resources: {}
1850
+ }
1851
+ }
1852
+ );
1853
+ this.setupHandlers();
1854
+ }
1855
+ server;
1856
+ logger = getLogger();
1857
+ setupHandlers() {
1858
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
1859
+ tools: [
1860
+ {
1861
+ name: "list_databases",
1862
+ description: "List all configured databases with their status",
1863
+ inputSchema: {
1864
+ type: "object",
1865
+ properties: {}
1866
+ }
1867
+ },
1868
+ {
1869
+ name: "introspect_schema",
1870
+ description: "Introspect database schema and cache it",
1871
+ inputSchema: {
1872
+ type: "object",
1873
+ properties: {
1874
+ dbId: {
1875
+ type: "string",
1876
+ description: "Database ID to introspect"
1877
+ },
1878
+ forceRefresh: {
1879
+ type: "boolean",
1880
+ description: "Force refresh even if cached",
1881
+ default: false
1882
+ },
1883
+ schemaFilter: {
1884
+ type: "object",
1885
+ description: "Optional schema filtering options",
1886
+ properties: {
1887
+ includeSchemas: {
1888
+ type: "array",
1889
+ items: { type: "string" }
1890
+ },
1891
+ excludeSchemas: {
1892
+ type: "array",
1893
+ items: { type: "string" }
1894
+ },
1895
+ includeViews: { type: "boolean" },
1896
+ maxTables: { type: "number" }
1897
+ }
1898
+ }
1899
+ },
1900
+ required: ["dbId"]
1901
+ }
1902
+ },
1903
+ {
1904
+ name: "get_schema",
1905
+ description: "Get cached schema metadata",
1906
+ inputSchema: {
1907
+ type: "object",
1908
+ properties: {
1909
+ dbId: {
1910
+ type: "string",
1911
+ description: "Database ID"
1912
+ },
1913
+ schema: {
1914
+ type: "string",
1915
+ description: "Optional schema name to filter"
1916
+ },
1917
+ table: {
1918
+ type: "string",
1919
+ description: "Optional table name to filter"
1920
+ }
1921
+ },
1922
+ required: ["dbId"]
1923
+ }
1924
+ },
1925
+ {
1926
+ name: "run_query",
1927
+ description: "Execute SQL query against a database",
1928
+ inputSchema: {
1929
+ type: "object",
1930
+ properties: {
1931
+ dbId: {
1932
+ type: "string",
1933
+ description: "Database ID"
1934
+ },
1935
+ sql: {
1936
+ type: "string",
1937
+ description: "SQL query to execute"
1938
+ },
1939
+ params: {
1940
+ type: "array",
1941
+ description: "Query parameters",
1942
+ items: {}
1943
+ },
1944
+ limit: {
1945
+ type: "number",
1946
+ description: "Maximum number of rows to return"
1947
+ },
1948
+ timeoutMs: {
1949
+ type: "number",
1950
+ description: "Query timeout in milliseconds"
1951
+ }
1952
+ },
1953
+ required: ["dbId", "sql"]
1954
+ }
1955
+ },
1956
+ {
1957
+ name: "explain_query",
1958
+ description: "Get query execution plan",
1959
+ inputSchema: {
1960
+ type: "object",
1961
+ properties: {
1962
+ dbId: {
1963
+ type: "string",
1964
+ description: "Database ID"
1965
+ },
1966
+ sql: {
1967
+ type: "string",
1968
+ description: "SQL query to explain"
1969
+ },
1970
+ params: {
1971
+ type: "array",
1972
+ description: "Query parameters",
1973
+ items: {}
1974
+ }
1975
+ },
1976
+ required: ["dbId", "sql"]
1977
+ }
1978
+ },
1979
+ {
1980
+ name: "suggest_joins",
1981
+ description: "Suggest join paths between tables based on relationships",
1982
+ inputSchema: {
1983
+ type: "object",
1984
+ properties: {
1985
+ dbId: {
1986
+ type: "string",
1987
+ description: "Database ID"
1988
+ },
1989
+ tables: {
1990
+ type: "array",
1991
+ description: "List of table names to join",
1992
+ items: { type: "string" },
1993
+ minItems: 2
1994
+ }
1995
+ },
1996
+ required: ["dbId", "tables"]
1997
+ }
1998
+ },
1999
+ {
2000
+ name: "clear_cache",
2001
+ description: "Clear schema cache",
2002
+ inputSchema: {
2003
+ type: "object",
2004
+ properties: {
2005
+ dbId: {
2006
+ type: "string",
2007
+ description: "Optional database ID (clears all if omitted)"
2008
+ }
2009
+ }
2010
+ }
2011
+ },
2012
+ {
2013
+ name: "cache_status",
2014
+ description: "Get cache status and statistics",
2015
+ inputSchema: {
2016
+ type: "object",
2017
+ properties: {
2018
+ dbId: {
2019
+ type: "string",
2020
+ description: "Optional database ID"
2021
+ }
2022
+ }
2023
+ }
2024
+ },
2025
+ {
2026
+ name: "health_check",
2027
+ description: "Check database connectivity and get version info",
2028
+ inputSchema: {
2029
+ type: "object",
2030
+ properties: {
2031
+ dbId: {
2032
+ type: "string",
2033
+ description: "Optional database ID (checks all if omitted)"
2034
+ }
2035
+ }
2036
+ }
2037
+ }
2038
+ ]
2039
+ }));
2040
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2041
+ const { name, arguments: args } = request.params;
2042
+ try {
2043
+ switch (name) {
2044
+ case "list_databases":
2045
+ return await this.handleListDatabases();
2046
+ case "introspect_schema":
2047
+ return await this.handleIntrospectSchema(args);
2048
+ case "get_schema":
2049
+ return await this.handleGetSchema(args);
2050
+ case "run_query":
2051
+ return await this.handleRunQuery(args);
2052
+ case "explain_query":
2053
+ return await this.handleExplainQuery(args);
2054
+ case "suggest_joins":
2055
+ return await this.handleSuggestJoins(args);
2056
+ case "clear_cache":
2057
+ return await this.handleClearCache(args);
2058
+ case "cache_status":
2059
+ return await this.handleCacheStatus(args);
2060
+ case "health_check":
2061
+ return await this.handleHealthCheck(args);
2062
+ default:
2063
+ throw new Error(`Unknown tool: ${name}`);
2064
+ }
2065
+ } catch (error) {
2066
+ this.logger.error({ tool: name, error }, "Tool execution failed");
2067
+ return {
2068
+ content: [
2069
+ {
2070
+ type: "text",
2071
+ text: JSON.stringify({
2072
+ error: error.message,
2073
+ code: error.code || "TOOL_ERROR"
2074
+ })
2075
+ }
2076
+ ]
2077
+ };
2078
+ }
2079
+ });
2080
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
2081
+ const statuses = await this._dbManager.getCacheStatus();
2082
+ const resources = statuses.filter((s) => s.exists).map((s) => ({
2083
+ uri: `schema://${s.dbId}`,
2084
+ name: `Schema: ${s.dbId}`,
2085
+ description: `Cached schema for ${s.dbId} (${s.tableCount} tables, ${s.relationshipCount} relationships)`,
2086
+ mimeType: "application/json"
2087
+ }));
2088
+ return { resources };
2089
+ });
2090
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2091
+ const uri = request.params.uri;
2092
+ const match = uri.match(/^schema:\/\/(.+)$/);
2093
+ if (!match) {
2094
+ throw new Error(`Invalid resource URI: ${uri}`);
2095
+ }
2096
+ const dbId = match[1];
2097
+ const cacheEntry = await this._dbManager.getSchema(dbId);
2098
+ return {
2099
+ contents: [
2100
+ {
2101
+ uri,
2102
+ mimeType: "application/json",
2103
+ text: JSON.stringify(cacheEntry, null, 2)
2104
+ }
2105
+ ]
2106
+ };
2107
+ });
2108
+ }
2109
+ async handleListDatabases() {
2110
+ const configs = this._dbManager.getConfigs();
2111
+ const statuses = await Promise.all(
2112
+ configs.map(async (config) => {
2113
+ const connected = await this._dbManager.testConnection(config.id);
2114
+ const cacheStatus = (await this._dbManager.getCacheStatus(config.id))[0];
2115
+ return {
2116
+ id: config.id,
2117
+ type: config.type,
2118
+ url: this._config.security?.redactSecrets ? redactUrl(config.url || "") : config.url,
2119
+ connected,
2120
+ cached: cacheStatus?.exists || false,
2121
+ cacheAge: cacheStatus?.age,
2122
+ version: cacheStatus?.version
2123
+ };
2124
+ })
2125
+ );
2126
+ return {
2127
+ content: [
2128
+ {
2129
+ type: "text",
2130
+ text: JSON.stringify(statuses, null, 2)
2131
+ }
2132
+ ]
2133
+ };
2134
+ }
2135
+ async handleIntrospectSchema(args) {
2136
+ const result = await this._dbManager.introspectSchema(
2137
+ args.dbId,
2138
+ args.forceRefresh || false,
2139
+ args.schemaFilter
2140
+ );
2141
+ const summary = {
2142
+ dbId: args.dbId,
2143
+ version: result.schema.version,
2144
+ introspectedAt: result.schema.introspectedAt,
2145
+ schemas: result.schema.schemas.map((s) => ({
2146
+ name: s.name,
2147
+ tableCount: s.tables.length,
2148
+ viewCount: s.tables.filter((t) => t.type === "view").length
2149
+ })),
2150
+ totalTables: result.schema.schemas.reduce((sum, s) => sum + s.tables.length, 0),
2151
+ totalRelationships: result.relationships.length
2152
+ };
2153
+ return {
2154
+ content: [
2155
+ {
2156
+ type: "text",
2157
+ text: JSON.stringify(summary, null, 2)
2158
+ }
2159
+ ]
2160
+ };
2161
+ }
2162
+ async handleGetSchema(args) {
2163
+ const cacheEntry = await this._dbManager.getSchema(args.dbId);
2164
+ let result = cacheEntry.schema;
2165
+ if (args.schema) {
2166
+ result = {
2167
+ ...result,
2168
+ schemas: result.schemas.filter((s) => s.name === args.schema)
2169
+ };
2170
+ }
2171
+ if (args.table) {
2172
+ result = {
2173
+ ...result,
2174
+ schemas: result.schemas.map((s) => ({
2175
+ ...s,
2176
+ tables: s.tables.filter((t) => t.name === args.table)
2177
+ }))
2178
+ };
2179
+ }
2180
+ return {
2181
+ content: [
2182
+ {
2183
+ type: "text",
2184
+ text: JSON.stringify(result, null, 2)
2185
+ }
2186
+ ]
2187
+ };
2188
+ }
2189
+ async handleRunQuery(args) {
2190
+ let sql = args.sql;
2191
+ if (args.limit && !sql.toUpperCase().includes("LIMIT")) {
2192
+ sql += ` LIMIT ${args.limit}`;
2193
+ }
2194
+ const result = await this._dbManager.runQuery(args.dbId, sql, args.params, args.timeoutMs);
2195
+ const cacheEntry = await this._dbManager.getSchema(args.dbId);
2196
+ const queryStats = this._dbManager.getQueryStats(args.dbId);
2197
+ return {
2198
+ content: [
2199
+ {
2200
+ type: "text",
2201
+ text: JSON.stringify(
2202
+ {
2203
+ ...result,
2204
+ metadata: {
2205
+ relationships: cacheEntry.relationships.filter(
2206
+ (r) => result.columns.some(
2207
+ (col) => col.includes(r.fromTable) || col.includes(r.toTable)
2208
+ )
2209
+ ),
2210
+ queryStats
2211
+ }
2212
+ },
2213
+ null,
2214
+ 2
2215
+ )
2216
+ }
2217
+ ]
2218
+ };
2219
+ }
2220
+ async handleExplainQuery(args) {
2221
+ const result = await this._dbManager.explainQuery(args.dbId, args.sql, args.params);
2222
+ return {
2223
+ content: [
2224
+ {
2225
+ type: "text",
2226
+ text: JSON.stringify(result, null, 2)
2227
+ }
2228
+ ]
2229
+ };
2230
+ }
2231
+ async handleSuggestJoins(args) {
2232
+ const joinPaths = await this._dbManager.suggestJoins(args.dbId, args.tables);
2233
+ return {
2234
+ content: [
2235
+ {
2236
+ type: "text",
2237
+ text: JSON.stringify(joinPaths, null, 2)
2238
+ }
2239
+ ]
2240
+ };
2241
+ }
2242
+ async handleClearCache(args) {
2243
+ await this._dbManager.clearCache(args.dbId);
2244
+ return {
2245
+ content: [
2246
+ {
2247
+ type: "text",
2248
+ text: JSON.stringify({
2249
+ success: true,
2250
+ message: args.dbId ? `Cache cleared for ${args.dbId}` : "All caches cleared"
2251
+ })
2252
+ }
2253
+ ]
2254
+ };
2255
+ }
2256
+ async handleCacheStatus(args) {
2257
+ const statuses = await this._dbManager.getCacheStatus(args.dbId);
2258
+ return {
2259
+ content: [
2260
+ {
2261
+ type: "text",
2262
+ text: JSON.stringify(statuses, null, 2)
2263
+ }
2264
+ ]
2265
+ };
2266
+ }
2267
+ async handleHealthCheck(args) {
2268
+ const configs = args.dbId ? [this._dbManager.getConfig(args.dbId)] : this._dbManager.getConfigs();
2269
+ const results = await Promise.all(
2270
+ configs.map(async (config) => {
2271
+ try {
2272
+ const connected = await this._dbManager.testConnection(config.id);
2273
+ const version2 = connected ? await this._dbManager.getVersion(config.id) : "N/A";
2274
+ return {
2275
+ dbId: config.id,
2276
+ healthy: connected,
2277
+ version: version2
2278
+ };
2279
+ } catch (error) {
2280
+ return {
2281
+ dbId: config.id,
2282
+ healthy: false,
2283
+ error: error.message
2284
+ };
2285
+ }
2286
+ })
2287
+ );
2288
+ return {
2289
+ content: [
2290
+ {
2291
+ type: "text",
2292
+ text: JSON.stringify(results, null, 2)
2293
+ }
2294
+ ]
2295
+ };
2296
+ }
2297
+ async start() {
2298
+ const transport = new StdioServerTransport();
2299
+ await this.server.connect(transport);
2300
+ this.logger.info("MCP server started");
2301
+ }
2302
+ };
2303
+
2304
+ // src/index.ts
2305
+ dotenv.config();
2306
+ var __filename2 = fileURLToPath(import.meta.url);
2307
+ var __dirname2 = dirname2(__filename2);
2308
+ var packageJson = JSON.parse(
2309
+ readFileSync(join2(__dirname2, "../package.json"), "utf-8")
2310
+ );
2311
+ var version = packageJson.version;
2312
+ async function main() {
2313
+ try {
2314
+ const { values } = parseArgs({
2315
+ options: {
2316
+ config: {
2317
+ type: "string",
2318
+ short: "c",
2319
+ default: "./.mcp-database-server.config"
2320
+ },
2321
+ help: {
2322
+ type: "boolean",
2323
+ short: "h"
2324
+ },
2325
+ version: {
2326
+ type: "boolean",
2327
+ short: "v"
2328
+ }
2329
+ }
2330
+ });
2331
+ if (values.version) {
2332
+ console.log(version);
2333
+ process.exit(0);
2334
+ }
2335
+ if (values.help) {
2336
+ console.log(`
2337
+ mcp-database-server - Model Context Protocol Server for SQL Databases
2338
+
2339
+ Usage:
2340
+ mcp-database-server [options]
2341
+
2342
+ Options:
2343
+ -c, --config <path> Path to configuration file (default: ./.mcp-database-server.config)
2344
+ -h, --help Show this help message
2345
+ -v, --version Show version number
2346
+
2347
+ Configuration:
2348
+ The config file should be a JSON file with database configurations.
2349
+ See mcp-database-server.config.example for reference.
2350
+
2351
+ Environment Variables:
2352
+ You can use environment variable interpolation in the config file:
2353
+ Example: "url": "\${DB_URL_POSTGRES}"
2354
+
2355
+ Examples:
2356
+ mcp-database-server --config ./my-config.json
2357
+ mcp-database-server -c ./config/production.json
2358
+ `);
2359
+ process.exit(0);
2360
+ }
2361
+ let configPath = values.config;
2362
+ if (configPath === "./.mcp-database-server.config") {
2363
+ const foundPath = findConfigFile(".mcp-database-server.config");
2364
+ if (foundPath) {
2365
+ configPath = foundPath;
2366
+ } else {
2367
+ console.error("Error: Config file .mcp-database-server.config not found");
2368
+ console.error("Searched in current directory and all parent directories");
2369
+ console.error("\nTo create a config file:");
2370
+ console.error(" cp mcp-database-server.config.example .mcp-database-server.config");
2371
+ console.error("\nOr specify a custom path:");
2372
+ console.error(" mcp-database-server --config /path/to/config.json");
2373
+ process.exit(1);
2374
+ }
2375
+ }
2376
+ const config = await loadConfig(configPath);
2377
+ initLogger(config.logging?.level || "info", config.logging?.pretty || false);
2378
+ const logger2 = getLogger();
2379
+ logger2.info({ configPath }, "Configuration loaded");
2380
+ const dbManager = new DatabaseManager(config.databases, {
2381
+ cacheDir: config.cache?.directory || ".sql-mcp-cache",
2382
+ cacheTtlMinutes: config.cache?.ttlMinutes || 10,
2383
+ allowWrite: config.security?.allowWrite || false,
2384
+ allowedWriteOperations: config.security?.allowedWriteOperations,
2385
+ disableDangerousOperations: config.security?.disableDangerousOperations ?? true
2386
+ });
2387
+ await dbManager.init();
2388
+ const mcpServer = new MCPServer(dbManager, config);
2389
+ await mcpServer.start();
2390
+ const shutdown = async (signal) => {
2391
+ logger2.info({ signal }, "Shutting down...");
2392
+ await dbManager.shutdown();
2393
+ process.exit(0);
2394
+ };
2395
+ process.on("SIGINT", () => shutdown("SIGINT"));
2396
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2397
+ } catch (error) {
2398
+ console.error("Fatal error:", error.message);
2399
+ if (error.details) {
2400
+ console.error("Details:", JSON.stringify(error.details, null, 2));
2401
+ }
2402
+ process.exit(1);
2403
+ }
2404
+ }
2405
+ main();
2406
+ //# sourceMappingURL=index.js.map