@bytebase/dbhub 0.19.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,479 @@
1
+ import {
2
+ SQLRowLimiter
3
+ } from "./chunk-BRXZ5ZQB.js";
4
+ import {
5
+ ConnectorRegistry,
6
+ SafeURL,
7
+ obfuscateDSNPassword
8
+ } from "./chunk-C7WEAPX4.js";
9
+
10
+ // src/connectors/sqlserver/index.ts
11
+ import sql from "mssql";
12
+ import { DefaultAzureCredential } from "@azure/identity";
13
+ var SQLServerDSNParser = class {
14
+ async parse(dsn, config) {
15
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
16
+ const queryTimeoutSeconds = config?.queryTimeoutSeconds;
17
+ if (!this.isValidDSN(dsn)) {
18
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
19
+ const expectedFormat = this.getSampleDSN();
20
+ throw new Error(
21
+ `Invalid SQL Server DSN format.
22
+ Provided: ${obfuscatedDSN}
23
+ Expected: ${expectedFormat}`
24
+ );
25
+ }
26
+ try {
27
+ const url = new SafeURL(dsn);
28
+ const options = {};
29
+ url.forEachSearchParam((value, key) => {
30
+ if (key === "authentication") {
31
+ options.authentication = value;
32
+ } else if (key === "sslmode") {
33
+ options.sslmode = value;
34
+ } else if (key === "instanceName") {
35
+ options.instanceName = value;
36
+ } else if (key === "domain") {
37
+ options.domain = value;
38
+ }
39
+ });
40
+ if (options.authentication === "ntlm" && !options.domain) {
41
+ throw new Error("NTLM authentication requires 'domain' parameter");
42
+ }
43
+ if (options.domain && options.authentication !== "ntlm") {
44
+ throw new Error("Parameter 'domain' requires 'authentication=ntlm'");
45
+ }
46
+ if (options.sslmode) {
47
+ if (options.sslmode === "disable") {
48
+ options.encrypt = false;
49
+ options.trustServerCertificate = false;
50
+ } else if (options.sslmode === "require") {
51
+ options.encrypt = true;
52
+ options.trustServerCertificate = true;
53
+ }
54
+ }
55
+ const config2 = {
56
+ server: url.hostname,
57
+ port: url.port ? parseInt(url.port) : 1433,
58
+ // Default SQL Server port
59
+ database: url.pathname ? url.pathname.substring(1) : "",
60
+ // Remove leading slash
61
+ options: {
62
+ encrypt: options.encrypt ?? false,
63
+ // Default to unencrypted for development
64
+ trustServerCertificate: options.trustServerCertificate ?? false,
65
+ ...connectionTimeoutSeconds !== void 0 && {
66
+ connectTimeout: connectionTimeoutSeconds * 1e3
67
+ },
68
+ ...queryTimeoutSeconds !== void 0 && {
69
+ requestTimeout: queryTimeoutSeconds * 1e3
70
+ },
71
+ instanceName: options.instanceName
72
+ // Add named instance support
73
+ }
74
+ };
75
+ switch (options.authentication) {
76
+ case "azure-active-directory-access-token": {
77
+ try {
78
+ const credential = new DefaultAzureCredential();
79
+ const token = await credential.getToken("https://database.windows.net/");
80
+ config2.authentication = {
81
+ type: "azure-active-directory-access-token",
82
+ options: {
83
+ token: token.token
84
+ }
85
+ };
86
+ } catch (error) {
87
+ const errorMessage = error instanceof Error ? error.message : String(error);
88
+ throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
89
+ }
90
+ break;
91
+ }
92
+ case "ntlm":
93
+ config2.authentication = {
94
+ type: "ntlm",
95
+ options: {
96
+ domain: options.domain,
97
+ userName: url.username,
98
+ password: url.password
99
+ }
100
+ };
101
+ break;
102
+ default:
103
+ config2.user = url.username;
104
+ config2.password = url.password;
105
+ break;
106
+ }
107
+ return config2;
108
+ } catch (error) {
109
+ throw new Error(
110
+ `Failed to parse SQL Server DSN: ${error instanceof Error ? error.message : String(error)}`
111
+ );
112
+ }
113
+ }
114
+ getSampleDSN() {
115
+ return "sqlserver://username:password@localhost:1433/database?sslmode=disable&instanceName=INSTANCE1";
116
+ }
117
+ isValidDSN(dsn) {
118
+ try {
119
+ return dsn.startsWith("sqlserver://");
120
+ } catch (error) {
121
+ return false;
122
+ }
123
+ }
124
+ };
125
+ var SQLServerConnector = class _SQLServerConnector {
126
+ constructor() {
127
+ this.id = "sqlserver";
128
+ this.name = "SQL Server";
129
+ this.dsnParser = new SQLServerDSNParser();
130
+ // Source ID is set by ConnectorManager after cloning
131
+ this.sourceId = "default";
132
+ }
133
+ getId() {
134
+ return this.sourceId;
135
+ }
136
+ clone() {
137
+ return new _SQLServerConnector();
138
+ }
139
+ async connect(dsn, initScript, config) {
140
+ try {
141
+ this.config = await this.dsnParser.parse(dsn, config);
142
+ if (!this.config.options) {
143
+ this.config.options = {};
144
+ }
145
+ this.connection = await new sql.ConnectionPool(this.config).connect();
146
+ } catch (error) {
147
+ throw error;
148
+ }
149
+ }
150
+ async disconnect() {
151
+ if (this.connection) {
152
+ await this.connection.close();
153
+ this.connection = void 0;
154
+ }
155
+ }
156
+ async getSchemas() {
157
+ if (!this.connection) {
158
+ throw new Error("Not connected to SQL Server database");
159
+ }
160
+ try {
161
+ const result = await this.connection.request().query(`
162
+ SELECT SCHEMA_NAME
163
+ FROM INFORMATION_SCHEMA.SCHEMATA
164
+ ORDER BY SCHEMA_NAME
165
+ `);
166
+ return result.recordset.map((row) => row.SCHEMA_NAME);
167
+ } catch (error) {
168
+ throw new Error(`Failed to get schemas: ${error.message}`);
169
+ }
170
+ }
171
+ async getTables(schema) {
172
+ if (!this.connection) {
173
+ throw new Error("Not connected to SQL Server database");
174
+ }
175
+ try {
176
+ const schemaToUse = schema || "dbo";
177
+ const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
178
+ const query = `
179
+ SELECT TABLE_NAME
180
+ FROM INFORMATION_SCHEMA.TABLES
181
+ WHERE TABLE_SCHEMA = @schema
182
+ ORDER BY TABLE_NAME
183
+ `;
184
+ const result = await request.query(query);
185
+ return result.recordset.map((row) => row.TABLE_NAME);
186
+ } catch (error) {
187
+ throw new Error(`Failed to get tables: ${error.message}`);
188
+ }
189
+ }
190
+ async tableExists(tableName, schema) {
191
+ if (!this.connection) {
192
+ throw new Error("Not connected to SQL Server database");
193
+ }
194
+ try {
195
+ const schemaToUse = schema || "dbo";
196
+ const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
197
+ const query = `
198
+ SELECT COUNT(*) as count
199
+ FROM INFORMATION_SCHEMA.TABLES
200
+ WHERE TABLE_NAME = @tableName
201
+ AND TABLE_SCHEMA = @schema
202
+ `;
203
+ const result = await request.query(query);
204
+ return result.recordset[0].count > 0;
205
+ } catch (error) {
206
+ throw new Error(`Failed to check if table exists: ${error.message}`);
207
+ }
208
+ }
209
+ async getTableIndexes(tableName, schema) {
210
+ if (!this.connection) {
211
+ throw new Error("Not connected to SQL Server database");
212
+ }
213
+ try {
214
+ const schemaToUse = schema || "dbo";
215
+ const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
216
+ const query = `
217
+ SELECT i.name AS index_name,
218
+ i.is_unique,
219
+ i.is_primary_key,
220
+ c.name AS column_name,
221
+ ic.key_ordinal
222
+ FROM sys.indexes i
223
+ INNER JOIN
224
+ sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
225
+ INNER JOIN
226
+ sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
227
+ INNER JOIN
228
+ sys.tables t ON i.object_id = t.object_id
229
+ INNER JOIN
230
+ sys.schemas s ON t.schema_id = s.schema_id
231
+ WHERE t.name = @tableName
232
+ AND s.name = @schema
233
+ ORDER BY i.name,
234
+ ic.key_ordinal
235
+ `;
236
+ const result = await request.query(query);
237
+ const indexMap = /* @__PURE__ */ new Map();
238
+ for (const row of result.recordset) {
239
+ const indexName = row.index_name;
240
+ const columnName = row.column_name;
241
+ const isUnique = !!row.is_unique;
242
+ const isPrimary = !!row.is_primary_key;
243
+ if (!indexMap.has(indexName)) {
244
+ indexMap.set(indexName, {
245
+ columns: [],
246
+ is_unique: isUnique,
247
+ is_primary: isPrimary
248
+ });
249
+ }
250
+ const indexInfo = indexMap.get(indexName);
251
+ indexInfo.columns.push(columnName);
252
+ }
253
+ const indexes = [];
254
+ indexMap.forEach((info, name) => {
255
+ indexes.push({
256
+ index_name: name,
257
+ column_names: info.columns,
258
+ is_unique: info.is_unique,
259
+ is_primary: info.is_primary
260
+ });
261
+ });
262
+ return indexes;
263
+ } catch (error) {
264
+ throw new Error(`Failed to get indexes for table ${tableName}: ${error.message}`);
265
+ }
266
+ }
267
+ async getTableSchema(tableName, schema) {
268
+ if (!this.connection) {
269
+ throw new Error("Not connected to SQL Server database");
270
+ }
271
+ try {
272
+ const schemaToUse = schema || "dbo";
273
+ const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
274
+ const query = `
275
+ SELECT c.COLUMN_NAME as column_name,
276
+ c.DATA_TYPE as data_type,
277
+ c.IS_NULLABLE as is_nullable,
278
+ c.COLUMN_DEFAULT as column_default,
279
+ ep.value as description
280
+ FROM INFORMATION_SCHEMA.COLUMNS c
281
+ LEFT JOIN sys.columns sc
282
+ ON sc.name = c.COLUMN_NAME
283
+ AND sc.object_id = OBJECT_ID(QUOTENAME(c.TABLE_SCHEMA) + '.' + QUOTENAME(c.TABLE_NAME))
284
+ LEFT JOIN sys.extended_properties ep
285
+ ON ep.major_id = sc.object_id
286
+ AND ep.minor_id = sc.column_id
287
+ AND ep.name = 'MS_Description'
288
+ WHERE c.TABLE_NAME = @tableName
289
+ AND c.TABLE_SCHEMA = @schema
290
+ ORDER BY c.ORDINAL_POSITION
291
+ `;
292
+ const result = await request.query(query);
293
+ return result.recordset.map((row) => ({
294
+ ...row,
295
+ description: row.description || null
296
+ }));
297
+ } catch (error) {
298
+ throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`);
299
+ }
300
+ }
301
+ async getTableComment(tableName, schema) {
302
+ if (!this.connection) {
303
+ throw new Error("Not connected to SQL Server database");
304
+ }
305
+ try {
306
+ const schemaToUse = schema || "dbo";
307
+ const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
308
+ const query = `
309
+ SELECT ep.value as table_comment
310
+ FROM sys.extended_properties ep
311
+ JOIN sys.tables t ON ep.major_id = t.object_id
312
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
313
+ WHERE ep.minor_id = 0
314
+ AND ep.name = 'MS_Description'
315
+ AND t.name = @tableName
316
+ AND s.name = @schema
317
+ `;
318
+ const result = await request.query(query);
319
+ if (result.recordset.length > 0) {
320
+ return result.recordset[0].table_comment || null;
321
+ }
322
+ return null;
323
+ } catch (error) {
324
+ return null;
325
+ }
326
+ }
327
+ async getStoredProcedures(schema, routineType) {
328
+ if (!this.connection) {
329
+ throw new Error("Not connected to SQL Server database");
330
+ }
331
+ try {
332
+ const schemaToUse = schema || "dbo";
333
+ const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
334
+ let typeFilter;
335
+ if (routineType === "function") {
336
+ typeFilter = "AND ROUTINE_TYPE = 'FUNCTION'";
337
+ } else if (routineType === "procedure") {
338
+ typeFilter = "AND ROUTINE_TYPE = 'PROCEDURE'";
339
+ } else {
340
+ typeFilter = "AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')";
341
+ }
342
+ const query = `
343
+ SELECT ROUTINE_NAME
344
+ FROM INFORMATION_SCHEMA.ROUTINES
345
+ WHERE ROUTINE_SCHEMA = @schema
346
+ ${typeFilter}
347
+ ORDER BY ROUTINE_NAME
348
+ `;
349
+ const result = await request.query(query);
350
+ return result.recordset.map((row) => row.ROUTINE_NAME);
351
+ } catch (error) {
352
+ throw new Error(`Failed to get stored procedures: ${error.message}`);
353
+ }
354
+ }
355
+ async getStoredProcedureDetail(procedureName, schema) {
356
+ if (!this.connection) {
357
+ throw new Error("Not connected to SQL Server database");
358
+ }
359
+ try {
360
+ const schemaToUse = schema || "dbo";
361
+ const request = this.connection.request().input("procedureName", sql.VarChar, procedureName).input("schema", sql.VarChar, schemaToUse);
362
+ const routineQuery = `
363
+ SELECT ROUTINE_NAME as procedure_name,
364
+ ROUTINE_TYPE,
365
+ DATA_TYPE as return_data_type
366
+ FROM INFORMATION_SCHEMA.ROUTINES
367
+ WHERE ROUTINE_NAME = @procedureName
368
+ AND ROUTINE_SCHEMA = @schema
369
+ `;
370
+ const routineResult = await request.query(routineQuery);
371
+ if (routineResult.recordset.length === 0) {
372
+ throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
373
+ }
374
+ const routine = routineResult.recordset[0];
375
+ const parameterQuery = `
376
+ SELECT PARAMETER_NAME,
377
+ PARAMETER_MODE,
378
+ DATA_TYPE,
379
+ CHARACTER_MAXIMUM_LENGTH,
380
+ ORDINAL_POSITION
381
+ FROM INFORMATION_SCHEMA.PARAMETERS
382
+ WHERE SPECIFIC_NAME = @procedureName
383
+ AND SPECIFIC_SCHEMA = @schema
384
+ ORDER BY ORDINAL_POSITION
385
+ `;
386
+ const parameterResult = await request.query(parameterQuery);
387
+ let parameterList = "";
388
+ if (parameterResult.recordset.length > 0) {
389
+ parameterList = parameterResult.recordset.map(
390
+ (param) => {
391
+ const lengthStr = param.CHARACTER_MAXIMUM_LENGTH > 0 ? `(${param.CHARACTER_MAXIMUM_LENGTH})` : "";
392
+ return `${param.PARAMETER_NAME} ${param.PARAMETER_MODE} ${param.DATA_TYPE}${lengthStr}`;
393
+ }
394
+ ).join(", ");
395
+ }
396
+ const definitionQuery = `
397
+ SELECT definition
398
+ FROM sys.sql_modules sm
399
+ JOIN sys.objects o ON sm.object_id = o.object_id
400
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
401
+ WHERE o.name = @procedureName
402
+ AND s.name = @schema
403
+ `;
404
+ const definitionResult = await request.query(definitionQuery);
405
+ let definition = void 0;
406
+ if (definitionResult.recordset.length > 0) {
407
+ definition = definitionResult.recordset[0].definition;
408
+ }
409
+ return {
410
+ procedure_name: routine.procedure_name,
411
+ procedure_type: routine.ROUTINE_TYPE === "PROCEDURE" ? "procedure" : "function",
412
+ language: "sql",
413
+ // SQL Server procedures are typically in T-SQL
414
+ parameter_list: parameterList,
415
+ return_type: routine.ROUTINE_TYPE === "FUNCTION" ? routine.return_data_type : void 0,
416
+ definition
417
+ };
418
+ } catch (error) {
419
+ throw new Error(`Failed to get stored procedure details: ${error.message}`);
420
+ }
421
+ }
422
+ async executeSQL(sqlQuery, options, parameters) {
423
+ if (!this.connection) {
424
+ throw new Error("Not connected to SQL Server database");
425
+ }
426
+ try {
427
+ let processedSQL = sqlQuery;
428
+ if (options.maxRows) {
429
+ processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
430
+ }
431
+ const request = this.connection.request();
432
+ if (parameters && parameters.length > 0) {
433
+ parameters.forEach((param, index) => {
434
+ const paramName = `p${index + 1}`;
435
+ if (typeof param === "string") {
436
+ request.input(paramName, sql.VarChar, param);
437
+ } else if (typeof param === "number") {
438
+ if (Number.isInteger(param)) {
439
+ request.input(paramName, sql.Int, param);
440
+ } else {
441
+ request.input(paramName, sql.Float, param);
442
+ }
443
+ } else if (typeof param === "boolean") {
444
+ request.input(paramName, sql.Bit, param);
445
+ } else if (param === null || param === void 0) {
446
+ request.input(paramName, sql.VarChar, param);
447
+ } else if (Array.isArray(param)) {
448
+ request.input(paramName, sql.VarChar, JSON.stringify(param));
449
+ } else {
450
+ request.input(paramName, sql.VarChar, JSON.stringify(param));
451
+ }
452
+ });
453
+ }
454
+ let result;
455
+ try {
456
+ result = await request.query(processedSQL);
457
+ } catch (error) {
458
+ if (parameters && parameters.length > 0) {
459
+ console.error(`[SQL Server executeSQL] ERROR: ${error.message}`);
460
+ console.error(`[SQL Server executeSQL] SQL: ${processedSQL}`);
461
+ console.error(`[SQL Server executeSQL] Parameters: ${JSON.stringify(parameters)}`);
462
+ }
463
+ throw error;
464
+ }
465
+ return {
466
+ rows: result.recordset || [],
467
+ rowCount: result.rowsAffected[0] || 0
468
+ };
469
+ } catch (error) {
470
+ throw new Error(`Failed to execute query: ${error.message}`);
471
+ }
472
+ }
473
+ };
474
+ var sqlServerConnector = new SQLServerConnector();
475
+ ConnectorRegistry.register(sqlServerConnector);
476
+ export {
477
+ SQLServerConnector,
478
+ SQLServerDSNParser
479
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
6
6
  "repository": {
@@ -43,17 +43,19 @@
43
43
  "@azure/identity": "^4.8.0",
44
44
  "@iarna/toml": "^2.2.5",
45
45
  "@modelcontextprotocol/sdk": "^1.25.1",
46
- "better-sqlite3": "^11.9.0",
47
46
  "dotenv": "^16.4.7",
48
47
  "express": "^4.18.2",
49
- "mariadb": "^3.4.0",
50
- "mssql": "^11.0.1",
51
- "mysql2": "^3.13.0",
52
- "pg": "^8.13.3",
53
48
  "ssh-config": "^5.0.3",
54
49
  "ssh2": "^1.16.0",
55
50
  "zod": "^3.24.2"
56
51
  },
52
+ "optionalDependencies": {
53
+ "better-sqlite3": "^11.9.0",
54
+ "mariadb": "^3.4.0",
55
+ "mssql": "^11.0.1",
56
+ "mysql2": "^3.13.0",
57
+ "pg": "^8.13.3"
58
+ },
57
59
  "devDependencies": {
58
60
  "@testcontainers/mariadb": "^11.0.3",
59
61
  "@testcontainers/mssqlserver": "^11.0.3",