@bytebase/dbhub 0.20.0 → 0.21.1

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