@bytebase/dbhub 1.0.0 → 1.2.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.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>DBHub - Minimal Database MCP Server</title>
8
- <script type="module" crossorigin src="/assets/index-DBYlgGks.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DOZ5HOZ9.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BJ-1UrcV.css">
10
10
  </head>
11
11
  <body>
@@ -2,9 +2,9 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-MQR6VX5F.js";
5
+ } from "./chunk-CUBIR72I.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
- import "./chunk-JEZZN2YZ.js";
7
+ import "./chunk-WIQ4DWGY.js";
8
8
  export {
9
9
  ToolRegistry,
10
10
  getToolRegistry,
@@ -4,13 +4,13 @@ import {
4
4
  import {
5
5
  SQLRowLimiter,
6
6
  closeQuietly
7
- } from "./chunk-S2V6RZ3C.js";
7
+ } from "./chunk-RSB7DPO2.js";
8
8
  import {
9
9
  ConnectorRegistry,
10
10
  SafeURL,
11
11
  obfuscateDSNPassword,
12
12
  splitSQLStatements
13
- } from "./chunk-JEZZN2YZ.js";
13
+ } from "./chunk-WIQ4DWGY.js";
14
14
 
15
15
  // src/connectors/sqlite/suppress-experimental-warning.ts
16
16
  var installed = false;
@@ -340,10 +340,10 @@ var SQLiteConnector = class _SQLiteConnector {
340
340
  if (isReadStatement) {
341
341
  if (parameters && parameters.length > 0) {
342
342
  const rows = this.prepare(processedStatement).all(...parameters);
343
- return { rows, rowCount: rows.length };
343
+ return { resultSets: [{ sql: statements[0], rows, rowCount: rows.length }] };
344
344
  } else {
345
345
  const rows = this.prepare(processedStatement).all();
346
- return { rows, rowCount: rows.length };
346
+ return { resultSets: [{ sql: statements[0], rows, rowCount: rows.length }] };
347
347
  }
348
348
  } else {
349
349
  let result;
@@ -352,7 +352,9 @@ var SQLiteConnector = class _SQLiteConnector {
352
352
  } else {
353
353
  result = this.prepare(processedStatement).run();
354
354
  }
355
- return { rows: [], rowCount: Number(result.changes) };
355
+ return {
356
+ resultSets: [{ sql: statements[0], rows: [], rowCount: Number(result.changes) }]
357
+ };
356
358
  }
357
359
  } else {
358
360
  if (parameters && parameters.length > 0) {
@@ -368,24 +370,23 @@ var SQLiteConnector = class _SQLiteConnector {
368
370
  writeStatements.push(statement);
369
371
  }
370
372
  }
371
- let totalChanges = 0;
373
+ const resultSets = [];
372
374
  for (const statement of writeStatements) {
373
375
  if (options.readonly) {
374
376
  this.db.exec("PRAGMA query_only = ON");
375
377
  }
376
378
  const result = this.prepare(statement).run();
377
- totalChanges += Number(result.changes);
379
+ resultSets.push({ sql: statement, rows: [], rowCount: Number(result.changes) });
378
380
  }
379
- let allRows = [];
380
- for (let statement of readStatements) {
381
+ for (const statement of readStatements) {
381
382
  if (options.readonly) {
382
383
  this.db.exec("PRAGMA query_only = ON");
383
384
  }
384
- statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
385
- const result = this.prepare(statement).all();
386
- allRows.push(...result);
385
+ const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
386
+ const rows = this.prepare(processedStatement).all();
387
+ resultSets.push({ sql: statement, rows, rowCount: rows.length });
387
388
  }
388
- return { rows: allRows, rowCount: totalChanges + allRows.length };
389
+ return { resultSets };
389
390
  }
390
391
  } finally {
391
392
  if (options.readonly) {
@@ -3,20 +3,25 @@ import {
3
3
  sqlServerDynamicSqlPattern,
4
4
  sqlServerPassThroughKeywords,
5
5
  sqlServerPassThroughPattern
6
- } from "./chunk-JO7YQPBQ.js";
6
+ } from "./chunk-PBN2UXB5.js";
7
7
  import {
8
8
  isDriverNotInstalled
9
9
  } from "./chunk-WVVMH6FJ.js";
10
+ import {
11
+ computeHitRatioPct,
12
+ toNullableNumber
13
+ } from "./chunk-FU2ZJE4E.js";
10
14
  import {
11
15
  SQLRowLimiter,
12
16
  closeQuietly
13
- } from "./chunk-S2V6RZ3C.js";
17
+ } from "./chunk-RSB7DPO2.js";
14
18
  import {
15
19
  ConnectorRegistry,
16
20
  SafeURL,
17
21
  obfuscateDSNPassword,
22
+ splitSQLStatements,
18
23
  stripCommentsAndStrings
19
- } from "./chunk-JEZZN2YZ.js";
24
+ } from "./chunk-WIQ4DWGY.js";
20
25
 
21
26
  // src/connectors/sqlserver/index.ts
22
27
  import sql from "mssql";
@@ -372,6 +377,81 @@ var _SQLServerConnector = class _SQLServerConnector {
372
377
  return null;
373
378
  }
374
379
  }
380
+ async getHealthCheck() {
381
+ if (!this.connection) {
382
+ throw new Error("Not connected to SQL Server database");
383
+ }
384
+ const notes = [];
385
+ const result = {};
386
+ try {
387
+ const [connResult, maxConnResult] = await Promise.all([
388
+ this.connection.request().query(`
389
+ SELECT
390
+ COUNT(*) AS total,
391
+ SUM(CASE WHEN r.session_id IS NOT NULL THEN 1 ELSE 0 END) AS active,
392
+ SUM(CASE WHEN r.session_id IS NULL THEN 1 ELSE 0 END) AS idle,
393
+ SUM(CASE WHEN r.session_id IS NULL AND t.session_id IS NOT NULL THEN 1 ELSE 0 END) AS idle_in_transaction,
394
+ MAX(CASE WHEN r.session_id IS NULL AND t.session_id IS NOT NULL
395
+ THEN DATEDIFF(SECOND, s.last_request_end_time, SYSDATETIME()) ELSE NULL END) AS longest_idle_in_transaction_seconds,
396
+ MAX(CASE WHEN r.session_id IS NOT NULL
397
+ THEN DATEDIFF(SECOND, r.start_time, SYSDATETIME()) ELSE NULL END) AS longest_active_query_seconds
398
+ FROM sys.dm_exec_sessions s
399
+ LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
400
+ LEFT JOIN (SELECT DISTINCT session_id FROM sys.dm_tran_session_transactions) t ON t.session_id = s.session_id
401
+ WHERE s.is_user_process = 1 AND s.session_id <> @@SPID
402
+ `),
403
+ this.connection.request().query(`
404
+ SELECT CAST(value_in_use AS INT) AS max_connections
405
+ FROM sys.configurations
406
+ WHERE name = 'user connections'
407
+ `)
408
+ ]);
409
+ const conn = connResult.recordset[0];
410
+ const maxConnections = maxConnResult.recordset.length > 0 && maxConnResult.recordset[0].max_connections > 0 ? maxConnResult.recordset[0].max_connections : null;
411
+ result.connections = {
412
+ total: conn.total ?? 0,
413
+ active: conn.active ?? 0,
414
+ idle: conn.idle ?? 0,
415
+ idleInTransaction: conn.idle_in_transaction ?? 0,
416
+ // SQL Server has no equivalent of Postgres's "idle in transaction
417
+ // (aborted)" state - a failed statement doesn't leave the session's
418
+ // transaction in a distinct aborted-but-open state the way Postgres does.
419
+ maxConnections,
420
+ longestIdleInTransactionSeconds: toNullableNumber(conn.longest_idle_in_transaction_seconds),
421
+ longestActiveQuerySeconds: toNullableNumber(conn.longest_active_query_seconds)
422
+ };
423
+ } catch {
424
+ notes.push(
425
+ "Connection pool metrics unavailable: connecting user lacks the VIEW SERVER STATE permission (VIEW DATABASE STATE on Azure SQL Database)."
426
+ );
427
+ }
428
+ try {
429
+ const bufferResult = await this.connection.request().query(`
430
+ SELECT counter_name, CAST(cntr_value AS BIGINT) AS cntr_value
431
+ FROM sys.dm_os_performance_counters
432
+ WHERE object_name LIKE '%Buffer Manager%'
433
+ AND counter_name IN ('Page lookups/sec', 'Page reads/sec')
434
+ `);
435
+ const counters = Object.fromEntries(
436
+ bufferResult.recordset.map((row) => [row.counter_name.trim(), Number(row.cntr_value)])
437
+ );
438
+ const pageLookups = counters["Page lookups/sec"] ?? 0;
439
+ const pageReads = counters["Page reads/sec"] ?? 0;
440
+ result.bufferCache = {
441
+ hitRatioPct: computeHitRatioPct(pageLookups, pageReads),
442
+ blocksHit: pageLookups - pageReads,
443
+ blocksRead: pageReads
444
+ };
445
+ } catch {
446
+ notes.push(
447
+ "Buffer cache metrics unavailable: connecting user lacks the VIEW SERVER STATE permission (VIEW DATABASE STATE on Azure SQL Database)."
448
+ );
449
+ }
450
+ if (notes.length > 0) {
451
+ result.notes = notes;
452
+ }
453
+ return result;
454
+ }
375
455
  async getStoredProcedures(schema, routineType) {
376
456
  if (!this.connection) {
377
457
  throw new Error("Not connected to SQL Server database");
@@ -485,8 +565,9 @@ var _SQLServerConnector = class _SQLServerConnector {
485
565
  if (options.maxRows) {
486
566
  processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
487
567
  }
568
+ const isSingleStatement = splitSQLStatements(processedSQL, "sqlserver").length === 1;
488
569
  if (options.readonly) {
489
- return await this.executeReadOnly(processedSQL, parameters);
570
+ return await this.executeReadOnly(processedSQL, parameters, isSingleStatement);
490
571
  }
491
572
  const request = this.connection.request();
492
573
  const messages = [];
@@ -505,14 +586,57 @@ var _SQLServerConnector = class _SQLServerConnector {
505
586
  _SQLServerConnector.bindParameters(request, parameters);
506
587
  const result = await request.query(processedSQL);
507
588
  return {
508
- rows: result.recordset || [],
509
- rowCount: result.rowsAffected[0] || 0,
589
+ resultSets: _SQLServerConnector.buildResultSets(
590
+ result.recordsets,
591
+ result.rowsAffected,
592
+ isSingleStatement ? processedSQL : void 0
593
+ ),
510
594
  ...messages.length > 0 ? { messages } : {}
511
595
  };
512
596
  } catch (error) {
513
597
  throw new Error(`Failed to execute query: ${error.message}`);
514
598
  }
515
599
  }
600
+ /**
601
+ * Builds one result set per SELECT-producing statement in the batch, plus
602
+ * (if any) a trailing result set summarizing the write-only statements.
603
+ *
604
+ * node-mssql only pushes a `recordsets` entry for statements that produce
605
+ * columns (SELECT); INSERT/UPDATE/DELETE statements in the same batch
606
+ * contribute to `rowsAffected` only, with no way to recover which
607
+ * `rowsAffected` entry belongs to which statement from the driver's final
608
+ * arrays (`recordsets`/`rowsAffected` aren't index-aligned - see the
609
+ * tedious `doneHandler`, which always pushes to `rowsAffected` but only
610
+ * conditionally to `recordsets`). So a batch of pure writes collapses to a
611
+ * single summed result set, and a batch mixing writes with selects gets
612
+ * one result set per select plus one trailing "writes" set for the rest -
613
+ * not a truly per-statement breakdown, but no read statement's rows are
614
+ * ever merged with another's, which is what mattered for #380.
615
+ *
616
+ * `sourceSql`, when given, is attributed to the single resulting set - it
617
+ * must be omitted (pass `undefined`) unless the caller has already
618
+ * confirmed the batch is unambiguously one statement, since for a genuine
619
+ * multi-statement batch there's no reliable way to say which source
620
+ * statement a given recordset (or the trailing writes set) came from.
621
+ */
622
+ static buildResultSets(recordsets, rowsAffected, sourceSql) {
623
+ const sets = (recordsets ?? []).map((recordset) => {
624
+ const rows = recordset ?? [];
625
+ return { rows, rowCount: rows.length };
626
+ });
627
+ const totalAffected = (rowsAffected ?? []).reduce((total, count) => total + (count ?? 0), 0);
628
+ const accountedFor = sets.reduce((total, set) => total + set.rowCount, 0);
629
+ const writesOnly = totalAffected - accountedFor;
630
+ if (sets.length === 0) {
631
+ sets.push({ rows: [], rowCount: totalAffected });
632
+ } else if (writesOnly > 0) {
633
+ sets.push({ rows: [], rowCount: writesOnly });
634
+ }
635
+ if (sets.length === 1 && sourceSql !== void 0) {
636
+ sets[0].sql = sourceSql;
637
+ }
638
+ return sets;
639
+ }
516
640
  /**
517
641
  * Bind positional parameters as @p1, @p2, ... inferring the SQL Server type
518
642
  * from each JavaScript value. Shared by every execution path so they cannot
@@ -594,7 +718,7 @@ var _SQLServerConnector = class _SQLServerConnector {
594
718
  * Dangerous constructs are rejected before the transaction opens; see
595
719
  * assertNoReadOnlyEscapes.
596
720
  */
597
- async executeReadOnly(processedSQL, parameters) {
721
+ async executeReadOnly(processedSQL, parameters, isSingleStatement) {
598
722
  this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
599
723
  const transaction = new sql.Transaction(this.connection);
600
724
  await transaction.begin();
@@ -631,8 +755,11 @@ var _SQLServerConnector = class _SQLServerConnector {
631
755
  }
632
756
  }
633
757
  return {
634
- rows: result.recordset || [],
635
- rowCount: result.rowsAffected[0] || 0,
758
+ resultSets: _SQLServerConnector.buildResultSets(
759
+ result.recordsets,
760
+ result.rowsAffected,
761
+ isSingleStatement ? processedSQL : void 0
762
+ ),
636
763
  ...messages.length > 0 ? { messages } : {}
637
764
  };
638
765
  }
@@ -677,8 +804,12 @@ var _SQLServerConnector = class _SQLServerConnector {
677
804
  const planRow = planResult.recordset?.[0];
678
805
  const planXml = planRow ? Object.values(planRow)[0] : null;
679
806
  return {
680
- rows: planXml != null ? [{ plan: planXml }] : [],
681
- rowCount: planXml != null ? 1 : 0
807
+ resultSets: [
808
+ {
809
+ rows: planXml != null ? [{ plan: planXml }] : [],
810
+ rowCount: planXml != null ? 1 : 0
811
+ }
812
+ ]
682
813
  };
683
814
  } catch (error) {
684
815
  throw new Error(`Failed to explain query: ${error.message}`);
@@ -799,8 +930,12 @@ var _SQLServerConnector = class _SQLServerConnector {
799
930
  }
800
931
  const planXml = _SQLServerConnector.extractPlanXml(planResult);
801
932
  return {
802
- rows: planXml != null ? [{ plan: planXml }] : [],
803
- rowCount: planXml != null ? 1 : 0
933
+ resultSets: [
934
+ {
935
+ rows: planXml != null ? [{ plan: planXml }] : [],
936
+ rowCount: planXml != null ? 1 : 0
937
+ }
938
+ ]
804
939
  };
805
940
  } catch (error) {
806
941
  throw new Error(`Failed to explain analyze query: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "1.0.0",
3
+ "version": "1.2.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": {
@@ -33,7 +33,9 @@
33
33
  "test:unit": "vitest run --project unit",
34
34
  "test:watch": "vitest",
35
35
  "test:integration": "vitest run --project integration",
36
- "test:build": "node scripts/smoke-test-build.mjs"
36
+ "test:build": "node scripts/smoke-test-build.mjs",
37
+ "sync-version": "node scripts/sync-version.mjs",
38
+ "version": "pnpm run sync-version && git add plugin/.claude-plugin/plugin.json plugin/.mcp.json server.json"
37
39
  },
38
40
  "engines": {
39
41
  "node": ">=22.5.0"
@@ -1,113 +0,0 @@
1
- import {
2
- obfuscateDSNPassword
3
- } from "./chunk-JEZZN2YZ.js";
4
-
5
- // src/utils/dsn-database.ts
6
- var MissingDatabaseError = class extends Error {
7
- constructor(message) {
8
- super(message);
9
- this.name = "MissingDatabaseError";
10
- }
11
- };
12
- function requireDatabaseInDSN(database, dsn, label) {
13
- if (database) {
14
- return;
15
- }
16
- throw new MissingDatabaseError(
17
- `${label} DSN must name a database.
18
- Provided: ${obfuscateDSNPassword(dsn)}
19
- Add the database to the DSN, e.g. ...:3306/mydb
20
- To work with several databases, define one [[sources]] entry per database in a TOML config file: https://dbhub.ai/config/toml`
21
- );
22
- }
23
-
24
- // src/utils/multi-statement-result-parser.ts
25
- function isMetadataObject(element) {
26
- if (!element || typeof element !== "object" || Array.isArray(element)) {
27
- return false;
28
- }
29
- return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
30
- }
31
- function isMultiStatementResult(results) {
32
- if (!Array.isArray(results) || results.length === 0) {
33
- return false;
34
- }
35
- const firstElement = results[0];
36
- return isMetadataObject(firstElement) || Array.isArray(firstElement);
37
- }
38
- function extractRowsFromMultiStatement(results) {
39
- if (!Array.isArray(results)) {
40
- return [];
41
- }
42
- const allRows = [];
43
- for (const result of results) {
44
- if (Array.isArray(result)) {
45
- allRows.push(...result);
46
- }
47
- }
48
- return allRows;
49
- }
50
- function extractAffectedRows(results) {
51
- if (isMetadataObject(results)) {
52
- return results.affectedRows || 0;
53
- }
54
- if (!Array.isArray(results)) {
55
- return 0;
56
- }
57
- if (isMultiStatementResult(results)) {
58
- let totalAffected = 0;
59
- for (const result of results) {
60
- if (isMetadataObject(result)) {
61
- totalAffected += result.affectedRows || 0;
62
- } else if (Array.isArray(result)) {
63
- totalAffected += result.length;
64
- }
65
- }
66
- return totalAffected;
67
- }
68
- return results.length;
69
- }
70
- function parseQueryResults(results) {
71
- if (!Array.isArray(results)) {
72
- return [];
73
- }
74
- if (isMultiStatementResult(results)) {
75
- return extractRowsFromMultiStatement(results);
76
- }
77
- return results;
78
- }
79
-
80
- // src/utils/readonly-transaction.ts
81
- async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
82
- if (!readonly) {
83
- return execute();
84
- }
85
- try {
86
- await conn.query(
87
- supportsReadOnlyTransaction ? "START TRANSACTION READ ONLY" : "START TRANSACTION"
88
- );
89
- const result = await execute();
90
- await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
91
- return result;
92
- } catch (error) {
93
- try {
94
- await conn.query("ROLLBACK");
95
- } catch {
96
- }
97
- throw error;
98
- }
99
- }
100
-
101
- // src/utils/server-flavor.ts
102
- function isTiDBVersion(version) {
103
- return typeof version === "string" && /tidb/i.test(version);
104
- }
105
-
106
- export {
107
- MissingDatabaseError,
108
- requireDatabaseInDSN,
109
- extractAffectedRows,
110
- parseQueryResults,
111
- withReadOnlyTransaction,
112
- isTiDBVersion
113
- };